훅의 시대가 열렸지만, 클래스 컴포넌트는 여전히 한 가지 독점적인 능력을 가지고 있었다. 그것은 리액트 애플리케이션의 안정성을 지키는 최후의 보루, ‘에러 바운더리(Error Boundary)’를 만드는 능력이었다.
에러 바운더리는 자신의 자식 컴포넌트 트리에서 발생하는 자바스크립트 에러를 감지하여, 전체 애플리케이션이 멈추는 대신 우아한 대체 UI(fallback UI)를 보여주는 특별한 컴포넌트다. 만약 채팅 위젯에서 에러가 발생하더라도, 뉴스피드나 다른 부분은 정상적으로 계속 작동하게 만드는 방화벽과도 같은 존재였다.
이 에러 바운더리를 만들기 위해서는, 두 개의 특별한 생명주기 메서드가 반드시 필요했다.
static getDerivedStateFromError(error): 렌더링 단계에서 에러가 발생했을 때 호출된다. 이 메서드는 대체 UI를 렌더링하기 위해 상태를 업데이트하는 역할을 한다.componentDidCatch(error, errorInfo): 에러가 커밋된 후에 호출된다. 주로 에러 정보를 로깅 서버로 보내는 등의 부수 효과를 처리하는 데 사용된다.
문제는, 이 두 메서드에 해당하는 훅(Hook)이 존재하지 않는다는 것이었다.
어느 날 팀 회의에서, 소피가 이 문제를 다시 한번 상기시켰다.
“개발자들이 계속해서 묻고 있습니다. ‘훅만으로 에러 바운더리를 만들 수 없나요?’ 라고요. 현재로서는 ‘아니요, 그 부분은 여전히 클래스 컴포넌트를 사용해야 합니다’라고 답할 수밖에 없는 상황입니다.”
그녀의 지적은 정확했다.
리액트 팀은 의도적으로 이 두 생명주기에 해당하는 훅을 만들지 않았다.
여기에는 기술적인, 그리고 철학적인 이유가 복합적으로 얽혀 있었다.
getDerivedStateFromError는 렌더링 단계 중에 호출되는데, 훅은 렌더링이 모두 끝난 후에 실행되는 useEffect를 제외하고는 렌더링 과정 자체에 개입하는 것을 최소화하도록 설계되었다.
componentDidCatch는 에러를 ‘잡는’ 행위 자체가, 일반적인 데이터 흐름과는 다른 예외적인 상황 처리 로직이었다. 리액트 팀은 이 중요한 에러 처리 로직이, 일반적인 상태 관리 훅과 섞이는 것을 원치 않았다.
댄이 설명했다.
“에러 바운더리는 그 자체로 매우 특수하고 중요한 역할을 합니다. 자주 바뀌는 로직이 아니며, 애플리케이션의 몇몇 주요 지점에만 전략적으로 배치되죠. 이 역할만큼은, 상태와 로직이 명확하게 분리된 클래스 컴포넌트가 더 적합하다고 판단했습니다.”
그의 말은 훅이 만능이 아니라는 사실을 인정하는 것이었다.
모든 문제를 하나의 망치(훅)로 해결하려 해서는 안 된다는, 실용적인 접근 방식이었다.
결과적으로, 훅 시대의 리액트 애플리케이션은 흥미로운 하이브리드 구조를 갖게 되었다.
애플리케이션의 99%는 간결하고 유연한 함수형 컴포넌트와 훅으로 구성되었다.
그리고 그 위를, 마치 성벽처럼, 몇 개의 견고한 클래스 기반 에러 바운더리가 감싸며 내부의 컴포넌트들을 보호했다.
이 작은 ‘풀지 못한 숙제’는 개발자들에게 중요한 교훈을 주었다.
새로운 기술을 맹목적으로 추종하는 대신, 각 도구(클래스와 훅)가 가진 장점과 한계를 이해하고, 문제에 맞는 적절한 도구를 선택하는 것이 진정한 엔지니어링이라는 사실을 말이다.
훅은 세상을 바꾸었지만, 모든 것을 바꾸지는 않았다.
그리고 이 현실적인 한계에 대한 인정은, 오히려 리액트 생태계를 더욱 건강하고 성숙하게 만들었다.
하지만 리액트 팀의 진짜 야망은 단순히 클래스의 문제점을 해결하는 데서 그치지 않았다.
훅은 사실, 그들이 그리고 있던 훨씬 더 거대한 그림을 위한, 필수적인 첫 번째 단계에 불과했다.
세바스티안은 마침내, 훅을 만들어야만 했던 진짜 이유를 팀에 공개할 준비를 하고 있었다.


