Context API는 리액트에서 전역적으로 사용할 데이터가 있을 때 유용한 기능이다.
리액트 애플리케이션에서 여러 컴포넌트에서 공유해야 할 상태나 함수등의 값을 트리 구조 상에서 상위에서 하위로 props를 전달하지 않고, 직접적으로 하위 컴포넌트에서 접근할 수 있도록 도와준다.
Context API는 createContext 라는 함수로 생성한다. createContext 함수는 provider와 consumer를 생성한다. provider는 context를 생성하고, 해당 컴포넌트 내의 모든 하위 컴포넌트에서 해당 context에 접근할 수 있도록 한다. consumer는 context를 사용해 값을 가져오는 함수이다.
Context API를 사용하면 props 전달을 줄일 수 있어 코드의 가독성을 높이고 유지보수를 용이하게 할 수 있다. 또한 Redux 등의 상태 관리 라이브러리와 연동하여 사용할 수도 있다. 하지만 Context API를 사용할 때에도 너무 많은 데이터를 전역으로 관리하면 복잡도가 증가할 수 있으므로 적절한 분량으로 사용하는 것이 중요하다.
Context API를 사용한 전역 상태 관리 흐름 이해하기
리액트는 컴포넌트 간에 데이터를 props로 전달하기 때문에 컴포넌트 여기저기서 필요한 데이터가 있을 때는 주로 최상위 컴포넌트인 App의 state에 넣어서 관리한다.
Context API를 사용한 전역 상태 관리 흐름은 다음과 같다.
- 애플리케이션 전체에 공유되는 상태를 관리하기 위해 Context 객체를 생성한다.
- Context 객체에 초기 상태를 설정한다.
- 상태를 변경할 수 있는 함수를 정의한다. 이 함수는 상태를 변경하는 데 필요한 데이터를 파라미터로 받아와서, 이전 상태와 새로운 상태를 결합해 새로운 상태를 만들고, Context 객체의 상태를 업데이트 한다.
- 상태를 사용하는 컴포넌트는 Context 객체를 import해서 Context 객체의 상태를 읽어올 수 있다.
- 컴포넌트가 상태를 변경해야하는 경우, Context 객체에 정의된 함수를 호출해 상태를 변경한다.
- Context 객체의 상태가 변경되면, Context 객체를 import한 모든 컴포넌트는 상태가 변경되었음을 인지하고, 자동으로 다시 렌더링된다.
위 과정들을 통해, Context API를 사용한 전역 상태 관리를 구현할 수 있다. 이를 통해 컴포넌트 간에 상태를 전달하는 것보다 코드가 간결해지고, 상태 변경 로직을 중앙 집중적으로 관리할 수 있어서 유지보수성이 좋아진다. 또한, 여러 컴포넌트가 하나의 상태를 공유하므로 컴포넌트 간의 연관성이 높아져서 효율적인 상태관리가 가능하다.
Context API 사용하기
먼저 프로젝트를 생성해주었다.
npx create-react-app context-tutorial --template typescript
Context 생성
프로젝트를 생성한 후, 새로운 Context를 만들어보자. 다른 파일과 구분짓기 위해 src 디렉토리 내에 context라는 디렉토리를 만들어주었다.
// src/contexts/color.tsx
import { createContext } from "vm";
const ColorContext = createContext({ color: "black" });
export default ColorContext;
createContext 함수를 사용해 새 Context를 만들어주었다.
Consumer 사용
이번에는 ColorBox라는 컴포넌트를 만들어 ColorContext 안에 들어 있는 색상을 보여주도록 하자. 이때 색상을 props로 받아 오는 것이 아니라 ColorContext 안에 들어 있는 Consumer라는 컴포넌트를 통해 색상을 조회할 것이다.
// src/components/ColorBox.tsx
import ColorContext from "../context/color";
export default function ColorBox() {
return (
<ColorContext.Consumer>
{(value: { color: string }) => (
<div
style={{
width: "64px",
height: "64px",
background: value.color,
}}
/>
)}
</ColorContext.Consumer>
);
}
Consumer 사이에 중괄호를 열어 그 안에 함수를 넣어주었다. 이러한 패턴을 Render Props라고 한다. 컴포넌트의 chlldren이 있어야 할 자리에 일반 JSX 혹은 문자열이 아닌 함수를 전달하는 것이다.
Render Props는 컴포넌트 간에 코드를 재사용하는 기술 중 하나이다. 이 패턴은 부모 컴포넌트에서 자식 컴포넌트로 함수를 전달하여 자식 컴포넌트에서 해당 함수를 호출하고 반환된 값을 렌더링하는 방식으로 동작하며, 이를 통해 코드 중복을 피하고 유연한 코드를 작성할 수 있다.
이제 App 컴포넌트에 ColorBox 컴포넌트를 넣어서 렌더링해주면 다음과 같은 화면이 나타난다.
Provider
Provider를 사용하면 Context의 value를 변경할 수 있다. App 컴포넌트를 다음과 같이 작성해보자.
import ColorBox from "./components/ColorBox";
import ColorContext from "./context/color";
function App() {
return (
<ColorContext.Provider value={{ color: "red" }}>
<div>
<ColorBox />
</div>
</ColorContext.Provider>
);
}
export default App;
App 컴포넌트에서는 ColorContext의 Provider를 사용하여 color값이 "red"인 객체를 value prop으로 전달하고 있다. 이는 ColorBox 컴포넌트에서 ColorContext의 Consumer를 사용하여 color값을 가져와 사용할 수 있도록 하기 위한 것이다.
따라서, ColorBox 컴포넌트는 ColorContext에서 color값을 가져와 배경색으로 설정하게 된다.
이러한 방식으로, ColorContext는 앱 전체에서 공유되는 상태를 관리하는 데 사용될 수 있다. 예를 들어, 다양한 컴포넌트에서 공유되는 테마, 로그인 정보, 언어 등의 상태를 관리할 수 있다.
기존에 createContext 함수를 사용할 때는 파라미터로 Context의 기본값을 넣어주었다. 이 기본값은 Provider를 사용하지 않았을 경우에만 사용된다. 만약 Provider를 사용했는데 value를 명시하지 않았다면, 이 기본값을 사용하지 않기 때문에 오류가 발생한다.
동적 Context 사용하기
동적 Context는, Context의 값이 동적으로 변경되는 경우에 사용된다. 예를 들어, 사용자가 로그인한 경우 로그인 정보를 Context에 저장하고, 사용자가 로그아웃할 때는 해당 정보를 제거해 Context의 값이 변경되도록 할 수 있다.
Context 파일 수정하기
Context의 value에는 무조건 상태 값만 있어야 하는 것은 아니다. 함수를 전달해 줄 수도 있다. 기존에 작성했던 ColorContext를 수정해보자.
// src/contexts/color.tsx
import { createContext, useState } from "react";
interface ColorContextType {
state: { color: string; subcolor: string };
actions: {
setColor: React.Dispatch<React.SetStateAction<string>>;
setSubcolor: React.Dispatch<React.SetStateAction<string>>;
};
}
const ColorContext = createContext<ColorContextType>({
state: { color: "black", subcolor: "red" },
actions: {
setColor: () => {},
setSubcolor: () => {},
},
});
function ColorProvider({ children }: { children: React.ReactNode }) {
const [color, setColor] = useState("black");
const [subcolor, setSubcolor] = useState("red");
const value = {
state: { color, subcolor },
actions: { setColor, setSubcolor },
};
return (
<ColorContext.Provider value={value}>{children}</ColorContext.Provider>
);
}
const { Consumer: ColorConsumer } = ColorContext;
export { ColorProvider, ColorConsumer };
export default ColorContext;
위 파일에서 ColorProvider라는 컴포넌트를 새로 작성해주었다.
ColorContextType이라는 새로운 인터페이스를 선언하여 state와 actions가 각각 어떤 타입을 가지는지 지정했다.
ColorContext는 createContext 함수를 사용하여 생성된 객체이고, createContext 함수는 기본값을 가지는 Context 객체를 반환한다. 이 코드에서는 state와 actions 라는 두 개의 속성을 가진 객체를 기본값으로 가지고 있다. 이렇게 state와 actions 객체를 따로 분리해두면 나중에 다른 컴포넌트에서 Context의 값을 사용할 때 편하다. 또한, createContext 함수의 기본값을 ColorContextType에 맞게 변경했다.
ColorProvider는 useState hook을 사용하여 color와 subColor 두 개의 상태를 관리하고 있다. 이 상태들은 value 객체에 담겨 ColorContext.Provider의 value 속성으로 전달된다. 따라서 ColorProvider를 사용하는 하위 컴포넌트에서 ColorContext의 값을 가져오면 state와 actions 속성에 접근할 수 있다.
ColorConsumer는 ColorContext를 사용하는 컴포넌트에서 ColorContext.Consumer 대신에 사용할 수 있는 별칭이다. ColorContext.Consumer는 Context의 값을 받아들이는 함수형 컴포넌트를 반환한다. ColorConsumer는 이 함수형 컴포넌트를 감싸고 있으므로, ColorContext.Consumer 대신에 ColorConsumer를 사용하여 Context의 값을 받아들이는 함수형 컴포넌트를 만들 수 있다.
Context 반영
작성한 코드를 토대로 App 컴포넌트의 ColorContext.Provider를 ColorProvider로 대체하자.
import ColorBox from "./components/ColorBox";
import { ColorProvider } from "./context/color";
function App() {
return (
<ColorProvider>
<div>
<ColorBox />
</div>
</ColorProvider>
);
}
export default App;
또, ColorBox 컴포넌트에서도 ColorContext.Consumer 대신 ColorConsumer로 변경하자.
import { ColorConsumer } from "../context/color";
export default function ColorBox() {
return (
<ColorConsumer>
{({ state }) => (
<>
<div
style={{
width: "64px",
height: "64px",
background: state.color,
}}
/>
<div
style={{
width: "32px",
height: "32px",
background: state.subcolor,
}}
/>
</>
)}
</ColorConsumer>
);
}
이 코드는 ColorBox라는 컴포넌트를 정의하고, 이 컴포넌트는 ColorConsumer를 사용해 ColorProvider에서 제공하는 color와 subcolor 상태값을 가져와서 사용한다.
ColorConsumer는 ColorProvider에서 제공하는 state값을 전달받아 하위 컴포넌트에게 제공하는 컴포넌트이다. ColorBox 컴포넌트 내부에서 ColorConsumer를 사용해 state값에 접근한다. 그리고 state값으로부터 color와 subcolor값을 추출해 두 개의 div요소를 생성한다. 이 div요소는 state.color와 state.subcolor값을 background 색상으로 갖고 있다.
색상 선택 컴포넌트
색상을 선택할 수 있는 SelectColor 컴포넌트를 생성하자.
// src/components/SelectColor.tsx
const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
export default function SelectColors() {
return (
<div>
<h2>색상을 선택하세요.</h2>
<div style={{ display: "flex" }}>
{colors.map((color) => (
<div
key={color}
style={{
background: color,
width: "24px",
height: "24px",
cursor: "pointer",
}}
/>
))}
</div>
<hr />
</div>
);
}
// src/App.tsx
import ColorBox from "./components/ColorBox";
import SelectColors from "./components/SelectColors";
import { ColorProvider } from "./context/color";
function App() {
return (
<ColorProvider>
<div>
<SelectColors />
<ColorBox />
</div>
</ColorProvider>
);
}
export default App;
이런 화면이 나타나는데, 이제 클릭 이벤트를 통해 왼쪽 클릭은 큰 정사각형의 색상을 변경하고, 오른쪽 클릭은 작은 정사각형의 색상을 변경하도록 구현해보자.
import { ColorConsumer } from "../context/color";
const colors = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
export default function SelectColors() {
return (
<div>
<h2>색상을 선택하세요.</h2>
<ColorConsumer>
{({ actions }) => (
<div style={{ display: "flex" }}>
{colors.map((color) => (
<div
key={color}
style={{
background: color,
width: "24px",
height: "24px",
cursor: "pointer",
}}
onClick={() => actions.setColor(color)}
onContextMenu={(e) => {
e.preventDefault();
actions.setSubcolor(color);
}}
/>
))}
</div>
)}
</ColorConsumer>
<hr />
</div>
);
}
onClick과 onContextMenu를 추가해 상태를 변경할 수 있도록 해주었다. onClick은 마우스 좌클릭으로 색상이 선택되었을 때 actions.setColor 함수를 호출해 color 값을 저장한다.
onContextMenu는 마우스 우클릭을 통해 색상을 선택했을 때, 오른쪽 버튼 클릭시 메뉴가 뜨는 기본동작을 무시하고 actions.setSubcolor 함수를 호출해 color 값을 저장하도록 해준다.
색상이 정상적으로 변경됨을 확인했다.
Consumer 대용 함수
Context에 있는 값을 사용할 때 Consumer 대신 다른 방식을 사용해 값을 받아 오는 방법을 알아보자.
useContext
useContext는 리액트에서 Context를 사용할 때 유용한 Hook이다. Context를 사용하면 컴포넌트 트리 전체에서 데이터를 전달하지 않고도 컴포넌트 간에 데이터를 공유할 수 있다.
useContext를 사용하면 Context에서 값을 이용할 수 있다. useContext를 사용하려면 createContext로 만든 Context 객체를 인자로 받아와야한다. 그러면 해당 Context의 Provider로부터 제공되는 값을 가져올 수 있다.
useContext는 useState, useReducer, useCallback과 비슷한 방식으로 동작한다. 즉, 컴포넌트 상태를 업데이트하는 데 사용된다. 만약 Context 값을 업데이트하려면 해당 Context의 Provider에서 값을 업데이트해야 한다.
ColorBox 컴포넌트의 코드를 다음과 같이 수정해보자.
import { useContext } from "react";
import ColorContext from "../context/color";
export default function ColorBox() {
const { state } = useContext(ColorContext);
return (
<>
<div
style={{
width: "64px",
height: "64px",
background: state.color,
}}
/>
<div
style={{
width: "32px",
height: "32px",
background: state.subcolor,
}}
/>
</>
);
}
이전보다 훨씬 간결한 코드를 작성할 수 있다. 만약 children에 함수를 전달하는 Render Props 패턴이 불편하다면, useContext Hook을 사용해 훨씬 편하게 Context 값을 조회할 수 있다.
static contextType
static contextType은 클래스 컴포넌트에서만 사용할 수 있으며, 컴포넌트 내부에서 context를 사용하기 위해 사용된다. contextType 속성을 사용해 this.context를 사용하여 해당 컴포넌트의 context 값을 읽어올 수 있다. contextType에 할당되는 값은 createContext 함수를 통해 생성된 context 객체이다.
import { Component } from "react";
import ColorContext from "../context/color";
class ColorBox extends Component {
static contextType = ColorContext;
render() {
const { state } = this.context;
return (
<>
<div
style={{
width: "64px",
height: "64px",
background: state.color,
}}
/>
<div
style={{
width: "32px",
height: "32px",
background: state.subcolor,
}}
/>
</>
);
}
}
export default ColorBox;
static contextType을 사용한 ColorBox 컴포넌트이다. static contextType 프로퍼티를 ColorContext로 설정하면, context 객체가 this.context 프로퍼티로 할당된다. 따라서, render 함수에서 this.context를 통해 state 프로퍼티를 참조할 수 있다.
정리
기존에는 컴포넌트 간 상태를 교류해야할 경우, 부모 -> 자식 흐름으로 props를 통해 전달해주었다. Context API는 더욱 쉽게 상태를 교류할 수 있도록 해준다.
하지만 프로젝트의 컴포넌트 구조가 간단하고 종류가 많지 않다면 Context를 사용하지 않아도 된다. 전역적으로 여기저기서 사용되는 경우가 많고, 컴포넌트의 갯수가 많은 상황이라면 권장할 뿐이다.
또한, Context API 기반으로 만들어진 리덕스라는 라이브러리가 있다. 리덕스 또한 전역적으로 상태 관리를 도와주며, 미들웨어 기능, 개발자 도구, 코드의높은 유지보수성을 제공하기 때문에 리덕스의 사용도 좋은 방안이 될 수 있다.
'WEB > React' 카테고리의 다른 글
리액트 리덕스 상태관리 - 2 [react/redux/typescript] (0) | 2023.03.24 |
---|---|
리액트 리덕스 상태관리 - 1 [react/redux/typescript] (0) | 2023.03.22 |
리액트 외부 API 연동 실습 [react/API/typescript] (0) | 2023.03.20 |
리액트 라우터 써보기 [react/router/typescript] (0) | 2023.03.19 |
리액트 불변성 유지하기 [react/immer/typescript] (0) | 2023.03.18 |