2023.02.06 Context
context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도
컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.
일반적인 React 애플리케이션에서 데이터는
위에서 아래로 (즉, 부모로부터 자식에게) props를 통해 전달되지만,
애플리케이션 안의 여러 컴포넌트들에 전해줘야 하는 데이터(props)의 경우
(예를 들면 로그인여부, 프로필 정보 등) 이 과정이 번거로울 수 있습니다.
context를 이용하면, 트리 단계마다 명시적으로 props를 넘겨주지 않아도 많은 컴포넌트가 이러한 값을 공유하도록 할 수 있습니다.
이를 통해 어떤 컴포넌트든지 데이터에 쉽게 접근할 수 있습니다.
위의 그림은 props를 통해 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 일반적인 방식이며,
지금까지 실습하는 동안 사용한 방식입니다.
하지만 이 방식에서는 여러 컴포넌트에 걸쳐서 자주 사용되는 데이터를 전달하려면
반복적인 코드가 많이 생기고 지저분해진다는 단점을 갖고 있습니다.
컨텍스트를 사용하면 일일이 props로 전달할 필요 없이 위 그림처럼
데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있습니다.
따라서 코드도 매우 깔끔해지고 데이터를 한 곳에서 관리하기 때문에 디버깅을 하기에도 굉장히 유리합니다.
context는 React 컴포넌트 트리 안에서 전역적(global)이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법입니다.
여러 개의 컴포넌트들이 접근해야 하는 데이터로는
현재 사용자의 로그인 여부, 로그인 정보, UI 테마, 현재 선택된 언어 등이 있습니다.
예를 들어, 아래의 코드는 현재 선택된 테마를 기존 방식대로 컴포넌트의 props로 전달하는 예제 입니다.
function App(props) {
return <Toolbar theme="dark" />;
}
function Toolbar(props) {
//이 Toolbar 컴포넌트는 ThemeButton에 theme를 넘겨주기 위해서 'theme'props을 가져야만합니다.
// 현재 테마를 알아야 하는 모든 버튼에 대해서 props로 전달하는 것은 굉장히 비효율적입니다.
return (
<div>
<ThemeButton theme={props.theme} />
</div>
);
}
function ThemeButton(props) {
return <Button theme={props.theme} />;
}
먼저 가장 상위 컴포넌트인 App 컴포넌트에서는 Toolbar 컴포넌트를 사용하고 있습니다.
이때 theme라는 이름의 prop으로 현재 테마인 dark를 넘깁니다.
Toolbar 컴포넌트에서는 ThemeButton 컴포넌트를 사용하는데 ThemeButton 컴포넌트에서 현재 테마를 필요로 합니다.
그래서 prop으로 받은 theme를 하위 컴포넌트인 ThemeButton 컴포넌트에 전달합니다.
최종적으로 ThemeButton 컴포넌트에서는 props.theme로 데이터에 접근하여 버튼에 어두운 테마를 입히게 됩니다.
위에서 살펴 본 것처럼 props를 통해서 데이터를 전달하는 기존 방식은 실제 데이터를 필요로하는 컴포넌트까지의 깊이가 깊어질수록 복잡해집니다.
반복적인 코드를 계속해서 작성해 주어야 하기 때문에 비효율적이고 직관직이지도 않습니다.
context를 사용하면 중간에 있는 엘리먼트들에게 props를 넘겨주지 않아도 됩니다.
// Context는 데이터를 매번 컴포넌트를 통해 전달할 필요 없이 컴포넌트 트리로 곧바로 전달하게 해줍니다.
// 여기에서는 현재 테마를 위한 컨텍스트를 생성하며, 기본값은 'light'입니다.
const ThemeContext = React.createContext('light');
// Provider를 사용하여 하위 컴포넌트들에게 현재 테마 데이터를 전달합니다.
// 모든 하위 컴포넌트들은 컴포넌트 트리 하단에 얼마나 깊이 있는지에 관계없이 데이터를 읽을 수 있습니다.
// 여기에서는 현재 테마값으로 'dark'를 전달하고 있습니다.
function App(props) {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
// 이제 중간에 위치한 컴포넌트는 테마 데이터를 하위 컴포넌트로 전달할 필요가 없습니다.
function Toolbar(props) {
return (
<div>
<ThemeButton />
</div>
);
}
function ThemeButton(props) {
// 리액트는 가장 가까운 상위 테마 Provider를 찾아서 해당되는 값을 사용합니다.
// 만약 해당되는 Provider가 없을 경우 기본값(여기에서는 'light')을 사용합니다.
// 여기에서는 상위 Provider가 있기 때문에 현재 테마의 값은 'dark'가 됩니다.
return (
<ThemeContext.Consumer>
{value => <Button theme={value} />}
</ThemeContext.Consumer>
);
}
위 코드에서는 먼저 Reac.createContext() 함수를 사용해서 ThemeContext라는 이름의 컨텍스트를 하나 생성했습니다.
컨텍스트를 사용할 컴포넌트의 상위 컴포넌트에서 Provider로 감싸주어야 하는데
여기에서는 최상위 컴포넌트인 App 컴포넌트를 ThemeContext.Provider로 감싸주었습니다.
이렇게 하면 Provider의 모든 하위 컴포넌트가 얼마나 깊이 위치해 있는지 관계없이 컨텍스트의 데이터를 읽을 수 있습니다.
컨텍스트를 사용한 코드를 보면 전체적으로 깔끔하며 직관적으로 바뀐 것을 확인할 수 있습니다.
이처럼 여러 컴포넌트에서 계속 접근이 일어날 수 있는 데이터들이 있는 경우에 컨텍스트를 사용하는 것이 좋습니다.
컨텍스트는 다른 레벨의 많은 컴포넌트가 특정 데이터를 필요로 하는 경우에 주로 사용합니다.
하지만 무조건 컨텍스트를 사용하는 것은 좋은 것이 아닙니다.
왜냐하면 컴포넌트와 컨텍스트가 연동되면 재사용성이 떨어지기 때문입니다.
그래서 다른 레벨의 많은 컴포넌트가 데이터를 필요로 하는 경우가 아니라면
기존에 사용하던 방식대로 props를 통해 데이터를 전달하는 컴포넌트 합성 방법이 더 적합합니다.
예제 코드를 봅시다.
// 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} size={avatarSize} />
</Link>
실제로 사용되는 곳은 Avatar 컴포넌트 뿐인데 user와 avatarSize props를 여러 단계에 걸쳐 보내줘야 한다는 게 번거로워 보일 수 있습니다.
게다가 위에서 Avatar 컴포넌트로 보내줘야하는 props가 추가된다면 그 또한 중간 레벨에 모두 추가해줘야 합니다.
여기에서 컨텍스트를 사용하지 않고 이러한 문제를 해결할 수 있는 한 가지 방법은
Avatar 컴포넌트를 변수에 저장하여 직접를 넘겨주면 context를 사용하지 않고 이를 해결할 수 있습니다.
9장에서 배운 엘리먼트 변수 형태로 말이죠.그러면 중간에 있는 컴포넌트들이 user나 avatarSize 에 대해 전혀 알 필요가 없습니다.
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={...} />
// NavigationBar 컴포넌트는 props로 전달받은 userLink element를 리턴
<NavigationBar userLink={...} />
위 코드에서는 user와 avatarSize가 props로 들어간 Avatar 컴포넌트를 userLink라는
변수에 저장한 뒤에 해당 변수를 하위 컴포넌트로 넘기고 있습니다.
이렇게 하면 가장 상위 레벨에 있는 Page 컴포넌트만 Avatar 컴포넌트에 필요로 하는
user와 avatarSize에 대해 알고 있으면 됩니다.
이런 방식은 중간 레벨의 컴포넌트를 동해 전달해야 하는 props를 없애고,
코드를 더욱 간결하게 만들어 줍니다.
또한 최상위에 있는 컴포넌트에 좀 더 많은 권한을 부여해 줍니다.
다만 모든 상황에 이 방식이 좋은 것은 아닙니다.
데이터가 많아질수록 상위 컴포넌트에 몰리기 때문에 상위 컴포넌트는 점점 더 복잡해지고,
하위 컴포넌트는 너무 유연해지게 됩니다.
앞에서 사용한 방법을 좀 더 응용해서 하위 컴포넌트를 여러 개의 변수로 나눠서 전달할 수도 있습니다.
function Page(props) {
const user = props.user;
const topBar = (
<NavigationBar>
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
</NavigationBar>
);
const content = <Feed user={user} />;
return (
<PageLayout
topBar={topBar}
content={content}
/>
);
}
이 방식은 하위 컴포넌트의 의존성을 상위 컴포넌트와 분리할 필요가 있는 대부분의 경우에 적절한 방법입니다.
또한 렌더링 전에 하위 컴포넌트가 상위 컴포넌트와 통신해야 하는 경우 render props를 사용하여 처리할 수도 있습니다.
하지만 어떤 경우에는 하나의 데이터에 다양한 레벨에 있는 중첩된 컴포넌트들의 접근이 필요할 수 있습니다.
이러한 경우에는 위 방식을 사용할 수 없고 컨텍스트를 사용해야 합니다.
컨텍스트는 해당 데이터와 데이터의 변경사항을 모두 하위 컴포넌트들에게 broadcast(널리 알려주는 것) 해주기 때문입니다.
컨텍스트를 사용하기에 적합한 데이터의 대표적인 예로
현재 지역 정보, UI 테마 그리고 캐싱된 데이터 등이 있습니다.