TIL 22-04-19
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의
state
와setState
함수들을 총 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
은 두개의 인자를 갖는다.
- reducer 함수 (state를 변경할 함수)
- 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'로 변경해주는 내용을 담음.
여기서, 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,
};
}
};
- 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로 넘겨주자.
- 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로 넘김.
- 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 순서대로 나타남.
Author And Source
이 문제에 관하여(TIL 22-04-19), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@thisisyjin/TIL-22-04-15저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)