React 공부 (17) 컨텍스트

seon·2024년 3월 11일

Web

목록 보기
33/33
post-thumbnail
  • 출처: 소플의 처음 만난 리액트

# Context

기존의 일반적인 리액트 애플리케이션에서는 데이터가 컴포넌트의 props를 통해 부모에서 자식으로 단방향 전달이 되었습니다. 하지만 여러 컴포넌트에 걸쳐 굉장히 자주 사용되는 데이터의 경우, 기존 방식을 사용하면 코드도 너무 복잡해지고 사용하기에 불편함이 많았습니다. 그래서 나오게 된 것이 바로 컨텍스트입니다.

컨텍스트는 리액트 컴포넌트들 사이에서 데이터를 기존의 props를 통해 전달하는 방식 대신 컴포넌트 트리(Component tree)를 통해 곧바로 컴포넌트에 전달하는 새로운 방식을 제공합니다.
이를 통해 어떤 컴포넌트든지 데이터에 쉽게 접근할 수 있습니다. 조금 더 쉬운 이해를 위해서 아래 그림을 한번 보겠습니다.
이 그림은 props를 통해 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 일반적인 방식을 보여줍니다. 지금까지 실습하는 동안 사용한 방식이죠. 이 방식의 단점은 여러 컴포넌트에 걸쳐서 자주 사용되는 데이터 예를 들면 로그인 여부나 프로필 정보 같은 것을 전달하려 하면 반복적인 코드가 많이 생기고 지저분해진다는 것입니다.

예를 들어 위 그림에서 루트 노드에 있는 데이터를 C 컴포넌트로 전달하려면 최소 2번 props로 전달해야 합니다. 만약 데이터를 전달하려는 컴포넌트가 10단계 밑에 있다면 10번이나 props를 타고 하위 컴포넌트로 내려가야 합니다. 그래서 이러한 불편한 점을 개선하기 위해 생겨난 것이 바로 컨텍스트입니다. 아래 그림에서는 컨텍스트를 사용하여 앞과 동일한 기능을 구현하는 방식을 보여 주고 있습니다. 컨텍스트를 사용하면 일일이 props로 전달할 필요 없이 위 그림처럼 데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있습니다. 따라서 코드도 매우 깔끔해지고 데이터를 한곳에서 관리하기 때문에 디버깅을 하기에도 굉장히 유리합니다.

언제 Context를 사용해야 할까?

컨텍스트를 사용하면 어떤 컴포넌트든지 데이터에 쉽게 접근할 수 있다고 했는데, 그렇다면 언제 컨텍스트를 사용하면 좋을까요? 먼저 여러 개의 컴포넌트들이 접근해야 하는 데이터는 어떤 것들이 있는지 알아보겠습니다.

여러 컴포넌트에서 자주 필요로 하는 데이터로는 사용자의 로그인 여부, 로그인 정보, UI 테마, 현재 선택된 언어 등이 있습니다. 예를 들어 웹사이트 상단에 위치한 내비게이션 바에 사용자의 로그인 여부에 따라서 로그인 버튼과 로그아웃 버튼을 선택적으로 보여 주고 싶은 경우, 현재 로그인 상태 데이터에 접근할 필요가 있겠죠? 마찬가지고 UI 테마, 현재 선택된 언어 같은 데이터도 곳곳에 있는 컴포넌트에서 접근이 자주 일어날 가능성이 높은 데이터입니다.

이러한 데이터들을 기존 방식대로 컴포넌트의 props를 통해 넘겨주게 되면 자식 컴포넌트의 자식 컴포넌트까지 계속해서 내려갈 수밖에 없게 됩니다. 아래 예제는 현재 선택된 테마를 기존 방식대로 컴포넌트의 props로 전달하는 예제 코드입니다.

function App(props) {
  return <Toolbar theme="dark" />
}
  
function Toolbar(props) {
  // 이 Toolbar 컴포넌트는 ThemedButton에 theme를 넘겨주기 위해서 'theme' prop을 가져야만 합니다.
  // 현재 테마를 알아야 하는 모든 버튼에 대해서 props로 전달하는 것은 굉장히 비효율적입니다.
  return (
    <div>
    	<ThemedButton theme={props.theme} />
    </div>
  );
}

fucnction ThemedButton(props) {
  return <Button theme={props.theme} />;
}

위의 코드에는 총 3개의 컴포넌트가 나옵니다. 먼저 가장 상위 컴포넌트인 App 컴포넌트에서는 Toolbar 컴포넌트를 사용하고 있습니다. 이때 theme라는 이름의 prop으로 현재 테마인 dark를 넘깁니다.

  • Toolbar 컴포넌트에서는 ThemedButton 컴포넌트를 사용하는데
    ThemedButton 컴포넌트에서 현재 테마를 필요로 합니다. 그래서 prop으로 받은 theme를 하위 컴포넌트인 ThemedButton 컴포넌트에 전달합니다. 최종적으로 ThemedButton 컴포넌트에서는 props.theme로 데이터에 접근하여 버튼에 어두운 테마를 입히게 됩니다.
  • 이처럼 props를 통해서 데이터를 전달하는 기존 방식은 실제 데이터를 필요로 하는 컴포넌트까지의 깊이가 깊어질수록 복잡해집니다.
  • 또한 반복적인 코드를 계속해서 작성해주어야 하기 떄문에 비효율적이고 직과적이지도 않습니다.

컨텍스트를 사용하면 이러한 방식을 깔끔하게 개선할 수 있습니다.

// 컨텍스트는 데이터를 매번 컴포넌트를 통해 전달할 필요 없이 컴포넌트 트리로 곧바로 전달하게 해줍니다.
// 여기에서는 현재 테마를 위한 컨텍스트를 생성하며, 기본값은 'light'입니다.
const ThemeContext = React.createContext('light');

// Provider를 사용하여 하위 컴포넌트들에게 현재 테마 데이터를 전달합니다.
// 모든 하위 컴포넌트들은 컴포넌트 트리 하단에 얼마나 깊이 있는지에 관계없이 데이터를 읽을 수 있습니다.
// 여기에서는 현재 테마값으로 'dark'를 전달하고 있습니다.
function App(props) {
  return (
    <ThemeContext.Provider value="dark">
    	<ToolBar />
    </ThemeContext.Provider>
  );
}

// 이제 중간에 위치한 컴포넌트는 테마 데이터를 하위 컴포넌트로 전달할 필요가 없습니다.
function Toolbar(props) {
  return (
    <div>
    	<ThemedButton />
    </div>
  );
}

function ThemedButton(props) {
  // 리액트는 가장 가까운 상위 테마 Provider를 찾아서 해당되는 값을 사용합니다.
  // 만약 해당되는 Provider가 없을 경우 기본값(여기에서는 "light")을 사용합니다.
  // 여기에서는 상위 Provider가 있기 때문에 현재 테마의 값은 'dark'가 됩니다.
  return (
    <ThemeContext.Consumer>
    	{value => <Button theme={value} />}
    </ThemeContext.Consumer>
  );
}
  • 이 코드는 컨텍스트를 사용하여 앞에 나왔던 코드와 동일한 기능을 구현한 것입니다.
  • 먼저 react.createContext() 함수를 사용해서 ThemeContext라는 이름의 컨텍스트를 생성했습니다.
  • 그리고 컨텍스트를 사용할 컴포넌트의 상위 컴포넌트에서 Provider로 감싸 주어야 하는데 여기에서는 최상위 컴포넌트인 App 컴포넌트를 ThemeContext.Provider로 감싸주었습니다.
  • 이렇게 하면 Provider의 모든 하위 컴포넌트가 얼마나 깊이 위치해 있는지 관계없이 컨텍스트의 데이터를 읽을 수 있습니다.
  • 컨텍스트를 사용한 코드를 보면 전체적으로 간결하고 깔끔하며 직관적으로 바뀐 것을 확인할 수 있습니다. 이처럼 여러 컴포넌트에서 계속해서 접근이 일어날 수 있는 데이터들이 있는 경우에는 컨텍스트를 사용하는 것이 좋습니다.

Context를 사용하기 전에 고려할 점

  • 컴포넌트와 컨텍스트가 연동되면 재사용성이 떨어짐
  • 다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는 경우가 아니라면
    기존에 사용하던 방식대로 props를 통해 데이터를 전달하는 컴포넌트 composition 방법이 더 적합함.
// Page 컴포넌트는 PageLayout 컴포넌트를 렌더링
<Page user={user} avatarSize={avatarSize} />
  
// PageLayout 컴포넌트는 NavigationBar 컴포넌트를 렌더링
<PageLAyout user={user} avatarSize={avatarSize} />

// NavigationBar 컴포넌트는 Link 컴포너트를 렌더링
 <NavigationBar user={user} avatarSize={avatarSize}

// Link 컴포넌트는 Avatar 컴포넌트를 렌더링
<Link href={user.permalink}>
  	<Avatar user={user} avatarSize={avatarSize}/>
</Link>
  • 이 코드에는 사용자 정보와 아바타 사이즈를 몇 단계에 걸쳐서
    하위 컴포넌트인 Link와,
    avatar로 전달하는 Page 컴포넌트가 있습니다.
  • 여기에서 가장 하위 레벨에 위치한 Avatar 컴포넌트가 user와 avatarSize를 필요로 하기 때문에
    이를 위해 여러 단계에 걸쳐서 props를 통해 user와 avatarSize를 전달해주고 있음.
  • 하지만 이 과정은 굉장히 불필요하게 느껴집니다.
  • 또한 Avatar 컴포넌트의 추가적인 데이터가 필요해지면 해당 데이터도 추가로 여러 단계에 걸쳐서 넘겨주어야 하기 때문에 굉장히 번거롭습니다.
  • 여기서 컨텍스트를 사용하지 않고 이러한 문제를 해결할 수 있는 한 가지 방법은 Avatar 컴포넌트를 변수에 저장하여 직접 넘겨주는 것입니다. 9강에서 배운 element variable 형태로 말이죠.
  • 그렇게 하면 중간 단계에 있는 컴포넌트들은 user와 avatarSize에 대해 전혀 몰라도 됩니다.

이 코드를 한번 보겠습니다. (소플 14-1강 5:18초)

function Page(props) {
  const user = props.user;
  
  const userLink = (
    <Link href={user.permalink}>
    	<Avatar user={user} size={props.avatarSize} />
    </Link>
  );
  
  // Page 컴포넌트는 PageLayout 컴포넌트를 렌더링
  // 이때 props로 userLink를 함께 전달함.
  return <PageLayout userLink={userLink} />;
}

// PageLayout 컴포넌트는 NavigationBar 컴포넌트를 렌더링
<PageLayout userLink={...} />

// NaviationBar 컴포넌트는 props로 전달받은 userLink element를 리턴
<NavigationBar userLink={...} />
profile
🌻

0개의 댓글