흩어지는 횡단 관심사를 하나의 모듈로 관리하기

반복적으로 사용되는 소리 재생 로직을 횡단 관심사로 인식하고, 이를 단일 모듈로 분리하여 재사용성을 높이고 유지보수가 용이한 React 컴포넌트를 작성하는 방법을 소개합니다.

24. 07. 03. (14일 전)

소리 재생 로직의 문제점

Bendd 웹앱에는 조금 더 생동감 있는 경험을 제공하기 위해 소리를 활용하고 있습니다.

보통 사용자 인터랙션에 따라 소리를 재생하는 기능을 구현할 때, 흔히 다음과 같은 패턴을 사용합니다:

  1. 사용자의 소리 설정 확인
  2. useSound 훅을 사용하여 소리(ex: mp3) 에셋 로드
  3. 이벤트 핸들러(ex: onClick)에 소리를 재생하는 함수 추가
'use client';
 
import useSound from 'use-sound';
import { getSoundEnabled } from '../model/get-sound';
 
function TestSoundButton() {
  const isSoundEnabled = getSoundEnabled();
  const [playClickSound] = useSound('/sounds/test.mp3', {
    soundEnabled: isSoundEnabled,
  });
 
  const onClick = () => {
    playClickSound();
    alert('hello!');
  };
 
  return (
    <button onClick={onClick}>
      <span role="img" aria-label="trumpet">
        🎺
      </span>
    </button>
  );
}

이 방식은 소리 재생이 필요한 컴포넌트마다 반복되어 코드의 중복을 야기하고, 주요 로직과 소리 재생 로직이 혼재되어 가독성과 유지보수성을 저하시킵니다.

해결 방안: 횡단 관심사의 모듈화

이러한 문제를 해결하기 위해, 소리 재생 로직을 횡단 관심사(Cross-cutting concern)로 인식하고 별도의 모듈로 분리할 수 있습니다. React의 cloneElement API를 활용하여 기존 컴포넌트의 이벤트 핸들러를 확장하는 방식으로 구현합니다.

WithSound 컴포넌트 구현

'use client';
 
import { Children, cloneElement, isValidElement, type ReactNode } from 'react';
import useSound from 'use-sound';
 
import { useSoundStore } from '../model/sound-store';
 
type WithSoundProps = {
  children: ReactNode;
  assetPath: string;
};
 
export function WithSound({ children, assetPath }: WithSoundProps) {
  const child = Children.only(children);
  const isSoundEnabled = useSoundStore(state => state.isSoundEnabled);
  const [playClickSound] = useSound(assetPath, {
    soundEnabled: isSoundEnabled,
  });
 
  const processChild = (child: ReactNode): ReactNode => {
    if (isValidElement(child)) {
      return cloneElement(child, {
        onClick: (event: React.MouseEvent) => {
          if (child.props.onClick) {
            child.props.onClick(event);
          }
          playClickSound();
        },
      });
    }
    return child;
  };
 
  return processChild(child);
}

이 컴포넌트는 다음과 같은 장점을 제공합니다:

  1. 소리 재생 로직을 중앙화하여 코드 중복을 제거
  2. 기존 컴포넌트의 로직을 유지하면서 소리 재생 기능을 쉽게 추가
  3. 소리 설정 관리를 단일 위치에서 처리

WithSound 사용 예시

function TestSoundButton() {
  return (
    <WithSound assetPath="/sounds/test.mp3">
      <button onClick={() => alert('hello!')}>
        <span role="img" aria-label="trumpet">
          🎺
        </span>
      </button>
    </WithSound>
  );
}

결론

이렇게 횡단 관심사를 별도의 모듈로 분리함으로써 다음과 같은 이점을 얻을 수 있습니다:

  1. 핵심 비즈니스 로직과 부가 기능의 명확한 분리
  2. 유지보수성 향상
  3. 기능 확장 및 변경 용이

또 소리 재생뿐만 아니라 로깅, 성능 측정 등 다양한 횡단 관심사에도 적용할 수 있습니다.