관리 메뉴

Jerry

리덕스 폴더 구조, combineReducer, 리덕스 미들웨어(redux 공부하기 #5) 본문

Front/Redux

리덕스 폴더 구조, combineReducer, 리덕스 미들웨어(redux 공부하기 #5)

juicyjerry 2021. 5. 4. 14:46
반응형

필자는 리덕스에 대한 코드 스테이츠 과정에서 리덕스에 대해서 학습하였지만 코드로 구현하는 부분과 리덕스 코드를 이해하는 부분이 부족하다고 느껴져 '인프런에서 제로초님의 'Redux vs MobX (둘 다 배우자!)'라는 유료 강의를 결제해 공부하고 있다. 강의 특성상 MobX 내용이 포함되어 있지만, 시간 관계상 redux에 관한 내용 위주로 학습하려고 한다.

 

이 글의 목적은 강의를 수강하면서 학습한 내용을 이해 및 정리하고자 이렇게 글을 적게 되었다.

이번 기회를 통해 리덕스에 대해 제대로 이해해보고 프로젝트에서 사용한 리덕스 코드도 같이 이해하려고 한다.

(프로젝트 관련한 내용은 언급하지 않습니다)

 

 

source:  https://redux.js.org/

 

 

 


 

리덕스 폴더 구조

아래 이미지에서 1.redux 디렉터리와 2.redux 디렉터리가 보인다. 

두 디렉터리는 차이점은 action과 reducer 부분을 분리해주었다. 

앞선 글에서 '데이터 중심으로 생각을 어떻게 할까?'란 질문에 대한 답변이 될 것 같다.

이렇게 action과 reducer를 분리해주어 미래에 대비해주려고 한다. 

장기적인 관점으로 보았을 때, action과 reducer가 길어지게 되므로 코드를 미리 여러 개로 분리를 시켜준다. 

 

 

 

 

actions은 사용자와 게시글에 대한 것이므로 post.js와 user.js로 나누었고 reducers도 동일하다. 

또한, 여기서 리듀서를 분리해줄 수 있는 이유가 리듀서가 순수 함수여서 그렇다!

순수 함수는 매개변수와 함수 내부에서 선언한 변수를 제외하고 나머지를 참조하지 않는 함수이므로 자유롭게 바깥으로 뺄 수 있다.

 

 

 

글로만 보면 무슨 말인지 알기 힘드니 코드를 살펴보자!

1.redux 디렉터리의 index.js 코드이다.

 

// 1.redux/index.js
const { createStore } = require("redux");

const reducer = (prevState, action) => {
  //! 새로운 state 만들어주기: 새로운 것으로 대체
  switch (action.type) {
    case "LOG_IN":
      return {
        ...prevState,
        user: action.data,
      };
    case "LOG_OUT":
      return {
        ...prevState,
        user: null,
      };
    case "ADD_POST":
      return {
        ...prevState,
        post: [...prevState.posts, action.data],
      };
    default:
      // default가 필요한 이유는 case에 해당되지 않는 것이 들어왔을 경우를 위해서 (ex. 오타)
      return prevState;
  }
};
// 리듀서와 맥션이 엄청 길어지겠구나.

const initialState = {
  user: null,
  posts: [],
};

// const nextState = {
//   ...initialState,
//   posts: [action.data],
// };

// 불변성 유지하면서 값을 전달
// const nextState = {
//   ...initialState,
//   posts: [...initialState.posts, action.data],
// };

//! store
const store = createStore(reducer, initialState);
store.subscribe(() => {
  // react-redux 안에 들어있어요.
  console.log("changed"); // 화면 바꿔주는 코드 여기서
});

console.log("1: ", store.getState()); // state가 나와요

//! action을 만드는 creator: 액션 생성자
const logIn = (data) => {
  return {
    type: "LOG_IN", // action name
    data,
  };
};

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

const addPost = (data) => {
  return {
    type: "ADD_POST",
    data,
  };
};

// console.log("2: " + store.getState()); '+'로 하면 [object Object]로 나온다.
console.log("2: ", store.getState());

// ---------------------------

//! 위에는 미리 만들어야 하는 것이고 밑에는 리액트 화면에서 실행되는 부분

store.dispatch(
  logIn({
    id: 1,
    name: "zerocho",
    admin: true,
  })
);
console.log("3: ", store.getState());

store.dispatch(
  addPost({
    userId: 1,
    id: 1,
    content: "안녕하세요. 리덕스",
  })
);
console.log("4: ", store.getState());

store.dispatch(
  addPost({
    userId: 1,
    id: 2,
    content: "두번째 리덕스",
  })
);
console.log("5: ", store.getState());

store.dispatch(logOut({}));
console.log("6: ", store.getState());

console.log("7: ", store);

 

 

reducer

 

먼저, action과 짝궁으로 action에 해당하는 새로운 객체(state/상태)를 리턴해주는 reducer가 보인다.

 

const reducer = (prevState, action) => {
  //! 새로운 state 만들어주기: 새로운 것으로 대체
  switch (action.type) {
    case "LOG_IN":
      return {
        ...prevState,
        user: action.data,
      };
    case "LOG_OUT":
      return {
        ...prevState,
        user: null,
      };
    case "ADD_POST":
      return {
        ...prevState,
        post: [...prevState.posts, action.data],
      };
    default:
      // default가 필요한 이유는 case에 해당되지 않는 것이 들어왔을 경우를 위해서 (ex. 오타)
      return prevState;
  }
};

 

 

action

상태에 어떠한 변화가 필요하게 될 때, 액션을 발생시키도록 하는데, 아래 코드에서 return 다음에 위치하고 있는 { type: "LOG_IN", data, }; 을 가리킵니다. 

그리고,  const logIn은 action을 만드는 action creator (액션 생성 함수)가 입니다.

 

const logIn = (data) => {
  return {
    type: "LOG_IN", // action name
    data,
  };
};

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

const addPost = (data) => {
  return {
    type: "ADD_POST",
    data,
  };
};

 

 

store

리덕스에서 한 애플리케이션 당 하나의 스토어를 만들게 되는데, 그 안에는 상태와 리듀서가 들어갑니다. 

subscritbe(구독)은 스토어의 내장 함수 중 하나입니다. 파라미터를 함수 형태의 값으로 받으며, 액션이 디스패치 됐을 때마다 전달해준 함수가 호출됩니다.

 

 

리덕스 공식 문서에서는 이렇게 이야기 합니다.

앱의 전체 상태 트리를 가지고 있는 저장소입니다. 이 안의 상태를 바꾸는 유일한 방법은 여기에 액션을 보내는 것뿐입니다.

저장소는 클래스가 아닙니다. 단지 안에 몇 가지 메서드가 들어있는 객체일 뿐입니다. 생성하기 위해서는 루트 리듀싱 함수를 createStore에 전달하면 됩니다.

 

 

createStore는

앱의 상태 트리 전체를 보관하는 Redux 저장소를 만듭니다. 앱 내에는 단 하나의 저장소만 있어야 합니다.

 

//! store
const store = createStore(reducer, initialState);
store.subscribe(() => {
  // react-redux 안에 들어있어요.
  console.log("changed"); // 화면 바꿔주는 코드 여기서
});

 

 

스토어의 내장 함수 중 하나인 디스패치(dispatch)는 액션을 발생시키는 것이기 때문에, 액션을 파라미터로 전달합니다.

그 후, 스토어가 리듀서 함수를 실행시켜 해당 액션 처리 로직에 따라 새로운 상태로 만듭니다. (reducer와 비교해보세요!)

 

공식문서에서는 이렇게 이야기합니다.

액션을 보냅니다. 이것이 상태 변경을 일으키기 위한 유일한 방법입니다.

 

getState()는 

애플리케이션의 현재 상태 트리를 반환합니다. 저장소의 리듀서가 마지막으로 반환한 값과 동일합니다.

//! 위에는 미리 만들어야 하는 것이고 밑에는 리액트 화면에서 실행되는 부분

store.dispatch(
  logIn({
    id: 1,
    name: "zerocho",
    admin: true,
  })
);
console.log("3: ", store.getState());

store.dispatch(
  addPost({
    userId: 1,
    id: 1,
    content: "안녕하세요. 리덕스",
  })
);
console.log("4: ", store.getState());

store.dispatch(
  addPost({
    userId: 1,
    id: 2,
    content: "두번째 리덕스",
  })
);
console.log("5: ", store.getState());

store.dispatch(logOut({}));

 

 

 

T.M.I

reducer 이름에 대해 알아보자..

영단어 Reduce 본래 의미를 보자면, 단순하게 줄이다는 의미보다 변경이라는 의미에 가깝습니다."to change something into a simpler or more general form"
그 예시로 어떤 복잡한 수학 문제를 다른 비슷한 문제로 변경해서 (더 간단하게만 드려고) 푸는 방법을 수학에서는 reduction이라고도 합니다."In mathematics, reduction refers to the rewriting of an expression into a simpler form."
그런 의미에서 완벽히 번역은 힘들지만 reduce는 "고쳐나간다" (간단하게 만들기 위해서, 혹은 특정 규칙을 적용하기 위해서)라고 생각해보면 좋을 것 같습니다. 따라서, 주어진 상태를 고쳐나가는 게 함수형 프로그래밍에서 자주 보이는 reduce() 함수입니다. [주어진 상태]. reduce([특정 규칙]) => 변경된 상태.
즉, 리덕스에서의 reduce()는 현재 상태(previousState)를 새로운 상태(newState)로 변경할 때 쓰는 함수가 됩니다.
리듀서에 대해서는:
리덕스 공식 홈페이지에서의 설명은"여러분이 이 형태의 함수를 Array.prototype.reduce(reducer,? initialValue)로 넘길 것이기 때문에 리듀서라고 부릅니다"
다시 말해, 리듀서라고 불리는 이유는 리듀서가 reduce() 함수에서 사용하는 콜백 함수이기 때문에 리듀서라고 불립니다.

T.M.I 출처: devlog.jwgo.kr/2018/08/23/redux-which-is-weird-term/  

 

 

 

 

여기까지 1.redux 디렉터리의 index.js 코드 요소들을 각각 소개해보았다.

index.js에 한데 모여있는 요소들을 데이터 중심적 사고를 통해 2.redux 디렉터리처럼 분리를 시켜준 것이다!

 

해당 데이터는 아래에서 보이는 초기 데이터의 레벨에 따라 분리를 하며, 그래서 초기에 데이터 뼈대(구조)를 잘 잡아놓으면, 전체적인 구조를 잘 잡을 수 있으며  action과 리듀서가 나뉘기 때문에 코드 짜기 좋다고 한다!

 

참고로, 1단계 레벨이 user, posts, comments... 이며 2단계 레벨이 isLogginIn, data이다.

 

 

 

하지만, 이 기준은 따로 정해져 있는 것이 아니어서, 애매한 게 프로젝트마다 상황이 다르기 때문에 프로젝트 경험을 쌓으면서 익혀야 한다고 합니다! 사람마다 뎁스를 잡는 기준이 다르기 때문이다. 

 

추가로, initialState 안에 있는 것들은 얕은 복사를 하기 때문에, 불변성을 유지한다고 해도, 메모리를 더 잡아먹거나 하지 않는다고 한다. 불변성을 유지할 때도, 위의  state들은 얕은 복사를 하고 있기 때문에 바뀌는 것만 바뀌고 나머지는 참조하기 때문에 메모리를 두 배로 잡아먹거나 하지 않는다.

 

해당 코드를 좀 더 보고 싶으시면 제 깃헙 링크를 타고 가 살펴보세요!

 

 

참고한 내용:

Redux | ko.redux.js.org/

velopert | bit.ly/3eQPGH8

 

 

 

combineReducer

헬퍼 함수인 combineReducer는

앱이 점점 복잡해지는 상황에서 리듀싱 함수를 상태의 독립된 부분들을 관리하는 함수들로 분리할 때 사용합니다!

 

한 마디로, 쪼개려고 쓰는 함수이다. 

그런데 리듀서는 함수인데 어떻게 쪼갤까요? 분리야 user(사용자)와 post(게시물)로 하면 되지만 함수 자체를 조갤 수 없기 때문에 우린 combineReducer를 이용한다!

 

 

 

 

 

 

리덕스 미들웨어

 action이 객체고 동기적으로 작동하며 dispatch 함수는 객체를 받아서 동작하는 함수이기 때문에, 비동기가 들어갈 틈이 없다고 한다. 이를 위해 리덕스에서 미들웨어를 사용한다고 한다.

 

미들웨어의 위치는 dispatch를 할 때, action이 reducer와 매칭이 되는데, 그 사이에 미들웨어가 들어가 동작하는데, 이렇게

dispatch와 reducer 사이에서 동작하기 때문에 미들웨어라고 불린다고 한다.

 

미들웨어로는 redux-thunk나 redux-saga를 많이 쓰며,

미들웨어는 항상 비동기를 다루기 위해서 사용하기보다는 둘 사이에 어떤 동작이든 할 수 있게 해주는 목적이 크다고 합니다.

 

 

 

 

 

 

다음 시간에는 리덕스 미들웨어의 redux-thunk와 react-redux에 대해 정리해보는 시간을 갖겠습니다!

참고로, redux-saga는 진도가 후반 부분이라 시간이 필요합니다 :)) 

 

 

반응형