하위 컴포넌트 사이에 공유되는 데이터를 위해 매번 Root Component (공통 부모 컴포넌트) 를 수정하여 모든 컴포넌트에 Props를 전달하여 데이터를 사용하는 것은 매우 비효율적이다.
이처럼 비효율적인 문제를 해결하기 위해 리액트에서는 Flux 라는 개념을 도입하였고 그에 걸맞은 Context API 를 제공학 시작했다.
Context 는 부모 컴포넌트로부터 자식 컴포넌트로 전달되는 데이터의 흐름과는 상관없이 전역적으로 데이터를 다룬다.
Context 를 사용하기 위해서는 Context API를 사용하여 Context 의 Provider와 Consumer를 생성해야 한다.
npx create-react-app context-todo-list --template=typescript
npm install --save styled-components
npm install --save-dev @types/styled-components jest-styled-components
변경전 컴포넌트 구조
변경예정인 컴포넌트 구조
Context API 적용 구조
작업 파일은 아래와 같다. App.tsx를 제외한 나머지는 생성해야한다.
코드
InputContainer/index.tsx
import React, {useState, useContext} from 'react';
import Styled from "styled-components";
import {ToDoListContext} from 'Contexts';
import {Button} from "Components/Button";
import {Input} from "Components/Input";
const Container = Styled.div`
display : flex;
`;
export const InputContainer = () => {
const [toDo, setToDo] = useState('');
const {addToDo} = useContext(ToDoListContext);
return (
<Container>
<Input placeholder="할 일을 입력해 주세요" value={toDo} onChange={setToDo}/>
<Button label="추가"
onClick={()=>{
addToDo(toDo);
setToDo('');
}}
/>
</Container>
);
}
ToDoList/index.tsx
import React, {useContext} from 'react';
import Styled from "styled-components";
import {ToDoListContext} from 'Contexts';
import {ToDoItem} from "Components/ToDoItem";
const Container = Styled.div`
min-width: 350px;
height: 400px;
overflow-y: scroll;
border: 1px solid #BDBDBD;
margin-bottom: 20px;
`;
export const ToDoList = () => {
const {toDoList, deleteToDo} = useContext(ToDoListContext);
return (
<Container data-testid='toDoList'>
{toDoList.map((item, index) => (
<ToDoItem key={item} label={item} onDelete={() => deleteToDo(index)} />
))}
</Container>
);
};
Components/index.tsx
export * from './Button';
export * from './Input';
export * from './ToDoItem';
export * from './InputContainer';
export * from './ToDoList';
Contexts/ToDoList/index.tsx
import React, {createContext, useState} from "react";
interface Context {
readonly toDoList: string[];
readonly addToDo: (toDo: string) => void;
readonly deleteToDo: (index: number) => void;
}
const ToDoListContext = createContext<Context>({
toDoList: [],
addToDo: (): void => {},
deleteToDo: (): void => {},
});
interface Props {
children: JSX.Element | JSX.Element[];
}
const ToDoListProvider = ({children}:Props): JSX.Element => {
const [toDoList, setToDoList] = useState<string[]>([]);
const addToDo = (toDo: string): void => {
if(toDo) {
setToDoList([...toDoList, toDo]);
}
};
const deleteToDo = (index: number): void => {
let list = [...toDoList];
list.splice(index, 1);
setToDoList(list);
};
return (
<ToDoListContext.Provider
value={{
toDoList,
addToDo,
deleteToDo,
}}>
{children}
</ToDoListContext.Provider>
);
};
export {ToDoListContext, ToDoListProvider};
Components/index.tsx
export * from './ToDoList';
App.tsx
import React from 'react';
import Styled from 'styled-components';
import {ToDoListProvider} from 'Contexts';
import { InputContainer, ToDoList } from 'Components';
const Container = Styled.div`
min-height: 100vh;
background-color: #EEEEEE;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
`;
const Contents = Styled.div`
display: flex;
background-color: #FFFFFF;
flex-direction: column;
padding: 20px;
border-radius: 8px;
box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
`;
function App() {
return (
<ToDoListProvider>
<Container>
<Contents>
<ToDoList/>
<InputContainer/>
</Contents>
</Container>
</ToDoListProvider>
);
}
export default App;
Context API를 사용하여 Context의 Provider 작성
위에있는 Contexts/ToDoList/index.tsx 코드 참고
1) 컨택스트를 만들기 위한 createContext와 리액트에서 데이터를 동적으로 다루기 위한 useState 를 사용
import React, {createContext, useState} from "react";
2) 컨텍스트로 공유할 데이터와 함수들을 인터페이스로 정의하고 정의
interface Context {
readonly toDoList: string[];
readonly addToDo: (toDo: string) => void;
readonly deleteToDo: (index: number) => void;
}
3) 초기값 지정
const ToDoListContext = createContext<Context>({
toDoList: [],
addToDo: (): void => {},
deleteToDo: (): void => {},
});
4) Provider 작성
- 컨텍스트를 사용하기 위해서는 Data를 공유하는 컴포넌트들의 공통 부모 컴포넌트에 Provider를 제공해야한다.
- 여기서 제공한다는 의미는 공통 부모 컴포넌트를 Context의 Provider 안에서 Rendering 되도록 하는것을 의미
- 컨텍스트도 하나의 리액트 컴포넌트이므로 기본적으로 컴포넌트의 구조로 되어 있다
- 따라서 렌더링해야 하는 공통 부모 컴포넌트를 Props로 전달받고 컨텍스트의 Provider 안에서 전달받은 컴포너트를 Rendering 하도록 함으로써 컨텍스트를 사용할 수 있는 환경을 만든다
- 또한 컨텍스트의 Provider 를 설정할 때는 value 라는 Provider 의 Props에 컨텍스트로 제공할 내용을 작성
- 컨텍스트도 하나의 리액트 컴포넌트이므로 동적인 데이터를 다루기 위해서는 State를 사용할 필요가 있다
(1) 공통 부모 컴포넌트 생성
interface Props {
children: JSX.Element | JSX.Element[];
}
(2) 공통 부모 컴포넌트를 Props 를 전달
+ (3) value 정의 (데이터 + 전역 컴포넌트에서 사용될 함수)
+ (4) 동적 데이터를 다루기 위한 usdState 사용
const ToDoListProvider = ({children}:Props): JSX.Element => {
const [toDoList, setToDoList] = useState<string[]>([]);
const addToDo = (toDo: string): void => {
if(toDo) {
setToDoList([...toDoList, toDo]);
}
};
const deleteToDo = (index: number): void => {
let list = [...toDoList];
list.splice(index, 1);
setToDoList(list);
};
return (
<ToDoListContext.Provider
value={{
toDoList,
addToDo,
deleteToDo,
}}>
{children}
</ToDoListContext.Provider>
);
};
5) App 컴포넌트에 Provider 적용
- 컨텍스트에서 할 일 목록 데이터를 전역적으로 다루기 때문에 이제는 App 컴포넌트에서 데이터를 다룰 필요가 없어졌다
- ToDoList 컴포넌트와 InputContainer 컴포넌트는 더이상 App 컴포넌트로부터 데이터를 전달받는 것이 아니라 컨텏스트 안에 있는 전역 데이터를 직접 참조할 예정이므로 ToDoList 컴포넌트와 InputContainer 컴포넌트로 전달하던 모든 props를 제거하였다.
import {ToDoListProvider} from 'Contexts';
import { InputContainer, ToDoList } from 'Components';
function App() {
return (
<ToDoListProvider>
<Container>
<Contents>
<ToDoList/>
<InputContainer/>
</Contents>
</Container>
</ToDoListProvider>
);
}
6) InputContainer 컴포넌트에 Consumer 적용
- 사용자가 입력한 데이터를 다루기 위해 State를 사용할 필요가 있으므로 useState를 추가
- 컨텍스트를 사용하기 위해서 useContext를 추가
- App 컴포넌트와 다르게 컨텍스트를 사용하기위해 ToDoListContext를 추가
- 사용자의 데이터와 함수를 다루기위해 export 안의 return 전 const로 정의
- 부모 컴포넌트인 App 컴포넌트로부터 Props로 데이터를 더는 전달받지 않기때문에 Props 관한 모든 코드 삭제
ex) InputContainer = () =>
import React, {useState, useContext} from 'react';
import Styled from "styled-components";
import {ToDoListContext} from 'Contexts';
import {Button} from "Components/Button";
import {Input} from "Components/Input";
export const InputContainer = () => {
const [toDo, setToDo] = useState('');
const {addToDo} = useContext(ToDoListContext);
return (
<Container>
<Input placeholder="할 일을 입력해 주세요" value={toDo} onChange={setToDo}/>
<Button label="추가"
onClick={()=>{
addToDo(toDo);
setToDo('');
}}
/>
</Container>
);
}
7) ToDoList 컴포넌트에 Consumer 적용
- 데이터를 다루지 않기때문에 useState 없음
- 사용자의 함수를 다루기위해 export 안의 return 전 const로 정의
- 부모 컴포넌트인 App 컴포넌트로부터 Props로 데이터를 더는 전달받지 않기때문에 Props 관한 모든 코드 삭제
ex) ToDoList = () =>
import React, {useContext} from 'react';
import Styled from "styled-components";
import {ToDoListContext} from 'Contexts';
import {ToDoItem} from "Components/ToDoItem";
export const ToDoList = () => {
const {toDoList, deleteToDo} = useContext(ToDoListContext);
return (
<Container data-testid='toDoList'>
{toDoList.map((item, index) => (
<ToDoItem key={item} label={item} onDelete={() => deleteToDo(index)} />
))}
</Container>
);
};
'2022 > 리액트+TDD(完)' 카테고리의 다른 글
Context API / localStorage TestCode (0) | 2022.01.16 |
---|---|
Context API / localStorage - 2 (0) | 2022.01.16 |
클래스 컴포넌트 라이프 사이클 (0) | 2022.01.13 |
함수 컴포넌트에서 클래스 컴포넌트 변환 (0) | 2022.01.12 |
Props/ State - 7 (0) | 2022.01.09 |