같은 로직을 여러 컴포넌트에서 쓰고 싶을 때, 복붙 말고 어떤 방법이 있을까요?

React에서 "로직을 재사용하는 방법"은 시대에 따라 계속 바뀌어 왔습니다. Mixins에서 시작해 HOC, Render Props를 거쳐 현재의 Hooks까지. 각 패턴이 왜 등장했고, 어떤 문제를 해결했으며, 왜 다음 패턴에 자리를 내줬는지 정리하겠습니다.

Mixins — 최초의 로직 공유 시도

React 초기에는 createClass와 함께 Mixin을 사용했습니다.

JS
// 더 이상 사용되지 않는 패턴
const TimerMixin = {
  componentDidMount() {
    this.timer = setInterval(this.tick, 1000);
  },
  componentWillUnmount() {
    clearInterval(this.timer);
  },
};

const Clock = React.createClass({
  mixins: [TimerMixin],
  tick() {
    this.setState({ time: Date.now() });
  },
  render() {
    return <div>{this.state.time}</div>;
  },
});

Mixin의 문제점

  • **이름 충돌 **: 여러 Mixin이 같은 이름의 메서드나 state를 정의하면 충돌합니다
  • ** 암묵적 의존성 **: Mixin이 this.statethis.props에 의존하지만 명시적이지 않습니다
  • ** 눈덩이 복잡성 **: Mixin끼리 의존하기 시작하면 어떤 Mixin이 어떤 상태를 변경하는지 추적이 불가능해집니다

결국 React 팀은 Mixin을 공식적으로 폐기하고, ES6 class에서는 아예 Mixin을 지원하지 않았습니다.

HOC (Higher-Order Component) — 합성으로 해결하기

HOC는 컴포넌트를 인자로 받아 새 컴포넌트를 반환하는 함수입니다.

JSX
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { user, loading } = useAuth();

    if (loading) return <Spinner />;
    if (!user) return <Navigate to="/login" />;

    return <WrappedComponent {...props} user={user} />;
  };
}

// 사용
const ProtectedDashboard = withAuth(Dashboard);

HOC의 장점

  • Mixin과 달리 ** 합성(composition)** 기반이라 이름 충돌이 없습니다
  • 렌더링 자체를 가로채거나 조건부로 변경할 수 있습니다
  • 관심사를 명확하게 분리할 수 있습니다

HOC의 문제점

JSX
// Wrapper Hell - 디버깅이 지옥
export default withRouter(
  withTheme(
    withAuth(
      withLogging(MyComponent)
    )
  )
);
  • Wrapper Hell: HOC를 여러 개 중첩하면 컴포넌트 트리가 깊어집니다
  • **Props 충돌 **: 여러 HOC가 같은 이름의 prop을 주입하면 덮어쓰기 됩니다
  • ** 정적 메서드 손실 **: 원래 컴포넌트의 static 메서드가 래퍼에 전달되지 않습니다
  • **Ref 전달 문제 **: React.forwardRef를 명시적으로 사용해야 합니다

Render Props — 제어의 역전

Render Props는 "무엇을 렌더링할지"를 함수 prop으로 위임하는 패턴입니다.

JSX
function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return render(position);
}

// 사용
<MouseTracker
  render={({ x, y }) => (
    <div>마우스 위치: {x}, {y}</div>
  )}
/>

Render Props의 장점

  • Props가 명시적이라 어떤 데이터가 전달되는지 명확합니다
  • HOC처럼 컴포넌트를 감싸지 않아도 됩니다

Render Props의 문제점

JSX
// Callback Hell과 비슷한 중첩
<Auth render={(user) => (
  <Theme render={(theme) => (
    <Language render={(lang) => (
      <MyComponent user={user} theme={theme} lang={lang} />
    )} />
  )} />
)} />
  • 중첩이 깊어지면 가독성이 급격히 떨어집니다
  • JSX 안에 로직이 섞여 코드가 복잡해집니다

Hooks — 현재의 표준

Hooks는 함수 컴포넌트 안에서 상태와 사이드 이펙트를 직접 사용할 수 있게 합니다.

JSX
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handleMove);
    return () => window.removeEventListener('mousemove', handleMove);
  }, []);

  return position;
}

// 사용 — 깔끔하게 로직 합성
function MyComponent() {
  const { x, y } = useMousePosition();
  const { user } = useAuth();
  const theme = useTheme();

  return <div style={{ color: theme.primary }}>
    {user.name}: {x}, {y}
  </div>;
}

Hooks가 해결한 것들

  • **Wrapper Hell 제거 **: 컴포넌트 트리에 추가 래퍼가 없습니다
  • ** 로직 합성이 자연스러움 **: 여러 Hook을 순서대로 호출하면 됩니다
  • ** 명시적 데이터 흐름 **: 어떤 값이 어디서 오는지 코드만 보면 알 수 있습니다
  • ** 테스트 용이 **: renderHook으로 로직만 독립 테스트가 가능합니다

커스텀 Hook 작성 패턴

JSX
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue];
}

커스텀 Hook을 만들 때 기억할 점:

  • 이름은 반드시 use로 시작합니다
  • 내부에서 다른 Hook을 자유롭게 호출할 수 있습니다
  • 반환값은 자유롭게 설계합니다 (값, 배열, 객체 등)

HOC가 여전히 유용한 경우

Hooks가 대부분의 경우 더 나은 선택이지만, HOC가 여전히 빛나는 순간이 있습니다.

1. 렌더링 가로채기

JSX
function withAuth(Component) {
  return function Protected(props) {
    const { user } = useAuth();
    // 렌더링 자체를 조건부로 변경
    if (!user) return <Navigate to="/login" />;
    return <Component {...props} />;
  };
}

커스텀 Hook은 JSX를 반환할 수 없기 때문에, 컴포넌트의 렌더링 자체를 가로채야 할 때는 HOC가 더 깔끔합니다.

2. 외부 라이브러리 통합

React Router의 이전 withRouter, Redux의 connect 등 라이브러리가 HOC 패턴을 제공하는 경우가 있습니다.

3. 크로스커팅 관심사

로깅, 에러 추적, 성능 측정처럼 컴포넌트 로직과 무관한 관심사를 주입할 때 유용합니다.

JSX
function withPerformanceTracking(Component, componentName) {
  return function Tracked(props) {
    useEffect(() => {
      performance.mark(`${componentName}-mount`);
      return () => performance.mark(`${componentName}-unmount`);
    }, []);

    return <Component {...props} />;
  };
}

패턴 비교 정리

특성MixinsHOCRender PropsHooks
로직 재사용OOOO
이름 충돌있음prop 충돌 가능없음없음
Wrapper 추가없음있음있음없음
타입 추론불가어려움가능쉬움
현재 권장X제한적제한적O

주의할 점

HOC의 Wrapper Hell과 DevTools 디버깅

HOC를 여러 겹 감싸면(withAuth(withTheme(withRouter(Component)))) React DevTools에서 컴포넌트 트리가 Wrapper로 가득 차 디버깅이 어렵습니다. displayName을 설정해도 근본적인 해결은 되지 않습니다.

Render Props의 콜백 지옥

Render Props를 중첩하면 JSX가 오른쪽으로 계속 들여쓰기되어 가독성이 급격히 떨어집니다. 커스텀 Hook이 같은 기능을 flat한 코드로 제공하므로, 대부분의 경우 Hook이 더 나은 선택입니다.

정리

패턴장점단점현재 상태
Mixins단순한 재사용이름 충돌, 암묵적 의존성폐기
HOC합성 기반Wrapper Hell, props 충돌제한적 사용
Render Props명시적중첩 시 가독성 저하제한적 사용
HooksWrapper 없이 합성훅 규칙 준수 필요** 현재 표준**

변천사를 한 줄로 요약하면 "암묵적에서 명시적으로, 컴포넌트 레벨에서 로직 레벨로"입니다. 새 프로젝트라면 커스텀 Hook을 기본 전략으로, HOC는 보조적으로 활용하는 것이 실용적입니다.

댓글 로딩 중...