CHOI BOO 블로그

[Nextjs, Redux] Nextjs + Redux-Toolkit + Typescript (with Hooks)

2021.03.31
React
Nextjs
Typescript
Redux
Redux-Toolkit

21.03.31 수 업데이트

useSelector 타입추론 추가 (공식문서 참고)

redux는 꽤나 typescript를 적용하는데 애를 먹었다.

지금 막 테스트를 해본거라 서버도 없으니(귀찮) get[Static, ServerSide]Props는 나중에 추가할 것이다.

dispatch까지 알아보겠다.

redux-toolkit에 대한 설명은 아래 링크에서

참고 Link: https://github.com/qnrjs42/redux_toolkit_mobx

버전에 따라 달라질 수 있으니 유의해야한다!!!!!

필요한 모듈

npm i react-redux next-redux-wrapper @reduxjs/toolkit npm i -D @types/react-redux typescript // package.json { "name": "next-front", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "next" }, "author": "qnrjs42", "license": "ISC", "dependencies": { "@ant-design/icons": "^4.4.0", "@reduxjs/toolkit": "^1.5.0", "antd": "^4.10.3", "next": "^10.0.5", "next-redux-wrapper": "^6.0.2", "react": "^17.0.1", "react-dom": "^17.0.1", "react-redux": "^7.2.2", "styled-components": "^5.2.1" }, "devDependencies": { "@types/node": "^14.14.21", "@types/react": "^17.0.0", "@types/react-redux": "^7.1.16", "@types/styled-components": "^5.1.7", "eslint": "^7.18.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", "typescript": "^4.1.3" } }

store.ts

import { configureStore, getDefaultMiddleware, EnhancedStore } from '@reduxjs/toolkit'; import { createWrapper, MakeStore } from 'next-redux-wrapper'; import slice from '../slices'; const devMode = process.env.NODE_ENV === 'development'; const store = configureStore({ reducer: slice, middleware: [...getDefaultMiddleware()], devTools: devMode, }); const setupStore = (context: any): EnhancedStore => store; const makeStore: MakeStore = (context) => setupStore(context); export const wrapper = createWrapper(makeStore, { debug: devMode, }); // 21.03.31 추가 export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch; export default wrapper;

interface

user.ts

export interface IUser { isLoggedIn: boolean; user: any; signUpdata?: any; loginData?: any; }

post.ts

// 예시로 들었기 때문에 posts는 그냥 뼈대만 있는거라고 생각하면 된다. export interface IPost {}

slices/index.ts

import { combineReducers, AnyAction } from '@reduxjs/toolkit'; import { HYDRATE } from 'next-redux-wrapper'; import users from './users'; import posts from './posts'; import { IUser } from '../interface/user'; import { IPost } from '../interface/post'; export interface State { users: IUser; posts: IPost; } const rootReducer = (state: State | undefined, action: AnyAction) => { switch (action.type) { case HYDRATE: console.log('HYDRATE'); return action.payload; default: { const combineReducer = combineReducers({ users, posts, }); return combineReducer(state, action); } } }; // 21.03.31 제거 // export type RootState = ReturnType<typeof rootReducer>; export default rootReducer;

slice/users.ts

import { createSlice } from '@reduxjs/toolkit'; import { logInAction, logOutAction } from '../actions/users'; import { IUser } from '../interface/user'; const initialState: IUser = { isLoggedIn: false, user: null, }; export const users = createSlice({ name: 'users', initialState, reducers: {}, extraReducers: (builder) => builder // builder의 addCase는 typescript의 타입 추론 사용할 때 편하다. .addCase(logInAction.pending, (state, action) => {}) .addCase(logInAction.fulfilled, (state, action) => { state.user = action.payload; state.isLoggedIn = true; }) .addCase(logInAction.rejected, (state, action) => {}) .addCase(logOutAction.pending, (state, action) => {}) .addCase(logOutAction.fulfilled, (state, action) => {}) .addCase(logOutAction.rejected, (state, action) => {}) .addDefaultCase(() => {}), }); export default users.reducer;

slice/posts.ts

// 예시로 들었기 때문에 posts는 그냥 뼈대만 있는거라고 생각하면 된다. (users 복사본) import { createSlice } from '@reduxjs/toolkit'; import { logInAction } from '../actions/users'; import { IPost } from '../interface/post'; const initialState: IPost = {}; export const posts = createSlice({ name: 'posts', initialState, reducers: {}, extraReducers: (builder) => builder // builder의 addCase는 typescript의 타입 추론 사용할 때 편하다. .addCase(logInAction.pending, (state, action) => {}) .addCase(logInAction.fulfilled, (state, action) => {}) .addCase(logInAction.rejected, (state, action) => {}) .addDefaultCase(() => {}), }); export default posts.reducer;

hooks/useSelector.ts

  • 21.03.31 추가
import { TypedUseSelectorHook, useSelector } from 'react-redux'; import { RootState } from 'store'; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

actions/users.ts

import { createAsyncThunk } from '@reduxjs/toolkit'; import { IUser } from '../interface/user'; interface rejectMessage { errorMessage: string; } // 아무것도 없음 export const logOutAction = createAsyncThunk('user/logOut', async (data, thnukAPI) => {}); export const logInAction = createAsyncThunk<IUser, any, { rejectValue: rejectMessage }>( 'user/logIn', async (data) => { console.log(data, 'logIn action'); return data; }, );

pages/_app.tsx

import React from 'react'; import Head from 'next/head'; import { AppContext } from 'next/app'; import 'antd/dist/antd.css'; import wrapper from '../store'; const App = ({ Component }: AppContext) => { return ( <> <Head> <meta charSet='utf-8' /> <title>App</title> </Head> <Component /> </> ); }; export default wrapper.withRedux(App);

components/AppLayout.tsx

  • 21.03.31 변경
import React from 'react'; import Link from 'next/link'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { Menu, Input, Row, Col } from 'antd'; import LoginForm from './LoginForm'; import UserProfile from './UserProfile'; import { IUser } from '../interface/user'; import { useAppSelector } from '../hooks/useSelector'; interface IProps { children: React.ReactNode; } const SearchInput = styled(Input.Search)` vertical-align: 'middle'; `; const AppLayout = ({ children }: IProps) => { // 21.03.31 추가 const isLoggedIn = useSelector((state) => state.users.isLoggedIn); // 21.03.31 제거 // const isLoggedIn = useSelector<RootState, boolean>((state) => state.users.isLoggedIn); console.log('>>>', isLoggedIn); return ( <> <Menu mode='horizontal'> <Menu.Item> <Link href='/'> <a>홈</a> </Link> </Menu.Item> <Menu.Item> <Link href='/profile'> <a>프로필</a> </Link> </Menu.Item> <Menu.Item> <SearchInput enterButton /> </Menu.Item> <Menu.Item> <Link href='/signup'> <a>회원가입</a> </Link> </Menu.Item> </Menu> <Row gutter={8}> <Col xs={24} md={6}> {isLoggedIn ? <UserProfile /> : <LoginForm />} </Col> <Col xs={24} md={12}> {children} </Col> <Col xs={24} md={6}> <a href='https://github.com/qnrjs42' target='_blank' rel='noreferrer noopener'> Made by Choi Boo </a> </Col> </Row> </> ); }; export default AppLayout;

components/LoginForm.tsx

import React, { useCallback } from 'react'; import { Form, Input, Button } from 'antd'; import Link from 'next/link'; import styled from 'styled-components'; import useInput from '../hooks/useInput'; import { useDispatch } from 'react-redux'; import { logInAction } from '../actions/users'; const ButtonWrapper = styled.div` margin-top: 10px; `; const FormWrapper = styled(Form)` padding: 10px; `; interface IProps {} const LoginForm = () => { const dispatch = useDispatch(); const [id, onChangeId] = useInput(''); const [password, onChangePassword] = useInput(''); const onSubmitForm = useCallback(() => { console.log(id, password); dispatch( logInAction({ id, password, }), ); }, [id, password]); return ( <FormWrapper onFinish={onSubmitForm}> <div> <label htmlFor='user-id'>아이디</label> <br /> <Input name='user-id' value={id} onChange={onChangeId} required /> </div> <div> <label htmlFor='user-password'>패스워드</label> <br /> <Input name='user-password' value={password} onChange={onChangePassword} required /> </div> <ButtonWrapper> <Button type='primary' htmlType='submit' loading={false}> 로그인 </Button> <Link href='/signup'> <a> <Button>회원가입</Button> </a> </Link> </ButtonWrapper> <div></div> </FormWrapper> ); }; export default LoginForm;

이미지로 상태 변화 보기

처음 실행했을 때

  • 크롬-개발자 도구-콘솔에서 처음 실행했을 때 해당 로그가 출력되어야 next-redux-wrapper가 제대로 실행된걸 알 수 있다.

로그인폼에 아이디/패스워드 입력

로그인 성공

로그인 성공 후 로그

  • 1 line log: components/LoginForm.tsx에서 dispatch하기 전
  • 2 line log: actions/users.ts에서 logInAction안에서
  • 3 line log: components/AppLayout.tsx에서 useSelectorisLoggedIn가져온 후

redux 상태 변화 / State / pending

redux 상태 변화 / Diff / pending

redux 상태 변화 / Diff / fulfilled

redux 상태 변화 / State/ fulfilled


참고 링크

© CHOI BOO 2021