use
훅이라는 결과물은 놀랍도록 간결했지만, 그곳에 도달하기까지의 과정은 결코 순탄치 않았다. 그것은 수년간 누적된 개발자들의 고통과, 그 고통을 해결하려는 React Core Team의 집요한 고민이 응축된 결과물이었다.
모든 것의 시작은 useEffect
였다.
훅이 처음 도입되었을 때, useEffect
는 클래스 컴포넌트의 복잡한 생명주기 메서드들을 대체하는 구원자처럼 여겨졌다. 하지만 비동기 데이터를 가져오는 작업에 사용되면서, useEffect
는 점차 새로운 복잡성의 온상이 되어갔다.
로렌 탄은 팀 회의에서 useEffect
를 사용한 데이터 페칭 코드의 문제점들을 조목조목 짚었다.
“첫째, 경쟁 상태(Race Condition) 문제입니다.”
그녀는 사용자가 검색어를 빠르게 연속으로 입력하는 시나리오를 예로 들었다.
“사용자가 ‘react’를 입력하고, 곧바로 ‘react 19’를 입력했다고 가정합시다. 두 개의 API 요청이 거의 동시에 출발합니다. 만약 네트워크 상황 때문에 ‘react’에 대한 응답이 ‘react 19’에 대한 응답보다 늦게 도착하면 어떻게 될까요? 화면에는 오래된 검색 결과가 최신 결과를 덮어쓰게 됩니다.”
개발자들은 이 문제를 해결하기 위해 useEffect
의 클린업(cleanup) 함수 안에서 요청을 취소하거나, 요청이 유효한지 확인하는 플래그 변수를 사용하는 등 복잡한 방어 코드를 매번 작성해야 했다.
“둘째, 의존성 배열(Dependency Array)의 함정입니다.”
그녀는 useEffect
의 두 번째 인자인 배열을 가리켰다. “개발자들은 이 배열에 무엇을 넣어야 할지 항상 헷갈려 합니다. 배열을 비워두면 데이터가 갱신되지 않고, 필요한 모든 값을 넣지 않으면 린터가 경고를 띄우죠. 이로 인해 불필요한 리렌더링이 발생하는 경우가 비일비재합니다.”
이 외에도 isLoading
, error
상태를 수동으로 관리하는 번거로움, 서버 사이드 렌더링 환경과의 비호환성 등 useEffect
를 사용한 데이터 페칭은 수많은 함정을 내포하고 있었다.
“이건 너무 복잡해.”
로렌은 단호하게 말했다. 그녀의 목소리에는 좌절감과 함께, 더 나은 해결책을 향한 갈망이 담겨 있었다.
“우리는 지금 구멍 난 댐을 손가락으로 막고 있는 것과 같아요. useEffect
는 애초에 이런 종류의 비동기 작업을 위해 설계된 도구가 아닙니다. 우리에게는 더 근본적인 해결책이 필요합니다.”
바로 그 순간, use
훅의 초기 아이디어가 싹텄다.
팀의 논의는 ‘만약 React가 Promise의 상태를 직접 이해할 수 있다면?’이라는 질문으로 옮겨갔다. 만약 컴포넌트가 렌더링 중에 Promise를 만나면, React가 그 Promise가 해결될 때까지 렌더링을 ‘멈추고’, 해결된 후에 그 지점부터 ‘다시 시작’할 수 있다면 어떨까?
이것은 useEffect
처럼 렌더링이 ‘끝난 후’에 부수 효과를 실행하는 방식과는 완전히 다른 접근이었다. 렌더링 과정 ‘중에’ 비동기 작업이 자연스럽게 통합되는 방식이었다.
그 아이디어를 실현하기 위한 구체적인 인터페이스가 바로 use
훅이었다. use
는 React에게 “나는 이 Promise의 결과가 필요하니, 준비될 때까지 렌더링을 잠시 멈춰줘”라는 신호를 보내는 약속의 증표였다.
use
의 탄생은 단순히 또 하나의 훅을 추가한 것이 아니었다. 그것은 useEffect
라는 잘못 사용되던 도구에 대한 깊은 반성이자, 비동기 데이터 페칭이라는 웹 개발의 본질적인 문제를 해결하기 위한 React의 공식적인 대답이었다. 복잡한 패턴과 함정으로 가득했던 길을 버리고, 가장 직관적이고 안전한 새로운 길을 제시하는, 패러다임의 전환이었다.