소리 재생 로직의 문제점
Bendd 웹앱에는 조금 더 생동감 있는 경험을 제공하기 위해 소리를 활용하고 있습니다.
보통 사용자 인터랙션에 따라 소리를 재생하는 기능을 구현할 때, 흔히 다음과 같은 패턴을 사용합니다:
- 사용자의 소리 설정 확인
useSound
훅을 사용하여 소리(ex: mp3) 에셋 로드- 이벤트 핸들러(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);
}
이 컴포넌트는 다음과 같은 장점을 제공합니다:
- 소리 재생 로직을 중앙화하여 코드 중복을 제거
- 기존 컴포넌트의 로직을 유지하면서 소리 재생 기능을 쉽게 추가
- 소리 설정 관리를 단일 위치에서 처리
WithSound 사용 예시
function TestSoundButton() {
return (
<WithSound assetPath="/sounds/test.mp3">
<button onClick={() => alert('hello!')}>
<span role="img" aria-label="trumpet">
🎺
</span>
</button>
</WithSound>
);
}
결론
이렇게 횡단 관심사를 별도의 모듈로 분리함으로써 다음과 같은 이점을 얻을 수 있습니다:
- 핵심 비즈니스 로직과 부가 기능의 명확한 분리
- 유지보수성 향상
- 기능 확장 및 변경 용이
또 소리 재생뿐만 아니라 로깅, 성능 측정 등 다양한 횡단 관심사에도 적용할 수 있습니다.