본문 바로가기
Front-End/React

리액트 디자인 패턴 - 상태 끌어올리기와 렌더링 Props 패턴

by jakejeong 2025. 5. 5.

리액트의 상태 관리: 상태 끌어올리기의 한계와 렌더 프롭스 패턴으로 해결하기

안녕하세요! 오늘은 리액트에서 자주 마주치는 문제인 '상태 관리'에 대해 알아보고, 특히 복잡한 컴포넌트 구조에서 발생하는 문제점과 이를 해결하는 '렌더 프롭스(Render Props)' 패턴에 대해 자세히 살펴보겠습니다. 😊

상태 끌어올리기(Lifting State Up)란?

리액트에서는 여러 컴포넌트가 동일한 데이터를 공유해야 할 때, 해당 데이터를 공통 부모 컴포넌트로 '끌어올리는' 방식을 권장합니다. 이것이 바로 '상태 끌어올리기' 패턴입니다.

간단한 예를 살펴볼까요?

// 전통적인 상태 끌어올리기 방식
function App() {
  const [temperature, setTemperature] = useState("");
  
  return (
    <div className="App">
      <h1>온도 변환기</h1>
      <input 
        type="text" 
        value={temperature} 
        onChange={(e) => setTemperature(e.target.value)} 
      />
      <Kelvin value={temperature} />
      <Fahrenheit value={temperature} />
    </div>
  );
}

이 방식에서는 App 컴포넌트가 온도 상태를 관리하고, 이를 Kelvin과 Fahrenheit 컴포넌트에 전달합니다. 간단한 예제에서는 문제없이 작동하지만...

상태 끌어올리기의 문제점

자식 컴포넌트가 많아지면 어떻게 될까요? 아래와 같은 문제가 발생합니다:

  1. Props Drilling - 상태와 상태 변경 함수를 여러 계층의 컴포넌트를 통해 전달해야 합니다.
  2. 부모 컴포넌트의 복잡도 증가 - 모든 상태 로직이 부모에 집중됩니다.
  3. 컴포넌트 재사용성 감소 - 특정 상태에 의존적인 컴포넌트는 다른 상황에서 재사용하기 어렵습니다.
  4. 테스트 어려움 - 컴포넌트 간 결합도가 높아져 단위 테스트가 어려워집니다.

이런 문제를 어떻게 해결할 수 있을까요? 바로 '렌더 프롭스' 패턴입니다!

렌더 프롭스(Render Props) 패턴

렌더 프롭스는 컴포넌트가 무엇을 렌더링할지 결정하는 함수를 prop으로 받아 사용하는 패턴입니다. 이를 통해 컴포넌트의 로직과 UI를 분리할 수 있습니다.

제가 처음 질문에서 공유한 코드를 살펴볼까요:

function Input(props) {
  const [value, setValue] = useState("");

  return (
    <>
      <input 
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      {props.render(value)}
    </>
  );
}

export default function App() {
  return (
    <div className="App">
      <h1>온도 변환기</h1>
      <Input 
        render={(value) => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      />
    </div>
  );
}

이 코드에서 Input 컴포넌트는 자체적으로 상태를 관리하고, render prop을 통해 그 상태를 활용할 방법을 부모 컴포넌트에게 위임합니다.

제어의 역전(Inversion of Control)

여기서 가장 중요한 개념은 '제어의 역전'입니다. 기존 상태 끌어올리기에서는:

  1. 부모 컴포넌트가 상태를 관리하고
  2. 자식 컴포넌트는 전달받은 props를 사용합니다.

하지만 렌더 프롭스에서는:

  1. 자식 컴포넌트가 상태를 관리하고
  2. 부모 컴포넌트는 그 상태를 어떻게 사용할지 결정합니다.

이것이 바로 '제어의 역전'입니다! 🔄

렌더 프롭스의 장점

  1. 관심사의 분리 - 상태 관리와 UI 로직을 명확히 분리할 수 있습니다.
  2. 재사용성 향상 - 다양한 시나리오에서 동일한 상태 로직을 재사용할 수 있습니다.
  3. props drilling 감소 - 중간 컴포넌트를 통해 props를 전달할 필요가 줄어듭니다.
  4. 유연성 증가 - 상태를 사용하는 방법을 런타임에 결정할 수 있습니다.

실용적인 예제 확장하기

온도 변환기를 더 확장해볼까요? 우리가 만든 Input 컴포넌트를 다양한 상황에서 활용할 수 있습니다:

// 다양한 온도 단위로 변환하기
function App() {
  return (
    <div className="App">
      <h1>온도 변환기</h1>
      <Input 
        render={(value) => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
            <Rankine value={value} />  {/* 새로운 변환 단위 추가 */}
            <Reaumur value={value} />  {/* 또 다른 변환 단위 추가 */}
          </>
        )}
      />
      
      {/* 같은 Input 컴포넌트를 다른 용도로 재사용 */}
      <h2>길이 변환기</h2>
      <Input 
        render={(value) => (
          <>
            <Meter value={value} />
            <Feet value={value} />
            <Inch value={value} />
          </>
        )}
      />
    </div>
  );
}

여기서 볼 수 있듯이, 동일한 Input 컴포넌트를 다양한 방식으로 재사용할 수 있습니다. 이것이 렌더 프롭스의 강력한 장점입니다! 🚀

내부 동작 이해하기

렌더 프롭스가 동작하는 방식을 단계별로 이해해봅시다:

  1. Input 컴포넌트는 자체적으로 value 상태를 관리합니다.
  2. 사용자가 input에 값을 입력하면 setValue를 통해 상태가 업데이트됩니다.
  3. 상태가 업데이트되면 컴포넌트가 리렌더링됩니다.
  4. 리렌더링 시 props.render(value)가 호출되어 현재 value 값을 인자로 전달합니다.
  5. App 컴포넌트에서 정의한 render 함수가 실행되어 Kelvin과 Fahrenheit 컴포넌트를 렌더링합니다.

children prop을 활용한 대안

렌더 프롭스 패턴을 구현하는 또 다른 방법은 children prop을 함수로 사용하는 것입니다:

function Input({ children }) {
  const [value, setValue] = useState("");

  return (
    <>
      <input 
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      {children(value)}
    </>
  );
}

function App() {
  return (
    <div className="App">
      <h1>온도 변환기</h1>
      <Input>
        {(value) => (
          <>
            <Kelvin value={value} />
            <Fahrenheit value={value} />
          </>
        )}
      </Input>
    </div>
  );
}

이 방식은 JSX가 더 자연스럽게 보이는 장점이 있습니다!

렌더 프롭스 vs 다른 패턴들

렌더 프롭스는 다른 상태 관리 패턴과 어떻게 비교될까요?

  1. 상태 끌어올리기: 간단한 상황에서는 좋지만, 컴포넌트 트리가 복잡해지면 관리가 어려워집니다.
  2. Context API: 전역 상태 관리에 적합하지만, 렌더 프롭스처럼 세밀한 제어가 어렵습니다.
  3. Redux/MobX: 애플리케이션 전체 상태 관리에 유용하지만, 간단한 컴포넌트 통신에는 과한 설정이 필요합니다.
  4. Hooks (useReducer, useState): 현대적인 해결책이지만, 여전히 상태를 공유하는 방식에 대한 고민이 필요합니다.

렌더 프롭스는 특히 '재사용 가능한 로직'과 '유연한 UI 구성'이 필요한 상황에서 빛을 발합니다.

마무리

리액트로 개발하다 보면 컴포넌트 간 상태 공유는 항상 고민거리입니다. 상태 끌어올리기는 간단한 앱에서는 좋은 방법이지만, 앱이 복잡해지면 관리가 어려워지죠. 이때 렌더 프롭스 패턴을 사용하면:

  1. 로직과 UI를 깔끔하게 분리할 수 있고
  2. 컴포넌트 재사용성이 높아지며
  3. 코드 유지보수가 쉬워집니다.

특히 제어의 역전 개념을 이해하면, 왜 렌더 프롭스가 강력한지 알 수 있습니다. 상태 관리 주체를 부모에서 자식으로 바꿈으로써, 더 유연하고 확장 가능한 컴포넌트를 만들 수 있는 거죠!

여러분도 다음 리액트 프로젝트에서 컴포넌트 간 통신이 복잡해질 때, 렌더 프롭스 패턴을 한번 시도해보세요. 코드가 얼마나 깔끔해지는지 경험하실 수 있을 겁니다. 🙌

다들 즐거운 코딩하세요!  😄

반응형