새로운 기능 요구사항은 언제나처럼 갑작스럽게 찾아왔다.
“사용자가 보고 있는 뉴스피드 게시물이, 현재 화면에 보이는지 여부를 추적해야 합니다. 그리고 사용자의 언어 설정에 맞춰 게시물 내용을 번역하는 기능도 필요합니다.”
개발팀은 망설이지 않았다. 이미 그들에게는 HOC라는 강력한 무기가 있었다.
팀은 먼저 withVisibilityTracking
이라는 HOC를 만들었다. 이 HOC는 컴포넌트가 화면에 보이는지 여부를 계산해 isVisible
이라는 prop을 내려주었다.
이어서 withTranslation
이라는 HOC도 만들었다. 이 HOC는 사용자의 언어 설정 데이터를 가져와 translate
라는 번역 함수를 prop으로 제공했다.
이제 뉴스피드의 게시물을 나타내는 Post
컴포넌트에 이 두 가지 능력을 부여해야 했다. 개발자들은 자연스럽게 HOC를 연달아 적용했다.
// 먼저 번역 기능을 입히고,
const PostWithTranslation = withTranslation(Post);
// 그 다음에 화면 추적 기능을 입힌다.
const PostWithAllFeatures = withVisibilityTracking(PostWithTranslation);
// 최종적으로 사용될 컴포넌트는 PostWithAllFeatures가 된다.
export default PostWithAllFeatures;
코드는 여전히 간결했다. 논리적으로도 완벽했다.
각 HOC는 자신의 책임에만 집중했고, Post
컴포넌트는 아무것도 모른 채 그저 isVisible
prop과 translate
함수를 사용하기만 하면 되었다.
문제는 코드가 아니라, 코드가 만들어낸 결과물의 구조에서 발생했다.
댄은 버그를 추적하기 위해 리액트 개발자 도구를 열고, 렌더링된 컴포넌트 트리를 확인했다. 그의 눈이 가늘어졌다.
화면에는 Post
컴포넌트가 있어야 할 자리에, 기이한 형태의 중첩 구조가 나타나 있었다.
<withVisibilityTracking>
<withTranslation>
<Post>... // 실제 게시물 내용</Post>
</withTranslation>
</withVisibilityTracking>
HOC는 컴포넌트를 ‘변형’하는 것이 아니었다. 원본 컴포넌트를 새로운 컴포넌트로 ‘감싸는(wrapping)’ 방식이었다. 마치 선물을 포장하듯, HOC를 하나 적용할 때마다 새로운 포장지가 한 겹씩 씌워졌다.
이것이 끝이 아니었다. 어떤 컴포넌트는 사용자 데이터를 가져오는 withUserData
, 로깅을 위한 withLogging
, 테마 적용을 위한 withTheme
등 더 많은 HOC로 감싸여 있었다.
<withTheme>
<withLogging>
<withUserData>
<withVisibilityTracking>
<withTranslation>
<Post>...</Post>
</withTranslation>
</withVisibilityTracking>
</withUserData>
</withLogging>
</withTheme>
컴포넌트 트리는 의미를 알 수 없는 이름들로 끝없이 깊어졌다. 개발자들은 자신들이 찾고 있는 진짜 Post
컴포넌트가 어디에 있는지 한눈에 파악하기 어려웠다.
더 심각한 문제는 props
의 충돌이었다.
만약 withUserData
와 withVisibilityTracking
이 둘 다 data
라는 이름의 prop을 내부적으로 사용하고 있다면? 바깥쪽 HOC가 내려준 data
prop이 안쪽 HOC의 data
prop을 덮어써 버리는, 예측하기 힘든 버그가 발생했다. 개발자는 각 HOC가 어떤 이름의 prop을 사용하는지 내부 구현을 모두 들여다봐야만 했다.
가장 큰 혼란은 데이터의 흐름이 불투명하다는 점이었다.
Post
컴포넌트가 사용하는 isVisible
prop은 대체 어디서 온 것인가? 개발자는 컴포넌트 트리 구조를 위로, 위로 거슬러 올라가며 withVisibilityTracking
이라는 이름을 찾아내야만 했다. 암묵적인 계약에 의해 데이터가 주입되고 있었기에, 코드만 봐서는 데이터의 출처를 직관적으로 알 수 없었다.
댄과 동료들은 이 현상을 ‘래퍼 지옥(Wrapper Hell)’이라고 부르기 시작했다.
로직 재사용이라는 문제를 해결하기 위해 도입한 HOC가, 디버깅과 코드 추적을 불가능하게 만드는 또 다른 지옥을 만들어낸 것이었다.
HOC는 구원자가 아니었다.
그것은 문제를 다른 문제로 바꾼, 영리하지만 근본적인 해결책은 아닌 임시방편에 가까웠다.
리액트 커뮤니티의 또 다른 한쪽에서는 이 래퍼 지옥을 탈출하기 위한 새로운 패턴을 제시하고 있었다. 그것은 HOC와는 전혀 다른 접근 방식이었다. 컴포넌트를 밖에서 감싸는 대신, 안에서 제어하겠다는 아이디어.
그 패턴의 이름은 ‘렌더 프롭(Render Prop)’이었다. 댄은 이 새로운 대안에 희망을 걸어보기로 했다.