래퍼 지옥을 탈출하라 (Render Props)

72025년 08월 22일4

래퍼 지옥(Wrapper Hell).
이 단어는 페이스북 리액트 팀의 회의실에 무겁게 내려앉아 있었다. HOC는 로직 재사용이라는 문을 열었지만, 그 대가로 코드의 구조를 복잡한 미로로 만들었다. 디버깅은 고통스러웠고, 데이터의 흐름은 안갯속처럼 불투명했다.

“밖에서 감싸는 게 문제라면, 안에서 꺼내 쓰게 하면 어떨까요?”

침묵을 깬 것은 소피 알퍼트(Sophie Alpert)였다. 그녀는 언제나 개발자의 실질적인 고통에 집중하는, 날카로운 통찰력을 지닌 엔지니어였다.

“HOC의 근본적인 문제는 데이터가 암묵적으로 주입된다는 겁니다. props가 어디서 오는지 알려면 컴포넌트 트리를 거슬러 올라가야 하죠. 이걸 투명하게 만들 순 없을까요?”

그녀의 아이디어는 발상의 전환이었다.
상태 로직을 가진 컴포넌트가 직접 UI를 그리는 대신, 자신의 상태를 인자로 넘겨주는 ‘함수’를 실행하게 만드는 것. 그리고 그 함수가 실제 UI 렌더링을 책임지는 방식이었다.

이해를 돕기 위해, 한 개발자가 화이트보드에 간단한 예시를 그리기 시작했다. 이번엔 마우스의 현재 위치를 추적하는 로직을 재사용하는 시나리오였다.

먼저, Mouse라는 이름의 컴포넌트를 만든다. 이 컴포넌트의 유일한 책임은 마우스의 x, y 좌표를 추적하여 자신의 state에 저장하는 것이다.

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.state = { x: 0, y: 0 };
  }

  // 마우스 움직임을 감지해 state를 업데이트하는 로직
  handleMouseMove = (event) => {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  };

  render() {
    // 중요한 부분! 아무것도 직접 렌더링하지 않는다.
    // 대신, this.props.render 라는 함수를 호출한다.
    // 그리고 자신의 state를 그 함수에 인자로 넘겨준다.
    return this.props.render(this.state);
  }
}

Mouse 컴포넌트의 render 메서드는 어떤 HTML 태그도 반환하지 않았다. 그 대신, props로 전달받은 render라는 이름의 함수를 호출하고, 자신의 상태 { x: 0, y: 0 }를 통째로 넘겨주었다. 이것이 바로 ‘렌더 프롭(Render Prop)’ 패턴의 심장이었다. Mouse 컴포넌트는 ‘무엇을’ 그릴지 결정하지 않았다. 오직 ‘언제’, ‘어떤 데이터와 함께’ 렌더링 함수를 실행할지만 결정했다.

이제 이 Mouse 컴포넌트를 사용하는 쪽의 코드는 다음과 같았다.

class App extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <p>The current mouse position is ({mouse.x}, {mouse.y})</p>
        )}/>
      </div>
    );
  }
}

마법 같은 일이 일어났다.
<Mouse> 컴포넌트는 render라는 이름의 prop을 받았고, 그 prop의 값은 UI를 반환하는 함수였다. Mouse 컴포넌트는 자신의 최신 좌표값을 mouse라는 이름으로 이 함수에 전달했고, 개발자는 그 데이터를 받아 원하는 UI를 마음껏 그릴 수 있었다.

회의실의 공기가 달라졌다. 팀원들은 이 패턴의 장점을 즉시 알아차렸다.

첫째, 래퍼 지옥이 사라졌다. 컴포넌트 트리는 <Mouse>라는 단 하나의 컴포넌트만 보여줄 뿐, 불필요한 중첩이 없었다.
둘째, 데이터의 출처가 명확했다. mouse라는 데이터는 바로 눈앞에 보이는 render={mouse => ...} 에서 온다는 사실이 너무나도 명백했다. 더 이상 데이터의 근원을 찾아 헤맬 필요가 없었다.
셋째, props 이름 충돌 문제가 원천적으로 해결되었다. 데이터를 받는 쪽에서 mouse든, position이든 원하는 변수명을 자유롭게 정할 수 있었다.

이것은 HOC보다 훨씬 정직하고 투명한 방식이었다. 로직의 공유와 렌더링의 제어를 완벽하게 분리해냈다.

댄은 희망을 보았다.
HOC의 망령에서 벗어날 진정한 탈출구가 드디어 모습을 드러낸 것 같았다. 커뮤니티 역시 렌더 프롭 패턴의 우아함에 열광하기 시작했다.

하지만 이 새로운 패턴 역시, 개발자들을 또 다른 형태의 미로로 이끌 것이라는 사실을, 그들은 아직 알지 못했다. 그 미로의 이름은 ‘피라미드’였다.