모든 소프트웨어는 비즈니스를 위해 존재한다. 특히, 프론트엔드는 사용자와 가장 밀접하게 연결되어 있으므로, 비즈니스 로직 외에도 비즈니스 관련 요구사항을 처리해야 하는 경우가 많다. 이 중에서 대표적인 것이 바로 로깅이다.

 

로깅이 필요한 이유는 간단하다. 더 나은 비즈니스 결정을 내리기 위함이다. 비즈니스를 구체화하면서, 추상적인 개념들이 구체화되어 특정 기준을 정해야 하는 경우가 많다. 초기에는 사람의 감각에 의존할 수밖에 없지만, 비즈니스가 계속되면서 감각에만 의존하는 것은 위험성이 매우 크다. 따라서, 의사결정에 도움이 될 데이터를 수집하기 위해 로깅이 필요한 것이다.

 

따라서 로깅은 직접적인 비즈니스 로직은 아닐지라도, 비즈니스 성공과 직결된 매우 중요한 요소이다. 로깅에는 다양한 종류가 있지만, 프론트엔드에서는 실제 사용자의 인터랙션 이벤트 로그를 수집할 수 있으므로, 비즈니스적 가치가 큰 데이터를 수집할 수 있다. 이렇게 수집된 이벤트 로그는 가설을 검증하거나 의사결정을 할 때 큰 도움이 된다.

 

현재 진행 중인 프로젝트에서 베타 버전 출시를 앞두고 로깅 시스템을 구현할 기회가 있었다. 이번 포스팅에서는 로깅 시스템의 구축 과정, 애플리케이션에 로깅 코드를 작성하는 방법, 그리고 로깅을 도입하면서 느낀 점에 대한 경험을 공유하려고 한다.

 

 

1. 무엇을 로깅해야 하는가?

로그를 생성하기로 결정했을 때, 가장 어려운 부분은 사용자의 어떤 상호작용을 로그로 만들어 데이터화 해야 하는지 결정하는 것이다. 모든 부분을 로깅하는 것이 이상적일 수 있지만, 개발 인원이 부족한 경우에는 필요한 데이터만 수집하는 것이 현실적이다.

 

앞서 로그를 만드는 이유가 비즈니스 의사결정을 돕기 위함이라고 언급했다. 따라서 로깅할 내용을 결정하는 것 역시 비즈니스 요인에 따라 결정되어야 하는 문제이다. 나의 경우 프로젝트 팀이 기획팀과 개발팀으로 나눠져 있어, 로깅할 내용을 결정하기 위해 기획팀과 협업을 진행했다. 협업에서는 주로 비즈니스 목표를 달성하기 위해 필요한 데이터에 대한 논의 과정이 이루어졌다.

기획팀과 협업하여 수립한 문서

이 문서는 기획팀과의 협업을 통해 결정된 내용을 정리한 것이다. 현재 가장 중요한 비즈니스 관심사는 일반 요금제와 pro 요금제의 차이를 결정하는 기준을 어떻게 정하냐는 것인데, 이를 결정하기 위해 필요한 데이터를 각 카테고리별로 수집하기로 하였다.

 

 

2. 로그 설계 및 명세화하기

비즈니스에 필요한 데이터가 결정되면 이를 위한 로그를 설계할 수 있다. 이때 주의할 점은 로그와 데이터가 1대 1로 매핑되는 관계가 아니라는 점이다. 로그는 원천 데이터에 가깝다. 필요한 데이터를 애플리케이션에서 바로 로깅하려면 로깅 데이터를 생성하는 로직이 애플리케이션 코드에 추가되어야 한다. 하지만 이보다는 애플리케이션에서는 원천 데이터만을 수집하고, 이 원천 데이터를 분석 데이터로 변환하는 작업은 데이터 분석 도구를 통해 처리하는 것이 더 바람직하다.

 

예를 들어, 우리 팀은 로그 수집 및 분석 도구로 구글 애널리틱스 4를 사용하는데, 뷰 모드의 체류시간을 구할 때, 애플리케이션에 체류시간을 계산하는 로직을 추가하는 것보다는 원천 데이터를 통해 구글 애널리틱스 4의 분석 기능을 활용하여 체류시간을 계산하는 것이 더 좋다는 것이다. 물론 이에 대한 생각은 팀마다 다를 수 있다고 생각하는데, 나의 경우 애플리케이션에서는 비즈니스 로직이 가장 우선시되어야 하기 때문에 이렇게 생각했다.

 

요지는 원천 데이터를 통해 데이터를 생성할 수 있도록 로그를 설계해야 한다는 것이다. 로그 설계는 개발팀이 앞서 수립한 내용을 기반으로 설계 및 명세화를 진행한 후, 기획팀과 논의하여 재조정하는 방식으로 진행하였다.

 

2-1. 로그 컨벤션 정의하기

로그를 설계할 때 처음으로 고민해야 하는 부분은 로그 컨벤션을 정의하는 것이다. 컨벤션은 한 번 결정되면 수정하는 데 큰 노력이 필요하므로, 가능한 한 명확하고 여러 유스케이스를 커버할 수 있어야 한다.

나의 경우, 사용자 이벤트 로그의 형식이 "사용자가 {어디에서} {무엇을} 한다"의 형식을 따르게 될 것이라고 생각했고, 이벤트 이름으로 "무엇을" 수집하고, .property에 path 속성으로 "어디에서"를 수집하도록 했다. 또한, 추가로 수집해야 할 애플리케이션 데이터는 property에 상황에 맞게 정의하여 수집하도록 하였다.

작성한 로그 명세서 일부

 

 

3. 애플리케이션에 로그 코드 넣기

 

3-1. 의존성 주입하기

로그를 수집하려면 먼저 로그 수집 도구에서 제공하는 라이브러리를 설치하고 사용해야 합니다. 내 프로젝트에서는 로그 수집 도구로 Google Analytics4를 사용하기로 결정했지만, 우리의 기획팀이 변덕스러우므로 도구 변경이 언제든 일어날 수 있다. 실제로, 애플리케이션 입장에서는 수집이 잘 이루어지기만 하면 어떤 도구를 사용하는지는 중요하지 않다

 

그러므로 로그 수집 도구를 추상화하여 애플리케이션 코드가 이에 의존하게 하고, 프로바이더를 통해 주입하도록 만들어 보자.

import { createContext, useContext } from 'react';

export type UserEvent = 'click_metadata_item' | ...

// logger를 추상화했다.
export type UserTracker = {
  track(event: UserEvent, properties?: Record<string, unknown>): void;
};

export const LoggerContext= createContext<UserTracker | null>(null);

export const useLogger = (): UserTracker => {
  const logger = useContext(LoggerContext);
  if (!logger) {
    throw new Error('useLogging must be used within a LoggingProvider');
  }
  return logger;
};

애플리케이션 코드에서는 아래와 같이 사용하면 된다.

// 애플리케이션 코드는 logger가 GA4인지 Amplitude인지 모른다.
export default function MetadataCreateButton() {
  const logger = useLogger(); 

	// ...
	
  const handleMetadataCreate = async () => {
    logger.track('click_metadata_create_button', {
      metadata_item_count: metadataList?.length ?? -1,
    });

    await createIndicatorBoardMetadata();
  };

  return (
    <Button
      onClick={handleMetadataCreate}
      label={'메타데이터 생성'}
    />
  );
}

실제 도구를 사용하는 로거를 주입하기 위해 이제 프로바이더를 만들어보자

'use client';

import { GoogleAnalytics } from '@next/third-parties/google';
import { LoggerContext, type UserTracker } from '../logging-context';
import { sendGAEvent } from '@next/third-parties/google';

export default function GoogleAnalyticsProvider({ children }: { children: React.ReactNode }) {
  const userTracker: UserTracker = {
    track(event, properties) {
      sendGAEvent('event', event, {
        path: location.pathname,
        ...properties,
      });
    },
  };

  return (
     <LoggerContext.Provider value={userTracker}>{children}</LoggerContext.Provider>
  );
}

이제 프로바이더를 아래와 같이 등록하면, 애플리케이션 전체에서 사용할 수 있다.

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html
      lang="kr"
    >
      <GoogleAnalyticsProvider>
        <body>{children}</body>
      </GoogleAnalyticsProvider>
    </html>
  );
}

 

3-2. 선언적으로 로깅하기

현재까지의 과정만으로도 로깅을 수행하는 데는 문제가 없다. 하지만 현재 코드에서는 이벤트 핸들러에서 로깅과 비즈니스 로직이라는 두 가지 관심사가 혼합되어 있다. 당장은 문제가 없어 보일 수 있어도 비즈니스 로직이 복잡해지거나, 새로운 로깅 요구사항 또는 새로운 관심사가 생길 경우 코드가 복잡해져 변경하기 어려운 코드가 될 수 있다. 따라서 로깅을 전담하는 컴포넌트를 만들어 로깅 관련 코드를 해당 컴포넌트에 캡슐화하고, 비즈니스 로직과 로깅 코드를 분리해 보자.

// click 로깅을 담당하는 LogClick 컴포넌트
type LogClickProps = {
  event: UserEvent;
  properties?: Record<string, unknown>;
};

export function LogClick({ children, event, properties }: React.PropsWithChildren<LogClickProps>) {
  const logger = useLogger();

  const child = React.Children.only(children);

  if (!React.isValidElement(child)) {
    return <>{children}</>;
  }

  return React.cloneElement(child as React.ReactElement, {
    onClick: (...args: any[]) => {
      logger.track(event, properties);

      if (child.props && typeof child.props['onClick'] === 'function') {
        return child.props['onClick'](...args);
      }
    },
  });
}

LogClick 컴포넌트를 사용하면 이벤트 핸들러에 로깅 코드를 제거하고 선언적으로 로깅을 처리할 수 있다.

import { LogClick } from '@/app/logging/component/log-click';

export default function MetadataCreateButton() {  
  // ...

  const handleMetadataCreate = async () => {
    await createIndicatorBoardMetadata();
  };

  // 로깅 관련 코드를 LogClick 컴포넌트를 내부로 캡슐화하고 선언적으로 처리하였다.
  return (
    <LogClick event={'click_metadata_create_button'} properties={{ metadata_item_count: metadataList?.length ?? -1 }}>
      <CreateButton
        onClick={handleMetadataCreate}
        label={'메타데이터 추가'}
        isLoading={isCreateIndicatorMetadataMutating}
      />
    </LogClick>
  );
}

 

 

4. 마무리하며

이번 포스트에서는 프론트엔드에서 로깅이 필요한 이유, 로깅해야 할 내용, 로깅 명세를 작성하고 애플리케이션에 로깅 코드를 삽입하는 과정을 살펴봤다. 개인적으로 매우 흥미로운 경험이었으며, 특히 이전까지는 직감에 의한 결정이 많았지만, 이제는 로깅을 통해 수집한 데이터를 기반으로 더욱 근거에 기반한 의사결정을 통해 사용자가 정말 필요로 하는 제품을 만들 수 있다는 생각에 열정적으로 작업하였다.

 

이번 활동을 통해 배운 것은 프론트엔드 개발자로서 다른 부서, 특히 비즈니스 관련 부서와의 협업이 중요하다는 것 이다. 프론트엔드는 사용자와 가장 밀접하게 연결되어 있으므로 모든 부서와의 협업이 중요하다. 비즈니스 이해관계자들은 백엔드 개발자나 디자이너같은 엔지니어와는 또 다른 관심사를 가지고 있어, 그들과의 협업은 완전히 새로운 경험이었다. 그들의 목적과 목표를 이해하고, 프론트엔드 개발자로서 어떤 관점에서 협업을 해야 하는지에 대해 고민할 수 있는 좋은 기회였다.

 

또 로깅 작업은 비즈니스 로직은 아니지만, 관리가 어렵고 볼륨이 큰 작업이라는 생각이 들었다. 현재는 로깅이 많지 않아서 위에서 서술한 수준으로도 감당 가능하지만, 로그 수집이 점점 많아지고 복잡해질수록 관리가 어려워질 것이다. 로깅 시스템의 관리 방법에 대해 프론트엔드 개발자가 고민해야 할 부분이라고 생각하기에, 작업을 계속하면서 더 나은 방법이 없는지 고민할 계획이다. 이상으로 이번 포스팅을 마치겠다.