useState
와 useEffect
라는 두 개의 강력한 도구를 손에 쥔 리액트 팀은, 마침내 그들이 출발했던 최초의 문제로 돌아왔다. 바로 ‘상태 로직의 재사용’이었다.
어느 날 오후, 한 개발자가 반응형 디자인을 위한 컴포넌트를 만들고 있었다. 그는 브라우저 창의 너비(width)가 바뀔 때마다, 그 값을 감지하여 화면의 레이아웃을 다르게 보여주고 싶었다.
그는 새로 배운 훅을 사용하여 코드를 작성했다.
function ResponsiveComponent() {
// 1. 창 너비를 저장할 상태를 만든다.
const [width, setWidth] = useState(window.innerWidth);
// 2. 창 크기 변경을 감지하는 효과를 만든다.
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
// 이벤트 리스너를 등록한다.
window.addEventListener('resize', handleResize);
// 컴포넌트가 사라질 때 리스너를 정리한다.
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 이 효과는 처음 한 번만 실행되면 된다.
return (
<div>
<p>Current window width: {width}px</p>
{width > 600 ? <p>Desktop Layout</p> : <p>Mobile Layout</p>}
</div>
);
}
코드는 완벽하게 작동했다.
useState
로 현재 너비를 상태로 관리하고, useEffect
로 resize
이벤트를 구독하고 해지했다. 모든 로직이 명확하고 간결했다.
문제는 다른 개발자가 또 다른 컴포넌트에서 똑같은 기능을 필요로 했을 때 발생했다.
Header
컴포넌트도 창 너비에 따라 메뉴 버튼을 보여주거나 숨겨야 했고, Sidebar
컴포넌트도 너비에 따라 접히거나 펼쳐져야 했다.
결국, 그들은 ResponsiveComponent
에 있던 useState
와 useEffect
코드를 그대로 복사해서, Header
와 Sidebar
컴포넌트에 각각 붙여넣어야 했다.
소프트웨어 공학의 제1원칙, ‘반복하지 말라(DRY)’가 또다시 깨지는 순간이었다.
“결국 우리는 다시 원점으로 돌아왔군요.”
소피가 씁쓸하게 말했다.
“훅이 코드를 보기 좋게 만들어준 것은 사실이지만, 이 로직 덩어리를 재사용할 방법이 없다면 HOC나 렌더 프롭 시절과 근본적으로 다를 게 없습니다.”
회의실에 잠시 침묵이 흘렀다.
그때, 댄의 머릿속에 아주 단순하면서도 대담한 아이디어가 스쳐 지나갔다.
그는 키보드를 가져와, ResponsiveComponent
의 코드를 수정하기 시작했다.
그는 useState
와 useEffect
로직 덩어리를 그대로 잘라내어, 컴포넌트 바깥의 새로운 함수 안으로 옮겼다.
// 창 너비를 추적하는 로직을 별도의 함수로 분리한다.
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// 그리고 최종적으로 계산된 너비 값을 반환한다.
return width;
}
새로 만들어진 useWindowWidth
함수는 useState
와 useEffect
를 호출하고, 그 결과물인 width
값을 반환했다. 지극히 평범한 자바스크립트 함수처럼 보였다.
그리고 그는 원래의 ResponsiveComponent
를 아주 간단하게 수정했다.
function ResponsiveComponent() {
const width = useWindowWidth(); // 방금 만든 함수를 호출한다.
return (
<div>
<p>Current window width: {width}px</p>
{/* ... */}
</div>
);
}
이론상으로는 말이 되지 않는 코드였다.
useState
와 useEffect
는 리액트 컴포넌트의 컨텍스트 안에서만 호출되어야 했다. useWindowWidth
는 컴포넌트가 아닌, 그냥 일반 함수였다.
하지만 댄은 useState
가 ‘호출 순서’에 의존한다는 사실을 떠올렸다.
만약 리액트가 컴포넌트 렌더링을 시작하고, ResponsiveComponent
가 useWindowWidth
를 호출하고, 그 안에서 useState
가 호출된다면?
리액트 입장에서는 ResponsiveComponent
가 직접 useState
를 호출한 것과 순서상 아무런 차이가 없었다.
그는 떨리는 마음으로 코드를 실행했다.
브라우저 화면에 ‘Current window width:’라는 문구와 함께 현재 창의 너비가 정확하게 표시되었다. 그가 브라우저 창의 크기를 조절하자, 숫자가 실시간으로 부드럽게 바뀌었다.
작동했다.
평범한 함수 안으로 옮겨진 useState
와 useEffect
가, 마치 마법처럼 원래의 컴포넌트와 연결되어 살아 움직이고 있었다.
회의실의 모두가 숨을 죽였다.
그들은 방금, 리액트 역사상 가장 위대한 발견 중 하나가 탄생하는 순간을 목격하고 있었다.
이것은 단순한 리팩토링이 아니었다.
이것은 HOC와 렌더 프롭의 시대를 끝낼, 새로운 패러다임의 발견이었다.