8-1. Context API 소개와 지뢰찾기

지난 시간에 useReducer 사용법을 배워보았다.

useReducer는 Redux에서 차용한 개념으로 관리해야할 state가 여러 개일 때 그런 state를 하나로 묶어주는 역할, 그리고 state를 변경할 때 dispatch로 action을 처리해주는 역할을 담당했다. useReducer에서 state를 바꿀 때 이 동작이 비동기로 실행되므로 값 변경 시점에 대해 유의하여야 했다. 또, dispatch를 할 때 해당 함수를 자식 컴포넌트에 상속시켜주는 과정에서 코드의 복잡도가 높아지는 경향이 있었다.(A → B → C → D)

위의 상속 단계의 복잡도를 개선해주기 위해 Context API를 사용해볼 수 있는데, 이번 지뢰찾기를 만들어보면서 사용법에 대해 알아보자

먼저 지뢰찾기 게임에는 아래와 같은 컴포넌트가 필요하다.

  1. Form 영역: 세로 n줄 + 가로 m줄 + 지뢰의 갯수 o개 + 시작버튼 (Form.jsx)
  2. 타이머 영역(state.timer)
  3. Table 영역(지뢰찾기 게임 영역) (Tables.jsx → Tr.jsx → Td.jsx)
  4. 결과 영역(state.result)

먼저 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;

8-2. createContext와 Provider

이번에는 useReducer를 이용해 dispatch를 상속하는 구조가 아닌, ContextAPI를 이용해 직접 actions을 실행시켜보자.

  1. 먼저 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;
    
    1. createContext 함수를 실행하여 default value를 선언해준다. 해당 값은 하위 컴포넌트에서 사용해야하므로 export 처리해줘야 한다.
    2. 선언한 TableContext를 Provider로 묶어주면 하위 컴포넌트에서 모두 접근이 가능하다.
      데이터는 value에 넣어주는데 바로 객체값을 넣어주지 않고 useMemo를 적용한 값으로 넣어준다.
      그 이유는 state가 변경될 때마다 하위 모든 값들이 매번 새로 리렌더링되기 떄문이다.
      아래 value 또한 마찬가지인데, 이를 방지하기 위해 상단에 useMemo를 적용히여 캐싱처리 해준다.(Context API는 성능 최적화가 어렵다.)
    3. useMemo를 적용하여 캐싱처리가 완료된 value값을 만들어준다. dispatch는 항상 같은 값을 유지하므로 두번째 인자값에 따로 넣어주지 않아도 된다.
  2. 하위 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;

8-3. useContext 사용해 지뢰 칸 렌더링

먼저 상단 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;
  1. Form컴포넌트에서 onClickBtn이벤트로 넘어온 START_GAME 액션에 대한 리듀서를 작성해준다. Form 컴포넌트에서 정해준 row, cell, mine 갯수를 바탕으로 지뢰찾기 table이 생성된다.
  2. 지뢰찾기 Table을 생성해주는 plantMine 함수를 별도로 구현해보자. 해당 함수는 기본 테이블 생성(row * cell) → 지뢰 위치 shuffle 정렬로 도출 → 모든 칸에 CODE.NORMAL값 부여 → shuffle 자리에 CODE.MINE(지뢰) 부여 → 데이터 변환의 과정을 거친다.