2025. 7. 16. 14:56ㆍStudy/React, next.js
팀빌딩이 어느 정도 완성되고 팀이 안정화가 되면서 제일 좋은 점이 있다.
코드에 대한 생산적인 얘기를 많이 나눌 수 있는 동료들이 생겼다는 것...

그러면서 나도 점점 코드를 보는 눈이 생기게 되었고, 뭣도 모르고 짠 냄새나는 나의 코드들이 눈에 띄기 시작했다.
그 중 하나가 관심사 분리가 되지 않은 채 복잡하게 얽혀있는 컴포넌트였다.
예시를 하나 들어보자.
발단
// 완전 초기
function Button({
variant = 'solid',
children,
color,
size,
type = 'button',
...props
}: Props) {
return (
<button
type={type}
className={cn(variant, size, color)}
{...props}
>
{children}
</button>
);
}
위와 같이 기존에 Button 이라는 밑바닥 컴포넌트가 있었다.
그러다 모든 버튼에 이벤트 로깅이 필요한건 아니고, 몇몇 버튼만 클릭 시 이벤트 로깅이 필요하다는 요구사항이 추가되었다.
그래서 왕초보 마인드로 props에 다 때려넣으면 되겠지~ 하고
기존에 있던 Button 컴포넌트에 Props를 추가하여 GA 이벤트 로깅이 가능하도록 다음과 같이 만들었다.
// 요구사항1 : 버튼 클릭 시 GA 이벤트 로깅 해주세요.
function Button({
...
gaEventKey, // new!
gaEventSubKey, // new!
gaTags, // new!
}: Props) {
const logEvent = useGA(gaEventKey, gaEventSubKey); // GA custom hook
return (
<button
type={type}
className={cn(variant, size, color)}
onClick={(e) => {
logEvent(tags);
props.onClick?.(e);
}}
{...props}
>
{children}
</button>
);
}
<Button
variant={selected ? 'solid-round' : 'outline-round'}
size={'xsmall'}
color={'primary'}
gaEventKey={'header'}
gaEventSubKey={'navigation'}
gaTags={['사이드바', '테스트버튼']}
>
테스트 버튼
</Button>
전개
이번엔 클릭 이벤트에 GA 이벤트 로깅 말고도 앱 통신을 위한 스킴을 전송해달라는 요구사항이 왔다.
그렇게 또 Button 컴포넌트에 props 추가하게 된다.
// 요구사항2: 버튼 클릭 시 앱 스킴 전송해주세요.
function Button({
...
appScheme, // new!
}: ButtonProps) {
...
return (
<button
type={type}
className={cn(variant, size, color)}
onClick={(e) => {
logEvent(tags);
if (appScheme) callAppScheme(appScheme);
props.onClick?.(e);
}}
{...props}
>
{children}
</button>
);
}
<Button
variant={selected ? 'solid-round' : 'outline-round'}
size={'xsmall'}
color={'primary'}
gaEventKey={'header'}
gaEventSubKey={'navigation'}
gaTags={['사이드바', '테스트버튼']}
appScheme={'testButton'}
>
테스트 버튼
</Button>
❗️여기서 잠깐
요구사항이 추가될 때 마다 공통 컴포넌트를 건드리는게 맞나? 라는 의구심이 들기 시작하였고, 동시에 나는 관심사 분리라는 키워드에 꽂히게 되었다.
그리고 그 관점에서 코드를 보니 코드에서 구린내가 폴폴 났다.
그 고민을 슬쩍 던져보았는데, 동료로 부터 토스 테크 블로그의 글을 하나 공유받게 되었다.
https://toss.tech/article/engineering-note-5
프론트엔드 로깅 신경 안 쓰기
프론트엔드 개발자라면 한 번쯤 고민해봤을 클라이언트 로깅 개선 과정을 공유합니다.
toss.tech

결말 (최종 완성 코드)
function GAEventTag({
children,
mainKey,
subKey,
tags,
}: Props) {
const logEvent = useGA(mainKey, subKey); // GA custom hook
const child = Children.only(children);
const handleClick = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
logEvent(...tags);
const originalOnClick = child.props?.onClick;
if (typeof originalOnClick === 'function') {
originalOnClick(event);
}
},
[logEvent, tags, child.props?.onClick],
);
return cloneElement(child, {
onClick: handleClick,
});
}
<GAEventTag
gaEventKey={'header'}
gaEventSubKey={'navigation'}
gaTags={['사이드바', '테스트버튼']}
>
<Button
variant={selected ? 'solid-round' : 'outline-round'}
size={'xsmall'}
color={'primary'}
>
테스트 버튼
</Button>
</GAEventTag>
간략하게 설명하자면... GAEventTag 컴포넌트로 관심사 분리를 하였다.
사용방법은 GAEventTag 컴포넌트로 버튼을 감싸서 사용하면 된다.
그러면 버튼을 클릭했을 때, GA 이벤트 로깅 동작이 작동 -> 버튼의 onClick 작동
요런 너낌으로 진행된다고 보면 된다.
GA 이벤트 로깅은 GAEventTag에서만 관리하면 되고,
Button은 UI Kit의 역할만 하게 되는.. 이 얼마나 깔끔한 코드인가
button 뿐만 아니라 a태그 등 이벤트 로깅이 필요한 곳 어디서든 아래와 같이 감싸주기만 하면 된다.
<GAEventTag
gaEventKey={'header'}
gaEventSubKey={'navigation'}
gaTags={['사이드바', '테스트버튼']}
>
<a href="/test">테스트</a>
</GAEventTag>
'Study > React, next.js' 카테고리의 다른 글
| [Next.js] public > images, fonts 트래픽(성능) 이슈 (Local fonts를 곁들인...) (0) | 2025.06.04 |
|---|---|
| [Next.js] Atomic을 지나 FSD를 거쳐서 태초마을로 오기까지 (2) | 2025.04.02 |
| [Next.js] export const dynamic = 'force-dynamic' (0) | 2025.02.04 |
| [Next.js] 사이드 프로젝트 초기 셋팅하기 (0) | 2024.05.27 |