* 소개
- 지난 장까지 우리는 부모와 자식관계가 다중관계일 때 props를 물려주기 위해서 한단계씩 아래로 물려주었다. 이는 가독성뿐만 아니라 유지보수를 하기도 번거롭다.
- 위와 같은 상황에서 더 간편하게 props를 물려주게 도와주는 것이 ContextAPI이다.
* 사용
- Form이라는 컴포넌트 파일에 main컴포넌트인 MineSearch에서 dispatch를 넘겨주려고 한다. Form은 다음과 같이 작성되어있다.
const Form = () => {
//...
const onClickBtn = useCallback( (e) => {
dispatch({ type: START_GAME, row, cell, mine})
},[row, cell, mine]);
return (
<div>
<input type="number" placeholder="세로" value={row} onChange={onChangeRow} />
<input type="number" placeholder="가로" value={cell} onChange={onChangeCell} />
<input type="number" placeholder="지뢰" value={mine} onChange={onChangeMine} />
<button onClick={onClickBtn}>시작</button>
</div>
)
};
export default Form;
- 기존에는 props로 dispatch를 넘겨줘서 dispatch했었으나 context API를 설정하면 아래에 존재하는 어떠한 컴포넌트에서라도 넘겨 받을 수 있다.
- 현재 main 컴포넌트인 MineSearch를 살펴보자.
import React, { useReducer, createContext, useMemo} from 'react';
import Table from './Table';
import Form from './Form';
export const TableContext = createContext({
tableData: [],
dispatch: () => {},
});
//...
export const START_GAME = 'START_GAME'
const reducer = (state, action) => {
switch (action.type){
case START_GAME:
return {
...state,
tableData: plantMine(action.row, action.cell, action.mine)
}
default:
return state;
}
}
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<TableContext.Provider value={ {tableData: state.tableData, dispatch} }>
<Form />
<div>{state.timer}</div>
<Table />
<div>{state.result}</div>
</TableContext.Provider>
);
};
- 먼저 react.createContext를 불러오고 함수로써 실행한다. 여기에 기본 값을 넣을 수 있는데, 여기서는 큰 의미가 없으므로 형식적인 모양만 맞춰주었다. dispatch는 함수이므로 함수모양으로 작성하였다. 이 때 export const로 작성해야 외부에서 사용가능하다. 액션 타입도 export하여 재사용할 것이다.
- 접근하고 싶은 컴포넌트를 context의 provider로 묶어야한다. 미리 제작했던 context의 Provider를 태그에 넣어주면 된다. 이 때 데이터는 value에 넣어준다. 여기서는 테이블데이터와 dispatch를 넣어주었다.
- 하지만 위의 코드는 수정이 필요하다. 리턴문은 새로 리랜더링 될 때마다 객체가 새로 생기고 contextAPI를 이용하는 자식들도 계속 리랜더링되므로 useMemo로 따로 빼서 캐싱하는 것이 좋다.
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = useMemo( () => ({ tableData: state.tableData, dispatch }), [state.tableData]);
return (
<TableContext.Provider value={value}>
<Form />
<div>{state.timer}</div>
<Table />
<div>{state.result}</div>
</TableContext.Provider>
);
};
- useMemo를 사용하여 tableData가 변경될 때에만 재생성되도록하였다.
- 이제 다시 Form 컴포넌트로 돌아가보자.
import React, { useState, useCallback, useContext } from 'react';
import {START_GAME, TableContext} from './MineSearch'
const Form = () => {
//...
const {dispatch} = useContext(TableContext);
//...
const onClickBtn = useCallback( (e) => {
dispatch({ type: START_GAME, row, cell, mine})
},[row, cell, mine]);
- 여기서는 useContext를 사용한다. 그리고 액션타입과 만들어두었던 TableContext도 불러온다.
- const value로 접근하여 value.dispatch로 접근 가능하지만 구조분해를 하여 dispatch로 작성하도록 하였다.
- 타입은 미리 만든 START_GAME으로 작성하고 row, cell, mine 데이터를 액션에 전달하도록 하였다.
- 위의 메인 jsx파일의 코드를 보면 액션을 제작하였음을 알 수 있다. 지뢰를 랜덤하게 넣는 plantMine함수에 각 값을 넣어서 리턴하도록 하였다.
- 이번에는 오른쪽 클릭 시에 일어나는 이벤트를 만들어 보자.
const onRightClickTd = useCallback( (e) => {
e.preventDefault();
if (halted) {
return;
}
switch (tableData[rowIndex][cellIndex]) {
case CODE.NORMAL:
case CODE.MINE:
dispatch( {type: FLAG_CELL, row: rowIndex, cell: cellIndex});
return;
case CODE.FLAG_MINE:
case CODE.FLAG:
dispatch( {type: QUESTION_CELL, row: rowIndex, cell: cellIndex});
return;
case CODE.QUESTION_MINE:
case CODE.QUESTION:
dispatch({ type: NORMALIZE_CELL, row:rowIndex, cell: cellIndex});
return;
default:
return;
}
}, [tableData[rowIndex][cellIndex], halted]);
return (
<td style={getTdStyle(tableData[rowIndex][cellIndex])}
onClick={onClickTd}
onContextMenu={onRightClickTd}
>{getTdText(tableData[rowIndex][cellIndex])}</td>
)
- 리턴 문을 보면 onContextMenu 이벤트를 통해 오른쪽 클릭을 제어함을 알 수 있다.
- 웹에서 오른쪽 클릭 시 기본적으로 뜨는 메뉴가 안뜨도록 해야 하므로 반드시 e.preventDefault를 넣어준다.
- 데이터 별로 다른 액션을 만들어 주었다. 이 때 바뀌는 값은 데이터이므로 useCallback의 두번째 인자의 배열에 넣어주었다.
- 여기서 액션들을 만들었으면 이제 메인 컴포넌트 파일에서 리듀서로 처리해야한다.
export const START_GAME = 'START_GAME';
export const OPEN_CELL = 'OPEN_CELL'
export const CLICK_MINE = 'CLICK_MINE'
export const FLAG_CELL = 'FLAG_CELL'
export const QUESTION_CELL = 'QUESTION_CELL'
export const NORMALIZE_CELL = 'NORMALIZE_CELL'
const reducer = (state, action) => {
switch (action.type){
//...
case CLICK_MINE:{
const tableData = [...state.tableData];
tableData[action.row] = [...state.tableData[action.row]];
tableData[action.row][action.cell] = CODE.CLICKED_MINE;
return {
...state,
tableData,
halted: true,
}
};
case FLAG_CELL:{
const tableData = [...state.tableData];
tableData[action.row] = [...state.tableData[action.row]];
if (tableData[action.row][action.cell] === CODE.MINE){
tableData[action.row][action.cell] = CODE.FLAG_MINE;
} else {
tableData[action.row][action.cell] = CODE.FLAG;
}
return {
...state,
tableData,
}
}
// ...
default:
return state;
}
}
- 변수들을 export 하는 것을 잊지 말아야 하며, 이것을 받는 곳에서도 import를 잊으면 안된다.
- 귀찮더라도 비동기처리를 위해서 case마다 필요한 데이터는 복제해서 사용하였다.
- 위와 같이 자식컴포넌트에서 액션들을 추상적으로 만들어서 선언하고. 메인이 되는 파일의 리듀서에서 state를 바꾸는 구현은 나중에 세세하게 처리하는 것이 좋다.
참고
이 글은 ZeroCho 님의 리액트 무료 강좌를 수강하며 개인적으로 정리하며 쓰는 글입니다.
인프런
유튜브
'Client > React.js' 카테고리의 다른 글
<리액트 기초> 동적 라우트 매칭 (0) | 2021.03.17 |
---|---|
<리액트 기초> 리액트 라우터 (0) | 2021.03.10 |
<리액트 기초> useReducer (0) | 2021.02.26 |
<리액트 기초> useMemo & useCallback (0) | 2021.02.24 |
<리액트 기초> useEffect (0) | 2021.02.22 |