벨로퍼트와 함께하는 모던 리액트 문서를 따라하면서 redux 튜토리얼을 진행해보았다. 대부분의 redux 튜토리얼이 (이것도 마찬가지지만) React와 JavaScript 기반으로 진행되어 생각했던 것보다는 시간이 조금 걸렸다 (헤매느라....),
그래서 나도 앞으로 안 헤매고, 또 나처럼 Next Typescript Redux를 한 번에 배우는.. 시도하는 ... 사람들을 위해서 기록해본당.
0. 설치하기
1) next 프로젝트 생성 : npx create-next-app 플젝이름 --typescript
2) redux 설치 : npm install react-redux next-reddux-wrapper
3) redux-devtools 설치 : npm install react-devtools-extension
1. 폴더 만들기
나는 tutorial-counter라는 이름으로 pages 아래에 만들어주었다.

2. 모듈 만들기
리덕스 모듈이란, 1) 액션 타입 2) 액션 생성 함수 3) 리듀서 가 모두 들어있는 파일을 말한다.
일단 튜토리얼 진행 시에는 tutorial-counter 폴더 내에 modules라는 폴더를 생성해주었다.
(너무 경로가 멀어지면 import할 때 귀찮아서...)

2-1. Counter 모듈 만들기
새로 만든 modules 폴더 내에 counter 모듈을 만들었다.
// pages/tutorial-counter/modules/counter.ts
/* 액션 타입 만들기 */
// Ducks 패턴을 따를땐 액션의 이름에 접두사를 넣어주세요.
// 이렇게 하면 다른 모듈과 액션 이름이 중복되는 것을 방지 할 수 있습니다.
const SET_DIFF = "counter/SET_DIFF";
const INCREASE = "counter/INCREASE";
const DECREASE = "counter/DECREASE";
/* 액션 생성함수 만들기 */
// 액션 생성함수를 만들고 export 키워드를 사용해서 내보내주세요.
export const setDiff = (diff: number) => ({ type: SET_DIFF, diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
/* 초기 상태 선언 */
const initialState = {
number: 0,
diff: 1,
};
/* 리듀서 선언 */
// 리듀서는 export default 로 내보내주세요.
export default function counter(state = initialState, action: any) {
switch (action.type) {
case SET_DIFF:
return {
...state,
diff: action.diff,
};
case INCREASE:
return {
...state,
number: state.number + state.diff,
};
case DECREASE:
return {
...state,
number: state.number - state.diff,
};
default:
return state;
}
}
2-2. 루트 리듀서 만들기
지금은 counter 모듈 하나만 만들었지만, 만약 여러 개의 리듀서를 사용할 때는 하나의 리듀서로 합쳐서 사용한다고 한다.
이 합쳐진 리듀서를 루트 리듀서라고 부른다. (combineReducers 사용)
// pages/tutorial-counter/modules/root.ts
import { combineReducers } from "redux";
import counter from "./counter";
const rootReducer = combineReducers({
counter,
});
export default rootReducer;
2-3. _app.ts에 스토어 만들기
대부분의 react redux 튜토리얼에서 말하는 index.js는 next에서는 _app.ts 파일로 생각하면 된다.
리덕스를 적용할 때는 최상단 App 컴포넌트를 Provider로 감싸준 후에 Provider의 props로 앞서 생성한 store를 전달해주면 된다.
이렇게 하면, 우리가 렌더링하는 그 어떤 컴포넌트에서라도 리덕스 스토어에 접근할 수 있게 된다.
그리고 redux-devtools를 사용하기 위해서는 해당 익스텐션을 import 해준 후, store를 만들 때 인자로 하나 더 주면 된다.
// pages/_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { createStore } from "redux";
import { Provider } from "react-redux";
import rootReducer from "./tutorial-counter/modules/root";
import { composeWithDevTools } from "redux-devtools-extension"; // 리덕스 개발자 도구
import { CounterContainer } from "./counter/CounterContainer";
const store = createStore(rootReducer, composeWithDevTools()); // 스토어를 만듭니다.
export default function App({ Component, pageProps }: AppProps) {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
3. 카운터 구현하기
typescript로 파일을 쓸 때 주의할 점은, JSX 문법을 사용하는 부분이 있다면 파일 확장자를 꼭 tsx로 해야 한다는 것이다.
javascript로 진행되는 튜토리얼을 따라하다보면 JSX 문법 써도 파일 확장자를 js로 해두는 경우가 있는데, typescript로 이렇게 하면 오류가 나니까 주의해야 한다.
3-1. Presentational Component
// pages/tutorial-counter/tutorial.tsx
import React from "react";
interface Tu {
number: number;
diff: number;
onIncrease: () => void;
onDecrease: () => void;
onSetDiff: (value: number) => void;
}
function Tutorial({ number, diff, onIncrease, onDecrease, onSetDiff }: Tu) {
const onChange = (e: any) => {
// e.target.value 의 타입은 문자열이기 때문에 숫자로 변환해주어야 합니다.
onSetDiff(parseInt(e.target.value, 10));
};
return (
<div>
<h1>{number}</h1>
<div>
<input type="number" value={diff} min="1" onChange={onChange} />
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
</div>
</div>
);
}
export default Tutorial;
이런 component를 presentational component라고 하는데, 여기선 주로 UI를 선언하는 것에 집중하며, 필요한 값들이나 함수는 props로 받아와서 사용하는 형태로 구현한다.
props를 받아올 때, typescript를 이용하기 때문에 타입을 정의해주는 부분이 필요하다. 나는 interface를 사용했다.
3-2. Container Component
Presentational Component에서는 UI를 선언하는 것에 집중했다면, Container Component는 리덕스 스토어의 상태를 조회하거나 액션을 디스패치한다.
// pages/tutorial-counter/index.tsx
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import Tutorial from "./tutorial";
import { increase, decrease, setDiff } from "./modules/counter";
function CounterContainer() {
// useSelector는 리덕스 스토어의 상태를 조회하는 Hook입니다.
// state의 값은 store.getState() 함수를 호출했을 때 나타나는 결과물과 동일합니다.
const { number, diff } = useSelector((state: any) => ({
number: state.counter.number,
diff: state.counter.diff,
}));
// useDispatch 는 리덕스 스토어의 dispatch 를 함수에서 사용 할 수 있게 해주는 Hook 입니다.
const dispatch = useDispatch();
// 각 액션들을 디스패치하는 함수들을 만드세요
const onIncrease = () => dispatch(increase());
const onDecrease = () => dispatch(decrease());
const onSetDiff = (diff: number) => dispatch(setDiff(diff));
return (
<Tutorial
// 상태와
number={number}
diff={diff}
// 액션을 디스패치 하는 함수들을 props로 넣어줍니다.
onIncrease={onIncrease}
onDecrease={onDecrease}
onSetDiff={onSetDiff}
/>
);
}
export default CounterContainer;
코드를 보면, 3-1에서 만든 Presentational Component인 <Tutorial /> props를 넣어주고 있는 것을 볼 수 있다.
4. 화면에서 확인하기
pages/tutorial-counter/index.tsx 가 JSX 형태를 가지고 있기 때문에 얘를 그대로 띄워도 괜찮고,
또는 따로 페이지를 파서 <CounterContainer /> 를 별도로 import해서 띄워도 괜찮다.
전자의 방법으로 하면, /tutorial-counter 라고 url을 입력하면 아래와 같은 화면과 redux devtools를 확인할 수 있다.

'Library-Framework > Redux' 카테고리의 다른 글
redux-persist로 localStorage 관리하기 (0) | 2023.05.10 |
---|---|
Next.js + TypeScript + Redux 시작하기 (+ redux-devtools 사용) (0) | 2023.05.07 |