React/게시판만들기 v1.

[React][CRUD] 리액트로 간단한 게시판 페이지 만들어보기 - 4. redux-toolkit에 redux-saga 적용하기(configureStore), redux-toolkit으로 reducer, action 한꺼번에 생성하기(createSlice)

binaryJournalist 2020. 10. 29. 09:47
반응형

 

 

목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial

 

[React][CRUD] create-board-tutorial

code: github.com/jwlee-lnd/react-create-board jwlee-lnd/react-create-board Description(korean) : https://binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial - jwlee-lnd/react-create-boar..

binaryjourney.tistory.com

 

 

 

 

 

3편에서 RegisterPage.js 를 글 수정할 때도 재사용할 것이라고 언급한 적이 있었는데 RegisterPage 컴포넌트는 프레젠테이셔널 컴포넌트와 컨테이너 컴포넌트가 합쳐져 있는 상태라 재사용할 수 없는 구조여서 파일 구조 수정이 필요하게 되었다.

 

 

RegisterPage.js 가 들어있는 디렉토리만 바꾸면 되고 바뀐 구조는 이렇다

 

changed directory

 

RegisterPage 를 컨테이너 컴포넌트로 사용하고 RegisterOrEdit을 프레젠테이셔널 컴포넌트로 사용할 예정이다.

 

 

containter component 와 presentational component에 대해선 이  redux 공식홈페이지에 잘 나와있다.

 

redux.js.org/basics/usage-with-react#presentational-and-container-components

 

Usage with React | Redux

Basic Tutorial > Usage with React: How to use Redux with React components

redux.js.org

 

 

 

 

우선 RegisterPage 컴포넌트에선 RegisterOrEdit 이라는 컴포넌트를 return 하는 방식으 대체했다.

(console.log 는 compile success 확인 여부이니 신경쓰지 않아도 된다)

 

container component 로 쓸 RegisterPage

 

 

 

RegisterOrEdit 컴포넌트의 모습은 DOM형태만 만들어주고 값과 이벤트는 RegisterPage의 props로 받아쓰므로 단순하게 생겼다. 사실 return 값은 이전 RegisterPage 컴포넌트와 거의 다를 게 없다.

 

presentational component 로 사용핧 RegisterOrEdit

 

 

console 창에 값이 잘 나오는지 확인해보자

 

성공!

 

 

 

 

 

 

 

이젠 form submit 부분을 만들 것이다.

 

우선  RegisterPage 컴포넌트에 이벤트를 간단하게 생성하고 RegisterOrEdit 컴포넌트에 props로 전달한다.

 

 

// RegisterPage.js

  const onSubmitArticle = (event) => {
    event.preventDefault();
    const article = { title: TitleValue, content: ContentValue };
    console.log(article);
  };
  
    return (
    <>
      <RegisterOrEdit
        titleValue={TitleValue}
        contentValue={ContentValue}
        handleTitleChange={onTitleChange}
        handleContentChange={onContentChange}
        handleSubmit={onSubmitArticle}
      />
    </>
  );

 

 

 

값이 많을 때는 프레젠테이셔널 컴포넌트의  form 태그 내 value를 모두 긁어와서 setStateValue 하는 방법도 있지만 보낼 값이 적으니 객체 만들어 보내는 쉬운 방법으로 하기로 한다.

 

 

const article = { title: TitleValue, content: ContentValue };

 

 

 

 

 

 

RegisterOrEdit 컴포넌트에서 전달받은 handleSubmit 이벤트는 form 태그의 onSubmit 에 아직 적용시키지 않고 aritcle 값을 먼저 확인하기 위해 button의 onClick에만 적용한다.

 

 

// RegisterOrEdit.js

<button onClick={props.handleSubmit}>Submit</button>

 

 

 

 

 

 

파일을 저장하고 버튼을 클릭해보자

 

 

RegisterPage 컴포넌트에 article 객체에 값이 잘 들어간 것을 볼 수 있다!

 

 

 

 

 

 

RegisterOrEdit 컴포넌트를 게시글 수정 때도 다시 사용하려면 새글 등록인지 글 수정인지 구별할 flag가 필요하다.

 

 

따라서 useState로 IsForUpdate 를 만들어주고 RegisterOrEdit 컴포넌트에 props로 넘긴다.

 

아직 인자 값이 없기 때문에 setIsForUpdate은 나중에 수정 기능을 만들 때 보기로 한다.

 

 

// RegisterPage.js

  const [IsForUpdate, setIsForUpdate] = useState(false);
  
    return (
    <>
      <RegisterOrEdit
        titleValue={TitleValue}
        contentValue={ContentValue}
        handleTitleChange={onTitleChange}
        handleContentChange={onContentChange}
        handleSubmit={onSubmitArticle}
        updateRequest={IsForUpdate}
      />
    </>
  );

 

 

 

 

 

 

RegisterOrEdit 컴포넌트에서는 RegisterPage 컴포넌트에서 보내준 IsForUpdate 값을 updateRequest로 받아 이 값이 true 이면 수정, false이면 등록으로 버튼이 바뀌어 나타날 수 있도록 코드를 수정해준다.

 

 

<button onClick={props.handleSubmit}>
  {props.updateRequest ? "수정" : "등록"}
</button>

 

 

 

웹 화면에서는 이렇게 나타나야 한다.

등록 버튼

 

 

 

 

 

 

수정이 완료된 RegisterPage 컴포넌트와 RegisterOrEdit 컴포넌트는 다음과 같은 모습이다.

 

 

 

RegisterPage

 

 

RegisterOrEdit

 

 

 

 

 

 

이젠 저장할 값을 서버에 반영해야 할 차례다.

 

 

긴 여정이 될 것 같은데 필요한 모듈을 설치하고 reducer와 middleware인 saga를 만들어줘야 한다.

 

액션 -> 스토어  전달 -> 리듀서(reducer)에서 액션타입에 따라 다른 payload 전달 -> 상태 변경

이 흐름은 모두 동기적으로 일어나므로 API 같은 외부 리소스를 가져오는 경우 동기적인 리덕스 흐름만으로 해결할 수없다.

 

그래서 리듀서를 타기 전 혹은 액션에서 스토어 상태 변경 전에 비동기 로직을 끼워넣을 수 있는데 이를 미들웨어(middleware)라고 한다.

 

 

참고:

jeonghwan-kim.github.io/dev/2019/07/22/react-saga-ts-1.html

 

리덕스 사가 사용하기 (타입스크립트 버전) - 1편

지난 글에서 정리한 것 처럼 리덕스는 다음 순서로 상태를 관리한다. 액션 객체 생성 스토어로 전달 리듀서가 액션 객체를 수신 액션 타입에 따라 전달받은 패이로드를 가지고 스토어 상태 변경

jeonghwan-kim.github.io

 

 

 

 

 

api 외부 리소스를 가져오는 데 사용하는 미들웨어로 대표적인 것은 redux-thunk와 redux-saga가 있으며 나는 redux-saga를 사용할 예정이다.

 

 

 

그리고 리듀서를 한 파일에서 쉽게 만들고 활용하기 위해서 redux-toolkit에서 제공하는 createAction, createSlice 등을 이용할 것이다.

(필요한 경우 다른 함수를 더 쓸 수도 있다.)

 

 

 

 

필요한 라이브러리를 설치하자.

root folder에서 terminal을 연다. (VSCode terminal을 kill 해도 되고 open native terminal 을 이용하여 열어도 된다)

 

 

 

terminal에 다음과 같이 입력한다.

 

 

yarn add redux-logger axios

 

 

 

 

설치가 끝나면 package.json에 반영되어 있을 것이다.

 

package.json

 

 

 

 

 

 

 

일단 새로 만들 파일과 그 파일들이 들어갈 디렉토리는 이렇게 생겼다.

saga와 slice(reducer)

 

 

 

 

 

sagas 디렉토리에는  saga 파일만 들어갈 것이고

slice 디렉토리에는 type, action, reducer 나 혹은 이 세 가지를 다 포함할 slice들이 만들어진 파일들이 들어갈 것이다.

 

 

 

saga와 slice가 종류별로 다양하지 않고 한 개만 있다면 rootSaga와 rootSlice는 굳이 안 만들어도 된다.

나는 혹시 몰라서 만들어놨다.

보통은 index.js로 만들어 놓는데 나중에 import할 때 헷갈릴까봐 이름을 달아놓았다.

 

 

 

reducer는 redux-toolkit의 createSlice로 만들어 사용할 건데 여기서부터는 redux를 이용한 상태 관리 흐름을 이미 알고 있다는 가정 하에 설명할 것이다.

 

모른다면 redux 공식홈페이지 설명을 먼저 읽고 간단한 tutorial을 한 뒤에 뒷 과정을 이어나가는 것을 추천한다.

 

#redux : actions

redux.js.org/basics/actions

 

Actions | Redux

Basic Tutorial > Actions: Core concept - actions are plain objects that describe events

redux.js.org

 

# redux : reducers

redux.js.org/basics/reducers

 

Reducers | Redux

Basic Tutorial > Reducers: Core concept - reducers are plain functions that return new state

redux.js.org

 

 

# redux: store

redux.js.org/basics/store

 

Store | Redux

Basic Tutorial > Store: Core concept - the Redux store

redux.js.org

 

# redux : data flow

redux.js.org/basics/data-flow

 

Data flow | Redux

Basic Tutorial > Data Flow: How data flows through a Redux app

redux.js.org

 

 

# redux : basic tutorial

redux.js.org/basics/example

 

Example: Todo List | Redux

Basic Tutorial > Todo List: Source code for the Todo List example

redux.js.org

 

 

 

 

그리고 이 사이트도 추천한다.

 

이곳도 마찬가지로 redux-saga와 redux-toolkit 를 동시에 사용했는데 특히 redux-toolkit 부분에서 엄청 도움이 되었다

 

mjn5027.tistory.com/39?category=1157737

 

[ React ] 리액트 Saga + Toolkit ( 미들웨어 사가, 리덕스 툴킷 )

지금껏 다룬 포스팅의 project 에 Middleware Saga 와 Redux Toolkit 을 적용시켜보자. 먼저 해당 포스팅에서 사용할 기능들을 설치하자. // Redux 와 React-Redux 설치 yarn add redux react-redux // Redux Tool..

mjn5027.tistory.com

 

 

 

 

 

우선 articleSlice.js 에 redux-toolkit 의 createSlice를 import 한다.

 

 

// /src/slice/articleSlice.js

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

 

 

 

redux-toolkit이 제공하는 API에서 createSlice 설명은 다음과 같다.

 

A function that accepts an initial state, an object full of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state.

 

출처: redux-toolkit.js.org/api/createSlice

 

 

 

 

그리고 추가할 내용은 

 

 

// articleSlice.js

export const articleSlice = createSlice({
  name: "article",
  initialState: { id: 0, title: "", content: "", views: 0 },
  reducers: {
    registerArticle: (state, article) => {
      console.log(article);
      return {
        ...article,
        id: state.id,
      };
    },
    registerArticleAsync: (state, { payload }) => {
      console.log(payload);
      debugger;
      return {
        ...state,
        id: payload.id, 
      };
    },
  },
});

 

 

 

initialState는 인용문에서 말한 initial State,

reducers: {registerArticle: ..., registerArticleAsync: ,,} 는  reducer 함수가 있는 객체,

그리고 name은 slice name 이다.

 

createSlice는 자동으로 액션 타입과 액션 생성함수를 만들어준다.

 

 

 

액션 생성함수 없는 reducer를 extraReducer 로 추가할 수 있다고 API문서에 나와있지만 나는 일반 리듀서에서 해결해볼 것이다. (일단은)

 

 

 

 

그리고 이후에 saga에서 편히 쓰기 위해 articleSlice 변수에서 action과 reducer를 빼와 export 변수로 따로 만들어 아랫줄에 추가하였다.

 

 

 

// articleSlice.js


export const articleReducers = articleSlice.reducer;
export const articleActions = articleSlice.actions;

 

 

 

 

 

 

 

앞의 내용을 정리하면 articleSlice.js 파일의 형태는 다음과 같다.

 

 

// articleSlice.js

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

export const articleSlice = createSlice({
  name: "article",
  initialState: { id: 0, title: "", content: "", views: 0 },
  reducers: {
    registerArticle: (state, article) => {
      console.log(article);
      return {
        ...article,
        id: state.id,
      };
    },
    registerArticleAsync: (state, { payload }) => {
      console.log(payload);
      debugger;
      return {
        ...state,
        id: payload.id,
      };
    },
  },
});

export const articleReducers = articleSlice.reducer;
export const articleActions = articleSlice.actions;

 

 

여기서 registerArtice 함수는 saga에서 감시할 액션으로 쓸 것이고

상태변경은 registerArticleAsync 함수는 서버 저장 후 게시물을 불러오는 데까지 이어 쓸 것이다.

 

 

 

 

 

 

articleSlice의 리듀서를 바로 store에 적용해도 되나 앞일은 모르니 rootSlice.js 파일을 만들어(앞에서 이미 만든 것과 동일한 파일임) 준다.

 

rootSlice는 사실 rootReducer라고 보면 되는데 /slice 디렉토리 안에 있기 때문에 이름을 rootSlice로 통일시킨 것뿐이다.

 

 

 

rootSlice.js

 

 

rootReducer라는 변수를 만들어 리듀서들을 묶어준다.

 

지금까지 사용한 리듀서는 일단 1개뿐이므로 articleReducer 하나만 import하여 combineReducer에 넣어준다.

 

 

 

 

 

 

 

그리고 /src 디렉토리에 바로 store.js 파일을 만들어준다.

 

 redux-saga를 미들웨어로 쓸 것이기 때문에 createStore가 아닌 redux-toolkit의 configureStore를 사용한다.

 

 

// store.js

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

 

 

 

 

 

 

그리고 사용할 미들웨어를 import 한다.

 

 

// store.js

import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";

 

 

 

 

 

saga를 실행시켜줄 createSagaMiddleware을 sagaMiddleware라는 변수를 만들어 받아놓고

initialState 도 세팅해준다.

 

 

// store.js

const sagaMiddleware = createSagaMiddleware();
const initialState = {};

 

 

 

 

 

 

 

configureStore 함수 내 필요한 데이터를 집어놓고 store 라는 변수로 받아 둔다.

 

 

// store.js

const store = configureStore({
  reducer: rootReducer,
  devTools: true,
  preloadedState: initialState,
});

export default store;

 

 

여기서  devTools 는 웹의 devTool 과 연결할 것인지 여부를 적는 것이다.

preloadState는 reducer에서 정해둔 initialState보다 더 앞서 prefix될 state이다.

 

 

 

 

 

 

redux-toolkit의 default middleware는 redux-thunk이기 때문에 sagaMiddleware 로 덮어씌워줘야 한다.

그러니 이젠 saga를 만들 차례다!

 

 

 

 

 

 

글 초반에 만들어둔 articleSaga.js 파일에 generate 함수를 만든다.

saga 는 generate 함수를 바탕으로 만들어야 한다.

 

 

// articleSaga.js

export function* registerArticleAsync(action) {
  console.log(action);
  debugger;
  yield console.log("finish");
}

 

(값 확인을 위해 기본형태로만 만들었다)

 

 

 

 

 

generate 함수를 모른다면 아래 링크들을 참고하길 바란다.

 

wonism.github.io/javascript-generator/

 

WONISM | JavaScript Generator 이해하기

WONISM's Blog

wonism.github.io

developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators

 

Iterators and generators

Iterators and Generators bring the concept of iteration directly into the core language and provide a mechanism for customizing the behavior of for...of loops.

developer.mozilla.org

developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Iterators_and_Generators

 

반복기 및 생성기

컬렉션 내 각 항목 처리는 매우 흔한 연산입니다. JavaScript는 간단한 for 루프에서 map() 및 filter()에 이르기까지, 컬렉션을 반복하는 많은 방법을 제공합니다. 반복기(iterator) 및 생성기(generator)는

developer.mozilla.org

developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Statements/function*

 

function*

function* 선언 (끝에 별표가 있는 function keyword) 은 generator function 을 정의하는데, 이 함수는 Generator 객체를 반환합니다.

developer.mozilla.org

exploringjs.com/es6/ch_generators.html

 

22. Generators

22. Generators 22.1. Overview 22.1.1. What are generators? 22.1.2. Kinds of generators 22.1.3. Use case: implementing iterables 22.1.4. Use case: simpler asynchronous code 22.1.5. Use case: receiving asynchronous data 22.2. What are generators? 22.2.1. Rol

exploringjs.com

 

 

 

 

 

 

saga 도 기능별로 다양하게 만들 수 있으므로 rootSaga를 하나 두기로 한다.

rootSaga 로서 사용할 함수도 generate 함수여야 한다.

generate 함수는 function에 별(*)이 붙어있는 모양으로 작성해야 하고 함수내 꼭 yield를 써줘야 한다. (안 그러면 오류남)

 

 

// rootSaga.js

import { takeLatest } from "redux-saga/effects";
import { articleActions } from "../slice/articleSlice";
import { registerArticleAsync } from "./articleSaga";

const { registerArticle } = articleActions;

export default function* rootWatcher() {
  yield takeLatest(registerArticle.type, registerArticleAsync);
}

 

 

rootSaga.js 에서는 일단 뷰에서 registerArticle 액션생성함수를 dispatch 하게 되면 articleSaga의 registerArticleAsync 함수를 호출하도록 하였다.

 

 

 

 

 

 

 

그리고 다시 store.js로 돌아와서 rootSaga 를 store에 탑재해준다.

 

 

// store.js

import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import rootReducer from "./slice/rootSlice";
import rootSaga from "./sagas/rootSaga";

const sagaMiddleware = createSagaMiddleware();
const initialState = {};

const store = configureStore({
  reducer: rootReducer,
  middleware: [sagaMiddleware, logger],
  devTools: true,
  preloadedState: initialState,
});

sagaMiddleware.run(rootSaga);

export default store;

 

 

여기서 logger도 다른 종류의 미들웨어인데 prevState (previous state), action, next State 를 console 창에 보여주는 기능을 한다.

 

 

 

saga가 제대로 돌아가려면  꼭 run()을 해줘야 한다.

 

 

// store.js

sagaMiddleware.run(rootSaga);

 

 

 

 

 

store가 준비되었으니 index.js로 가서 store를 import 하자!

 

 

index.js

 

 

 

 

 

 

 

마지막으로 saga를 타도록 할 액션함수를 RegisterPage에서 dispatch 해야 한다.

 

 

RegisterPage 컴포넌트에서 useDispatch 라는 redux 훅과 articleSlice에서 빼둔 액션함수를 import하고  onSubmitArticle 함수를 다음과 같이 수정해야 한다.

 

 

// RegisterPage.js

import { useDispatch } from "react-redux";
import { articleActions } from "../../../slice/articleSlice";

function RegisterPage() {
  const dispatch = useDispatch();
  
  ...

  const onSubmitArticle = (event) => {
    event.preventDefault();
    const article = { title: TitleValue, content: ContentValue };
    dispatch(articleActions.registerArticle(article));
  };
  
  return (
  ...
  );
}

 

 

 

 

 

RegisterPage 컴포넌트의 최종적 모습은 다음과 같다.

 

RegisterPage

 

 

 

그리고 RegisterOrEdit 컴포넌트의 form tag 에 onSubmit 이벤트도 마무리 해주자.

<form onSubmit={props.handleSubmit}>

...

</form>

 

 

 

 

 

 

등록 버튼을 눌렀을 때 articleSaga.js 에 적은 console.log까지 잘 가는지 확인해보자.

 

 

// articleSaga.js


import { articleActions } from "../slice/articleSlice";

export function* registerArticleAsync(action) {
  console.log(action);
  debugger;
  yield console.log("finish");
}

 

 

 

 

 debugger 걸린 articleSaga

 

 

 

 

 

console 창 articleSaga

 

 

 

Axios post 는 다음 편에서 다루겠다.

 

 

 

 

 

 

목차 돌아가기: binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial

 

[React][CRUD] create-board-tutorial

code: github.com/jwlee-lnd/react-create-board jwlee-lnd/react-create-board Description(korean) : https://binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial - jwlee-lnd/react-create-boar..

binaryjourney.tistory.com

 

반응형