npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@teamsparta/react-logger

v0.1.1

Published

## 시작

Readme

@teamsparta/react-logger

시작

설치

pnpm add @teamsparta/react-logger

초기화

  1. /app/_global/_logging/index.ts, /src/features/log/react-logger/index.ts 와 같은 파일에서 Logger를 정의하고 생성하는 코드를 작성합니다.
import { sendCPLog } from '@teamsparta/cross-platform-logger';
import { createLogger } from '@teamsparta/react-logger';

type Context = Record<string, unknown>;

type EventMap = {
  kdt_lms_atani_click: {
    button_text: string;
    question_text?: string;
    category?: string;
  };
  kdc_virtualApply_submit: {
    product_name: string;
    product_id: string;
    button_text: string;
    course_id: string;
  };
  scc_lecture_start: {
    enrolled_id: string;
    course_id: string;
    course_title: string;
    start_time: number;
    end_time?: number;
  };
  sc_lxp_popup_impression: {
    popup_name: string;
    button_name: string;
  };
  kdt_apply_page_view: Record<string, unknown>;
};

type EventParams = {
  [K in keyof EventMap]: { eventName: K } & EventMap[K];
}[keyof EventMap];

export const [Logger, useLogger] = createLogger<Context, EventParams>({
  send: ({ eventName, ...restParams }) => sendCPLog(eventName, restParams),
  pageView: {
    onPageView: ({ eventName, ...restParams }) =>
      sendCPLog(eventName, restParams),
  },
  impression: {
    onImpression: ({ eventName, ...restParams }) =>
      sendCPLog(eventName, restParams),
  },
  DOMEvents: {
    onClick: ({ eventName, ...restParams }) => sendCPLog(eventName, restParams),
    onFocus: ({ eventName, ...restParams }) => sendCPLog(eventName, restParams),
    onSubmit: ({ eventName, ...restParams }) =>
      sendCPLog(eventName, restParams),
  },
});
  1. root에 Provider를 배치합니다.
export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ko">
      <body>
        <Logger.Provider initialContext={{}}>{children}</Logger.Provider>
      </body>
    </html>
  );
}

사용

  1. Click

요소 클릭 시 로깅하고 싶을 때 사용합니다.

export function Menu({
  name,
  href,
  selected = false,
  leftAddon,
  buttonText,
}: Props) {
  return (
    <Logger.Click
      enabled={Boolean(buttonText)}
      params={{
        eventName: 'kdt_lms_atani_click',
        button_text: buttonText ?? '',
      }}
    >
      <Link href={href}>
        <S.AIDiagnosisQuizLinkContainer selected={selected}>
          {leftAddon}
          <Text as="span" font="bodyCompact" color={vars.text.secondary}>
            {name}
          </Text>
        </S.AIDiagnosisQuizLinkContainer>
      </Link>
    </Logger.Click>
  );
}
  1. Page View

특정 페이지/뷰에 진입한 시점을 1회 기록하고 싶을 때 사용합니다.

export function ApplyPage() {
  return (
    <>
      <Logger.PageView params={{ eventName: 'kdt_apply_page_view' }} />
      <ApplyContent />
    </>
  );
}
  1. Impression

요소가 화면에 노출된 시점을 1회 기록하고 싶을 때 사용합니다.

export function PopupBanner({ popupName }: Props) {
  return (
    <Logger.Impression
      params={{
        eventName: 'sc_lxp_popup_impression',
        popup_name: popupName,
        button_name: '',
      }}
    >
      <Banner />
    </Logger.Impression>
  );
}
  1. 기타 dom event

클릭 외의 DOM 이벤트(focus, submit 등) 발생 시 로깅하고 싶을 때 사용합니다.

export function ApplyForm({ productId, productName, courseId }: Props) {
  return (
    <Logger.DOMEvent
      type="onSubmit"
      params={{
        eventName: 'kdc_virtualApply_submit',
        product_id: productId,
        product_name: productName,
        course_id: courseId,
        button_text: '신청하기',
      }}
    >
      <form onSubmit={handleSubmit}>
        <Logger.DOMEvent
          type="onFocus"
          params={{
            eventName: 'kdt_lms_atani_click',
            button_text: 'email_input_focus',
          }}
        >
          <input type="email" name="email" />
        </Logger.DOMEvent>
        <button type="submit">신청하기</button>
      </form>
    </Logger.DOMEvent>
  );
}
  1. log

JSX 로 감싸기 어려운 비동기 콜백/타이머/외부 라이브러리 콜백 등에서 직접 로깅해야 할 때 사용합니다.

export function LectureStartButton({
  enrolledId,
  courseId,
  courseTitle,
}: Props) {
  const { log } = useLogger();

  return (
    <button
      onClick={async () => {
        // 비동기로 동작하는 HTTP 요청의 응답값을 바탕으로 로깅을 해야 하는 경우
        const { startTime } = await startLecture({ enrolledId });
        log({
          eventName: 'scc_lecture_start',
          enrolled_id: enrolledId,
          course_id: courseId,
          course_title: courseTitle,
          start_time: startTime,
        });
      }}
    >
      학습 시작
    </button>
  );
}

무엇을 해결하나요?

1. 로깅 관심사 분리

이벤트 핸들러 안에 sendCPLog(...)와 같은 로깅 로직이 섞이면 비즈니스 로직 파악이 어렵게 됩니다. 이를 분리하여 비즈니스 로직 파악을 용이하게 합니다.

Before

function ApplyButton({ courseId, productId, productName }: Props) {
  function handleClick() {
    sendCPLog('kdc_virtualApply_submit', {
      course_id: courseId,
      product_id: productId,
      product_name: productName,
      button_text: '신청하기',
    });
    router.push(`/apply/${courseId}`);
  }

  return <Button onClick={handleClick}>신청하기</Button>;
}

After

function ApplyButton({ courseId, productId, productName }: Props) {
  return (
    <Logger.Click
      params={{
        eventName: 'kdc_virtualApply_submit',
        course_id: courseId,
        product_id: productId,
        product_name: productName,
        button_text: '신청하기',
      }}
    >
      <Button onClick={() => router.push(`/apply/${courseId}`)}>
        신청하기
      </Button>
    </Logger.Click>
  );
}

2. 로깅 로직을 선언적으로 작성

"어떤 요소에 어떤 이벤트가 붙는지" 가 JSX 트리에 그대로 드러납니다. 핸들러 코드를 파악할 필요 없이 컴포넌트 구조만 보고 어떤 로깅이 발생하는지 파악이 쉬워집니다.

function ApplyForm({ courseId }: Props) {
  return (
    <Logger.DOMEvent
      type="onSubmit"
      params={{ eventName: 'kdc_virtualApply_submit' }}
    >
      <form onSubmit={() => submit()}>
        <Logger.DOMEvent
          type="onFocus"
          params={{ eventName: 'kdc_apply_input_focus' }}
        >
          <input />
        </Logger.DOMEvent>
        <button type="submit">신청</button>
      </form>
    </Logger.DOMEvent>
  );
}

3. Type-safe

로깅 시 필요한 속성들의 타입이 자동으로 추론됩니다.

Before

// eventName에 오타가 발생했고(submitt가 아니라 submit)
// product_name, product_id 등 필수 parameter가 누락되었지만 타입 에러가 발생하지 않음.
sendCPLog('kdc_virtualApply_submitt', { course_id: 'r24' });

After

<Logger.Click
  params={{
    // 오타로 인해 타입 에러 발생.
    eventName: 'kdc_virtualApply_submitt',
    // product_id, product_name 등 필수 parameter가 누락되어 타입 에러 발생
    course_id: 'r24',
  }}
>

4. 로깅 로직에 관한 추상화 레벨 통일

레포마다 sendCPLog 직접 호출, 자체 wrapper, 컴포넌트 추상화 등 여러 방식이 존재하고 있습니다. @teamsparta/react-logger로 방식을 통일하면 코드 이해 비용을 줄일 수 있습니다.

Before

// 레포 A, sendCPLog 그대로 사용
sendCPLog('cta_click', { course_id });

// 레포 B, sendCPLog를 한 번 감싸서 사용
sendLog('cta_click', { course_id });

// 레포 C, 컴포넌트로 추상화
<LoggingClick>
  <button onClick={onClick}>생성하기</button>
</LoggingClick>;

After

<Logger.Click params={{ eventName: 'cta_click', course_id }}>

5. 로깅 로직 구현 간단화

impression 처럼 IntersectionObserver 보일러플레이트가 필요한 케이스도 <Logger.Impression>을 통해 간단하게 로깅 로직을 구현할 수 있습니다. 즉 "어떻게 찍어야 할지"라는 고민을 생략할 수 있습니다.

Before

function Banner({ popupName }: Props) {
  const ref = useRef<HTMLDivElement>(null);
  const fired = useRef(false);

  useEffect(() => {
    if (!ref.current) return;
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting && !fired.current) {
          sendCPLog('sc_lxp_popup_impression', {
            popup_name: popupName,
            button_name: '',
          });
          fired.current = true;
          observer.disconnect();
        }
      },
      { threshold: 0.2 },
    );
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, [popupName]);

  return <div ref={ref}>{/* ... */}</div>;
}

After

function Banner({ popupName }: Props) {
  return (
    <Logger.Impression
      params={{
        eventName: 'sc_lxp_popup_impression',
        popup_name: popupName,
        button_name: '',
      }}
    >
      <div>{/* ... */}</div>
    </Logger.Impression>
  );
}

심화 사용

조건부 로깅

enabled가 false로 평가되면 로깅을 하지 않습니다.

<Logger.Click
  params={{ eventName: 'kdt_admin_action', action: 'delete' }}
  enabled={(context) => context.userId !== null}
>
  <Button>삭제</Button>
</Logger.Click>

Context 업데이트와 사용

setContext 로 Provider 의 context 를 업데이트하고, params 를 함수형으로 작성하면 로깅 시점의 최신 context 값을 꺼내 사용할 수 있습니다.

function CategoryTabs() {
  const { setContext } = useLogger();

  return (
    <>
      <button
        onClick={() => setContext((prev) => ({ ...prev, category: 'react' }))}
      >
        React
      </button>
      <button
        onClick={() => setContext((prev) => ({ ...prev, category: 'node' }))}
      >
        Node
      </button>
    </>
  );
}

function StartButton() {
  return (
    <Logger.Click
      params={(context) => ({
        eventName: 'kdt_lms_atani_click',
        button_text: '시작하기',
        category: context.category, // 'react' or 'node'
      })}
    >
      <Button>시작하기</Button>
    </Logger.Click>
  );
}

도메인별 Logger 분리

공통 context와 전용 context가 따로 필요한 경우, Logger 인스턴스를 분리해서 사용할 수 있습니다. 각 Provider 는 독립 Context 라 동시에 공존이 가능합니다.

<UserLogger.Provider initialContext={{ userId: 'u1', pageType: 'lms' }}>
  <UserHeader />

  <CourseLogger.Provider initialContext={{ course_id: 'react-2024', round: 3 }}>
    <CoursePage />
  </CourseLogger.Provider>
</UserLogger.Provider>

API 요약

// 팩토리
createLogger<Context, EventParams>(config): [Logger, useLogger]

// LoggerConfig
type LoggerConfig<Context, EventParams> = {
  send?: EventFunction;                    // log() 호출 시
  DOMEvents?: {                            // Click/DOMEvent 용
    onClick?: EventFunction;
    onFocus?: EventFunction;
    onBlur?: EventFunction;
    onChange?: EventFunction;
    // ... 모든 React DOM 이벤트
  };
  impression?: {
    onImpression: EventFunction;
    options?: { threshold?: number; freezeOnceVisible?: boolean };
  };
  pageView?: { onPageView: EventFunction };
};

// 컴포넌트
<Logger.Provider initialContext>
<Logger.Click params enabled?>
<Logger.Impression params options? enabled?>
<Logger.PageView params enabled?>
<Logger.DOMEvent type params enabled?>

// 훅
const { log, getContext, setContext } = useLogger();

에러 처리

로깅은 부가 기능 이라는 원칙을 지킵니다. 로깅 시 에러가 발생해도 UI를 깨뜨리지 않습니다.

AI로 빠르게 셋업

Claude Code를 Plan Mode로 설정하고 다음 프롬프트를 입력하면 EventMap 정의와 Provider 배치까지 자동으로 진행됩니다.

@teamsparta/react-logger 를 이 레포에 도입하려고 합니다. 아래 가이드대로 셋업하세요.
시작 전에 사전 확인 3개를 사용자에게 묻고, 답에 맞춰 Step 1~2 를 순서대로 적용하세요.

# 사전 확인 (사용자에게 질문)

1. `package.json` 에 `@teamsparta/react-logger` 가 설치되어 있는가? 없으면 `pnpm add @teamsparta/react-logger` 를 먼저 안내.
2. 이벤트 송신 SDK 는 무엇인가? (예: `@teamsparta/cross-platform-logger` 의 `sendCPLog`, Amplitude, Hackle 등) — Step 1 의 핸들러 본문이 이 SDK 호출로 채워집니다.
3. 프레임워크 — Next.js App Router / Pages Router / Vite / CRA 중 무엇? Provider 배치 위치가 달라집니다.

# Step 1. EventMap 정의 + Logger 인스턴스 생성

파일 위치는 **기존 프로젝트 컨벤션을 우선** 따릅니다. 파일을 만들기 전에 다음 순서로 적절한 위치를 결정하세요.

1. 프로젝트의 `src/`, `app/`, `apps/*/src` 등 루트를 훑어 기존 feature/module 폴더 구조를 파악 (예: `src/features/*`, `src/shared/*`, `src/lib/*`, `app/_global/*` 등).
2. `sendCPLog`, `track`, `amplitude`, `analytics`, `logger` 같은 기존 로깅 / 트래킹 관련 파일이 이미 있다면 그 위치를 우선 (예: `src/features/log/*`, `src/shared/lib/analytics/*` 등 기존 폴더 안에 자연스럽게 합류).
3. 컴포넌트 파일 컨벤션 (`Foo/index.tsx` vs `Foo.tsx`) 과 export 스타일도 동일하게 맞춥니다.
4. 파일 위치를 결정했으면 사용자에게 한 줄로 보고하고 진행. 명확한 컨벤션이 없을 때만 폴백으로 Next.js App Router → `app/_global/_logging/index.ts`, 그 외 → `src/features/log/react-logger/index.ts` 사용.

아래 보일러플레이트를 결정한 위치에 생성하고 `EventMap` 만 도메인에 맞게 채우세요. 이벤트가 아직 정해져 있지 않으면 첫 1개만 예시로 두고 나머지는 `// TODO` 주석으로.

```ts
import { sendCPLog } from '@teamsparta/cross-platform-logger';
import { createLogger } from '@teamsparta/react-logger';

type Context = Record<string, unknown>;

type EventMap = {
  // TODO: 도메인 이벤트 추가
  // 예: kdt_apply_cta_click: { course_id: string; button_text: string };
  // 예: kdt_apply_page_view: Record<string, unknown>;
};

type EventParams = {
  [K in keyof EventMap]: { eventName: K } & EventMap[K];
}[keyof EventMap];

export const [Logger, useLogger] = createLogger<Context, EventParams>({
  send: ({ eventName, ...restParams }) => sendCPLog(eventName, restParams),
  pageView: {
    onPageView: ({ eventName, ...restParams }) =>
      sendCPLog(eventName, restParams),
  },
  impression: {
    onImpression: ({ eventName, ...restParams }) =>
      sendCPLog(eventName, restParams),
  },
  DOMEvents: {
    onClick: ({ eventName, ...restParams }) => sendCPLog(eventName, restParams),
    onFocus: ({ eventName, ...restParams }) => sendCPLog(eventName, restParams),
    onSubmit: ({ eventName, ...restParams }) =>
      sendCPLog(eventName, restParams),
  },
});
```

규칙 / 함정:

- `EventParams` 는 반드시 `{ [K in keyof EventMap]: { eventName: K } & EventMap[K] }[keyof EventMap]` 형태의 discriminated union. `EventMap[keyof EventMap]` 만 쓰면 discriminator 가 없어 타입 안전성이 깨집니다.
- payload 안에 직접 `eventName` 필드를 넣지 말고, 위 매핑 타입이 자동으로 붙이게 둡니다.
- 송신 SDK 가 `sendCPLog` 가 아니면 핸들러 본문(`sendCPLog(eventName, restParams)`) 만 해당 SDK 호출로 교체. 구조는 동일.
- `send` 누락 시 `useLogger().log()` 가 silent no-op. 비동기 콜백에서 직접 로깅할 일이 있으면 반드시 등록.
- `DOMEvents` 에 등록하지 않은 이벤트 타입을 `<Logger.DOMEvent type="onMouseEnter">` 식으로 사용하면 동작하지 않습니다. 보통 `onClick` / `onFocus` / `onSubmit` 으로 충분.
- `impression`, `pageView` 는 해당 컴포넌트를 쓸 때만 등록. 안 쓰면 빼도 됩니다.

# Step 2. Provider 배치

프레임워크별 위치:

- Next.js App Router → `app/layout.tsx` 의 `<body>` 안
- Next.js Pages Router → `pages/_app.tsx` 최상위
- CRA / Vite → `src/main.tsx` 또는 `src/App.tsx` 최상위

```tsx
import { Logger } from '@/features/log/react-logger'; // 실제 경로로 교체

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ko">
      <body>
        <Logger.Provider initialContext={{}}>{children}</Logger.Provider>
      </body>
    </html>
  );
}
```

함정:

- `initialContext` 는 필수 prop. 비어 있어도 `{}` 로 명시.
- Provider 밖에서 `useLogger()` 호출 시 throw — 테스트/스토리북도 동일하게 감싸야 합니다.
- 비동기 사용자 정보 주입에는 Provider 의 init 콜백이 없습니다. 셋업 단계에서는 빈 context 로 시작하고, 사용자가 별도로 요청하면 그때 `setContext` 패턴 안내.

# 작업 진행 원칙

- Step 1 (logger 파일 생성, EventMap 1개 예시) → Step 2 (Provider 삽입) 순서로 마무리한 뒤 결과를 사용자에게 보고. 실제 사용 예시(`<Logger.Click>` 등)는 README 의 "사용" 섹션을 참고해 사용자가 직접 적용.
- 심화 주제 (`setContext` 로 동적 context, 도메인별 Logger 인스턴스 분리, `enabled` 응용 등) 는 사용자가 별도로 물어볼 때만 안내. 셋업 단계에서 먼저 들이밀지 마세요.
- 기존에 `sendCPLog` 직접 호출 코드가 있어도 자동 마이그레이션 하지 말 것 — 별도 작업.