-
예제로 배우는 react contextReact.js & Next.js 2022. 12. 18. 15:13
Prerequisite
- next.js
- typescript
- react-context
들어가기에 앞서
Context는 리액트 컴포넌트간에 어떠한 값을 공유할수 있게 해주는 기능입니다. 주로 Context는 전역적(global)으로 필요한 값을 다룰 때 사용하는데요, 꼭 전역적일 필요는 없습니다.
Context를 단순히 "리액트 컴포넌트에서 Props가 아닌 또 다른 방식으로 컴포넌트간에 값을 전달하는 방법이다" 라고 접근을 하시는 것이 좋습니다. - velopert상태관리와 react context의 차이?
상태 관리 라이브러리는 상태가 수정되면 관련 컴포넌트만 다시 렌더링됩니다. 이는 업데이트로 인해 모든 컨텍스트 컨슈머(Consumer) 및 그 하위 항목이 다시 렌더링되는 Context API와는 대조적입니다. 즉, 컨텍스트는 적절한 최적화 또는 메모이제이션이 없으면 컨텍스트 프로바이더(Provider)로 부터 시작되는 전체 서브 트리가 리렌더링 됩니다.
Context
context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다.
일반적인 React 애플리케이션에서 데이터는 위에서 아래로 (즉, 부모로부터 자식에게) props를 통해 전달되지만, 애플리케이션 안의 여러 컴포넌트들에 전해줘야 하는 props의 경우 (예를 들면 선호 로케일, UI 테마) 이 과정이 번거로울 수 있습니다. context를 이용하면, 트리 단계마다 명시적으로 props를 넘겨주지 않아도 많은 컴포넌트가 이러한 값을 공유하도록 할 수 있습니다
Method
1.React.createContext
const MyContext = React.createContext(defaultValue);
Context 객체를 만듭니다. Context 객체를 구독하고 있는 컴포넌트를 렌더링할 때 React는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider 로부터 현재값을 읽습니다.
defaultValue 매개변수는 트리 안에서 적절한 Provider를 찾지 못했을 때만 쓰이는 값입니다. 이 기본값은 컴포넌트를 독립적으로 테스트할 때 유용한 값입니다. Provider를 통해 undefined 을 값으로 보낸다고 해도 구독 컴포넌트들이 defaultValue 를 읽지는 않는다는 점에 유의하세요.
상황에 따라서 defaultValue는 null 또는 객체로 표시할 수도 있습니다.
const MyContext = React.createContext(null);
const MyContext = createContext({ user: { name: 'John Doe', age: 33, hobbies: ['learning'], }, logInfo: () => {}, setName: (name: string) => {}, });
2.Context.Provider
<MyContext.Provider value={/* 어떤 값 */}>
Context 오브젝트에 포함된 React 컴포넌트인 Provider는 context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 합니다.
Provider 컴포넌트는 value prop을 받아서 이 값을 하위에 있는 컴포넌트에게 전달합니다.
값을 전달받을 수 있는 컴포넌트의 수에 제한은 없습니다. Provider 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값이 우선시됩니다.
Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 됩니다. Provider로부터 하위 consumer(.contextType와 useContext을 포함한)로의 전파는 shouldComponentUpdate 메서드가 적용되지 않으므로, 상위 컴포넌트가 업데이트를 건너 뛰더라도 consumer가 업데이트됩니다.
context 값의 바뀌었는지 여부는 Object.is 와 동일한 알고리즘을 사용해 이전 값과 새로운 값을 비교해 측정됩니다.
3.Context.Consumer
<MyContext.Consumer> {value => /* context 값을 이용한 렌더링 */} </MyContext.Consumer>
context 변화를 구독하는 React 컴포넌트입니다. 이 컴포넌트를 사용하면 함수 컴포넌트안에서 context를 구독할 수 있습니다.
Context.Consumer의 자식은 함수여야합니다.
이 함수는 context의 현재값을 받고 React 노드를 반환합니다. 이 함수가 받는 value 매개변수 값은 해당 context의 Provider 중 상위 트리에서 가장 가까운 Provider의 value prop과 동일합니다. 상위에 Provider가 없다면 value 매개변수 값은 createContext()에 보냈던 defaultValue와 동일할 것입니다.
context 값을 이용하여 렌더링할 때, 함수 컴포넌트에서 useContext로 대체할 수 있습니다.
4.React.useContext
const value = useContext(MyContext);
context 객체(React.createContext에서 반환된 값)을 받아 그 context의 현재 값을 반환합니다. context의 현재 값은 트리 안에서 이 Hook을 호출하는 컴포넌트에 가장 가까이에 있는 <MyContext.Provider>의 value prop에 의해 결정됩니다.
컴포넌트에서 가장 가까운 <MyContext.Provider>가 갱신되면 이 Hook은 그 MyContext provider에게 전달된 가장 최신의 context value를 사용하여 렌더러를 트리거 합니다. 상위 컴포넌트에서 React.memo 또는 **shouldComponentUpdate** 를 사용하더라도 useContext 를 사용하고 있는 컴포넌트 자체에서부터 다시 렌더링됩니다.
useContext 로 전달한 인자는 context 객체 그 자체 이어야 함을 잊지 마세요.
- 맞는 사용: useContext(MyContext)
- 틀린 사용: useContext(MyContext.Consumer)
- 틀린 사용: useContext(MyContext.Provider)
useContext를 호출한 컴포넌트는 context 값이 변경되면 항상 리렌더링 될 것입니다. 컴포넌트를 리렌더링 하는 것에 비용이 많이 든다면, 메모이제이션을 사용하여 최적화할 수 있습니다.
팁
여러분이 Hook 보다 context API에 친숙하다면 useContext(MyContext) 는 클래스에서의 static contextType = MyContext 또는 <MyContext.Consumer> 와 같다고 보면 됩니다.
useContext(MyContext) 는 context를 읽고 context의 변경을 구독하는 것만 가능합니다. context의 값을 설정하기 위해서는 여전히 트리의 윗 계층에서의 <MyContext.Provider> 가 필요합니다.
실전 동작 확인하기
- 프로젝트 초기화하기
- 폴더 구조 정하기
- context로 객체 관리하기
- 객체의 상태를 관찰하기
- 객체의 상태를 업데이트하기
- 객체를 초기화하기
- _app.tsx 감싸기
- context 사용하기
1. 프로젝트 초기화하기
타입스크립트를 바탕으로 프로젝트를 초기화합니다.
$ npx create-next-app context --typescript
2. 폴더 구조 정하기
📦 ├─ .eslintrc.json ├─ .gitignore ├─ README.md ├─ context │ └─ UserContext.tsx ├─ next.config.js ├─ package.json ├─ pages │ ├─ _app.tsx │ └─ index.tsx ├─ public │ ├─ favicon.ico │ └─ vercel.svg ├─ styles │ ├─ Home.module.css │ └─ globals.css ├─ tsconfig.json └─ yarn.lock ©generated by [Project Tree Generator](<https://woochanleee.github.io/project-tree-generator>)
폴더는 컨텍스트를 관리할 context 폴더 하나만 만들면 됩니다. 이후 사용하고자 하는 이름으로 컨텍스트 파일을 만들어 주세요.
3. context로 객체 관리하기
// context/UserContext.tsx import React, { createContext, useCallback, useState } from 'react'; export interface UserContextValues { user: { name: string; age: number; hobbies: string[]; }; logInfo: () => void; defaultUserInfo: () => void; setName: (name: string) => void; } const contextDefaultValues: UserContextValues = { user: { name: 'John Doe', age: 26, hobbies: ['learning'], }, logInfo: () => {}, defaultUserInfo: () => {}, setName: (name: string) => {}, }; const UserContext = createContext(contextDefaultValues); export const useUser = () => React.useContext(UserContext); const UserProvider = ({ children }: any) => { const [user, setUser] = useState(contextDefaultValues.user); const logInfo = () => { console.log('- user:', user); }; const setName = useCallback( (name: string) => { console.log('- change name to:', name); setUser({ ...user, name: name }); }, [user] ); const defaultUserInfo = useCallback(() => { console.log('- make default user info'); setUser(contextDefaultValues.user); }, []); const contextValue: UserContextValues = { user, logInfo, setName, defaultUserInfo, }; return <UserContext.Provider value={contextValue}>{children}</UserContext.Provider>; }; export default UserProvider;
쉬운 구조 파악을 위해 한번에 몰아 넣었습니다. 안에 들어 있는 내용을 간략하게 설명하면 다음과 같습니다.
- 컨텍스트 객체의 인터페이스 (UserContextValues)
- 컨텍스트 객체의 초기값 (contextDefaultValues)
- 컨텍스트 객체 (UserContext)
- 훅 함수 생성 (useUser)
- Provider 생성 (UserProvider)
- 실제 동작을 위한 함수 오버라이딩
- Provider 제공을 위한 value 생성 (contextValue)
4. _app.tsx 감싸기
사전에 UserContext.tsx에서 Provider를 통해 사용할 value를 지정해줬기 때문에 <Component {…pageProps}/> 를 단지 감싸는 것만으로도 컨텍스트를 사용할 수 있습니다.
import type { AppProps } from 'next/app'; import UserContext from '../context/UserContext'; function MyApp({ Component, pageProps }: AppProps) { return ( <UserContext> <Component {...pageProps} /> </UserContext> ); } export default MyApp;
5 context 사용하기
🗂 pages/index.tsx 에 context를 사용해보겠습니다. 앞서 코드에서 본 것처럼 우리는 useUser 로 컨텍스트를 생성했기 때문에 따로 Consumer를 사용하지 않아도 됩니다.
export const useUser = () => React.useContext(UserContext);
그렇기 때문에 useUser를 통해 사용하고자 하는 변수 및 메서드를 불러오겠습니다.
// pages/index.tsx import React, { useState } from 'react'; import { useUser } from '../context/UserContext'; const Immsi = () => { const { user, logInfo, setName, defaultUserInfo } = useUser(); const [input, setInput] = useState(''); return ( <div> <h1>Hello User!</h1> {Object.entries(user).map(([key, value], idx) => ( <div key={idx}> <span>{key}</span> - <span>{value}</span> </div> ))} <hr /> <button type='button' onClick={logInfo}> info </button> <hr /> <input type='text' value={input} onChange={(e) => setInput(e.target.value)} /> <button type='button' onClick={() => setName(input)}> change the name </button> <button type='button' onClick={defaultUserInfo}> make default user info </button> </div> ); }; export default Immsi;
context api는 기본적으로 ‘전역 상태 관리’를 위한 툴이 아니기 때문에 setState를 통해 직접적으로 상태를 변경할 수 없습니다. 따라서 실제 동작을 위한 오버라이딩 함수가 필요한 것입니다.
컨텍스트 default value를 초기화할 때, 총 네가지로 변수 및 메서드를 만들었습니다.
const contextDefaultValues: UserContextValues = { user: { name: 'John Doe', age: 26, hobbies: ['learning'], }, logInfo: () => {}, defaultUserInfo: () => {}, setName: (name: string) => {}, };
이 contextDefaultValues 를 오버라이딩하여 전달합니다.
const UserProvider = ({ children }: any) => { const [user, setUser] = useState(contextDefaultValues.user); const logInfo = () => { console.log('- user:', user); }; const setName = useCallback( (name: string) => { console.log('- change name to:', name); setUser({ ...user, name: name }); }, [user] ); const defaultUserInfo = useCallback(() => { console.log('- make default user info'); setUser(contextDefaultValues.user); }, []); const contextValue: UserContextValues = { user, logInfo, setName, defaultUserInfo, }; return <UserContext.Provider value={contextValue}>{children}</UserContext.Provider>; }; export default UserProvider;
user
useState 로 관리 중인 state, user는 contextDefaultValues.user 를 초기값으로 넣어주었습니다.
logInfo
지속적으로 user 의 변화를 감지할 함수입니다.
setName
contextDefaultValues의 user는 변경되지 않지만, useState인 user를 통해 변경된 값을 감지할 수 있습니다. 또한 setUser 를 직접 내리지 못하기 때문에 setName 이라는 함수를 선언하여 내부에서 setUser 를 핸들링합니다.
defaultUserInfo
contextDefaultValues.user 는 변경되지 않기 때문에, 다시 초기값으로 돌리고 싶을 경우 setUser 에 contextDefaultValues.user 를 재 할당 해줍니다.
결과 보기
레퍼런스
'React.js & Next.js' 카테고리의 다른 글
냅다 시작하는 리액트 쿼리 (개념편) (1) 2022.12.31 냅다 시작하는 리액트 쿼리 (예제편) (0) 2022.12.27 Next.js - 다양한 SNS 플랫폼에 대응할 수 있는 SEO 도입하기 (0) 2022.10.21 Next.js - next/image blur 사용하기 (0) 2022.10.19 Next.js - middleware 사용하기 (로그인 연동하기) (0) 2022.09.25