useReducer + Context API로 Redux를 흉내내보았으니, 이제 진짜 Redux를 구현해볼 차례이다.
state
는 dispatch
로 action
을 보내야만 변화할 수 있다. Reducer
함수 이외의 곳, state
의 값을 받아서 사용하는 컴포넌트에서는 읽기만 할 수 있는 것이다.
Reducer
는 순수 함수Reducer
함수는 인자로 state
와 dispatch
의 인자인 action
을 받는다. state를 변화시키고 리턴할 때, 새로운 객체로 리턴해야 한다. 보통 {...state} 문법으로 새로운 객체를 만든다.
[React ] Redux와 useReducer의 차이, Context API 사용하기 — @Devme (tistory.com)
https://ridicorp.com/story/how-to-use-redux-in-ridi/
- 리덕스는 dispatch를 통해 state가 동기적으로 변경, useReducer는 비동기적으로 변경된다고 한다.
- 컴포넌트에서 특정 값을 의존할 때 리덕스에서는 해당 값이 바뀔 때에만 리렌더링을 하지만 Context API에서는 쓸데없는 리렌더링이 발생하게 된다.
링크를 참조.
액션, 액션 함수, 리듀서를 하나의 파일에서 관리하는 ducks 패턴을 이용해서 구현하였다.
// 액션 타입 선언
const CHANGE_TEAM = 'Provider/CHANGE_TEAM' as const;
const CHANGE_TEST = 'Provider/CHANGE_TEST' as const;
const ADD_TEAM = 'Provider/ADD_TEAM' as const;
const ADD_TEST = 'Provider/ADD_TEST' as const;
// 액션 생성 함수 선언
export const changeTeam = (dst: number) => ({
type: CHANGE_TEAM,
payload: dst
})
export const changeTest = (dst: number) => ({
type: CHANGE_TEST,
payload: dst
})
export const addTeam = (name: string) => ({
type: ADD_TEAM,
payload: name
});
export const addTest = (curTeam: number, curTestArray: string[], newTestName: string) => ({
type: ADD_TEST,
payload: {
curTeam, newTestName, curTestArray
}
});
// 액션 객체들에 대한 타입 설정
export type curStateAction =
| ReturnType<typeof changeTeam>
| ReturnType<typeof changeTest>
| ReturnType<typeof addTeam>
| ReturnType<typeof addTest>
// 상태의 타입 및 초깃값 선언
export type teamDataType = {
index: number,
name: string,
test: string[]
}
export type StateType = {
stateData: {
curTeam: number,
curTest: number,
},
teamData: teamDataType[]
};
const initalState: StateType = {
stateData: {
curTeam: 0,
curTest: 0,
},
teamData: [
{ index: 0, name: 'asg Team', test: ['qwre', 'asdg', 'asd'] }, // 각 테스트는 객체 형태여야 함.
{ index: 1, name: 'qwer Team', test: ['test1', 'test2', 'test3'] },
],
};
// 리듀서 선언
const Provider = (state: StateType = initalState, action: curStateAction) => {
const stateData = state.stateData;
const teamData = state.teamData;
switch (action.type) {
case ADD_TEAM: {
const newTeamIndex = teamData.length;
const newTeamName = action.payload;
const newTeam = { index: newTeamIndex, name: newTeamName, test: [] };
stateData.curTeam = newTeamIndex;
state.teamData.push(newTeam);
return { ...state };
}
case ADD_TEST: {
console.log('실행됨!');
const curTeamIndex = action.payload.curTeam; // state에서 끌어다 써도 될 듯
const testName = action.payload.newTestName;
const temp = [...action.payload.curTestArray, testName];
teamData[curTeamIndex].test = temp;
return { ...state };
}
case CHANGE_TEAM: {
stateData.curTeam = action.payload;
stateData.curTest = 0;
return { ...state };
}
case CHANGE_TEST: {
stateData.curTest = action.payload;
return { ...state };
}
default: {
return state;
}
}
}
export default Provider;
액션들의 이름이 중복되지 않게 리듀서의 이름을 앞에 붙인다. 액션 생성 함수들과 모든 액션 생성 함수들에 해당하는 타입, 상태의 타입과 초깃값, 리듀서 함수를 선언한다. 기본적인 리듀서 파일에 타입이 붙은 것 뿐, 큰 틀은 리듀서와 변함이 없다.
import { combineReducers } from 'redux';
import Provider from './Provider';
import Login from './Login';
const rootReducer = combineReducers({
Provider
});
export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;
모든 리듀서들을 하나의 리듀서로 묶어주고 내보내준다. RootState
는 컴포넌트에서 useState, useDispatch로 리듀서를 사용하게 될 때 인자인 state의 타입을 위해 선언해준다.
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './modules';
import { composeWithDevTools } from 'redux-devtools-extension'; // 리덕스 개발자 도구
import 'semantic-ui-css/semantic.min.css';
const store = createStore(rootReducer, composeWithDevTools());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
위에서 선언된 rootReducer
가 Provider의 store로 저장된다. App에 포함되는, 즉 모든 컴포넌트가 useSelector
와 useDispatch
를 통해 state
와 dispatch
에 접근할 수 있게 된 것이다.
import { useSelector } from 'react-redux';
import { RootState } from '../../modules';
import { TeamBar, MainScreen } from '../../components/index';
import './Main.scss';
const ProviderMainPage = () => {
const state = useSelector((state: RootState) => state.Provider);
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 ProviderMainPage;
src/modules/index.ts
에서 선언한 RootState
로 전체 state
의 타입을 지정해주고 나서야 state
를 사용할 수 있다. 저 타입 지정이 되지 않으면 렌더링 자체가 실패하게 된다...
import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { toggleLogin } from "../../modules/Login"
import { LoginForms } from '../../components';
const LoginPage: React.FC = () => {
const dispatch = useDispatch();
const handleLogin = () => {
dispatch(toggleLogin());
}
return (
<>
<div className="page-login">
<LoginForms />
</div>
<Link to="pro">
<div>to pro</div>
</Link>
<div onClick={handleLogin}>로그인하기</div>
<Link to="use">
<div>to use</div>
</Link>
</>
);
};
export default LoginPage;
dispatch
의 경우 useDispatch
말고도 사용하려는 액션 함수도 같이 끌어와야만 한다.