icon안동민 개발노트

컴포넌트 간 상태 공유의 어려움


 React 애플리케이션이 복잡해지고 규모가 커질수록 여러 컴포넌트 간에 상태를 공유하고 관리하는 것이 중요한 과제가 됩니다.

 이 과정에서 발생하는 여러 문제점들을 살펴보고, 이를 해결하기 위한 접근 방식들을 알아보겠습니다.

Props Drilling의 문제

 Props drilling은 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하기 위해 중간에 있는 여러 컴포넌트들을 거쳐야 하는 상황을 말합니다.

 예를 들어, 다음과 같은 컴포넌트 구조가 있다고 가정해 봅시다.

function App() {
  const [user, setUser] = useState({ name: "John", age: 30 });
 
  return (
    <div>
      <Header user={user} />
      <MainContent user={user} setUser={setUser} />
      <Footer />
    </div>
  );
}
 
function Header({ user }) {
  return <h1>Welcome, {user.name}!</h1>;
}
 
function MainContent({ user, setUser }) {
  return (
    <div>
      <UserProfile user={user} />
      <UserSettings user={user} setUser={setUser} />
    </div>
  );
}
 
function UserProfile({ user }) {
  return <p>Name: {user.name}, Age: {user.age}</p>;
}
 
function UserSettings({ user, setUser }) {
  // 사용자 설정을 변경하는 로직
}

 이 예제에서 user 상태는 App 컴포넌트에서 관리되지만, 이를 사용하는 UserProfileUserSettings 컴포넌트로 전달하기 위해 MainContent 컴포넌트를 거쳐야 합니다.

 이것이 바로 props drilling입니다.

 props drilling의 주요 문제점

  1. 코드의 가독성 저하
  2. 컴포넌트 재사용성 감소
  3. 유지보수의 어려움

컴포넌트 트리가 깊어질 때의 문제

 컴포넌트 트리가 깊어질수록 상태 관리의 어려움은 더욱 커집니다.

 예를 들어, 다음과 같은 깊은 컴포넌트 트리 구조를 생각해 봅시다.

<App>
  <Header />
  <Sidebar>
    <Navigation>
      <NavItem>
        <SubMenu>
          <MenuItem /> // 여기서 App의 상태가 필요하다면?
        </SubMenu>
      </NavItem>
    </Navigation>
  </Sidebar>
  <MainContent>
    <Article>
      <Comments>
        <CommentForm /> // 여기서도 App의 상태가 필요하다면?
      </Comments>
    </Article>
  </MainContent>
  <Footer />
</App>

 이런 구조에서 App 컴포넌트의 상태를 MenuItem이나 CommentForm 컴포넌트에서 사용해야 한다면, 그 사이의 모든 컴포넌트들이 해당 props를 전달해야 합니다.

 이는 코드를 복잡하게 만들고, 오류 가능성을 높이며, 성능에도 영향을 줄 수 있습니다.

성능 문제

 props drilling은 성능 문제도 야기할 수 있습니다. 상위 컴포넌트의 상태가 변경될 때마다 그 아래의 모든 컴포넌트가 리렌더링될 수 있기 때문입니다. 특히 큰 애플리케이션에서는 이로 인한 성능 저하가 눈에 띌 수 있습니다.

 예를 들어,

function ParentComponent() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <DeeplyNestedComponent count={count} />
    </div>
  );
}
 
function DeeplyNestedComponent({ count }) {
  // 이 컴포넌트는 count를 직접 사용하지 않지만,
  // ParentComponent가 리렌더링될 때마다 함께 리렌더링됩니다.
  return <div>...</div>;
}

 이 경우 count 상태가 변경될 때마다 DeeplyNestedComponent도 함께 리렌더링되어 성능 저하를 일으킬 수 있습니다.

해결 방안

 이러한 문제들을 해결하기 위한 여러 접근 방식이 있습니다.

  1.  Context API : React의 내장 기능인 Context API를 사용하여 props drilling 없이 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.

  2.  상태 관리 라이브러리 : Redux, MobX, Recoil 등의 상태 관리 라이브러리를 사용하여 전역 상태를 효율적으로 관리할 수 있습니다.

  3.  Composition : 컴포넌트 합성을 통해 props drilling을 줄일 수 있습니다.

  4.  Hooks : 커스텀 훅을 만들어 로직을 재사용하고, 상태 관리를 더 효율적으로 할 수 있습니다.

  5.  상태 끌어올리기 : 공통 조상 컴포넌트로 상태를 끌어올려 관리할 수 있습니다.

  6.  메모이제이션 : React.memo, useMemo, useCallback 등을 사용하여 불필요한 리렌더링을 방지할 수 있습니다.

 이러한 방법들은 각각의 장단점이 있으며, 애플리케이션의 규모와 복잡도, 팀의 선호도 등을 고려하여 적절한 방법을 선택해야 합니다.

 다음 절에서는 이러한 해결 방안들을 더 자세히 살펴볼 것입니다.

 컴포넌트 간 상태 공유의 어려움은 React 애플리케이션 개발에서 자주 마주치는 도전 과제입니다.

 이를 효과적으로 해결하기 위해서는 애플리케이션의 구조를 잘 설계하고, 적절한 상태 관리 전략을 선택하는 것이 중요합니다.

 또한, 컴포넌트의 책임을 명확히 분리하고, 필요한 경우에만 상태를 공유하도록 하는 것도 좋은 방법입니다.

 이러한 접근 방식을 통해 더 유지보수가 쉽고, 성능이 좋은 React 애플리케이션을 개발할 수 있습니다.