EventTracker 코드를 작성한 이후, 스스로 코드를 리뷰하며 발견한 문제점과 리팩토링 과정을 기록한다. 이 글은 설계 과정에서 어떤 부분이 부족했는지, 왜 이를 개선해야 했는지에 대해 정리한 글이다.
이 전에 작성했던, EventTracker를 초기 설계했던 과정과 기존 코드에 대한 글은 아래 링크에서 확인할 수 있다.
[이벤트 로깅을 위한 EventTracker] 파트 1. 요구사항 분석과 설계 및 구현 과정 기록
https://kodywiththek.tistory.com/26
[이벤트 로깅을 위한 EventTracker] 파트 1. 요구사항 분석과 설계 및 구현 과정
이번 글은, 개발 요청 문서를 바탕으로, 로그 이벤트를 추적하는 EventTracker를 설계하고 구현한 과정에 대한 글이다.요청문서의 요구사항을 고려하며, 유지보수성과 확장성을 고려한 구조로 설계
kodywiththek.tistory.com
1. Tracker 클래스의 문제점과 개선 과정
1) 타입 안전성 부족
Before: 초기 코드에서는 createLogString 메서드의 매개변수로 EventType, LinkLog, AdLog를 정의했으나, EventType과 LinkLog의 타입 정의가 명확하지 않아, 잘못된 데이터가 들어올 가능성이 있었다. 특히, 문자열 리터럴 타입("pv" | "ch" | "cb")로 정의된 EventType 타입 정의는 문자열 기반으로 특정 값만 허용하려는 의도가 있긴 하지만, 여전히 가독성과 타입 안정성에서 부족한 부분이 존재했다. 특히, 문자열 리터럴을 허용하기 때문에 새로운 값이 추가될 경우 수정해야 할 코드 위치를 정확히 파악하기 어렵다.
export type EventType = "pv" | "ch" | "cb";
export type LinkLog = [LinkType] | [LinkType, string] | undefined;
type LinkType =
| "mainbanner"
| "article"
// ...생략
After: Enum 타입을 도입하여 특정 문자열 값만 허용하도록 강제했다. 이를 통해 허용 가능한 값의 범위를 명확히 정의하고, 코드 작성 시 타입 체크를 강화하여 런타임 에러를 예방할 수 있도록 했다.
export enum EventTypeEnum {
PAGE_VIEW = "pv",
CLICK_HOMEPAGE = "ch",
CLICK_BANNER = "cb",
}
export enum LinkTypeEnum {
MAINBANNER = "mainbanner",
ARTICLE = "article",
EVENT_BIG = "event_big",
// ...생략
}
2) 하드코딩된 유저 ID 처리 로직
Before: getUserId 메서드에서 user#와 같은 접두사가 하드코딩되어 있었다. 이러한 문자열은 유지보수 시 누락되기 쉽고, 코드 가독성을 저해한다.
private getUserId(): string {
if (this.userId.startsWith("user#")) {
return `${encryptString(this.userId)}:lt`;
} else {
return `${encryptString("user#guest#" + this.userId)}:lf`;
}
}
After: 상수를 사용하여 하드코딩된 문자열을 관리하게 했다. 이를 통해 가독성을 높이고, 유지보수를 용이하게 했다.
const USER_PREFIX = "user#";
const GUEST_PREFIX = "user#guest#";
private getUserId(): string {
if (this.userId.startsWith(USER_PREFIX)) {
return `${encryptString(this.userId)}:lt`;
} else {
return `${encryptString(GUEST_PREFIX + this.userId)}:lf`;
}
}
3) 로그 문자열 생성 로직 개선
Before: createLogString 메서드에서 조건문을 통해 문자열을 조합하는 방식은 코드가 길고 반복적이었다. 이를 템플릿 리터럴과Array.filter를 활용해 간결하게 수정했다.
private createLogString(
eventType: EventType,
linkLog?: LinkLog,
adLog?: AdLog,
): string {
const user = this.getUserId();
let linkLogString = "";
if (linkLog) {
linkLogString = linkLog[1]
? `${linkLog[0]}:${linkLog[1]}`
: `${linkLog[0]}`;
}
const adLogString = adLog
? `${adLog[0]}:${adLog[1]}${adLog[2] ? ":" + adLog[2] : ""}`
: "";
return `${user} ${this.path} ${eventType} ${linkLogString} ${adLogString}`.trim();
}
After: Array.filter를 사용하여 조건부로 문자열을 조합하고, 간결한 템플릿 리터럴을 사용해 중복 코드를 제거했다.
private createLogString(
eventType: EventTypeEnum,
linkLog?: LinkLog,
adLog?: AdLog,
): string {
const user = this.getUserId();
const linkLogString = linkLog ? `${linkLog[0]}:${linkLog[1] || ""}` : "";
const adLogString = adLog ? `${adLog[0]}:${adLog[1]}${adLog[2] ? `:${adLog[2]}` : ""}` : "";
return [user, this.path, eventType, linkLogString, adLogString]
.filter(Boolean)
.join(" ");
}
4) 이벤트 메서드의 확장성 개선
Before: pv,ch,cb메서드는 특정 이벤트 타입만 처리할 수 있었다. 새로운 이벤트 타입이 추가될 경우 메서드를 계속 추가해야 하므로 유지보수가 어려웠다.
public pv(linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLogString("pv", linkLog, adLog);
}
public ch(linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLogString("ch", linkLog, adLog);
}
public cb(linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLogString("cb", linkLog, adLog);
}
After: pv, ch, cb 이외에 새로운 이벤트가 추가될 경우를 대비해 createLog 메서드를 추가하여 유연성을 높일 수 있었다.특정 이벤트가 필요할 때마다 메서드를 추가할 필요 없이 createLog 메서드를 활용할 수 있도록 확장성을 개선했다.
public createLog(eventType: EventTypeEnum, linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLogString(eventType, linkLog, adLog);
}
5) 메서드 이름 명확히 읽히도록 개선
Before: pv, ch, cb와 같은 이름은 코드 읽는 사람이 메서드의 목적을 즉시 이해하기 어려웠다.
public pv(linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLogString("pv", linkLog, adLog);
}
public ch(linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLogString("ch", linkLog, adLog);
}
public cb(linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLogString("cb", linkLog, adLog);
}
After: 이를 보다 명확한 이름으로 변경했다.
public pageView(linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLog(EventTypeEnum.PAGE_VIEW, linkLog, adLog);
}
public clickHomePage(linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLog(EventTypeEnum.CLICK_HOMEPAGE, linkLog, adLog);
}
public clickBanner(linkLog?: LinkLog, adLog?: AdLog): string {
return this.createLog(EventTypeEnum.CLICK_BANNER, linkLog, adLog);
}
2. EventLogProvider의 문제점과 개선 과정
1) Context 타입을 명확히 했다
Before: LogContext 타입이 Tracker | null로 정의되어 있었다.
const LogContext = createContext<Tracker | null>(null);
After: 불필요한 null 체크를 줄이고자 null값 최소화.
const LogContext = createContext<Tracker | undefined>(undefined);
export const useEventTracker = (): Tracker => useContext(LogContext)
2) 게스트 ID 생성 방식 개선
Before: Date.now()를 사용하여 게스트 ID를 생성하면 중복될 가능성이 있었다.
const GUEST_KEY = "bt-guest";
useEffect(() => {
const storedGuestId = localStorage.getItem(GUEST_KEY) ?? String(Date.now());
setUserId(storedGuestId);
}, []);
After: uuid 라이브러리를 사용하여 고유한 게스트 ID를 생성
import { v4 as uuidv4 } from "uuid";
const GUEST_KEY = "bt-guest";
useEffect(() => {
const storedGuestId = localStorage.getItem(GUEST_KEY) ?? uuidv4();
localStorage.setItem(GUEST_KEY, storedGuestId);
setUserId(storedGuestId);
}, []);
3) Tracker 인스턴스 관리 방식 개선 및 성능 최적화
Before: useMemo를 이용해서 tracker가 관리되고있었고, userId, searchParams가 변경될 때 마다 Tracker 인스턴스가 변경되는 구조였다.
const tracker = useMemo(() => {
if (userId) {
const currentPath = window.location.pathname + window.location.search;
return new Tracker(userId, currentPath);
}
return null;
}, [userId, searchParams]);
After: useMemo 대신 useRef를 사용하여 Tracker 인스턴스를 유지하여 성능을 개선했다. 이를 통해 렌더링과 무관하게 Tracker 상태를 관리했다. userId와, searchParams로 인한 의존성을 분리하여 관리하도록 해서 더 명확한 로직을 유지하도록 했다.
const trackerRef = useRef<Tracker | null>(null);
useEffect(() => {
if (userId) {
trackerRef.current = new Tracker(userId, window.location.pathname + window.location.search);
}
}, [userId]);
useEffect(() => {
if (trackerRef.current) {
const currentPath = window.location.pathname + window.location.search;
trackerRef.current = new Tracker(trackerRef.current.userId, currentPath);
}
}, [searchParams]);
추가설명: useRef 사용이 왜 더 성능에 좋을까?
useMemo와 useRef는 React에서 상태를 효율적으로 관리하고 재연산을 방지하기 위한 도구로 사용된다. 두 방법 모두 특정 값의 재생성을 제어하지만, 아래와 같은 이유로 useRef를 사용하는 것이 성능 최적화에 유리할 수 있다.
useMemo
- React 컴포넌트가 재렌더링될 때마다 useMemo가 호출되어 종속성 배열을 확인하고, 필요한 경우 재계산한다.
- 메모이제이션된 값을 반환하며, 렌더링 중에도 의존성이 변하면 새로운 값을 계산한다.
useRef
- 컴포넌트의 재렌더링과 상관없이 동일한 참조 값을 유지한다.
- 내부적으로는 변경 가능한 객체(current 속성)로 구현되어 값이 변경되어도 렌더링을 유발하지 않는다.
- 렌더링이 자주 발생하는 상황에서도 동일한 객체를 재사용하므로, 불필요한 재계산을 완전히 방지한다.
결론 1) useRef는 렌더링 사이에서 값을 유지하면서 재생성을 방지
- 의존성이 변경될 때마다 값을 재계산하는 useMemo는 위 코드에서 userId가 변경될 때마다 Tracker 인스턴스를 다시 생성한다.
- 반면, useRef는 값의 변경 여부와 관계없이 동일한 객체 참조(trackerRef.current)를 유지하므로, 불필요한 객체 생성 및 메모리 할당을 방지한다.
결론 2) 외부 상태와 독립적 관리
- Tracker는 특정 userId와 currentPath를 기반으로 인스턴스를 생성한다. 이 상태는 UI와 직접적으로 연결되지 않으므로, useRef로 관리하는 것이 적합하다.
- useMemo를 사용하면, React가 종속성 변경을 감지하여 새로운 값을 생성해야 하므로 불필요한 렌더링 논리에 얽힐 수 있다.
useMemo는 의존성 관리가 필요한 메모이제이션에 적합하지만, 렌더링과 독립적인 상태를 유지하는 경우에는 useRef를 사용하는 것이 더 효과적이다. Tracker와 같은 객체는 UI 렌더링과 관련이 없기 때문에 useRef를 사용하여 렌더링 성능을 최적화하는 것이 적합하다.
3. 트래킹 데이터 전송 방식 개선
Before: 기존 방식에서는 로그 문자열을 생성하는 책임과 전송하는 책임이 분리되어 있었다. Tracker 클래스는 문자열 생성만 담당하고, 로그 전송은 useEventLog 훅에서 처리했다. 이 방식은 각각의 역할을 분리하여 SRP(단일 책임 원칙)를 따랐지만, 두 컴포넌트 간의 연결성이 약해져 사용성에서 좀 불편했었다.
After: 이를 개선하기 위해 Tracker 클래스에 로그 전송 기능을 추가해서, createLogString으로 로그 문자열을 생성한 뒤, 해당 문자열을 API로 전송하는sendLogEvent 메서드를 제공하도록 했다.
import { postEventLog } from "@/utils/api";
export class Tracker {
private userId: string;
private path: string;
constructor(userId: string, path: string) {
this.userId = userId;
this.path = path;
}
public async sendLogEvent(logString: string): Promise<void> {
await postEventLog(logString);
}
// ... 나머지 로직 동일
}
적용: 로그 생성과 전송
import { AdLog, LinkTypeEnum } from "@/types/log";
import { useEventTracker } from "@/context/EventLogProvider";
//... 생략
// 이베트 로그 생성
const tracker = useEventTracker();
const logString = tracker.clickHomePage([LinkTypeEnum.EVENT_BIG, eventValue.companyId], adTracker)
// 로그 전송
tracker.sendLogEvent(logString);
이번 리팩토링 과정을 통해 타입 안전성을 강화하고, 가독성과 유지보수성을 높였다.
또한, 성능 최적화와 트래킹 데이터 전송 방식을 개선하여 코드의 효율성과 확장성을 향상시킬 수 있었다.
앞으로도 코드 리뷰와 리팩토링을 통해 더 나은 구조와 설계를 꾸준히 고민하고자 한다.
'개발 일기' 카테고리의 다른 글
스타트업 프론트엔드 개발자의 사내 디자인시스템 구축기 (1) | 2025.01.26 |
---|---|
검색 기능 간단한 개선/리팩토링 과정 (1) | 2025.01.14 |
[이벤트 로깅을 위한 EventTracker] 파트 1. 요구사항 분석과 설계 및 구현 과정 기록 (1) | 2024.11.22 |
NextAuth의 동작방식, 토큰 보안과 검증 과정 분석 (2) | 2024.11.20 |
Dropdown 컴포넌트 개선: useImperativeHandle 훅으로 외부에서 드롭다운 상태 제어하기 (0) | 2024.11.18 |