커스텀 훅은 리액트 hooks들을 한데 모은 것이다. 즉 사용자가 원하는 로직을 실행시키는 함수와 비슷하다. 단지 그 동작들을 리액트 hooks가 해줄 뿐이다.
커스텀 훅을 왜 사용할까? 그냥 컴포넌트 내부에서 useState나 useEffect 등을 사용해도 차이가 없을 텐데 말이다.
맞는 말이다. 결국 같은 동작을 할 테지만 좋은 프로그래밍은 결과만으로 이루어질 수 없다.
코드의 재사용성을 높이고 추상화 계층을 제공하여 내가 봐도 좋고 남들이 봐도 직관적인 코드를 구현해야한다.
종합하자면 커스텀 훅은 반복되는 코드를 재사용할 수 있도록 바꾸고, 구체적인 구현은 숨기고 추상화된 인터페이스를 제공하여 프로그래밍을 용이하게 하는데 그 목적이 있다고 생각한다.
다음과 같이 하나의 버튼으로 두 개의 카운터를 관리하고 싶다. 매우 간단한 프로그램이고 코드의 구성 또한 간단하다.
function App() {
const [counter0, setCounter0] = useState(5);
const [counter1, setCounter1] = useState(3);
const handleChange0 = (value) => {
setCounter0(value);
};
const handleChange1 = (value) => {
setCounter1(value);
};
return (
<div className='App'>
<div>{counter0}</div>
<div>{counter1}</div>
<button
onClick={() => {
handleChange0(counter0 + 1);
handleChange1(counter1 + 1);
}}
>
+
</button>
<button
onClick={() => {
handleChange0(counter0 - 1);
handleChange1(counter1 - 1);
}}
>
-
</button>
</div>
);
}
한눈에 봐도 아름답지 못한 코드다. 제일 먼저 보이는 개선점을 꼽으라면 반복되는 표현을 줄여야한다는 점이다.
커스텀 훅을 이용해 해결해보자.
const useCounter = (initialValue) => {
const [count, setCount] = useState(initialValue || 0);
const handleChange = (value) => {
setCount(value);
};
return [count, handleChange];
};
function App() {
const [counter0, handleChange0] = useCounter(5);
const [counter1, handleChange1] = useCounter(3);
return (
<div className='App'>
<div>{counter0}</div>
<div>{counter1}</div>
<button
onClick={() => {
handleChange0(counter0 + 1);
handleChange1(counter1 + 1);
}}
>
+
</button>
<button
onClick={() => {
handleChange0(counter0 - 1);
handleChange1(counter1 - 1);
}}
>
-
</button>
</div>
);
}
커스텀 훅을 정의하는 부분이 추가됐지만 App 컴포넌트 내부만 봤을 땐 조금 더 깔끔해졌다.
counter가 추가된다면 커스텀 훅을 재사용하면 되니 수정도 간편하다.
빈약한 예시를 적어서 가슴을 울리지는 못했다. 상상력의 한계가 드러난다.
지금은 간단한 카운터지만 effect 훅, 비동기 처리 등을 하는 로직을 상상해보면 커스텀 훅의 위력이 더욱 강해진다는 것을 예상할 수 있다.
커스텀 훅을 이용해 반복되는 작업을 피하고 보기 좋은 코드를 만들자.
const useCounter = (initialValue) => {
const [count, setCount] = useState(initialValue || 0);
const handleChange = (value) => {
setCount(value);
};
return [count, handleChange];
};
function counterSetter(...args) {
const counters = [];
const handlers = [];
args.forEach((item) => {
const [counter, handler] = useCounter(item);
counters.push(counter);
handlers.push(handler);
});
return [counters, handlers];
}
function App() {
const [counters, handlers] = counterSetter(1, 2, 3, 4, 5);
return (
<div className='App'>
{counters.map((item) => (
<div>{item}</div>
))}
<button
onClick={() => {
handlers.forEach((handler, index) => handler(counters[index] + 1));
}}
>
+
</button>
<button
onClick={() => {
handlers.forEach((handler, index) => handler(counters[index] - 1));
}}
>
-
</button>
</div>
);
}
다음과 같이 다양한 상황에 대응하는 로직을 구현할 수도 있다. 🙉