Today I Learned ... react.js
🙋♂️ React.js Lecture
🙋 My Dev Blog
state를 하나로 관리하기 위함.
action객체를 실행하는 dispatch.
reducer로 action을 처리함.
Redux는 state를 동기적으로 변경되지만,
useReducer은 비동기적으로 변경됨. (useEffect
필요)
React Lecture CH 8
1 - context API
2 - createContext와 Provider
3 - useContext
4 - 좌클릭, 우클릭 로직
5 - 지뢰 개수 표시
6 - 빈칸 한번에 열기
7 - 승리 조건 체크, 타이머
8 - context API 최적화
MineSearch.jsx
import React, { useReducer } from 'react';
import Table from './Table';
import Form from './Form';
const initialState = {
tableData: [],
timer: 0,
result: '',
};
const reducer = (state, action) => {
switch (action.type) {
default:
return state;
}
};
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
<Form />
<div>{state.timer}</div>
<Table />
<div>{state.result}</div>
</>
);
};
export default MineSearch;
하위 컴포넌트인 Form은 가로*세로의 크기와 지뢰 개수를 정하는 Input들과 '시작'버튼이 존재함.
Form 컴포넌트는 하위 컴포넌트이고, useReducer이 아닌 useState를 이용해서 작성.
Form.jsx
import React, { useState, useCallback } from 'react';
const Form = () => {
const [row, setRow] = useState(10);
const [cell, setCell] = useState(10);
const [mine, setMine] = useState(20);
const onChangeRow = useCallback((e) => {
setRow(e.target.value);
});
const onChangeCell = useCallback((e) => {
setCell(e.target.value);
});
const onChangeMine = useCallback((e) => {
setMine(e.target.value);
});
const onClickBtn = useCallback(() => {});
return (
<>
<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>
</>
);
};
export default Form;
Form 컴포넌트의 button을 클릭시 (onClickBtn)
게임이 시작되도록 해야 함.
-> MineSearch 컴포넌트에서 dispatch
를 넘겨줘야함.
- 기존 방법 - props로 넘겨준다.
return <Form dispatch={dispatch}/>
- 새로운 방법 - context API 이용
-> 그 아래에 있는 모든 컴포넌트가 다 받을 수 있음.
(중간 컴포넌트들을 불필요하게 거칠 필요 X)
import React, { useReducer, createContext } from 'react';
const TableContext = createContext(); // 기본값 넣을수 있음
return (
<TableContext.Provider
value={{tableData: state.tableData, dispatch}}
>
<Form />
<div>{state.timer}</div>
<Table />
<div>{state.result}</div>
</TableContext.Provider>
);
Provider 란?
- 관찰자 패턴 (데이터는 공급자가 관리하고, 관찰자는 공급자를 구독하여 데이터를 얻는 방식.)
-> 지난 포스팅 참조
export const TableContext = createContext({
// 초기값. 여기선 중요하지 않으니 모양만 맞춰주기
tableData: [],
dispatch: () => {}
});
-> 다른 컴포넌트에서 import 하여 사용 가능하도록 export 해줌.
Form.jsx
import React, { useState, useCallback, useContext } from 'react';
import { TableContext } from './MineSearch';
const Form = () => {
const [row, setRow] = useState(10);
const [cell, setCell] = useState(10);
const [mine, setMine] = useState(20);
const value = useContext(TableContext);
...
}
value.dispatch
와 같이 사용 가능.{ dispatch }
와 같이 구조분해 할당 권장.✅ 주의 - 성능 최적화 문제
- context API 사용시 성능 최적화가 매우 어려움.
return ( <TableContext.Provider value={{ tableData: state.tableData, dispatch }}> <Form /> <div>{state.timer}</div> <Table /> <div>{state.result}</div> </TableContext.Provider> );
-> 여기서 컴포넌트가 렌더링 될 때 마다
value={{ tableData: state.tableData, dispatch }} 객체가 계속 생성되므로 자식들도 매번 새롭게 리렌더링 됨.
- useMemo를 통한 캐싱(caching) 이 필요하다.
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = useMemo(
() => ({
tableData: state.tableData,
dispatch,
}),
[state.tableData]
);
return (
<TableContext.Provider value={value}>
<Form />
<div>{state.timer}</div>
<Table />
<div>{state.result}</div>
</TableContext.Provider>
);
};
tableData
가 바뀔 때마다 값을 다시 저장하도록 한다.Form.jsx
const onClickBtn = useCallback(() => {
dispatch({type: START_GAME, row, cell, mine })
}, [row, cell, mine]);
MineSearch.jsx
...
export const START_GAME = 'START_GAME';
const reducer = (state, action) => {
switch (action.type) {
case START_GAME: {
return {
...state,
tableData: plantMine(action.row, action.cell, action.mine),
};
}
default:
return state;
}
};
tableData를 변경하는데, 지난 틱택토 예제와 마찬가지로 2차원 배열로 구성된다.
지뢰는 -7로 표시하고, 일반 셀은 -1로, 그 외에도 다양한 상태들을 숫자로 표기하여 구분한다.
예>
[-1, -1, -1, -1, -1, -7, -1, -1],
[-1, -1, -7, -1, -1, -1, -1, -1],
[-1, -1, -1, -7, -1, -1, -1, -1],
[-1, -1, -1, -1, -7, -1, -1, -1],
[-7, -1, -7, -1, -1, -1, -1, -1],
[-1, -1, -1, -7, -1, -1, -1, -1],
[-1, -1, -1, -1, -7, -7, -1, -1],
[-1, -1, -7, -1, -1, -1, -1, -1],
-1, -7과 같이 숫자로만 표기하면 숫자가 뭘 의미하는지 모르므로, 코드 변수들을 저장한 객체를 선언한다.
export 하여 모듈로 사용 가능하도록 한다.
export const CODE = {
MINE: -7,
NORMAL: -1,
QUESTION: -2,
FLAG: -3,
QUESTION_MINE: -4,
FLAG_MINE: -5,
CLICKED_MINE: -6,
OPENED: 0,
};
* * *
const plantMine = (row, cell, mine) => {
console.log(row, cell, mine);
const candidate = Array(row * cell)
.fill()
.map((arr, i) => i);
const shuffle = [];
while (candidate.length > row * cell - mine) {
shuffle.push(
candidate.splice(Math.floor(Math.random() * candidate.length), 1)[0]
);
}
const data = [];
for (let i = 0; i < row; i++) {
const rowData = [];
data.push(rowData);
for (let j = 0; j < cell; j++) {
const cellData = [];
rowData.push(CODE.NORMAL);
}
}
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;
}
console.log(data);
return data;
};
1) data = []
2) data.push(rowData)
-> data = [[],[],[],[],[],[],[],[],[],[]]
3) rowData.push(CODE.NORMAL)
-> data = [[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1],
... ]
-> 10*10의 경우 모든 100개의 요소들이 CODE.NORMAL(-1)이 된 것임.
✅ 참고 - 이차원배열 위치 구하기
console.log(data)를 해보면
아래와 같이 총 10*10 테이블에 20개의 지뢰가 심어져있다.
import React, { useContext } from 'react';
import Tr from './Tr';
import { TableContext } from './MineSearch';
const Table = () => {
const { tableData } = useContext(TableContext);
return (
<table>
{Array(tableData.length)
.fill()
.map((tr, i) => (
<Tr rowIndex={i} />
))}
</table>
);
};
export default Table;
useContext
와 TableContext 임포트.import React, { useContext } from 'react';
import Td from './Td';
import { TableContext } from './MineSearch';
const Tr = ({ rowIndex }) => {
const { tableData } = useContext(TableContext);
return (
<tr>
{tableData[0] &&
Array(tableData[0].length)
.fill()
.map((td, i) => <Td rowIndex={rowIndex} cellIndex={i} />)}
</tr>
);
};
export default Tr;
useContext
와 tableContext 임포트.getTdStyle
, getTdText
)Td.jsx
import React, { useContext } from 'react';
import { CODE, TableContext } from './MineSearch';
const getTdStyle = (code) => {
switch (code) {
case CODE.NORMAL:
case CODE.MINE:
return {
background: '#444',
};
case CODE.OPENED:
return {
background: '#fff',
};
default:
return {
background: '#fff',
};
}
};
const getTdText = (code) => {
switch (code) {
case CODE.NORMAL:
return '';
case CODE.MINE:
return 'X';
default:
return '';
}
};
const Td = ({ rowIndex, cellIndex }) => {
const { tableData } = useContext(TableContext);
return (
<td style={getTdStyle(tableData[rowIndex][cellIndex])}>
{getTdText(tableData[rowIndex][cellIndex])}
</td>
);
};
export default Td;
style
어트리뷰트에 함수의 리턴값을 넣어줄 수 있다.