안녕하세요! RyuWoong입니다.
이번에 이야기의 주제는 Context API 입니다.
Context?
우선 Context에 대해 먼저 알아봅시다. Context는 React 컴포넌트 트리 안에서 전역적(global)이라고 볼 수 있는 데이터를 공유할 수 있도록 고안된 방법입니다. React를 공부하다보면 전역상태관리라는 말을 한번쯤은 들어보신적이 있을 겁니다.
짧게 말하면 전역상태관리를 위해 고안된 방법이라고 말씀드릴수 있겠네요. 전역으로 관리하는 데이터로는 로그인 정보나 테마, 언어 설정 등을 예로 들수 있을 것 같습니다.
어떤 때에 사용하면 좋을까요?
일반적으로 React는 데이터 구조가 하향식(단방향)입니다. 따라서 하위 컴포넌트에 데이터를 전달하기 위해선 Props Drilling이 발생하게 되는데 깊은 Drilling (3~4개의 하위 컴포넌트 정도?)이 발생하게 되면 유지보수에 어려움이 생기게 됩니다.
이런 경우 Context를 활용할 수 있을거 같습니다. React 공식문서에도 아래와 같이 말하고 있습니다.
🌻 context의 주된 용도는 다양한 레벨에 네스팅된 많은 컴포넌트에게 데이터를 전달하는 것입니다. context를 사용하면 컴포넌트를 재사용하기가 어려워지므로 꼭 필요할 때만 쓰세요.
여러 레벨에 Props를 전달하는 것은 합성 컴포넌트를 만드는 것이 더 간단한 해결책이 될 수 있다라고 덧 붙이는데 다음번 포스팅에서 알아보도록 하겠습니다.
제어의 역전
<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
Page Component 내부에 PageLayout, NavigationBar를 거쳐 Avatar에게 user, avatarSize 값을 전달합니다.
실제로 값을 사용하는 곳은 Avatar Component 뿐인데 위 많은 컴포넌트를 거쳐서 전달한다는 것은 꽤나 번거로운 작업입니다.
어떤 해결책이 있을까요?
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// 이제 이렇게 쓸 수 있습니다.
<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout userLink={...} />
// ... 그 아래에 ...
<NavigationBar userLink={...} />
// ... 그 아래에 ...
{props.userLink}
위와 같이 Page Component 내에서 Avatar Component를 묶어 하위로 전달하면 더이상 user, avatarSize 값에 대해 나머지 Component들은 신경쓰지 않아도 되기 때문에 Context를 사용하지 않고 해결 할 수 있습니다.
이것을 제어의 역전 이라고 합니다. 넘겨줘야하는 Props의 수는 줄고 최상위 Component의 제어력은 더 커지기 때문에 코드를 깔끔하게 작성할 수 있습니다. 하지만 역전이 항상 옳은 것은 아닙니다. 복잡한 로직을 상위에 옮기면 상위 Component들은 더 난해해지고, 하위 Component들은 필요 이상으로 유연해져야 합니다.
function Page(props) {
const user = props.user;
const content = <Feed user={user} />;
const topBar = (
<NavigationBar>
<Link href={user.permalink}>
{user ? <Avatar user={user} size={props.avatarSize} /> : <LoingButton />}
</Link>
</NavigationBar>
);
return (
<PageLayout
topBar={topBar}
content={content}
/>
);
}
만약 위 패턴을 사용한다면 자식과 직속 부모를 분리(decouple)하는 문제는 대개 해결할 수 있습니다. 그리고 Rendering 되기 이전에 자식 Component가 부모 Componet와 소통할 수 있습니다. 이 뜻은 부모 Component의 값에 따라 자식을 어떻게 Rendering 할지 다룰 수 있다는 의미입니다. 만약 user에 값이 없다면, Avatar Component 대신 Login Button을 렌더링 하도록 할 수 있는 것이죠.
하지만 같은 데이터를 트리 안 여러 레벨이 있는 많은 컴포넌트에 전달해야할 때도 있습니다. 데이터 값이 변할 때마다 모든 하위 컴포넌트에게 전달이 필요할 때 Context API를 사용하면 깔끔하고, 명확하게 값을 전달 할 수 있습니다.
Context API
createContext
const MyContext = React.createContext(defaultValue);
위 함수를 이용하여 Context 객체를 만듭니다. Context 객체를 구독하고 있는 Component를 렌더링 할때는 React는 트리 상위에서 가장 가까이 짝이 맞는 Provider로 부터 현재 값을 읽습니다.
🌻 defaultValue 매개변수는 트리 안에서 적절한 Provider를 찾지 못했을 때만 쓰이는 값입니다. 이 기본값은 컴포넌트를 독립적으로 테스트할 때 유용한 값입니다. Provider를 통해 undefined을 값으로 보낸다고 해도 구독 컴포넌트들이 defaultValue 를 읽지는 않는다는 점에 유의하세요.
Context.Provider
<MyContext.Provider value={/* 어떤 값 */}>
Provider는 Context를 구독하는 Component들 에게 Context의 변화를 알리는 역할을 합니다.
Provider는 value prop을 받아서 이 값을 하위에 있는 Component에게 전달합니다. 값을 전달 받을 수 있는 Component 수에 제한은 없습니다. 또한 Provider 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값이 우선시됩니다.
Provider 하위에서 Context를 구독하는 모든 Component는 Provider의 Value가 바뀔 때마다 다시 렌더링 됩니다.
Context.Consumer
<MyContext.Consumer>
{value => /* context 값을 이용한 렌더링 */}
</MyContext.Consumer>
context 변화를 구독하는 React Componenet 입니다만, useContext 훅을 사용하면 더 쉽고 간편하게 사용할 수 있습니다.
useContext
const value = useContext(MyContext);
// 옳은 사용 = useContext(MyContext);
// 틀린 사용 = useContext(MyContext.Consumer);
// 틀린 사용 = useContext(MyContext.Provider);
context 객체를 받아 그 context의 현재 값을 반환합니다. context의 현재 값은 트리 안에서 이 Hook을 호출하는 컴포넌트에 가장 가까이 있는 <MyContext.Provider>의 value prop에 의해 결정 됩니다.
컴포넌트에서 가장 가까운 <MyContext.Provider>가 갱신되면 이 Hook은 MyContext.Provier에게 전달된 가장 최신의 context value를 받아오면서 Re-Rendering 합니다. React.memo나 shouldComponentUpdate를 사용하더라도 useContext를 사용하고 있는 컴포넌트 자체부터 Re-Rendering 됩니다. 즉 useContext를 호출한 컴포넌트는 Context 값이 변경되면 항상 Re-Rendering 됩니다. 만약 Component Re-Rendering 하는 것에 비용이 많이 든다면 Memozation을 사용하세요!
🌻 여러분이 Hook 보다 context API에 친숙하다면 useContext(MyContext)는 클래스에서의 static contextType = MyContext 또는 <MyContext.Consumer>와 같다고 보면 됩니다.
useContext(MyContext)는 context를 읽고 context의 변경을 구독하는 것만 가능합니다. context의 값을 설정하기 위해서는 여전히 트리의 윗 계층에서의 <MyContext.Provider>가 필요합니다.
예제.
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
이렇게 Context API 를 알아 보았습니다.
처음 Context라는 것이 등장했을 때 React 공식 문서에서도 쓰지말라고 권고하기도 하고 말이 많았던걸로 기억합니다.
그래서 Hook이 등장하고 해당 개념을 접할때 매우 혼란스러웠던적이 있습니다. 분명 이론적으로 필요는 한데 사용하지 말라고하니까
현재는 Context가 잘 정립되고, Redux 라이브러리 내에서도 사용되니 이제는 사용해도 될 것 같습니다.
특히 작은 규모의 어플리케이션에서는 전역상태관리 라이브러리를 쓰지 않고 Context API 만으로 충분하지 않을까 생각합니다!
Context 이야기는 여기까지 입니다!
'Front-End > React' 카테고리의 다른 글
[React] Compound Component .10 (0) | 2023.02.15 |
---|---|
[React] Custom Hook .09 (0) | 2023.02.14 |
[React] Hooks - useReducer .07 (0) | 2023.02.03 |
[React.js] Hooks - useRef .06 (0) | 2021.01.20 |
[React.js] Hooks - useEffect .05 (0) | 2021.01.05 |
삽질의 기록과 일상을 남기는 블로그입니다. 주로 React Native를 다룹니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!