1. 탐구
1. Context 세팅
1. CRA
npx create-react-app . —template typescript: 현재 폴더에 typescript 템플릿으로 생성
2. Context 생성
// components/ThemeProvider.tsx
const ThemeContext = createContext({
theme: "light",
toggleTheme: () => {},
});
3. Context.Provider로 상태값 공유
// components/ThemeProvider.tsx
const ThemeProvider = ({ children }: PropsWithChildren) => {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme((prevTheme) => {
return prevTheme === "light" ? "dark" : "light";
});
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export { ThemeProvider};
// App.tsx
export default function App (){
return (
<ThemeProvider>
<Main />
</ThemeProvider>
)
}
4. useContext로 Context 값에 접근
// components/ThemeProvider.tsx
const useTheme = () => {
if(!context) throw new Error("useTheme must be used within a ThemeProvider");
return useContext(ThemeContext);
}
// components/Main.tsx
export default function Main () {
const {theme,toggleTheme} = useTheme();
return (
<div>
{theme}
<button onClick={toggleTheme}>toggle</button>
</div>
)
}
2. Context API와 렌더링 이슈
1. 원시값을 Context로 공유했을 때
// src/App.tsx
export default function App() {
return (
<div className="App">
<ThemeProvider>
<Main />
<Sub value="in ThemeProvider" />
</ThemeProvider>
</div>
);
}
// src/compoennts/Main.tsx
export default function Main() {
console.log("Main");
const { theme, toggleTheme } = useTheme();
return (
<div>
<h1>Main</h1>
<p>{theme}</p>
<button onClick={toggleTheme}>toggle</button>
<Sub value="in Main" />
</div>
);
}
Main에서만 ThemeContext를 사용하고, Sub에서는 사용하지 않는다.
Provider안에 있는 컴포넌트 중 Context를 사용하지 않는 Sub컴포넌트는 리렌더링이 일어나지 않는다. 하지만, Context를 사용하는 Main컴포넌트 내부에 있는 Sub컴포넌트는 리렌더링이 일어난다.
왜냐고? 부모 컴포넌트가 리렌더링이 일어났기에 자식 컴포넌트가 리렌더링이 일어나는 것은 당연하다.
Sub컴포넌트를 메모이제이션해서, props의 값이 변경되지 않는한 리렌더링이 일어나지 않도록 하겠다.
Main컴포넌트만 리렌더링이 일어난다.
이러한 현상은 Context가 아니라 redux, zustand를 사용해도 동일하다. 왜냐하면, 앞에서도 말했듯이 부모 컴포넌트가 리렌더링이 일어났을 때, 자식 컴포넌트가 리렌더링이 일어나는 것은 당연한 것이니깐.
❓ 왜 두 번씩 console에 찍힐까?
react의 strict모드를 활성화하면, 개발 모드에 한해서 두 번씩 렌더링이 일어난다.
이를 원치 않다면 index.js에서 아래의 React.StrictMode컴포넌트를 제거해주면 된다.
// src/index.js root.render( <React.StrictMode> <App /> </React.StrictMode> );
2. 객체를 Context로 공유했을 때
-
Context.Provider
// src/compoentns/UserProvider.tsx const UserContext = createContext({ user: defaultUserValue, setUser: (user: User) => {}, }); const defaultUserValue = { userId: 0, name: "", age: 0, }; export default function UserProvider({ children }: PropsWithChildren) { const [user, setUser] = useState(defaultUserValue); return ( <UserContext.Provider value={{ user, setUser }}> {children} </UserContext.Provider> ); } -
Main 컴포넌트
유저에 대한 정보 userId, name, age를 담은 user객체를 Context.Provider를 이용하여 뿌려준다.
// src/components/Main.tsx export default function Main() { console.log("Main"); const { user, setUser } = useUserContext(); return ( <div> <h1>Main</h1> <h2>나이 : {user.age}</h2> <button onClick={() => setUser((user) => { return { ...user, age: user.age + 1 }; }) } > 나이 + 1 </button> <Sub value="in Main" /> </div> ); } -
Sub 컴포넌트 - Context에 접근하지 않을 때
Main컴포넌트에서는 age값만 변경한다.
export default memo(function Sub({ value }: SubProps) { console.log(value); return <h1></h1>; });Sub컴포넌트는 부모 컴포넌트가 리렌더링이 일어나도 메모이제이션했기에 리렌더링이 일어나지 않는다. 또한, Context값에 접근하지 않는다.
-
Sub 컴포넌트 - Context 에 접근할 때
// src/components/Sub.tsx export default memo(function Sub({ value }: SubProps) { console.log(value); const { user } = useUserContext(); return <h1>{user.name}</h1>; });Context에 접근하기에 리렌더링이 일어난다.
이 때 중요한 점은, Main컴포넌트에서 수정한 age가 아니라, name에 접근했음에도 리렌더링이 일어난다는 것이다.
결론
그렇다. Context API는 같은 Context를 구독한 모든 컴포넌트에서 리렌더링이 일어난다.
3. Zustand으로 렌더링 이슈 해결
가장 최근의 프로젝트에서 사용한 Zustand로 같은 상황을 재연해보겠다.
1. CRA
npx create-react-app . —template typescript: 현재 폴더에 typescript 템플릿으로 생성
2. Zustand 설치 및 세팅
-
npm i zustand -
store
// src/store/user.ts const useUserStore = create((set) => ({ user: { userId: 0, age: 0, name: "", }, setUserAgePlus: () => { set((state) => ({ user: { ...state.user, age: state.user.age + 1 } })); }, })); export { useUserStore };zustand는 slice 패턴으로 함수를 미리 만들어서, 가져다가 사용할 수 있다.
또한, Context나 recoil과 달리 컴포넌트를 감싸지 않아도 된다.
3. 렌더링 테스트
-
Main컴포넌트
// Main.tsx export default function Main() { console.log("Main"); const age = useUserStore((state) => state.user.age); const setUserAgePlus = useUserStore((state) => state.setUserAgePlus); return ( <div> <h1>Main</h1> <h2>나이 : {age}</h2> <button onClick={setUserAgePlus}>나이 + 1</button> <Sub value="in Main" /> </div> ); }Main컴포넌트에서는 age에 접근한다. 나이를 +1하는 로직은 store에서 만들고 관리함으로써 코드가 확실히 간결하다.
이 때, 주의할 점은 age와 setUserAgePlus만 구독해야 하기에 useUserStore에서 user.age, setUserAgePlus만 가져와야 한다. const {age setUserAgePlus} = useUserStore()로 가져올 경우, 모든 값과 함수를 구독해, 구독 중인 다른 값을 변경해도 리렌더링이 일어난다.
-
Sub컴포넌트
export default memo(function Sub({ value }: Subprops) { console.log("Sub " + value); const name = useUserStore((state) => state.user.name); return ( <div> <h1>{name}</h1> </div> ); });위와 동일하다. 구독할 user.name만을 구독한다.
user.name을 변경하지 않는다면 Sub컴포넌트는 리렌더링이 일어나지 않는다.
결론
Zustand를 사용하면, 구독 중인 값이 변경되지 않는 한 리렌더링이 일어나지 않는다.
4. recoil로 렌더링 이슈 해결
1. CRA
npx create-react-app: React 프로젝트 생성
2. recoil 설치 및 세팅
-
npm i recoil -
RecoilRoot로 앱을 감싼다 -
atom 생성
// src/store/index.js export const userState = atom({ key: "userState", default: { userId: 0, age: 3, name: "박규성", }, });atom은 redux, zustand의 store와 같은 개념이다.
redux와 zustand는 flux패턴이었다면 recoil은 atom아키텍처를 따른다. atom은 데이터 상태의 단위이며, 업데이트와 구독이 가능하다. atom값이 바뀌면 구독하고 있는 컴포넌트는 모두 새로운 값으로 리렌더링된다.
-
selector 생성
// src/store/index.js export const userNameSelector = selector({ key: "filteredUserState", get: ({ get }) => { const user = get(userState); return user.name; }, });selector는 불가능한 비동기 처리와 복잡한 로직을 구현할 수 있다.
Selector는 파생된 상태의 일부를 나타낸다. 파생된 상태를 어떤 방법으로든 주어진 상태를 수정하는 순수 함수에 전달된 상태의 결과물로 생각할 수 있다.
3. 렌더링 테스트
-
main컴포넌트
export default function Main() { console.log("Main"); const [user, setUser] = useRecoilState(userState); const handleButton = () => setUser({ ...user, age: user.age + 1 }); return ( <div> <h1>Main</h1> <h2>나이 : {user.age}</h2> <button onClick={handleButton}>나이 + 1</button> <Sub value="in Main" /> </div> ); }useRecoilState는 Read와 Update할 수 있는 훅이다. 위에서 store에 만든 atom을 가져와 상태를 읽고 수정한다.
-
Sub컴포넌트
export default memo(function Sub({ value }) { console.log("Sub", value); const userName = useRecoilValue(userNameSelector); return ( <div> <h1>{userName}</h1> </div> ); });useRecoilValue는 Read만 할 수 있는 훅이다. 위에서 store에서 만든 selector를 가져와 상태를 읽는다.
처음 렌더링 된 이후 Sub컴포넌트는 리렌더링되지 않는다.
❓ 만약, Sub컴포넌트를 memo로 메모이제이션하지 않았다면?
부모 컴포넌트가 리렌더링되었으므로 ‘당연히’ 자식 컴포넌트인 Sub컴포넌트도 리렌더링이 일어나야 한다. 하지만, memo로 메모이제이션을 함으로써, props값이 변경되지 않는 한, 부모 컴포넌트가 리렌더링이 일어나도 Sub컴포넌트는 리렌더링이 일어나지 않게된다.l
3. Articles
Context API란? 소개 및 사용법
Context API
: 리액트에 내장된 기능으로, Props를 사용하지 않아도 특정 값이 필요한 컴포넌트끼리 쉽게 값을 공유할 수 있게 해준다.
-
새로운 Context를 만들 때는 createContext함수를 사용한다.
import { createContext } from "react"; const LogContext = createContext('Hello'); export default LogContext; -
이렇게 만든 Contxt로 Context.Provider와 Context.Consumer라는 컴포넌트를 사용할 수 있다.
<LogContext.Provider value="Hello World"> <FeedsStack /> </LogContext.Provider>Provider는 Context안에 있는 값을 사용할 컴포넌트를 감싸주는 역할을 한다. 이 때 공유하고 싶은 값을 value props로 넘겨준다.
[React] Context API
Context
- props : 부모 컴포넌트 → 자식 컴포넌트로 데이터를 넘기기 위해 사용
- state : 한 컴포넌트 안에서 데이터를 다루기 위해 사용
- 단방향 : 이 둘을 사용하면 위에서 아래로 한쪽으로 데이터가 흐른다.
- Props Drilling을 해결하기 위해 React에서는 Flux 개념을 도입 → Context API 제공
-
createContext로 Context 생성
const CountContext = createContext({ count: 0, plusCount: () => {}, });Context는 하나의 React 컴포넌트. 내부적으로 변경 가능한 데이터를 사용하려면 useState를 사용해서 state를 사용해야 한다.
-
Context.Provider로 감싸서 사용
... const CountProvider = ({ children }: Props): JSX.Element => { ... return ( <CountContext.Provider> {children} </CountContext.Provider> ); }; export { CountContext, CountProvider };function App() { return ( <CountProvider> <CountLabel /> <PlusButton /> </CountProvider> ); }Provider내부에서는 Context에 자유롭게 접근할 수 있다.
-
useContext를 사용하여 사용
import { useContext } from 'react'; import { CountContext } from '../../Contexts/Count'; export const CountLabel = () => { const { count } = useContext(CountContext); return <div>{count}</div>; };class형 컴포넌트에서는 Consumer를 사용하지만, 함수형 컴포넌트에서는 useContext훅을 사용한다.
초보자를 위한 리액트 Context - 완벽 가이드 (2021)
리액트 Context를 사용해야 하는 경우
- 앱의 모든 컴포넌트에서 사용할 수 있는 데이터를 전달할 때 유용
- theme 데이터
- user 데이터
- locale 데이터
- Context는 전체적인 상태 관리가 아니라, 데이터를 쉽게 사용하기 위해 만들어졌다. 데이터는 자주 업데이트할 필요가 없는 리액트 Context에 위치해야 한다.
Context가 redux를 대체하는가
- state를 업데이트하는 것이 아니라 컴포넌트에 전달만 해주는 경우라면 redux같은 전역 상태 관리 라이브러리가 필요하지 않을 수 있다
리액트 Context사용 시 주의 사항
- context는 서드 파티 라이브러리 없이 임시 상태 관리 라이브러리를 생성하는 것이 가능하다.
- 하지만 성능 상의 문제로 권장되지 않는다.
- Context를 사용하고 있는 모든 컴포넌트에서 재렌더링이 일어난다.
- state가 거의 없는 작은 앱의 경우(theme 데이터 등) 이러한 성능 이슈가 일어나지 않는다. 하지만, 상태 변경이 자주 일어난다면 문제가 될 것이다.
동료의 이슈에 이제라도 응답 해 보기!(context API 관련)
1. Context API는 왜 쓰고, 그렇다면 Redux는 필요없을까?
- 링크 내용 : 위와 같이 ChildComponentOne은 Context를 사용하지도 않았고 그저 Context를 사용하는 컴포넌트를 렌더링 했을 뿐인데 리렌더링이 발생합니다.
- 절반은 맞고 절반은 틀린 내용
- Context를 구독하는 컴포넌트의 자식 컴포넌트는 함게 리렌더링되지만, 부모 컴포넌트는 이와 관련이 없다.
- 부모 컴포넌트의 상태값이 바뀌면 자식이 리렌더링되는 당연한 일이다.
- state를 App단에서 사용하였기에, App이 리렌더링이 일어나서, 부모 컴포넌트에 리렌더링이 일어난 것임.
2. 언제 context API를 사용하고 언제 zustand나 redux를 사용할까요?
- Context API에는 selector가 없다.
- useSelector를 활용해 컴포넌트 단위별 적절한 slice만 선택하여 사용할 수 있다. 다른 slice가 업데이트 되든 말든, 각 컴포넌트는 자신이 이용하는 slice에만 반응하여 리렌더링 된다.
왜 컨텍스트로 zustand나 Recoil, redux를 대체할 수 없을까요?
- Context는 Reactivity를 제공하지 않는다. 즉 selector 등으로 상태 변화에 대한 컴포넌트 구독 매커니즘을 구현할 수 없다.
- 해당 Context의 상태가 바뀌면, 해당 Context에 의존하는 모든 컴포넌트는 상태가 변경된다.
- redux, zustnad는 Selector구독이 가능하기에 상태가 아얘 새로 만들어져도 변경된 셀만 렌더링이 가능하다.
- recoil은 해당 atom에 의존적인 컴포넌트만 변겨된다.
결론
Context API는 객체로 상태를 관리할 때, 하나의 값이 변경되면 다른 모든 상태를 구독한 컴포넌트가 리렌더링이 일어난다. 하지만, zustand, redux는 useSelector를 제공하여, 각 변경한 상태에 대해서만 해당하는 컴포넌트에 리렌더링이 일어나게 할 수 있다.
React Context를 쓰면 전체가 리렌더링된다고?
Context Provider로 감싸진 모든 컴포넌트는 Context값이 변경되면 모두 리렌더링이 일어난다. 그래서, React.memo를 이용해서 메모이제이션을 해야한다.
Context API는 왜 쓰고, 그렇다면 Redux는 필요없을까?
- React는 부모 > 자식 컴포넌트로 데이터가 단방향으로 흐르는 단방향 데이터 흐름이다.
- 자식 컴포넌트 > 부모 컴포넌트로 데이터를 전달하기 위해서는 상태 끌어올리기를 해야 한다.
- 이에 대한 방법은 props로 setState Dispatch함수를 넘겨주는 방식도 있다.
Context API의 리렌더링 이슈
- Context를 둘러싸고 있는 모든 컴포넌트가 리렌더링된다.
Redux와의 차이점
- Context Provider는 value에 직접 setState를 넣어주었다.
- 상태 관리의 정의
- 초기 값을 저장한다
- 현재 값을 읽을 수 있다
- 값 업데이트가 가능하다
- Context API는 위와같은 동작을 수행하지 않는다.
- Redux는 상태 관리 정의에 들어맞는다.
- store로 초기 값을 지정한다.
- useSelector롤 state를 읽을 수 있다.
- Actions을 Dispatch(전달)해서 state를 업데이트할 수 있다.
- Redux는 리렌더링 관련 이슈도 최적화르 해주고, 상태 값을 종속시키지 않고 바깥에서 할 수 있게 해준다.
결론
Redux는 전역 상태 관리하는 라이브러리이고, Context API는 상태를 관리하는 게 아니다. Context API는 초기 값 지정도, 상태값 변경도, 읽기도 useState로 직접 만들어줘야 한다.