goongoguma's blog

합성 컴포넌트를 이용해 props 내려주기

리액트 문서의 Context파트를 읽어보던 도중 흥미로운 글을 읽었다.

context의 주된 용도는 다양한 레벨에 네스팅된 많은 컴포넌트에게 데이터를 전달하는 것입니다. context를 사용하면 컴포넌트를 재사용하기가 어려워지므로 꼭 필요할 때만 쓰세요. 여러 레벨에 걸쳐 props 넘기는 걸 대체하는 데에 context보다 컴포넌트 합성이 더 간단한 해결책일 수도 있습니다.


컴포넌트 합성(composition component)를 이용해 props를 넘긴다는뜻이 무엇일까? 그리고 context api를 사용하는것보다 더 간단한 방법이 될 수 있다고?

궁금증이 생겨 컴포넌트 합성에 관한 글을 이리저리 찾다가 유튜브에 Using Composition in React to Avoid Prop Drilling이라는 이름으로 올라와있는 영상덕분에 이해를 하게되었다.

영상에서 나온 내용을 정리를 하자면…

  import React, { useState } from 'react';
  import './App.css';

  function App() {
    const [currentUser, setCurrentUser] = useState(null);
    return (
      <div>
        <div>
          <Header />
        </div>
        <div>
          {
            currentUser ? (
              <Dashboard user={currentUser} />
            ) : (
              <LoginScreen onLogin={() => setCurrentUser({ name: 'Michael'})} />
            )
          }
        </div>
        <div>
          <Footer />
        </div>
      </div>
    );
  };

  const Header = () => {
    return (
      <div>
        <h1>header</h1>
      </div>
    )
  };

  const LoginScreen = ({ onLogin }) => {
    return (
      <div>
        <h3>Please Login</h3>
        <button onClick={onLogin}>Login</button>
      </div>
    )
  };

  const Dashboard = ({ user }) => {
    return (
      <div>
        <h2>The Dashboard</h2>
        <DashboardNav />
        <DashboardContent user={user} />
      </div>
    )
  };

  const DashboardNav = () => {
    return (
      <div>
        <h3>Dashboard Nav</h3>
      </div>
    )
  };

  const DashboardContent = ({ user }) => {
    return (
      <div>
        <h3>Dashboard Content</h3>
        <WelcomMessage user={user} />
      </div>
    )
  };

  const WelcomMessage = ({ user }) => {
    return (
      <div>
        <p>Welcome {user.name}!</p>
      </div>
    )
  };

  const Footer = () => {
    return (
      <div>
        <h1>Footer</h1>
      </div>
    )
  };

  export default App;

화면 가운데에 있는 Login 버튼을 클릭하면 유저의 이름이 들어간 Dashboard 컴포넌트를 띄워주는 간단한 앱이다.

컴포넌트의 구조는 단순하나 상태인 currentUser는 App -> Dashboard -> DashboardContent -> WelcomMessage 컴포넌트를 거친다.

WelcomMessage 컴포넌트에 currentUser의 상태를 전달해주기 위해 무려 3개의 컴포넌트를 거친다.

prop drilling을 최소화 하기 위해 먼저 context를 사용해보겠다.

const Context = React.createContext();

function App() {
  const [currentUser, setCurrentUser] = useState(null);
  return (
      <Context.Provider value={currentUser}>
        <div>
          <div style=>
            <Header />
          </div>
          <div>
            {
              currentUser ? (
                <Dashboard />
              ) : (
                <LoginScreen onLogin={() => setCurrentUser({ name: 'Michael'})} />
              )
            }
          </div>
          <div>
            <Footer />
          </div>
        </div>
      </Context.Provider>
  );
};

const Header = () => {
  return (
    <div>
       <WelcomMessage />
      <h1>header</h1>
    </div>
  )
};

const LoginScreen = ({ onLogin }) => {
  return (
    <div>
      <h3>Please Login</h3>
      <button onClick={onLogin}>Login</button>
    </div>
  )
};

const Dashboard = () => {
  return (
    <div>
      <h2>The Dashboard</h2>
      <DashboardNav />
      <DashboardContent />
    </div>
  )
};

const DashboardNav = () => {
  return (
    <div>
      <h3>Dashboard Nav</h3>
    </div>
  )
};

const DashboardContent = () => {
  return (
    <div>
      <h3>Dashboard Content</h3>
      <WelcomMessage />
    </div>
  )
};

const WelcomMessage = () => {
  let { currentUser } = useContext(Context)
  return (
    <div>
      <p>Welcome {currentUser}!</p>
    </div>
  )
};

context를 사용하니 전과 다르게 Dashboard와 DashboardContent 컴포넌트에 prop drilling이 생략되어 깔끔해졌다.

하지만 만일 WelcomMessage 컴포넌트가 Provider의 밖에도 존재할경우 에러가 발생한다.

function App() {
  const [currentUser, setCurrentUser] = useState(null);
  return (
      <div>
        <Context.Provider value={ currentUser }>
          <div>
            <div>
              <Header />
            </div>
            <div>
              {
                currentUser ? (
                  <Dashboard />
                ) : (
                  <LoginScreen onLogin={() => setCurrentUser({ name: 'Michael'})} />
                )
              }
            </div>
            <div>
              <Footer />
            </div>
          </div>
        </Context.Provider>
        <WelcomMessage />
      </div>
  );
};

WelcomMessage 컴포넌트가 Context.Provider밖에 존재한다.

그렇다면 DashboardContent 컴포넌트안의 WelcomMessage는 상태값을 context로 받고있지만 Provider의 밖에 위치한 WelcomMessage는 상태값을 받지 못하는 불균형적인 문제가 발생하게 된다.

이제 합성 컴포넌트를 사용해 prop drilling을 최소화 해보자.

function App() {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <div>
      <div>
        <Header />
      </div>
      <div>
        {
          currentUser ? (
            <Dashboard>
              <DashboardNav />
              <DashboardContent>
                <WelcomMessage user={currentUser} />
              </DashboardContent>
            </Dashboard>
          ) : (
            <LoginScreen onLogin={() => setCurrentUser({ name: 'Michael'})} />
          )
        }
      </div>
      <div>
        <Footer />
      </div>
    </div>
  );
};

const Header = () => {
  return (
    <div>
      <h1>header</h1>
    </div>
  )
};

const LoginScreen = ({ onLogin }) => {
  return (
    <div>
      <h3>Please Login</h3>
      <button onClick={onLogin}>Login</button>
    </div>
  )
};

const Dashboard = ({ children }) => {
  return (
    <div>
      <h2>The Dashboard</h2>
      {children}
    </div>
  )
};

const DashboardNav = () => {
  return (
    <div>
      <h3>Dashboard Nav</h3>
    </div>
  )
};

const DashboardContent = ({ children }) => {
  return (
    <div>
      <h3>Dashboard Content</h3>
      {children}
    </div>
  )
};

const WelcomMessage = ({ user }) => {
  return (
    <div>
      <p>Welcome {user.name}!</p>
    </div>
  )
};

const Footer = () => {
  return (
    <div>
      <h1>Footer</h1>
    </div>
  )
};

합성 컴포넌트 패턴을 사용해 prop drilling을 최소화 했다.
뿐만 아니라 Dashboard의 성격상 해당 컴포넌트 안에 있는 다양한 컴포넌트들을 렌더링 하게 되는데 children prop을 사용하게 되면 컴포넌트안의 자식 컴포넌트들을 식별하기 쉬워질뿐만 아니라 컴포넌트의 커스터마이징(예를들어 DashboardNav를 제거하거나 새로운 컴포넌트를 추가하는등)이 한결 편해졌다.
codesandbox