지난 시간에 useReducer 사용법을 배워보았다.
useReducer는 Redux에서 차용한 개념으로 관리해야할 state가 여러 개일 때 그런 state를 하나로 묶어주는 역할, 그리고 state를 변경할 때 dispatch로 action을 처리해주는 역할을 담당했다. useReducer에서 state를 바꿀 때 이 동작이 비동기로 실행되므로 값 변경 시점에 대해 유의하여야 했다. 또, dispatch를 할 때 해당 함수를 자식 컴포넌트에 상속시켜주는 과정에서 코드의 복잡도가 높아지는 경향이 있었다.(A → B → C → D)
위의 상속 단계의 복잡도를 개선해주기 위해 Context API를 사용해볼 수 있는데, 이번 지뢰찾기를 만들어보면서 사용법에 대해 알아보자
먼저 지뢰찾기 게임에는 아래와 같은 컴포넌트가 필요하다.
먼저 useReducer를 사용해서 Form 영역에 시작버튼 액션을 실행시키려면 어떻게 할 수 있을까? 아래와 같이 가장 최상단의 MineSearch 컴포넌트에서 useReducer를 선언한 뒤 dispatch 함수를 자식 컴포넌트에게 상속해준다.
// MineSearch.jsx
import React, { useReducer } from "react";
import Tables from "./Tables";
// 1. 초기 state 설정
const initialState = {
tableData: [],
timer: 0,
result: "",
};
// 2. action Reducer 설정
const reducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
const MineSearch = () => {
// 3. useReducer 선언
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<Form dispatch={dispatch} />{/* dispatch 함수의 상속 */}
<div>{state.timer}</div>
<Tables />
<div>{result}</div>
</>
);
};
export default MineSearch;
이번에는 useReducer를 이용해 dispatch를 상속하는 구조가 아닌, ContextAPI를 이용해 직접 actions을 실행시켜보자.
먼저 MineSearch 컴포넌트에서 createContext를 호출하여 필요한 것들을 선언해준다.
import React, { createContext, useMemo } from "react";
import Tables from "./Tables";
export const START_GAME = "START_GAME";
export const CODE = {
OPENED: 0, // 0 이상이면 다 Opened : 정상적으로 연 칸
NORMAL: -1, // 보통
QUESTION: -2, // 물음표 심기
FLAG: -3, // 깃발 심기
QUESTION_MINE: -4, // 물음표를 꽂았는데 지뢰가 있을 때
FLAG_MINE: -5, // 깃발을 꽂았는데 지뢰가 있을 때
CLICK_MINE: -6, // 지뢰를 눌렀을 때
MINE: -7, // 지뢰
};
// 1. createContext로 초기 값 선언
export const TableContext = createContext({
tableData: [],
dispatch: () => {},
});
const plantMine = (row, cell, mine) => {};
const MineSearch = () => {
// 3. useMemo를 적용한 value 생성
const value = useMemo(
() => {
tableData: state.tableData;
},
dispatch,
[state.tableData]
);
return (
// 2. Provider메서드로 선언한 contextAPI를 적용해준다.
<TableContext.Provider value={value}>
<Form />
<div>{state.timer}</div>
<Tables />
<div>{result}</div>
</TableContext.Provider>
);
};
export default MineSearch;
하위 Form 컴포넌트에서 action dispatch 구현
// Form.jsx
import React, { useState, useCallback, useContext } from "react";
// 1. 상위 컴포넌트의 default value import
import { TableContext } from "./MineSearch";
const Form = () => {
const [row, setRow] = useState(10);
const [cell, setCell] = useState(10);
const [mine, setMine] = useState(20);
// 2. useContext를 사용해 dispatch 함수를 선언
const { dispatch } = useContext(TableContext);
const onChangeRow = useCallback((e) => setRow(e.target.value), []);
const onChangeCell = useCallback((e) => sestCell(e.target.value), []);
const onChangeMine = useCallback((e) => setMine(e.target.value), []);
// 3. 시작버튼 클릭 시 액션(START_GAME) dispatch
const onClickBtn = useCallback(() => 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;
먼저 상단 onClickBtn 동작시 이루어지는 START_GAME에 대한 action dispatch를 구현해보자.
import React, { useReducer, createContext, useMemo } from "react";
import Tables from "./Tables";
import Form from "./Form";
export const START_GAME = "START_GAME";
export const CODE = { ... };
export const TableContext = createContext({ ... });
const initialState = { ... };
const reducer = (state, action) => {
// 1. START_GAME 액션 리듀서 선언
switch (action.type) {
case START_GAME:
return {
...state,
tableData: plantMine(action.row, action.cell, action.mine),
};
default:
return state;
}
};
// 2. 지뢰심기 함수 구현
const plantMine = (row, cell, mine) => {
// 2-1. 지정한 row * cell만큼의 기본 배열(0 ~ (row*cell - 1))을 만들어준다.
const candidate = Array(row * cell)
.fill()
.map((arr, i) => {
return i;
});
// 2-2. 몇번째 칸에 지뢰가 있는지 shuffle 정렬로 뽑는다.
const shuffle = [];
while (candidate.length > row * cell - mine) {
const chosen = candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0];
shuffle.push(chosen);
}
const data = [];
// 2-3. 모든 칸에 닫힌 칸을 만든다.
for (let i = 0; i < row; i++) {
const rowData = [];
data.push(rowData);
for (let j = 0; j < cell; j++) {
rowData.push(CODE.NORMAL);
}
}
// 2-4. shuffle의 갯수만큼 지뢰를 심어준다.
for (let k = 0; k < shuffle.length; k++) {
const ver = Math.floor(shuffle[k] / cell);
const hor = shuffle[k] % cell;
data[ver][hor] = CODE.MINE;
}
// 2-5. 데이터 반환
return data;
};
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => ({ tableData: state.tableData, dispatch }), [state.tableData]);
return {
<TableContext.Provider value={value}>{/* components.. */}</TableContext.Provider>
);
};
export default MineSearch;
START_GAME
액션에 대한 리듀서를 작성해준다.
Form 컴포넌트에서 정해준 row, cell, mine 갯수를 바탕으로 지뢰찾기 table이 생성된다.