Context API
Context는 state나 data에 접근하고자하는 컴포넌트를 Provider로 감싸 여러 컴포넌트가 데이터에 접근할 수 있도록 해준다.
이는 일반적으로 리액트에는 발생하는 Props drilling 문제를 해결해줄 수 있다.
예를 들어 아래와 같이 App - Header - UserInfo - Greeting 처럼 중첩되어있다고 가정하자. 우리는 단지 사용자에게 Hi, {user}! 라는 메세지를 Greeting 컴포넌트에서 표시하고 싶을 뿐이다.
따라서 Header, UserInfo에서는 user state가 필요없음에도 불구하고 중간 전달자로서 children인 Greeting 컴포넌트를 위해 props를 내려줘야하는 문제가 발생한다.
import { useState } from "react";
function App() {
const [user, setUser] = useState("user1");
return (
<Header user={user} />
)
}
function Header({ user }) {
return (
<div>
<h3>Header</h3>
<UserInfo user={user} />
</div>
);
}
function UserInfo({ user }) {
return (
<div>
<UserIcon />
<Greeting user={user} />
</div>
)
}
function Greeting({ user }) {
return <p>Hello {user}!</p>;
}
이런 문제를 Context API를 사용하면 Context를 구독하는 모든 children에서 value에 접근할 수 있도록 함으로써 불필요한 props 전달을 없애준다.
import { createContext, useContext } from 'react';
const UserContext = createContext();
function App() {
return (
<UserContext.Provider value={{ "user": "user1" }}>
<Header />
</UserContext.Provider>
)
}
function Header() {
return (
<div>
<h3>Header</h3>
<UserInfo />
</div>
);
}
function UserInfo() {
return (
<div>
<UserIcon />
<Greeting />
</div>
)
}
function Greeting() {
const { user } = useContext(UserContext);
return <p>Hello {user}!</p>;
}
하지만 Context API에는 두가지 문제점이 있으므로 남용해서는 안된다.
바로 1) 컴포넌트 재사용성 2) 성능 측면에서 문제가 발생할 수 있기 때문이다.
1) 컴포넌트 재사용성
먼저 컴포넌트 재사용성 측면을 살펴보자.
예를 들어 우리가 Greeting 컴포넌트를 Header가 아닌 MainPage의 어딘가에서 표시하고 싶다고 가정하자.
import { createContext, useContext } from 'react';
const UserContext = createContext();
function App() {
return (
<UserContext.Provider value={{ "user": "user1" }}>
<Header />
</UserContext.Provider>
<MainPage />
)
}
function MainPage() {
return (
...
<Greeting />
...
}
}
function Greeting() {
const { user } = useContext(UserContext);
return <p>Hello {user}!</p>;
}
MainPage 컴포넌트는 UserContext Provider의 바깥에 있고 그 안에 Greeting 컴포넌트를 포함하고 있을 경우 리액트는 render error을 발생시킨다.
즉 Greeting 컴포넌트는 Context Provider 내부에서만 사용될 수 있으며 그렇기에 컴포넌트를 재사용하기 어려워지는 것이다.
위 예제를 보고 이렇게 생각할 수도 있겠다.
그렇다면 전체 앱을 Context Provider로 감싸면 되지 않을까?
하지만 이 해결책은 규모가 큰 앱에서는 성능 문제를 야기할 수 있다.
2) 성능
왜냐하면 Context API는 state가 업데이트되면 해당 Context를 구독하는 모든 컴포넌트에 state 변경사항을 전파하고, 이는 모든 Consumer 컴포넌트가 리렌더링 되는 것을 의미한다.
따라서 전체 앱을 하나의 Context로 감싸면 해당 state가 변경될 때 마다 컴포넌트 전체가 리렌더링되므로 비효율적이다.
그러면 어떻게 하면 props drilling 문제를 해결하면서도 Context API의 단점을 피해갈 수 있을까?
리액트 공식문서를 보면 단순히 여러 레벨에 걸쳐 props를 전달하는 문제를 해결할 때는 context보다 Component Composition(컴포넌트 합성)이 더 간단한 해결책일 수도 있다고 적혀있다.
If you only want to avoid passing some props through many levels, component composition is often a simpler solution than context.
그렇다면 컴포넌트 합성은 무엇이고 Context 와 다른 점은 무엇일까?
컴포넌트 합성(Component Composition)
리액트는 컴포넌트 합성이라는 기본 원칙을 사용한다.
리액트에서는 props로 stae만 전달할 수 있는 것이 아니라 다른 Component도 전달가능하다.
function App() {
const [user, setUser] = useState("user1");
return <UserInfo Greeting={<Greeting user={user} />} />;
}
function UserInfo({ Greeting }) {
return (
<UserIcon />
{Greeting}
)
}
따라서 위와 같이 중간 컴포넌트(UserInfo)에 props를 일일이 전달하는 방법 대신 상위 컴포넌트(App)에서 하위 컴포넌트(Greeting)를 직접 불러와 state를 전달한 후 그 하위 컴포넌트(Greeting)를 중간 컴포넌트(UserInfo)에 직접 전달할 수 있다.
혹은 props의 children을 사용하는 방법도 있다.
function App() {
const [user, setUser] = useState("user1")
return (
<Header>
<UserInfo>
<Greeting user={user} />
</UserInfo>
</Header>
)
}
function Header({ children }) {
return (
<div>
<h3>Header</h3>
{children}
</div>
);
}
function UserInfo({ children }) {
return (
<div>
<UserIcon />
{children}
</div>
)
}
function Greeting({ user }) {
return <p>Hello {user}!</p>;
}
이렇듯 컴포넌트 합성을 사용하면 오히려 문제를 더 쉽게 해결할 수 있으며, 컴포넌트 재사용성 측면에서도 더 나은 것을 볼 수 있다.
리액트에서 굳이 필요하지 않은데도 Context API나 Redux로 state를 관리하는 오버 엔지니어링 사례가 흔하다.
하지만 내가 개발하는 앱을 잘 분석해 그의 규모와 필요에 맞게 컴포넌트 재사용성 및 성능 문제를 고려하며 그에 맞는 방법을 사용하는 것이 중요하다.
따라서 컴포넌트 합성으로도 충분히 해결할 수 있는 문제라면 Context API 사용을 지양하는 것이 좋겠다.
Reference
- React 공식문서 - Composition vs Inheritance
- A better way of solving prop drilling in React apps [Post] by David Herbert
'개발 > 프론트엔드 (JS & React)' 카테고리의 다른 글
[React Hooks] useMemo & useCallback (0) | 2022.09.26 |
---|---|
[React Hooks] useReducer 사용해서 Todo list 구현하기 (0) | 2022.09.25 |
[React & Storybook] 2 - 스토리 작성 방법 (1) | 2022.09.23 |
[React & Storybook] 1 - 리액트에서 스토리북 사용하기 (0) | 2022.09.22 |
Github Action으로 CI/CD workflow 추가하기 | Azure로 리액트 앱 배포하기 (1) | 2022.09.22 |