리액트의 컴포넌트에서는 자식 컴포넌트에게 값을 전달할 때 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를 쓸 때와 동일하게 객체 형태로 타입을 설정해서 날려주면 된다.