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 - 테이블 최적화
Redux
의 핵심 개념을 도입함.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의 state
와 setState
함수들을 총 3단계를 거쳐 Prop로 전달해야함.
-> 이런 문제를 해결하기 위해 ontext API
를 사용함.
context API를 배우기에 앞서, state 개수 자체를 줄여주는 useReducer을 알아보자.
- state가 많아지면 관리가 힘들어짐.
- useReducer을 사용하면, 하나의 state와 하나의 setState함수로 한번에 관리 가능.
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
은 두개의 인자를 갖는다.const [state, dispatch] = useReducer(reducer, initialState)
마치 useState를 작성하듯이, 배열 구조분해 할당으로 state와 dispatch를 선언해준다.
-> useReducer()
함수의 반환값은 배열이므로.
그 다음으로, reducer(함수) 그리고 initialState를 선언해줘야 한다.
-> Hooks는 state가 바뀌면 함수 전체가 재실행되므로, 함수 바깥에 정의해준다.
(= 지난 시간 Lotto 예제에서 getWinNumbers를 바깥에 빼줬듯이.)
useReducer(reducer, initialState)
- 컴포넌트의 상태 업데이트 로직을 컴포넌트에서 분리시킬 수 있다.
- 상태 업데이트 로직을 컴포넌트 바깥에 작성 할 수도 있고, 심지어 다른 파일에 작성 후 불러와서 사용 할 수도 있다.
- 참고 문서
우선, Table 컴포넌트를 클릭했을 때 실행되는 onClickTable 함수를 작성한다.
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'로 변경해주는 내용을 담음.
여기서, dispatch
와 action
의 개념이 등장함.
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,
};
}
};
❗️주의
- 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.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';
TicTacToe.jsx
...
return (
<>
<Table onClick={onClickTable} tableData={state.tableData} />
{state.winner && <div>{state.winner}님의 승리</div>}
</>
);
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;
-> Tr이 3개인 테이블 완성됨.
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로 살펴보면,
테이블 셀 하나하나가 컴포넌트로 구성되어있음을 알 수 있다.
이제 하나의 셀(Td)을 클릭할 때 마다 O 또는 X가 렌더링 되게 해야함.
const Table = ({ onClick, tableData }) => {
return (
<table onClick={onClick}>
{Array(tableData.length)
.fill()
.map((tr, i) => (
<Tr rowIndex={i} rowData={tableData[i]} />
))}
</table>
);
};
rowIndex
라는 props로 넘김.const Tr = ({ rowData, rowIndex }) => {
return (
<tr>
{Array(rowData.length)
.fill()
.map((td, i) => (
<Td rowIndex={rowIndex} cellIndex={i}>
{''}
</Td>
))}
</tr>
);
};
cellIndex
라는 props로 넘김Td.jsx
const Td = ({ rowIndex, cellIndex }) => {
const onClickTd = useCallback(() => {
console.log(rowIndex, cellIndex);
}, []);
return <td onClick={onClickTd}>{''}</td>;
};
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>;
};
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이다.
2-2) CHANGE_TURN 로직
CLICK_CELL과 같은 타입명은 export를 붙여 모듈화 해준다.
-> 하위 컴포넌트에서도 import 해서 사용 가능하도록.// 하위 컴포넌트에서 사용할 수 있게 모듈로 만듬 export const SET_WINNER = 'SET_WINNER'; export const CLICK_CELL = 'CLICK_CELL'; export const CHANGE_TURN = 'CHANGE_TURN';
위 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>
);
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 순서대로 나타남.