기존의 일반적인 리액트 애플리케이션에서는 데이터가 컴포넌트의 props를 통해 부모에서 자식으로 단방향 전달이 되었습니다. 하지만 여러 컴포넌트에 걸쳐 굉장히 자주 사용되는 데이터의 경우, 기존 방식을 사용하면 코드도 너무 복잡해지고 사용하기에 불편함이 많았습니다. 그래서 나오게 된 것이 바로 컨텍스트입니다.
컨텍스트는 리액트 컴포넌트들 사이에서 데이터를 기존의 props를 통해 전달하는 방식 대신 컴포넌트 트리(Component tree)를 통해 곧바로 컴포넌트에 전달하는 새로운 방식을 제공합니다.
이를 통해 어떤 컴포넌트든지 데이터에 쉽게 접근할 수 있습니다. 조금 더 쉬운 이해를 위해서 아래 그림을 한번 보겠습니다.
이 그림은 props를 통해 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 일반적인 방식을 보여줍니다. 지금까지 실습하는 동안 사용한 방식이죠. 이 방식의 단점은 여러 컴포넌트에 걸쳐서 자주 사용되는 데이터 예를 들면 로그인 여부나 프로필 정보 같은 것을 전달하려 하면 반복적인 코드가 많이 생기고 지저분해진다는 것입니다.
예를 들어 위 그림에서 루트 노드에 있는 데이터를 C 컴포넌트로 전달하려면 최소 2번 props로 전달해야 합니다. 만약 데이터를 전달하려는 컴포넌트가 10단계 밑에 있다면 10번이나 props를 타고 하위 컴포넌트로 내려가야 합니다. 그래서 이러한 불편한 점을 개선하기 위해 생겨난 것이 바로 컨텍스트입니다. 아래 그림에서는 컨텍스트를 사용하여 앞과 동일한 기능을 구현하는 방식을 보여 주고 있습니다.
컨텍스트를 사용하면 일일이 props로 전달할 필요 없이 위 그림처럼 데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있습니다. 따라서 코드도 매우 깔끔해지고 데이터를 한곳에서 관리하기 때문에 디버깅을 하기에도 굉장히 유리합니다.
컨텍스트를 사용하면 어떤 컴포넌트든지 데이터에 쉽게 접근할 수 있다고 했는데, 그렇다면 언제 컨텍스트를 사용하면 좋을까요? 먼저 여러 개의 컴포넌트들이 접근해야 하는 데이터는 어떤 것들이 있는지 알아보겠습니다.
여러 컴포넌트에서 자주 필요로 하는 데이터로는 사용자의 로그인 여부, 로그인 정보, 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를 넘깁니다.
컨텍스트를 사용하면 이러한 방식을 깔끔하게 개선할 수 있습니다.
// 컨텍스트는 데이터를 매번 컴포넌트를 통해 전달할 필요 없이 컴포넌트 트리로 곧바로 전달하게 해줍니다.
// 여기에서는 현재 테마를 위한 컨텍스트를 생성하며, 기본값은 '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로 감싸주었습니다.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>
이 코드를 한번 보겠습니다. (소플 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={...} />