개발/프론트엔드 (JS & React)

[React Hooks] Context API와 컴포넌트 합성(Component Composition)

jungwon_ 2022. 9. 25. 13:44

Context API

Context는 state나 data에 접근하고자하는 컴포넌트를 Provider로 감싸 여러 컴포넌트가 데이터에 접근할 수 있도록 해준다.

 

이는 일반적으로 리액트에는 발생하는 Props drilling 문제를 해결해줄 수 있다.

 

예를 들어 아래와 같이 App -  Header - UserInfo - Greeting 처럼 중첩되어있다고 가정하자. 우리는 단지 사용자에게 Hi, {user}! 라는 메세지를 Greeting 컴포넌트에서 표시하고 싶을 뿐이다.

 

따라서 Header, UserInfo에서는 user state가 필요없음에도 불구하고 중간 전달자로서 children인 Greeting 컴포넌트를 위해 props를 내려줘야하는 문제가 발생한다.

import { useState } from "react";

function App() {
  const [user, setUser] = useState("user1");
  return (
    <Header user={user} />
  )
}

function Header({ user }) {
  return (
    <div>
      <h3>Header</h3>
      <UserInfo user={user} />
    </div>
  );
}

function UserInfo({ user }) {
  return (
    <div>
      <UserIcon />
      <Greeting user={user} />
    </div>
  )
}

function Greeting({ user }) {
  return <p>Hello {user}!</p>;
}

이런 문제를 Context API를 사용하면 Context를 구독하는 모든 children에서 value에 접근할 수 있도록 함으로써 불필요한 props 전달을 없애준다.

import { createContext, useContext } from 'react';

const UserContext = createContext();

function App() {
  return (
    <UserContext.Provider value={{ "user": "user1" }}>
      <Header />
    </UserContext.Provider>
  )
}

function Header() {
  return (
    <div>
      <h3>Header</h3>
      <UserInfo />
    </div>
  );
}

function UserInfo() {
  return (
    <div>
      <UserIcon />
      <Greeting />
    </div>
  )
}

function Greeting() {
  const { user } = useContext(UserContext);
  return <p>Hello {user}!</p>;
}

하지만 Context API에는 두가지 문제점이 있으므로 남용해서는 안된다.

 

바로 1) 컴포넌트 재사용성 2) 성능 측면에서 문제가 발생할 수 있기 때문이다.


1) 컴포넌트 재사용성

먼저 컴포넌트 재사용성 측면을 살펴보자.

 

예를 들어 우리가 Greeting 컴포넌트를 Header가 아닌 MainPage의 어딘가에서 표시하고 싶다고 가정하자.

import { createContext, useContext } from 'react';

const UserContext = createContext();

function App() {
  return (
    <UserContext.Provider value={{ "user": "user1" }}>
      <Header />
    </UserContext.Provider>
    <MainPage />
  )
}


function MainPage() {
  return (
    ...
    <Greeting />
    ...
  }
}

function Greeting() {
  const { user } = useContext(UserContext);
  return <p>Hello {user}!</p>;
}

MainPage 컴포넌트는 UserContext Provider의 바깥에 있고 그 안에 Greeting 컴포넌트를 포함하고 있을 경우 리액트는 render error을 발생시킨다.

 

즉 Greeting 컴포넌트는 Context Provider 내부에서만 사용될 수 있으며 그렇기에 컴포넌트를 재사용하기 어려워지는 것이다.

 

위 예제를 보고 이렇게 생각할 수도 있겠다.

 

그렇다면 전체 앱을 Context Provider로 감싸면 되지 않을까?

 

하지만 이 해결책은 규모가 큰 앱에서는 성능 문제를 야기할 수 있다.

 

2) 성능

왜냐하면 Context API는 state가 업데이트되면 해당 Context를 구독하는 모든 컴포넌트에 state 변경사항을 전파하고, 이는 모든 Consumer 컴포넌트가 리렌더링 되는 것을 의미한다.

 

따라서 전체 앱을 하나의 Context로 감싸면 해당 state가 변경될 때 마다 컴포넌트 전체가 리렌더링되므로 비효율적이다.


그러면 어떻게 하면 props drilling 문제를 해결하면서도 Context API의 단점을 피해갈 수 있을까?

 

리액트 공식문서를 보면 단순히 여러 레벨에 걸쳐 props를 전달하는 문제를 해결할 때는 context보다 Component Composition(컴포넌트 합성)이 더 간단한 해결책일 수도 있다고 적혀있다.

If you only want to avoid passing some props through many levels, component composition is often a simpler solution than context.

 

그렇다면 컴포넌트 합성은 무엇이고 Context 와 다른 점은 무엇일까?

 

컴포넌트 합성(Component Composition)

리액트는 컴포넌트 합성이라는 기본 원칙을 사용한다.

 

리액트에서는 props로 stae만 전달할 수 있는 것이 아니라 다른 Component도 전달가능하다.

function App() {
  const [user, setUser] = useState("user1");  
  return <UserInfo Greeting={<Greeting user={user} />} />;
}

function UserInfo({ Greeting }) {
 return (
   <UserIcon />
   {Greeting}
 )
}

따라서 위와 같이 중간 컴포넌트(UserInfo)에 props를 일일이 전달하는 방법 대신 상위 컴포넌트(App)에서 하위 컴포넌트(Greeting)를 직접 불러와 state를 전달한 후 그 하위 컴포넌트(Greeting)를 중간 컴포넌트(UserInfo)에 직접 전달할 수 있다.

 

혹은 props의 children을 사용하는 방법도 있다.

function App() {
  const [user, setUser] = useState("user1")
  return (
    <Header>
      <UserInfo>
        <Greeting user={user} />
      </UserInfo>
    </Header>
  )
}

function Header({ children }) {
  return (
    <div>
      <h3>Header</h3>
      {children}
    </div>
  );
}

function UserInfo({ children }) {
  return (
    <div>
      <UserIcon />
      {children}
    </div>
  )
}

function Greeting({ user }) {
  return <p>Hello {user}!</p>;
}

이렇듯 컴포넌트 합성을 사용하면 오히려 문제를 더 쉽게 해결할 수 있으며, 컴포넌트 재사용성 측면에서도 더 나은 것을 볼 수 있다.


리액트에서 굳이 필요하지 않은데도 Context API나 Redux로 state를 관리하는 오버 엔지니어링 사례가 흔하다.

 

하지만 내가 개발하는 앱을 잘 분석해 그의 규모와 필요에 맞게 컴포넌트 재사용성 및 성능 문제를 고려하며 그에 맞는 방법을 사용하는 것이 중요하다.

 

따라서 컴포넌트 합성으로도 충분히 해결할 수 있는 문제라면 Context API 사용을 지양하는 것이 좋겠다.


Reference

- React 공식문서 - Context

- React 공식문서 - Composition vs Inheritance

- A better way of solving prop drilling in React apps [Post] by David Herbert