컨텍스트란 무엇인가?
컨텍스트는 리액트 컴포넌트들 사이에서 데이터를 기존의 props를 통해 전달하는 방식 대신 데이터를 컴포넌트 트리를 통해 곧바로 컴포넌트에 전달하는 새로운 방식을 제공합니다. 컨텍스트를 사용하면 여러 컴포넌트에 걸쳐 굉장히 자주 사용되는 데이터를 어떤 컴포넌트든지 쉽게 접근할 수 있습니다.
props를 통해 상위 컴포넌트에서 하위 컴포넌트로 데이터를 전달하는 방식은 데이터가 여러 컴포넌트에 걸쳐 자주 사용된다면 반복적인 코드가 많이 생기고 지저분해진다는 단점이 있습니다. 이러한 불편함을 개선하기 위해 생겨난 것이 바로 컨텍스트입니다.
컨텍스트를 사용하면 일일이 props로 전달할 필요없이 데이터를 필요로 하는 컴포넌트에 곧바로 데이터를 전달할 수 있습니다. 컨텍스트를 사용하면 코드가 매우 깔끔해지고 데이터를 한 곳에서 관리하기 때문에 디버깅을 하기에도 굉장히 유리합니다.
언제 컨텍스트를 사용해야 할까?
여러 컴포넌트에서 자주 필요로 하는 데이터로는 사용자의 로그인 여부, 로그인 정보, UI 테마, 현재 선택된 언어 등이 있습니다. 이러한 데이터들을 props를 통해 넘겨주게 되면 자식 컴포넌트의 자식 컴포넌트까지 계속해서 내려갈 수밖에 없게 됩니다. 아래 예제를 살펴 봅시다.
function App(props) {
return <Toolbar theme="dark" />;
}
function Toolbar(props) {
return (
<div>
<ThemeButton theme={props.theme} />
</div>
);
}
function ThemeButton(props) {
return <Button theme={props.theme} />;
}
위 코드의 가장 상위 컴포넌트인 App 컴포넌트에 theme이라는 이름의 prop으로 현재 테마인 dark를 넘깁니다. Toolbar 컴포넌트는 prop으로 전달받은 theme을 ThemeButton 컴포넌트에 전달합니다. 최종적으로 ThemeButton 컴포넌트에서 props.theme으로 데이터에 접근하여 버튼에 어두운 테마를 입히게 됩니다. 이처럼 props를 통해 데이터를 전달하는 방식은 실제 데이터를 필요로 하는 컴포넌트까지의 깊이가 깊어질수록 복잡해집니다. 코드가 반복되기 때문에 비효율적이고 직관적이지도 않습니다. 컨텍스트를 사용하면 이러한 방식을 깔끔하게 개선할 수 있습니다. 아래 코드는 컨텍스트를 사용하여 위와 동일한 기능을 구현한 것입니다.
// 현재 테마를 위한 컨텍스트를 생성하며, 기본값은 'light'입니다.
const ThemeContext = React.createContext('light');
// Provider를 사용하여 하위 컴포넌트들에게 현재 테마 데이터를 전달합니다.
function App(props) {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemeButton />
</div>
);
}
// 리액트는 가장 가까운 상위 테마 Provider를 찾아서 해당되는 값을 사용합니다.
// 만약 해당되는 Provider가 없을 경우 기본값을 사용합니다.
function ThemeButton(props) {
return (
<ThemeContext.Consumer>
{value => <Button theme={value} />}
</ThemeContext.Consumer>
);
}
먼저 React.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} avatarSize={avatarSize} />
</Link>
위 코드에는 사용자 정보와 아바타 사이즈를 몇 단계에 걸쳐서 하위 컴포넌트인 Link와 Avatar로 전달하는 Page 컴포넌트가 있습니다. 여기에서 가장 하위 레벨에 위치한 Avatar 컴포넌트가 user와 avatarSize를 필요로 하기 때문에, 이를 위해 여러 단계에 걸쳐서 props를 통해 user와 avatarSize를 전달해주고 있습니다. 하지만 이 과정은 굉장히 불필요하게 느껴지고, Avatar 컴포넌트에 추가적인 데이터가 필요해지면 해당 데이터를 추가로 여러 단계에 걸쳐서 넘겨주어야 하기 때문에 굉장히 번거롭습니다.
컨텍스트를 사용하지 않고 이러한 문제를 해결할 수 있는 한 가지 방법은 Avatar 컴포넌트를 변수에 저장하여 직접 넘겨주는 것입니다. 그렇게 하면 중간 단계에 있는 컴포넌트들은 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={...} />
이렇게 하면 가장 상위 레벨에 있는 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 테마, 캐싱된 데이터 등이 있습니다.
컨텍스트 API
1) React.createContext
컨텍스트를 생성하기 위해서 React.createContext() 함수를 사용합니다. 함수의 파라미터로 기본값을 넣어주면 됩니다.
const MyContext = React.createContext(기본값);
리액트에서 렌더링이 일어날 때 컨텍스트 객체를 구독하는 하위 컴포넌트가 나오면 현재 컨텍스트의 값을 가장 가까이에 있는 상위 레벨의 Provider로부터 받아오게 됩니다. 그런데 만약 상위 레벨에 매칭되는 Provider가 없다면, 이 경우에만 기본값이 사용됩니다. 그렇기 때문에 기본값은 Provider 없이 컴포넌트를 테스트할 때 유용합니다. 참고로 기본값으로 undefined를 넣으면 기본값이 사용되지 않습니다.
2) Context.Provider
컨텍스트를 생성한 후 하위 컴포넌트들이 컨텍스트의 데이터를 받을 수 있도록 설정해주기 위해 Provider를 사용합니다. Provider는 데이터를 제공해주는 컴포넌트라고 이해하면 됩니다. 모든 컨텍스트 객체는 Provider라는 리액트 컴포넌트를 가지고 있습니다. Context.Provider 컴포넌트로 하위 컴포넌트들을 감싸주면 모든 하위 컴포넌트들이 해당 컨텍스트의 데이터에 접근할 수 있게 됩니다. Provider는 다음 예제처럼 사용하면 됩니다.
<MyContext.Provider value={/* some value */}>
/* some component */
</MyContext.Provider>
Provider 컴포넌트에는 value라는 prop이 있으며, 이것은 Provider 컴포넌트 하위에 있는 컴포넌트들에게 전달됩니다. 이 값을 사용하는 하위 컴포넌트들을 consumer 컴포넌트라고 부릅니다. consumer 컴포넌트는 컨텍스트 값으 변화를 지켜보다가 만약 값이 변경되면 리렌더링됩니다. 참고로 하나의 Provider 컴포넌트는 여러 개의 consumer 컴포넌트와 연결될 수 있으며, 여러 개의 Provider 컴포넌트는 중첩되어 사용될 수 있습니다.
Provider 컴포넌트로 감싸진 모든 consumer 컴포넌트는 Provider의 value prop이 바뀔 때마다 리렌더링됩니다. 값이 변경되었을 때 상위 컴포넌트가 업데이트 대상이 아니더라도 하위에 있는 컴포넌트가 컨텍스트를 사용한다면 하위 컴포넌트에서는 업데이트가 일어납니다. 이때 값의 변화를 판단하는 기준은 자바스크립트 객체의 Object.is라는 함수와 같은 방식으로 판단합니다.
Provider value에서 주의해야할 사항
컨텍스트는 리렌더링 여부를 결정할 때 레퍼런스 정보를 사용하기 때문에 Provider의 부모 컴포넌트가 리렌더링되었을 경우, 의도치 않게 consumer 컴포넌트의 리렌더링이 일어날 수 있습니다. 예를 들어 아래 코드는 Provider 컴포넌트가 리렌더링될 때마다 모든 하위 consumer 컴포넌트의 리렌더링이 발생합니다. 왜냐하면 value prop을 위한 새로운 객체가 매번 새롭게 생성되기 떄문입니다.
function App(props) {
return (
<MyContext.Provider value={{something: 'something'}}>
<ToolBar />
</MyContext.Provider>
);
}
이를 방지하기 위해서는 value를 직접 넣는 것이 아니라 컴포넌트의 state로 옮기고 해당 state의 값을 넣어주어야 합니다.
function App(props) {
const [value, setValue] = useState({something: 'something'});
return (
<MyContext.Provider value={value}>
<ToolBar />
</MyContext.Provider>
);
}
3) Class.contextType
Class.contextType은 Provider 하위에 있는 클래스 컴포넌트에서 컨텍스트의 데이터에 접근하기 위해 사용하는 것입니다. 클래스 컴포넌트는 현재 거의 사용하지 않기 때문에 이런 방법이 있다는 정도로만 참고하기 바랍니다.
아래 코드에 나와 있는 것처럼 MyClass.contextType = MyContext;라고 해주면 MyClass라는 클래스 컴포넌트는 MyContext의 데이터에 접근할 수 있게 됩니다.
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* MyContext의 값을 이용하여 원하는 작업을 수행 가능 */
}
componentDidUpdate() {
let value = this.context;
/* MyContext의 값을 이용하여 원하는 작업을 수행 가능 */
}
componentWillUnmount() {
let value = this.context;
/* MyContext의 값을 이용하여 원하는 작업을 수행 가능 */
}
render() {
let value = this.context;
/* MyContext의 값에 따라서 컴포넌트들을 렌더링 */
}
}
MyClass.contextType = MyContext;
클래스 컴포넌트에 있는 contextType 속성에는 React.createContext() 함수로 생성된 컨텍스트 객체가 대입될 수 있습니다. 이 속성을 사용하게 되면 this.context를 통해 상위에 있는 Provider 중에서 가장 가까운 것의 값을 가져올 수 있습니다. 또한 render() 함수를 포함한 모든 생명주기 함수에서 this.context를 사용할 수 있습니다.
참고로 이 API를 사용하면 단 하나의 컨텍스트만을 구독할 수 있습니다.
4) Context.Consumer
consumer 컴포넌트는 컨텍스트의 데이터를 구독하는 컴포넌트입니다. 클래스 컴포넌트에서는 Class.contextType을 사용하면 되고, 함수 컴포넌트에서는 Context.Consumer를 사용하여 컨텍스트를 구독할 수 있습니다. 아래는 예제 코드입니다.
<MyContext.Consumer>
{value => /* 컨텍스트의 값에 따라서 컴포넌트들을 렌더링 */}
</MyContext.Consumer>
컴포넌트의 자식으로 함수가 올 수 있는데 이것을 function as a child 라고 부릅니다. Context.Consumer로 감싸주면 자식으로 들어간 함수가 현재 컨텍스트의 value를 받아서 리액트 노드로 리턴하게 됩니다. 이때 함수로 전달되는 value는 Provider의 value prop과 동일합니다. 만약 상위 컴포넌트에 Provider가 없다면 이 value 파라미터는 createContext()를 호출할 때 넣는 기본값이 됩니다.
function as a child
function as a child는 컴포넌트의 자식으로 함수를 사용하는 방법입니다. 리액트에서는 기본적으로 하위 컴포넌트들을 children이라는 prop으로 전달해주는데 children으로 컴포넌트 대신 함수를 사용하여 아래와 같이 사용할 수 있습니다.
// children이라는 prop을 직접 선언하는 방식
<Profile children={name => <p>이름: {name}</p>} />
// Profile 컴포넌트로 감싸서 children으로 만드는 방식
<Profile>{name => <p>이름: {name}</p>}</Profile>
5) Context.displayName
컨텍스트 객체는 displayName이라는 문자열 속성을 가집니다. 또한 크롬의 리액트 개발자 도구에서는 컨텍스트의 Provider나 Consumer를 표시할 때 이 displayName을 함께 표시해줍니다. 에를 들어 아래와 같이 코드를 작성하면 MyDisplayName이 리액트 개발자 도구에 표시됩니다.
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
// 개발자 도구에 "MyDisplayName.Provider"로 표시됨
<MyContext.Provider>
// 개발자 도구에 "MyDisplayName.Consumer"로 표시됨
<MyContext.Consumer>
여러 개의 컨텍스트 사용하기
클래스 컴포넌트에서 Class.contextType을 사용하면 한 번에 하나의 컨텍스트만 사용할 수 있습니다. 여러 개의 컨텍스트를 동시에 사용하려면 Context.Provider를 중첩해서 사용하는 방식을 사용하면 됩니다. 아래 예제 코드를 봅시다.
// 테마를 위한 컨텍스트
const ThemeContext = React.createContext('light');
// 로그인 한 사용자를 위한 컨텍스트
const UserContext = React.createContext({name: 'Guest'});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// Content 컴포넌트는 두 개의 컨텍스트로부터 값을 가져와서 렌더링함
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
App 컴포넌트에서는 각 컨텍스트에 대해 두 개의 Provider를 사용하여 자식 컴포넌트인 Layout을 감싸주었습니다. 그리고 실제 컨텍스트의 데이터를 사용하는 Content 컴포넌트에서는 두 개의 Consumer 컴포넌트를 사용하여 데이터를 전달하고 있습니다.
이렇게 하면 여러 개의 컨텍스트를 동시에 사용할 수 있습니다. 하지만 두 개 또는 그 이상의 컨텍스트의 값이 자주 함께 사용될 경우 모든 값을 한 번에 제공해 주는 별도의 render prop 컴포넌트를 직접 만드는 것을 고려하는 것이 좋습니다.
useContext
함수 컴포넌트에서 컨텍스트를 사용하기 위해 Consumer 컴포넌트로 감싸주는 것 대신에 useContext()를 사용하면 컨텍스트를 더 쉽게 사용할 수 있습니다. useContext() 훅은 React.createContext() 함수 호출로 생성된 컨텍스트 객체를 인자로 받아서 현재 컨텍스트의 값을 리턴합니다. useContext() 훅은 아래와 같이 사용합니다.
function MyComponent(props) {
const value = useContext(MyContext);
return (
...
);
}
useContext() 훅을 사용하면 다른 방식과 동일하게 컴포넌트 트리상에서 가장 가까운 상위 Provider로부터 컨텍스트의 값을 받아오게 됩니다. 만약 컨텍스트의 값이 변경되면 변경된 값과 함께 useContext() 훅을 사용하는 컴포넌트가 리렌더링됩니다. 그렇기 때문에 만약 useContext() 훅을 사용하는 컴포넌트의 렌더링이 꽤 무거운 작업일 경우에는 별도로 최적화 작업을 해줄 필요가 있습니다.
또한 useContext() 훅을 사용할 때는 파라미터로 컨텍스트 객체를 넣어줘야 한다는 것을 꼭 기억하기 바랍니다. 아래 코드처럼 Consumer나 Provider를 넣으면 안됩니다.
// 올바른 사용법
useContext(MyContext);
// 잘못된 사용법
useContext(MyContext.Provider);
useContext(MyContext.Consumer);'개발 공부' 카테고리의 다른 글
| 데이터 파이프라인에 대하여 (0) | 2025.05.01 |
|---|---|
| [Front-end] React 스타일링 (0) | 2023.10.21 |
| [Front-end] React 합성 vs. 상속 (0) | 2023.10.16 |
| [Front-end] React 폼 (0) | 2023.10.01 |
| [Front-end] React 리스트와 키 (0) | 2023.10.01 |