과거 프로그래밍 자료들/React

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

평부 2022. 10. 13. 16:48

 

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

 

[리뉴얼] React로 NodeBird SNS 만들기 - 인프런 | 강의

리액트 & 넥스트 & 리덕스 & 리덕스사가 & 익스프레스 스택으로 트위터와 유사한 SNS 서비스를 만들어봅니다. 끝으로 검색엔진 최적화 후 AWS에 배포합니다., - 강의 소개 | 인프런...

www.inflearn.com

 

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

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

 

Next.js와 리덕스 설명(로그인, 로그아웃)

출처 : 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 [리뉴얼] React로 NodeBird SNS 만들기 - 인프런 | 강의 리..

ba-gotocode131.tistory.com

 

 

* 수정 이유

 

▶ 기존에 올렸던 자료를 수정하는 글

▶ 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()로 함수처럼 넣음(당연히 안 됨)

 

* 참고 : https://www.inflearn.com/questions/603940

 

action.payload 관련 질문입니다. - 인프런 | 질문 & 답변

안녕하세요 선생님 강의 잘 보고 있는데, 제가 앞에서 놓친게 있는지, 이해가 안되는 부분이 있어 질문드립니다.  userSlice 파일 중에서 setUser()의state.email = action.payload.email 처럼 페이로드 위에 변

www.inflearn.com

 

//오류난 코드
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

     └ AppLayout.js

         └ LoginForm.js

 

 

[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 styled from "styled-components";
import { Menu, Input, Row, Col, Button } from "antd";
const { Search } = Input;
import UserProfile from "./UserProfile";
import LoginForm from "./LoginForm";
import { createGlobalStyle } from "styled-components";

const Global = createGlobalStyle`
.ant-row {
  margin-right: 0 !important;
  margin-left: 0 !important;
}

.ant-col:first-child {
    padding-left: 0 !important;
}

.ant-col:last-child {
  padding-right: 0 !important;
}
`;

const SearchInput = styled(Search)`
  vertical-align: "middle";
`;

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

 

[Redux] Redux 기본 개념 정리(action, reducer, store, dispatch...)

이 글은 Redux 공식 문서를 참고해서 정리한 글입니다. Actions action은 type field를 가지고 있는 plain JavaScript object이다. 단순하게 action은 어플리케이션에서 무언가 일어나는 것을 설명한 이벤트라고..

developerntraveler.tistory.com

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;

 

 

5. 결과

0123
로그인 -> 로그아웃 -> 다시 로그인 -> 포스팅 올리기