리액트의 컴포넌트에서는 자식 컴포넌트에게 값을 전달할 때 props로 전달하는 것이 일반적이다. 그런데 거쳐야 할 자식이 많아진다면, 쓰지도 않을 props를 계속 전달한다면 코드 가독성도 떨어지게 된다. useReducer + useContext, Redux를 이용하면 자식을 통해 props를 전달하는 구조에서 전역적으로 state에 접근하고 dispatch를 날려서 state를 변경하는 구조로 바꿀 수 있다.
Redux 라이브러리를 설치할 필요없이 react hooks인 useReducer와 useContext를 합쳐서 리덕스를 흉내내서 구현할 수 있다.
state와 dispatch, reducer 함수를 이용해서 리덕스를 흉내낼 수 있다.
useContext를 이용해 Context를 만들어서 컴포넌트를 감싸주면 감싸인 컴포넌트들은 Context의 value에 들어간 값에 props 없이도 접근할 수 있다.
import { Dispatch } from 'react';
export type StateType = {
stateData: {
curTeam: number,
curTest: number
},
teamData: teamDataType[]
};
type teamDataType = {
index: number,
name: string,
test: string[]
}
export type Action =
| { type: 'addTeam', name: string }
| { type: 'addTest', curTeam: number, newTestName: string, curTestArray: string[] }
| { type: 'changeTeam', dst: number }
| { type: 'changeTest', dst: number }
export type DispatchType = Dispatch<Action>;
reducer의 인자로 들어갈 state와 Action의 타입이 될 타입들을 지정한다. 해당 타입을 import해서 사용할 것이기에 export로 내보내준다.
import { StateType, Action } from './Type'
export const initalState = {
stateData: {
curTeam: 0,
curTest: 0,
},
teamData: [
{ index: 0, name: 'asg Team', test: ['qwre', 'asdg', 'asd'] }, // 각 테스트는 객체 형태여야 함.
{ index: 1, name: 'qwer Team', test: ['test1', 'test2', 'test3'] },
],
};
export const reducer = (state: StateType, action: Action): StateType => {
const stateData = state.stateData;
const teamData = state.teamData;
switch (action.type) {
case 'addTeam': {
const newTeamIndex = teamData.length;
const newTeamName = action.name;
const newTeam = { index: newTeamIndex, name: newTeamName, test: [] };
stateData.curTeam = newTeamIndex;
state.teamData.push(newTeam);
return { ...state };
}
case 'addTest': {
console.log('실행됨!');
const curTeamIndex = action.curTeam; // state에서 끌어다 써도 될 듯
const testName = action.newTestName;
const temp = [...action.curTestArray, testName];
teamData[curTeamIndex].test = temp;
return { ...state };
}
case 'changeTeam': {
stateData.curTeam = action.dst;
stateData.curTest = 0;
return { ...state };
}
case 'changeTest': {
stateData.curTest = action.dst;
return { ...state };
}
default: {
return state;
}
}
};
리듀서 부분의 코드. state에 초기값으로 들어 갈 initalState 와 dispatch를 통해 들어오는 액션을 처리하는 reducer 로 나뉘게 된다.
import React, { createContext, useReducer, useContext } from 'react';
import { StateType, DispatchType } from './Type'
import { reducer, initalState } from "./useReducer";
const StateContext = createContext<StateType | undefined>(undefined);
const DispatchContext = createContext<DispatchType | undefined>(undefined);
// 컴포넌트에서 useContext를 사용할 때 값이 유효한지 유효하지 않은지 체크해주는 커스텀 훅을 만든다.
// 에러 체크 기능이 추가되었을 뿐 자식 컴포넌트에서 자신을 감싼 Context를 useContext로 불러온다는 점은 변함없다.
export const cusUseState = (): StateType => {
const state = useContext(StateContext);
if (!state) throw new Error('Provider not found');
return state;
}
export const cusUseDispatch = (): DispatchType => {
const dispatch = useContext(DispatchContext);
if (!dispatch) throw new Error('Provider not found');
return dispatch;
}
// 데이터 통신으로 받아올 것.
export const ContextProvider = ({ children }: { children: React.ReactNode }) => {
const [state, dispatch] = useReducer(reducer, initalState);
return (
<DispatchContext.Provider value={dispatch}>
<StateContext.Provider value={state}>
{children}
</StateContext.Provider>
</DispatchContext.Provider>
);
}
useContext는 useRedux로 만든 state를 실제로 사용할 컴포넌트에서 state와 dispatch를 받을 용도로 사용하는 함수인데, 그 기능에서 값이 유효한지 유효하지 않은지 체크를 하기 위해 커스텀 훅으로 만들어 준 것이다.
이제 useContext(StateContext)는 cusUseState로, useContext(DispatchContext)는 cusUseDispatch로 대체해서 사용하게 될 것이다.
createContext로 useContext의 Context를 만들어준다. 이렇게 만들어진 Context는 Provider 역할을 할 컴포넌트에 집어넣어 줄 것이다.
reducer와 initalState를 하나로 합치는 ContextProvider 컴포넌트를 만든다. useReducer의 인자로 집어넣으면 리턴하는 객체를 구조 분할한다. state와 dispatch를 위에서 만든 Context.Provider의 value 값에 집어넣으면 해당 Provider로 감싸진 컴포넌트는 어디서든 value에 저장된 값에 접근할 수 있게된다.
인자로 받는 children은 들어올 컴포넌트, 사실상 모든 컴포넌트를 의미한다.
import React from 'react';
import Routes from './Routes';
import { ContextProvider } from './contexts/Context';
import './App.scss';
function App() {
return (
<ContextProvider>
<Routes />
</ContextProvider>
)
}
export default App;
props로 집어넣은 Routes 하위의 모든 컴포넌트는 ContextProvider로 감싸져서 useRedux로 설정한 dispatch, state에 접근할 수 있게 되었다.
import { React, useEffect, useReducer } from 'react'; // useContext
import {cusUseState} from "../../contexts/Context" // StateContext
import { TeamBar, MainScreen } from '../../components';
import './Main.scss';
const Main = () => {
const state = cusUseState(); // useContext(StateContext);
const curTeam = state.stateData.curTeam;
const curTest = state.stateData.curTest;
const teamData = state.teamData;
return (
<>
{teamData && (
<div className="page-main">
<TeamBar teamData={teamData} />
<MainScreen
teamData={teamData}
curTeam={curTeam}
curTest={curTest}
/>
</div>
)}
</>
);
};
export default Main;
Context를 만들 때 에러를 체크하는 기능을 추가한 useContext 훅을 만들어주었기에 useContext(StateContext) 와 사실상 동일한 cusUseState()를 사용해준다.
state에는 useReducer로 리턴받은 state가 담겨있다!
import React from 'react';
import { Link } from 'react-router-dom';
import {cusUseDispatch} from '../../contexts/Context';
const MainScreenBody = ({ teamData, curTeam, curTest }) => {
const dispatch = cusUseDispatch();
const addTestEvent = () => {
dispatch({
type: 'addTest',
curTeam: curTeam,
curTestArray: teamData[curTeam].test, // push로 끝에 넣을꺼니 의미 X ?
newTestName: 'xwxa',
});
};
return (
<div className="main-screen-body">
<div className="main-screen-tests">
{teamData[curTeam].test.map((e, i) => {
return (
<div
key={`${curTeam}-${i}`}
className="main-screen-test"
onClick={() => dispatch({ type: 'changeTest', dst: i })}
>
{e}
</div>
);
})}
<Link
onClick={addTestEvent}
to="/makeTest"
className="main-screen-test add"
style={{ textDecoration: 'none', color: 'black' }}
>
{' '}
+
</Link>
</div>
<div className="main-screen-test-info">
{teamData[curTeam].test[curTest]}
</div>
</div>
);
};
export default MainScreenBody;
위와 동일하게, useContext(DispatchContext) 와 동일한 cusUseDispatch()를 사용해서 useReducer로 생성한 dispatch를 가지고 온다. useRedux를 쓸 때와 동일하게 객체 형태로 타입을 설정해서 날려주면 된다.