TIL 22-04-19

68857 단어 ReactReact

React.js

Today I Learned ... react.js

🙋‍♂️ React.js Lecture

🙋‍ My Dev Blog


React Lecture CH 7

1 - useReducer
2 -reducer, action, dispatch
3 - action 만들어 dispatch
4 - 틱택토 게임
5 - 테이블 최적화

useReducer

  • Redux 의 핵심 개념을 도입함.
  • 소규모 앱에서는 Redux를 쓰지 않고, context API와 useReducer만으로 어느정도는 사용 가능.

틱택토(Tic-Tac-Toe) 초기 코드

TicTacToe.jsx

import React, { useState, useReducer } from 'react';
import Table from './Table';

const TicTacToe = () => {
  const [winner, setWinner] = useState('');
  const [turn, setTurn] = useState('O');
  const [tableData, setTableData] = useState([
    ['', '', ''],
    ['', '', ''],
    ['', '', ''],
  ]);

  return (
    <>
      <Table />
      {winner && <div>{winner}님의 승리!</div>}
    </>
  );
};

export default TicTacToe;

Td.jsx

import React from 'react';

const Td = () => {
  return <td>{''}</td>;
};

export default Td;

Tr.jsx

import React from 'react';
import Td from './Td';

const Tr = () => {
  return (
    <tr>
      <Td>{''}</Td>
    </tr>
  );
};

export default Tr;

Table.jsx

import React from 'react';
import Tr from './Tr';

const Table = () => {
  return (
    <table>
      <Tr>{''}</Tr>
    </table>
  );
};

export default Table;

전체적인 구조는 아래와 같다.

td태그(즉 Td컴포넌트)가 모여 Tr이 되고, Tr이 모여 Table이 된다.
그런 Table 컴포넌트를 TicTacToe에 렌더링 한다.

총 4개의 컴포넌트 트리로 구성되어있기 때문에,
최상위


  • state는 최상위 컴포넌트인 TicTacToe가 관리하지만,
    실질적으로 클릭하고 다루는 것은 최하위 컴포넌트인 Td 이다.

  • 띠라서, TicTacToe의 statesetState함수들을 총 3단계를 거쳐 Prop로 전달해야함.
    -> 이런 문제를 해결하기 위해 ontext API를 사용함.


* * *

context API를 배우기에 앞서, state 개수 자체를 줄여주는 useReducer을 알아보자.

  • state가 많아지면 관리가 힘들어짐.
  • useReducer을 사용하면, 하나의 state와 하나의 setState함수로 한번에 관리 가능.

useReducer 사용

import React, { useState, useReducer } from 'react';
import Table from './Table';

const initialState = {
  winner: '',
  turn: 'O',
  tableData: [
    ['', '', ''],
    ['', '', ''],
    ['', '', ''],
  ],
};

const reducer = (state, action) => {
    // state를 어떻게 변경할지 작성
}

const TicTacToe = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const [winner, setWinner] = useState('');
  const [turn, setTurn] = useState('O');
  const [tableData, setTableData] = useState([
    ['', '', ''],
    ['', '', ''],
    ['', '', ''],
  ]);

  return (
    <>
      <Table />
      {winner && <div>{winner}님의 승리!</div>}
    </>
  );
};

export default TicTacToe;
  • 우선, React.useReducer두개의 인자를 갖는다.
  1. reducer 함수 (state를 변경할 함수)
  2. initialState (state들의 초기값 정의)
const [state, dispatch] = useReducer(reducer, initialState)

마치 useState를 작성하듯이, 배열 구조분해 할당으로 state와 dispatch를 선언해준다.
-> useReducer() 함수의 반환값은 배열이므로.

그 다음으로, reducer(함수) 그리고 initialState를 선언해줘야 한다.
-> Hooks는 state가 바뀌면 함수 전체가 재실행되므로, 함수 바깥에 정의해준다.
(= 지난 시간 Lotto 예제에서 getWinNumbers를 바깥에 빼줬듯이.)

useReducer(reducer, initialState)

  • 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있다.
  • 상태 업데이트 로직을 컴포넌트 바깥에 작성 할 수도 있고, 심지어 다른 파일에 작성 후 불러와서 사용 할 수도 있다.
  • 참고 문서

reducer, action, dispatch의 관계

onClickTable 함수

우선, Table 컴포넌트를 클릭했을 때 실행되는 onClickTable 함수를 작성한다.

  • TicTacToe.jsx 내부에 작성함.
  • return()부분, 즉 컴포넌트 어트리뷰트에 들어가는 함수는 무조건 useCallback을 해주자.
// JSX
  return (
    <>
      <Table onClick={onClickTable} />
      {winner && <div>{winner}님의 승리!</div>}
    </>
  );

-> useCallback으로 감싸주어 한번만 선언되도록 함수 자체를 저장해버림.

 const onClickTable = useCallback(() => {
    dispatch({ type: 'SET_WINNER', winner: 'O' });
    // action 객체와 action을 실행하는 dispatch
  }, []);

함수 내부에는 winner을 'O'로 변경해주는 내용을 담음.
여기서, dispatchaction의 개념이 등장함.

dispatch()함수 안에 action 객체를 집어 넣음.
즉, dispatch는 action객체를 실행시켜주는 것.

dispatch({action객체}) 만 한다고 바로 state가 변경되는 것은 ❌
action을 해석해서 직접 state를 바꿔주는 reducer가 필요.

const reducer = (state, action) => {
  // state를 어떻게 변경할지 작성
  switch (action.type) {
    case 'SET_WINNER':
      return {
        ...state,
        winner: action.winner,
      };
  }
};
  • action객체의 type마다 구분할 수 있도록 switch문으로 작성함.
  • state를 스프레드 한 후, winner프로퍼티가 action.winner인 객체 반환.
    -> state객체를 스프레드하여 동일한 새로운 객체를 만들어서 바뀐 값만 수정하여 반환함.

❗️주의

  • state를 직접 바꿔서는 안됨!
state.winner = action.winner;

지금까지의 코드

TicTacToe.jsx

import React, { useState, useReducer, useCallback } from 'react';
import Table from './Table';

const initialState = {
  winner: '',
  turn: 'O',
  tableData: [
    ['', '', ''],
    ['', '', ''],
    ['', '', ''],
  ],
};

const reducer = (state, action) => {
  // state를 어떻게 변경할지 작성
  switch (action.type) {
    case 'SET_WINNER':
      return {
        ...state,
        winner: action.winner,
      };
  }
};

const TicTacToe = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const onClickTable = useCallback(() => {
    dispatch({ type: 'SET_WINNER', winner: 'O' });
  }, []);

  return (
    <>
      <Table onClick={onClickTable} />
      {state.winner && <div>{state.winner}님의 승리</div>}
    </>
  );
};

export default TicTacToe;

Table 컴포넌트

Table.jsx

import React from 'react';
import Tr from './Tr';

const Table = ({ onClick }) => {
  return (
    <table onClick={onClick}>
      <Tr>{''}</Tr>
    </table>
  );
};

export default Table;

props로 onClick을 받으므로 (TicTacToe에서는 onClickTable)
인자로 받아주고, (구조분해 할당으로 받으면 편리) onClick 어트리뷰트에 대입한다.

-> 이제 테이블을 클릭하면 onClickTable에 의해 winner가 dispatch된다.

참고

  • 참고로, action의 type(이름)은 변수로 따로 빼두는 것이 좋다. (오타 방지 + 가독성)
const SET_WINNER = 'SET_WINNER';

  • 기존 state인 state.winner에 action.winner로 넣어줌.
    (단, 직접 할당은 아니고 reducer가 해줌)

reducer, action, dispatch 원리

  • state는 아무도 직접 수정할 수 없음.
  • state를 수정하려면 action을 만들어서 dispatch로 실행해야함.
  • action을 어떻게 처리할지는 -> reducer

3*3 테이블 만들기

1. tr을 세개로

TicTacToe.jsx

...
return (
    <>
      <Table onClick={onClickTable} tableData={state.tableData} />
      {state.winner && <div>{state.winner}님의 승리</div>}
    </>
  );
  • Table 컴포넌트로 tableData를 props로 전달함.
    (tableData도 TicTacToe 컴포넌트의 state인 state.tableData임)

Table.jsx

import React from 'react';
import Tr from './Tr';

const Table = ({ onClick, tableData }) => {
  return (
    <table onClick={onClick}>
      {Array(tableData.length)
        .fill()
        .map((tr) => (
          <Tr />
        ))}
    </table>
  );
};

export default Table;
  • props로 받은 tableData를 받아옴.
  • tableData는 초기값인 [['', '', ''], ['', '', ''], ['', '', '']] 이므로 length는 3임.
  • Array(3)이므로 length가 3인 빈 배열을 만들고, fill()로 공백으로 채움.
  • map 메서드로 각 요소를 로 해줌.

-> Tr이 3개인 테이블 완성됨.

2. td를 세개로

  • 각각의 Tr안에서 td를 세개로 나눠주면 3*3 테이블이 완성됨.
  • 우선, 이차원 배열인 tableData의 각 요소를 rowData라는 props로 Tr 컴포넌트에 전달.

-> 예) tableData = [[1,2,3], [4,5,6], [7,8,9]] 라면?
첫 Tr에는 [1,2,3]가
그다음 Tr은 [4,5,6]가
마지막 Tr은 [7,8,9]가 rowData로 넘어가도록.

Table.jsx

...
return (
    <table onClick={onClick}>
      {Array(tableData.length)
        .fill()
        .map((tr, i) => (
          <Tr rowData={tableData[i]} />
        ))}
    </table>
  );

map 메서드의 두번째 인자인 인덱스를 활용.
-> 인덱스대로 순회하므로, tableData 배열의 0,1,2번째 요소를 각각 Tr의 rowData라는 props로 전달함.


Tr.jsx

import React from 'react';
import Td from './Td';

const Tr = ({ rowData }) => {
  return (
    <tr>
      {Array(rowData.length)
        .fill()
        .map((td) => (
          <Td>{''}</Td>
        ))}
    </tr>
  );
};

export default Tr;
  • 3*3 테이블이 완성됨.

  • React Dev Tools로 살펴보면,
    테이블 셀 하나하나가 컴포넌트로 구성되어있음을 알 수 있다.


action 만들어 dispatch 하기

이제 하나의 셀(Td)을 클릭할 때 마다 O 또는 X가 렌더링 되게 해야함.

  • 지금 클릭한 셀의 위치를 알려면?
    -> map 함수에서 썼던 index도 props로 넘겨주자.
  1. Table.jsx
const Table = ({ onClick, tableData }) => {
  return (
    <table onClick={onClick}>
      {Array(tableData.length)
        .fill()
        .map((tr, i) => (
          <Tr rowIndex={i} rowData={tableData[i]} />
        ))}
    </table>
  );
};
  • map 함수의 인덱스인 i를 Tr 컴포넌트에 rowIndex라는 props로 넘김.
  1. Tr.jsx
const Tr = ({ rowData, rowIndex }) => {
  return (
    <tr>
      {Array(rowData.length)
        .fill()
        .map((td, i) => (
          <Td rowIndex={rowIndex} cellIndex={i}>
            {''}
          </Td>
        ))}
    </tr>
  );
};
  • map 함수의 인덱스인 i를 Td 컴포넌트에 cellIndex라는 props로 넘김
  • Td 컴포넌트로 rowIndex, cellIndex 모두 넘겨줌
    -> Td 컴포넌트에서는 두 props를 다 사용 가능해짐.

Td.jsx

const Td = ({ rowIndex, cellIndex }) => {
  const onClickTd = useCallback(() => {
    console.log(rowIndex, cellIndex);
  }, []);

  return <td onClick={onClickTd}>{''}</td>;
};
  • onClickTd 함수를 만듬.
  • 컴포넌트에 넣어준 함수는 반드시 useCallback으로 감싸줘야함!
  • onClickTd 함수에는 클릭한 위치를 알 수 있도록, rowIndex와 cellIndex가 뜨게 콘솔로그 해봄.


새로운 action 객체 생성

  • TicTacToe의 state.tableData를 복사해서
    해당 클릭한 셀 위치에 'O' 또는 'X'가 들어가게.
  • 'O'또는 'X'는 state.turn을 토글되도록 함.

1. Td.jsx

const Td = ({ rowIndex, cellIndex, dispatch, cellData }) => {
  const onClickTd = useCallback(() => {
    // dispatch (1. 셀클릭 / 2. 턴 교대)
    dispatch({ type: CLICK_CELL, row: rowIndex, cell: cellIndex });
    dispatch({ type: CHANGE_TURN });
  }, []);

  return <td onClick={onClickTd}>{cellData}</td>;
};
  • action 객체는 맘대로 작성해도 된다.
  • reducer로 action 객체를 잘 처리하기만 하면 된다.

2. TicTacToe.jsx reducer 부분

TicTacToe.jsx

const reducer = (state, action) => {
  switch (action.type) {
    // 생략
    case CLICK_CELL: {
      const tableData = [...state.tableData];
      tableData[action.row] = [...tableData[action.row]];
      tableData[action.row][action.cell] = state.turn;
      return {
        ...state,
        tableData,
      };
    }
      
    case CHANGE_TURN: {
      return {
        ...state,
        turn: state.turn === 'O' ? 'X' : 'O',
      };
    }
  }
};

2-1) CLICK_CELL 로직
예를 들어 지금 정 가운데 셀을 클릭했다고 치면
action.row = 1, action.cell = 1이다.

  • tableData의 얕은복사를 한다.
  • tableData[1]을 얕은복사를 한다. -> ['', '', '']
  • tableData[1][1]에 state.turn을 대입.
    (state이므로 현재 turn)

2-2) CHANGE_TURN 로직

  • 삼항연산자 이용.
  • 현재 state.turn이 'O'면 'X'로 바꿔주고,
  • 아니면 'O'로 바꿔줌.

CLICK_CELL과 같은 타입명은 export를 붙여 모듈화 해준다.
-> 하위 컴포넌트에서도 import 해서 사용 가능하도록.

// 하위 컴포넌트에서 사용할 수 있게 모듈로 만듬
export const SET_WINNER = 'SET_WINNER';
export const CLICK_CELL = 'CLICK_CELL';
export const CHANGE_TURN = 'CHANGE_TURN';
  • 두 reducer은 onClickTd에서 생성되는 action 객체를 처리해줌.

3. cellData 전달

위 CLICK_CELL에서 tableData를 변경했으므로,
(정확히는 얕은복사를 해서 셀의 위치에 맞게 state.turn을 대입한 것)

cellData를 화면에 렌더링하기 위해서 rowData 말고도 cellData도 Td에 전달해줘야 함.
= rowData[i]

Tr.jsx

return (
    <tr>
      {Array(rowData.length)
        .fill()
        .map((td, i) => (
          <Td
            rowIndex={rowIndex}
            cellIndex={i}
            cellData={rowData[i]} 
            dispatch={dispatch}
          >
            {''}
          </Td>
        ))}
    </tr>
  );

4. td 설정 (import, JSX)

1) import

import { CLICK_CELL, CHANGE_TURN } from './TicTacToe';

2) JSX

const Td = ({ rowIndex, cellIndex, dispatch, cellData }) => {
  ...
  return <td onClick={onClickTd}>{cellData}</td>;
};

<결과>


-> O, X 순서대로 나타남.

좋은 웹페이지 즐겨찾기