[React] 관심사 분리하기

2025. 7. 16. 14:56Study/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>

 

반응형