개발

Amplitude로 사용자 이벤트 추적하여 탈퇴 사유 수집하기

hskkkk 2025. 4. 15. 18:03

내가 참여 중인 프로젝트에서는, 탈퇴 사유 수집을 서버 DB에 저장해놓지 않고 Amplitude에 분석 데이터로 저장해놓는 방법을 선택했다. 

Amplitude란?

Amplitude는 서비스를 이용하는 사용자가 취하는 모든 행동(클릭, 페이지 방문, 구매 등)을 트래킹할 수 있는 툴이다. 기존에 사용하던 툴인 GA는 개인별 추적을 지원하지 않고, 데이터가 실시간으로 수집되지않아 즉각적인 변화를 확인할 수 없다. 그러나 Amplitude는 이러한 단점을 모두 보완한다.

 

Amplitude는 다양한 방식으로 활용할 수 있지만, 우리 서비스에서는 회원탈퇴 페이지에서 '탈퇴하기' 버튼을 누른 사용자와 그들이 선택한 탈퇴 사유를 추적하는 데 활용하기로 했다. 사용자가 선택한 옵션 값은 Amplitude 대시보드에서 바로 확인할 수 있으며, 이를 통해 서비스 탈퇴의 주요 원인을 분석하고 개선 방향을 도출할 수 있다. 또한, 이는 서버 자원을 아끼기 위한 선택이기도 하다 —> 굳이 별도 서버 이벤트 저장 없이 Amplitude를 통해 손쉽게 수집 및 시각화가 가능하기 때문이다.

 

confeti의 회원탈퇴 페이지

 

Amplitude로 탈퇴 사유 트래킹하기

1. 시작하기

https://amplitude.com/

package로 설치하는 방법이 있고, 사이트에 바로 나와있는대로 script를 추가해주는 방법이 있다.

내 프로젝트는 TypeScript를 사용하고 있기 때문에, 나는 타입 정의가 자동으로 제공되는 패키지 설치 방식을 선택했다.

pnpm add @amplitude/analytics-browser

 

위 명령어를 입력해주면 된다.

 

2. 이벤트 추적 시작

프로젝트의 최상단에 amplitude를 시작한다는 코드를 넣어보자. 나 같은 경우는 App.tsx에 추가했다!

이때 API key는 amplitude 사이트에서 회원가입만 하면 발급된다.

import { init } from '@amplitude/analytics-browser';

init(import.meta.env.VITE_AMPLITUDE_API_KEY);

function App() {}

 

이렇게 init 함수를 App함수 밖으로 빼주면, amplitude 초기화가 애플리케이션이 시작될 때 한 번만 발생하게 된다. 

컴포넌트 함수 내부에서 실행하면 컴포넌트가 리렌더링될 때마다 실행될 수 있어 불필요한 초기화가 반복될 수 있으므로..

 

3. 이벤트 설정 후 보내기

이벤트를 Amplitude 서버로 전송하려면 Amplitude SDK에서 제공하는 핵심 메소드인 track()을 사용하면 된다.

 

예를 들어, 추적하려는 이벤트가 "버튼 클릭"일때:

import { track } from '@amplitude/analytics-browser';

// 1
const handleButtonClick = () => {
  track('Button Clicked');
 }
 
// 2
const eventProperties = {
  buttonColor: 'primary',
};

const handleButtonClick = () => {
  track('Button Clicked', eventProperties);
}

 

1번 형태는 가장 간단한 형태로, 이벤트 이름만 전달한다.

2번 속성이 있는 이벤트를 추적할 때, 이벤트 이름과 함께 추가 속성을 객체 형태로 전달한다.

 

이 외에도 더 복잡한 이벤트 구조를 정의할 수도 있다고 한다. (ex: BaseEvent)

 

자 그럼 내가 실제 탈퇴 페이지 컴포넌트에서 작성한 코드를 예시로 보여주겠다.

 

1. 탈퇴 사유 수집:

const [selectedReason, setSelectedReason] = useState<string>('');

 

사용자가 선택한 탈퇴 사유를 state로 관리한다.

 

2. 이벤트 트래킹 실행:

const handleConfirmDeleteAccount = () => {
  // Amplitude에 탈퇴 사유 이벤트 전송
  track('User Withdrawal', {
    reason: selectedReason,
    reason_text: reasons.find((r) => r.value === selectedReason)?.text,
  });
  deleteAccountMutation.mutate();
};

 

사용자가 최종적으로 탈퇴를 확정할 때 track 함수를 호출하여 Amplitude에 데이터를 전송한다.

 

- 이벤트 이름:

  • 'User Withdrawal'

- 이벤트 속성:

  • reason: 선택된 사유의 코드값 (예: 'no_events')
  • reason_text: 선택된 사유의 실제 텍스트 (예: '원하는 공연이 많이 없어서')

전체 코드 ⏬

더보기
import { useState } from 'react';
import { track } from '@amplitude/analytics-browser';
import { useDeleteAccountMutation } from '@pages/my/hooks/use-delete-account';
import { useOverlay } from 'node_modules/@confeti/design-system/src/context/overlay-context';

import { Button, Dialog, Footer, Header } from '@confeti/design-system';

import * as styles from './delete-account.css';

const DeleteAccount = () => {
  const overlay = useOverlay();
  const [selectedReason, setSelectedReason] = useState<string>('');
  const deleteAccountMutation = useDeleteAccountMutation();

  const reasons = [
    { value: 'no_events', text: '원하는 공연이 많이 없어서' },
    { value: 'frequent_errors', text: '잦은 오류, 장애가 발생해서' },
    { value: 'inconvenient', text: '이용하는데 편리하지 않아서' },
    { value: 'rejoin', text: '다른 계정으로 재가입하려고' },
  ];

  const handleConfirmDeleteAccount = () => {
    // Amplitude에 탈퇴 사유 이벤트 전송
    track('User Withdrawal', {
      reason: selectedReason,
      reason_text: reasons.find((r) => r.value === selectedReason)?.text,
    });
    deleteAccountMutation.mutate();
  };

  const handleDialogOpen = () => {
    overlay.open(({ isOpen, close }) => (
      <Dialog open={isOpen} handleClose={close}>
        <Dialog.Content>
          <Dialog.Title>정말 confeti를 탈퇴하실건가요?</Dialog.Title>
          <Dialog.Description>
            탈퇴 시 계정 및 이용 기록은 모두 삭제되며, <br />
            삭제된 데이터는 복구가 불가능합니다. <br />
            탈퇴를 진행할까요?
          </Dialog.Description>
        </Dialog.Content>
        <Dialog.Action>
          <Button text="취소하기" onClick={close} variant="back" />
          <Button
            text="탈퇴하기"
            onClick={() => {
              handleConfirmDeleteAccount();
              close();
            }}
          />
        </Dialog.Action>
      </Dialog>
    ));
  };

  return (
    <>
      <Header variant="detail" title="회원탈퇴" />
      <main className={styles.selectSection}>
        <div className={styles.textStyle}>
          탈퇴하시려는 이유를 선택해주세요.
        </div>
        <div className={styles.radioWrapper}>
          {reasons.map((reason) => (
            <label key={reason.value} className={styles.label}>
              <input
                type="radio"
                name="withdrawal_reason"
                value={reason.value}
                checked={selectedReason === reason.value}
                onChange={() => setSelectedReason(reason.value)}
                className={styles.radioStyle}
              />
              <span>{reason.text}</span>
            </label>
          ))}
        </div>
      </main>
      <div className={styles.buttonWrapper}>
        <Button
          variant="add"
          text="탈퇴하기"
          disabled={!selectedReason}
          onClick={handleDialogOpen}
        />
      </div>
      <Footer />
    </>
  );
};

export default DeleteAccount;

 

탈퇴하기 버튼 클릭 시, 이벤트 이름과 이벤트 속성이 Amplitude로 전송되고, 이는 대쉬보드에서 확인할 수 있다!

 

해당 데이터는 차트나 데이터베이스 형태로 시각화하여 확인할 수 있으며, CSV 파일로도 다운로드가 가능하다.
나는 이 데이터베이스 링크를 슬랙에 공유하여 기획 파트가 쉽게 확인할 수 있도록 했다.

 

 

참고 링크: 

https://amplitude.com/docs/sdks/analytics/browser/browser-sdk-2

 

Browser SDK 2

Starting from v2.8.0 the SDK supports getting the device ID from the URL paramter ampDeviceId. The SDK configuration, for example, init('API_KEY', { deviceId: 'custom-device-id' }) still takes precedence over the URL parameter. Previous versions of the SDK

amplitude.com

https://blog.wiselycompany.com/wisely-amplitude