TIL 22-04-20 (2)
React.js
Today I Learned ... react.js
🙋♂️ React.js Lecture
🙋 My Dev Blog
지난 시간 리뷰
- useReducer
state를 하나로 관리하기 위함.
action객체를 실행하는 dispatch.
reducer로 action을 처리함.
- useReducer과 Redux의 차이
Redux는 state를 동기적으로 변경되지만,
useReducer은 비동기적으로 변경됨. (useEffect
필요)
- BUT. props를 자손 컴포넌트에게 물려줄때 모든 중간 컴포넌트들을 다 거쳐야 했음.
(예- tableData)
-> 이를 해결하기 위해서는 context API를 사용.
React Lecture CH 8
1 - context API
2 - createContext와 Provider
3 - useContext
4 - 좌클릭, 우클릭 로직
5 - 지뢰 개수 표시
6 - 빈칸 한번에 열기
7 - 승리 조건 체크, 타이머
8 - context API 최적화
지뢰 찾기 초기코드
- 컴포넌트 구성
- useReducer을 사용하되, 이번에는 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)
createContext와 Provider
createContext
import React, { useReducer, createContext } from 'react';
const TableContext = createContext(); // 기본값 넣을수 있음
Provider
- 데이터에 접근하고 싶은 컴포넌트를 context API의 Provider로 묶어줘야함.
- 전달해줄 데이터는 value 어트리뷰트로 적어줌.
return (
<TableContext.Provider
value={{tableData: state.tableData, dispatch}}
>
<Form />
<div>{state.timer}</div>
<Table />
<div>{state.result}</div>
</TableContext.Provider>
);
Provider 란?
- 관찰자 패턴 (데이터는 공급자가 관리하고, 관찰자는 공급자를 구독하여 데이터를 얻는 방식.)
-> 지난 포스팅 참조
초기값 설정 + export 해주기
export const TableContext = createContext({
// 초기값. 여기선 중요하지 않으니 모양만 맞춰주기
tableData: [],
dispatch: () => {}
});
-> 다른 컴포넌트에서 import 하여 사용 가능하도록 export 해줌.
useContext
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);
...
}
- useContext도 임포트해줌
- MineSearch 컴포넌트의 TableContext를 임포트해옴.
- const value = useContext(TableContext) 해줌.
-> dispatch를 사용하기 위해서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>
);
};
- useMemo는 함수의 값을 저장한다.
tableData
가 바뀔 때마다 값을 다시 저장하도록 한다.
dispatch
Form.jsx
const onClickBtn = useCallback(() => {
dispatch({type: START_GAME, row, cell, mine })
}, [row, cell, mine]);
- action을 적어주고 dispatch로 실행해줌.
-> Form 컴포넌트에서 사용자가 설정한 row, cell, mine을 전달해줘야 함.
MineSearch.jsx
- action을 등록해주고, reducer에도 case문으로 작성.
...
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;
}
};
- 게임이 시작되면
-> 1) row*cell 테이블을 만들고, 2) 지뢰 개수만큼 심어야 함.
-> 코드의 양이 많으므로 하나의 함수로 빼줌. (plantMine 함수)
지뢰찾기 로직
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],
CODE 변수 선언
-
-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,
};
* * *
plantMine 함수 로직
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. candidate 배열 만들기
- row * cell 만큼의 칸 만들기.
- 만약 10*10이라면? 0~99까지를 갖는 배열 만들기.
(map으로 index를 리턴함)
2. shuffle
- candidate 배열에서 mine 만큼 랜덤으로 뽑기.
-> 0~99까지의 숫자 중 20개가 뽑히게 됨. - 랜덤으로 뽑기 위해 빈 배열인 shuffle에
push 하는 방식으로. - candidate 배열을 splice하여 잘라냄.
3. NORMAL 코드 채움
- 중첩 반복문을 통해 row~cell을 순회하며
1) data에 rowData (빈 배열)을 push한 후,
2) rowData에 CODE.NORMAL을 push함.
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)이 된 것임.
4. MINE 심기
- shuffle 배열 length만큼 순회하는 반복문
- 이차원 배열이므로 행, 열을 구해서
(ver, hor) 위치에 맞게 CODE.MINE을 대입함.
✅ 참고 - 이차원배열 위치 구하기
- array [number / cell][number % cell]
console.log(data)를 해보면
아래와 같이 총 10*10 테이블에 20개의 지뢰가 심어져있다.
useContext로 렌더링
- 이제 지뢰 칸을 각각 렌더링 해주면 된다.
- Table.jsx
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 임포트.- 지난 예제에서처럼 Array(length).fill().map()을 이용하여
tableData의 요소들을 렌더링함.
- 10*10일때, 2차원 배열인 tableData의 length=10이다.
- 총 10개의 Tr이 렌더링됨.
- Tr.jsx
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 임포트. - 여기서는 한 row에 해당하는 셀의 length만큼 반복.
-> tableData[0]은 [-1,-1,-1,-1,-1,-1,-7,-1,-1,-1]과 같이 총 10개의 요소가 존재함.
- 따라서 10개의 Td가 렌더링됨
Td 컴포넌트 스타일링
- 스타일과 텍스트를 입히기.
- 함수를 사용. (
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;
- JSX의
style
어트리뷰트에 함수의 리턴값을 넣어줄 수 있다. - 함수의 인자로 tableData배열의 요소를 받는다.
-1 (CODE.NORMAL), -7(CODE.MINE) 등을 구분할 수 있다.
- text의 경우에는 지뢰는 x로 표시되게 함.
- background 색을 시커멓게 하고,
클릭하면 하얗게 되도록 함.
<결과>
Author And Source
이 문제에 관하여(TIL 22-04-20 (2)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@thisisyjin/TIL-22-04-20-2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)