회사에서 DownloadModal
을 구현해야 했다. 이 컴포넌트는 다음 세가지 상태를 가지고 있다.
[디자인 파일. 혹시 몰라서 모자이크 처리..]
그리고 각 상태마다 보여지는 UI가 달랐다. (다운로드 중과 다운로드 후는 전체적으로 비슷했다.)
상태 기반 웹 프레임워크인 리액트를 사용하고 있었으므로 우선 다음과 같이 구현을 했다. (다운로드 중 상태와 다은로드 완료 상태가 왜 각기 다른 곳에서 받고 있는지는 신경쓰지말자…)
function DownloadModal(props) {
const [isDownloading, setIsDownloading] = useState(false);
const {
isDone: isDownloaded,
} = useProgressBar();
return (
{!isDownloading
? <View>before download...</View>
: !isDownloaded
? <View>downloading...</View>
: <View>downloaded...</View>;}
);
}
어차피 다운로드 중 UI와 다운로드 후 UI는 유사했으며, JSX에서 삼항 연산자를 두개나 중첩하는게 싫어서 다음과 같이 수정했다. (지금 보니 딱히 삼항연산자를 줄이진 않았네.. 😅)
function DownloadModal(props) {
// ...
return (
<View>
{!isDownloading ?
<View>before download...</View> :
<View>
{!isDownloaded ?
<View>downloading...</View> :
<View>downloaded...</View>
}
</View>
}
</View>
);
}
이제 다운로드 중 UI와 다운로드 후 UI의 세세한 설정을 위해 상태 상수 맵을 함수 컴포넌트 밖에 선언한 후, useMemo
를 이용해서 현재 상태인 currentState
를 유지하도록 했다. 그리고 세부 스타일 객체는 currentState
에 의존하도록 했다.
// 상태 상수 맵
const state = {
DOWNLOAD_OPTION: 0,
DOWNLOADING: 1,
DOWNLOADED: 2,
};
function DownloadModal(props) {
// 현재 상태
const currentState = useMemo(() => {
return !isDownloading
? state.DOWNLOAD_OPTION
: !isDownloaded
? state.DOWNLOADING
: state.DOWNLOADED;
}, [isDownloaded, isDownloading]);
// 현재 상태에 따라 달라지는 모달 제목
const modalTitle = useMemo(() => {
switch (currentState) {
case state.DOWNLOAD_OPTION:
return '다운로드 옵션';
case state.DOWNLOADING:
return '다운로드 중...';
case state.DOWNLOADED:
return '다운로드 완료!';
}
}, [currentState]);
// 현재 상태에 따라 달라지는 진행 텍스트 스타일
const progressTextStyle = useMemo(() => {
switch (currentState) {
case state.DOWNLOAD_OPTION:
case state.DOWNLOADING:
return {color: color.palette.t_black};
case state.DOWNLOADED:
return {color: color.palette.t_primary_1};
}
}, [currentState]);
return (
<View>
<Text>{modalTitle}</Text>
{!isDownloading ?
<View>before download...</View> :
<View>
<SomeComponent/>
<View style={progressTextStyle}>downloaded...</View>
</View>
}
</View>
);
}
modalTitle
과 progressTextStyle
은 똑같은 로직을 사용하고 있다. 중복을 제거하기 위해 다음과 같이 리팩토링했다.
function DownloadModal(props) {
...
function doSomethingByCurrentState({
currentState,
doWhenBeforeDownload,
doWhenDownloading,
doWhenAfterDownload,
}) {
switch (currentState) {
case state.DOWNLOAD_OPTION:
return doWhenBeforeDownload();
case state.DOWNLOADING:
return doWhenDownloading();
case state.DOWNLOADED:
return doWhenAfterDownload();
}
}
const modalTitle = useMemo(
() =>
doSomethingByCurrentState({
currentState,
doWhenBeforeDownload: () => '다운로드 옵션',
doWhenDownloading: () => '다운로드 중...',
doWhenAfterDownload: () => '다운로드 완료!',
}),
[currentState],
);
const progressTextStyle = useMemo(
() =>
doSomethingByCurrentState({
currentState,
doWhenBeforeDownload: () => ({color: color.palette.t_black}),
doWhenDownloading: () => ({color: color.palette.t_black}),
doWhenAfterDownload: () => ({
color: color.palette.t_primary_1,
}),
}),
[currentState],
);
return (
<View>
...
</View>
);
}
doSomethingByCurrentState
라는 새로운 순수 함수를 추가하고, modalTitle
과 progressTextStyle
은 doSomethingByCurrentState
의 반환값이 되도록 했다.
추가로, 해당 로직을 커스텀 훅으로 추상화를 할 수 있다.
import {useState} from 'react';
const state = {
FOO: 'FOO',
BAR: 'BAR',
...
};
export function usePatternMatching(stateList) {
const doSomethingByCurrentState = ({
currentState,
doWhenFoo,
doWhenBar,
...
}) => {
switch (currentState) {
case state.FOO:
return doWhenFoo();
case state.BAR:
return doWhenBar();
..
}
};
return {
doSomethingByCurrentState,
};
}
위와 같이 추상화를 이용해서 중복을 제거하고, OC 원칙도 지켰다. (인자 및 case만 추가하면 된다) 또한 useMemo
를 사용했으므로 성능 최적화도 챙겼다.
다만 이 방식은 커스텀 훅과, 커스텀 훅을 사용하는 컴포넌트가 서로 state
를 알고 있어야 한다는 단점이 있다. (FOO
나 BAR
같은..)
FE 스터디에서 나온 내용인데, 이런 것을 Pattern Matching이라고 한다. 패턴 매칭은 하스켈 같은 함수형 언어에서 사용하는 패턴이라고 한다. 진짜 switch case문을 선언적으로 표현하는 것이다. 현재 JS의 패턴 매칭이 proposal 단계라서, 라이브러리를 사용해야 한다. 대표적인 라이브러리로는 다음과 같은 것들이 있다.