[next.js + @reduxjs/toolkit] createStore에서 createSlice로 적용해보기

개발공부·2022년 10월 17일
0

React 공부하기

목록 보기
1/14

* createStore로 강의 내용 : 섹션 2 Redux 연동하기

https://www.inflearn.com/course/%EB%85%B8%EB%93%9C%EB%B2%84%EB%93%9C-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EB%A6%AC%EB%89%B4%EC%96%BC/dashboard

* 지금의 포스팅은 아래의 'Next.js와 리덕스 설명(로그인, 로그아웃)'의 수정 부분

https://ba-gotocode131.tistory.com/250

* 수정 이유

▶ 기존에 올렸던 자료를 수정하는 글
▶ createStore 이용 시 밑줄이 쳐지면서 아래와 같은 글로 나오면서 @reduxjs/toolkit의 이용방법을 권장

we recommend using the configurestore method of the @reduxjs/toolkit package, which replaces createStore

실제 변경 시 어떠한 문제점이 있었나

1. userSlice에서 ...state를 사용하지 않고 원하는 값만 변경하려고 함

→ 리덕스 저장소 값을 React 상태로 사용하므로 직접 변경할 수 없고 리듀서가 '순수'하게 만들어졌다는 것을 보관(기록)하기 위해서임(잊지 말기)

2. postSlice.js에서 기존의 addPost reducer를 변경 시 새로운 값(dummyPost)를 추가할 때 원하는 값(post/addPost)가 나오지 않음

→ 강의자분께 직접 질문드림, 정말 감사합니다... 정말 감사합니다....

▶ 무엇이 문제였나? : 처음에 코드를 입력 시 action.payload에 대한 정확한 이해 부족

▶ 데이터를 {email:'값'} 으로 넘기면 action.payload.email이 되고 바로 넘기면 action.payload인데 데이터 자리에 action.payload()로 함수처럼 넣음(당연히 안 됨)

//오류난 코드
export const postSlice = createSlice({
  name: "post",
  initialState,
  reducers: {
    addPost: (state, action) => {
     (state.mainPosts = action.payload(dummyPost)),
      (state.mainPosts = [...state.mainPosts]),
        (state.postAdded = true);
    },
  },
});

//수정된 코드
export const postSlice = createSlice({
  name: "post",
  initialState,
  reducers: {
    addPost: (state, action) => {
     state.mainPosts.push(action.payload),
      (state.mainPosts = [...state.mainPosts]),
        (state.postAdded = true);
    },
  },
});

[@reduxjs/toolkit 적용 전 redux]

components 폴더

└ LoginForm.js

└ PostForm.js

pages 폴더

└ _app.js

reducers 폴더

└ index.js

└ post.js

└ user.js

store 폴더

└ configureStore.js

[@reduxjs/toolkit 적용 후 redux]

- components 폴더

LoginForm.js

└ PostForm.js

pages 폴더

└ _app.js

store 폴더

└ postSlice.js

└ store.js

└ userSlice.js

1. [user.js]와 [userSlice.js] 비교

[user.js]

export const initialState = {
  isLoggedIn: false,
  me: null,
  signUpData: {},
  loginData: {},
};

export const loginAction = (data) => {
  return {
    type: "LOG_IN",
    data,
  };
};

export const logoutAction = () => {
  return {
    type: "LOG_OUT",
  };
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case "LOG_IN":
      return {
        ...state,
        isLoggedIn: true,
        me: action.data,
      };
    case "LOG_OUT":
      return {
        ...state,
        isLoggedIn: false,
        me: null,
      };

    default:
      return state;
  }
};

export default reducer;

[userSlice.js]

▶ 타입과 case 부분을 따로 만드는 것이 아닌 한 번에 합쳐줄 수 있음(코드량이 줄어들음)

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  isLoggedIn: false,
  me: null,
  signUpData: {},
  loginData: {},
};

export const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {
    loginAction: (state, action) => ({
      ...state,
      isLoggedIn: true,
      me: action.payload,
    }),
    logoutAction: (state, action) => ({
      ...state,
      isLoggedIn: false,
      me: null,
    }),
  },
});

export const { loginAction, logoutAction } = userSlice.actions;

export default userSlice.reducer;

2. [post.js]와 [postSlice.js] 비교

[post.js]

export const initialState = {
  mainPosts: [
   { 
    id: 1,
   content: "첫 게시글입니다.",
  User: {
    id: 1,
    nickname: "제로초",
  },
  Images: [],
  Comments: []
  },
  postAdded: false,
};

const ADD_POST = "ADD_POST";

export const addPost = {
  type: ADD_POST,
};

const dummyPost = {
  id: 2,
  content: "더미데이터입니다.",
  User: {
    id: 1,
    nickname: "제로초",
  },
  Images: [],
  Comments: [],
};

export default (state = initialState, action) => {
  switch (action.type) {
    case ADD_POST: {
      return {
        ...state,
        mainPosts: [dummyPost, ...state.mainPosts],
        postAdded: true,
      };
    }
    default: {
      return {
        ...state,
      };
    }
  }
};

[postSlice.js]

▶ 위의 mainPosts를 state.mainPosts로 dummyPost를 action.paylode로 넣음
▶ dummyPost는 components/PostForm.js 에서 진행 됨

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  mainPosts: [
      { 
    id: 1,
   content: "첫 게시글입니다.",
  User: {
    id: 1,
    nickname: "제로초",
  },
  Images: [],
  Comments: []
  },
  imagePaths: [],
  postAdded: false,
};

const dummyPost = {
  id: 2,
  content: "더미데이터입니다.",
  User: {
    id: 1,
    nickname: "제로초",
  },
  Images: [],
  Comments: [],
};

export const postSlice = createSlice({
  name: "post",
  initialState,
  reducers: {
    addPost: (state, action) => {
      state.mainPosts.push(action.payload),
        (state.mainPosts = [...state.mainPosts]),
        (state.postAdded = true);
    },
  },
});

export const { addPost } = postSlice.actions;

export default postSlice.reducer;

2. [store.js]

▶ postSlice와 userSlice를 합쳐주는 역할
▶ 여기서 생긴 wrapper로 __app.js에 감싸줌

import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { createWrapper, HYDRATE } from "next-redux-wrapper";
import user from "./userSlice";
import post from "./postSlice";

const rootReducer = combineReducers({
  user,
  post,
});

const masterReducer = (state, action) => {
  if (action.type === HYDRATE) {
    const nextState = {
      user: user.reducer,
      post: post.reducer,
    };
    return nextState;
  } else {
    return rootReducer(state, action);
  }
};

export const makeStore = () =>
  configureStore({
    reducer: masterReducer,
  });

export const wrapper = createWrapper(makeStore, { debug: true }); //마지막 이 부분

3. _app.js

import React from "react";
import Head from "next/head";
import PropTypes from "prop-types";
import "antd/dist/antd.css";
import { wrapper } from "../store/store";

const NodeBird = ({ Component }) => {
  return (
    <>
      <Head>
        <title>NodeBird</title>
      </Head>
      <Component />
    </>
  );
};

NodeBird.propTypes = {
  Component: PropTypes.elementType.isRequired,
};

export default wrapper.withRedux(NodeBird); //createStore, createSlice 모두 동일하게 적용

4. LoginForm.js

▶ 컴포넌트이므로 화면에 보여주고 싶다면 import LoginForm from "./LoginForm" 해야 함
▶ 최종화면은 pages에서 보여짐
▶ pages/index.js (1)
└ AppLayout.js (2)
└ LoginForm.js (3)

  • index.js
import React from "react";
import { useSelector } from "react-redux";

import AppLayout from "../components/AppLayout";
import PostForm from "../components/PostForm";
import PostCard from "../components/PostCard";

const Home = () => {
  const { isLoggedIn } = useSelector((state) => state.user);
  const { mainPosts } = useSelector((state) => state.post);
  return (
    <AppLayout>
      {isLoggedIn && <PostForm />}
      {mainPosts.map((c) => {
        return <PostCard key={c.id} post={c} />;
      })}
    </AppLayout>
  );
};

export default Home;
  • AppLayout.js
import React from "react";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
import Link from "next/link";
import { Menu, Input, Row, Col, Button } from "antd";
const { Search } = Input;
import UserProfile from "./UserProfile";
import LoginForm from "./LoginForm";


const onSearch = (value) => console.log(value);

const items = [
  { label: <Link href="/">노드버드</Link>, key: "item-1" },
  { label: <Link href="/profile">프로필</Link>, key: "item-2" },
  {
    label: <SearchInput placeholder="검색하기" onSearch={onSearch} />,
    key: "item-3",
  },
  { label: <Link href="/signup">회원가입</Link>, key: "item-4" },
];

const AppLayout = ({ children }) => {
  const { isLoggedIn } = useSelector((state) => state.user);
  console.log("isLoggedIn", isLoggedIn);

  return (
    <div>
      <Global />
      <Menu items={items} mode="horizontal" />

      <Row gutter={8}>
        <Col xs={24} md={6}>
          {isLoggedIn ? <UserProfile /> : <LoginForm />}
          왼쪽 메뉴
        </Col>
        <Col xs={24} md={12}>
          {children}
        </Col>
        <Col xs={24} md={6}>
          <Button
            type="link"
            href="https://ba-gotocode131.tistory.com/"
            target="_blank"
            rel="noopener noreferrer"
          >
            블로그
          </Button>
        </Col>
      </Row>
    </div>
  );
};

AppLayout.prototype = {
  children: PropTypes.node.isRequired,
};

export default AppLayout;

[LoginForm.js]

▶ loginAction을 불러옴
dispatch를 이용해 action을 발생시키고 상태를 업데이트 하기 위함
▶ 상태를 업데이트 하는 유일한 방법은 store.dispatch()를 부르고 action 객체를 넘겨주는 것
store은 reducer function을 실행시키고 새로운 state를 내부에 저장할 것

dispatch 참고 : https://developerntraveler.tistory.com/144

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 "../store/userSlice";

const ButtonWrapper = styled.div`
  margin-top: 10px;
`;

const FormWrapper = styled(Form)`
  padding: 10px;
`;

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} style={{ padding: "10px" }}>
      <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}
          type="password"
          required
        />
      </div>
      <div style={{ marginTop: "10px" }}>
        <Button type="primary" htmlType="submit" loading={false}>
          로그인
        </Button>
        <Link href="/signup">
          <a>
            <ButtonWrapper>회원가입</ButtonWrapper>
          </a>
        </Link>
      </div>
    </FormWrapper>
  );
};

export default LoginForm;
profile
개발 블로그, 티스토리(https://ba-gotocode131.tistory.com/)로 갈아탐

0개의 댓글