lottie
Seungjun's blog
blog
Redux

Redux

간단하게 말하면 전역 상태관리 라이브러리다.

  • 각 컴포넌트간에 데이터를 주고받을 때, 부모 컴포넌트를 통해서 주고 받았었다.→ 자식이 많아지면 상태관리가 엄청나게 복잡해진다.

  • 이런 복잡성을 관리하기 위해 Redux를 사용할 수 있다.


Redux의 세 가지 원칙

  1. Single source of truth

    • 동일한 데이터는 항상 같은 곳에서 데이터를 가지고 온다.

    • 데이터를 저장하는 store라는 하나뿐인 공간이 있다.

  2. State is read-only

    • React에서도 setState라는 메소드를 활용해야만 변화가 가능

    • Redux에서는 Action이라는 객체를 통해 변경 가능

  3. Changes are made with pure functions

    • 변경은 순수함수로만 가능하다. → Reducer와 관련된 개념


Store

  • 상태가 관리되는 오직 하나의 공간

  • 컴포넌트들과 별개의 공간에 앱에서 필요한 state를 두고,

  • 컴포넌트에서 state 정보가 필요할 때 Store에 접근


Action

  • 객체 안에 타입을 비롯한 다양한 데이터들이 담긴다.

  • Store에 앱의 데이터를 운반해주는 형태


Reducer

  • Action을 통해 앱의 데이터를 Store에 옮길 때, Reducer를 거쳐서 가야 한다.

  • Action 객체는 Dispatch 메소드에게 전달되고, Dispatch는 Reducer를 호출해서 새로운 state 생성

  • 데이터가 한 방향으로 흘러야 하기 때문에


Redux의 장점

  1. 상태를 예측 가능하게 만들어 준다.

  2. 유지보수에 용이하다.

  3. 디버깅에 유리하다.(action과 state log 기록 시)

  4. 테스트를 붙이기 쉽다.

image


react-redux 사용법

사용하는 이유 1. state 종속성 탈피


  우리는 useState를 사용 할 경우 컴포넌트 내부에 state를 만들고, 함수로 state를 바꿉니다.그렇기 때문에 state는 컴포넌트에 종속되는 것은 당연한 결과 입니다.redux는 컴포넌트에 종속되지 않고, 상태관리를 컴포넌트 바깥에서 합니다.프로젝트 루트레벨에서 store라는 곳에 state를 저장하고, 모든 컴포넌트는 store에 구독을 하면서 state와 그 state를 바꾸는 함수를 전달 받게 되죠. 함수를 바꿈으로 state가 바뀌면 해당 state를 바라보고 있는 컴포넌트는 모두 리렌더링 됩니다.


사용하는 이유 2. props 지옥 탈출

  우리가 원하는 state가 자식의 자식의 자식에서 사용한다면 props을 내리고 또 내리고 또 내리죠. 또 그 state를 바꾸기 위한 함수를 또 내리고 ..내리고....내립니다.이렇게 되면 내가 잘 하고 있는지 의문이 들게 되고, 코딩을 실수 하게 됩니다.위에서 본 대로, redux의 store는 프로젝트 루트레벨에 위치하고, 해당 store를 구독하는 컴포넌트는 모두 state와 state를 바꾸는 함수를 받을 수 있습니다.어느 위치에 있든 상관 없이 단 한번에 상태를 받을 수 있게 됩니다


redux 기본 원리

 redux는 기본적으로 flux 패턴을 따릅니다

Action -> Dispatch -> Store -> View

 redux의 데이터 흐름은 동일하게 단방향으로 view(컴포넌트)에서 Dispatch(store에서 주는 state를 바꾸는 함수)라는 함수를 동해 action(디스 패치 함수 이름)이 발동되고 reducer에 정의된 로직에 따라 store의 state가 변화하고 그 state를 쓰는 view(컴포넌트)가 변하는 흐름을 따릅니다.


react에서 redux 사용하기

reducer 정의

  • reducer는 store에 들어갈 state와 state를 바꿀 함수를 정의하는 곳입니다.

  • 기본적으로 순수함수로 코딩하고, 불변성을 지켜야 합니다.


불변성을 지켜야하는 이유

  • 불변성을 지켜야하는 이유는 redux는 이전 state와 바뀐 state를 구분하는 방법이 참조값이 바뀌었는지 확인하고, 참조값이 바뀌면, state가 바뀌었다고 redux가 인식하여, 해당 state를 사용하는 컴포넌트에게 리렌더링을 요청하기 때문입니다.

    • 그렇기 때문에, state.test = action.test와 같이 직접적으로 state를 변경하면 참조값이 변하지 않아 redux는 값이 바뀌었다고 인식하지 않고 리렌더링 되지 않습니다.

    • **state.test = {...test, action.test}**

    • 또는 immer라는 라이브러리를 사용하여 쉽게 불변성을 유지합니다.

다시 reducer 함수 정의로 돌아와서

1. rootReducer를 정의합니다.

// reducers/index.js

/** root reducer */
import { combineReducers } from "redux";
import counter from "./counter";

// 여러 reducer를 사용하는 경우 reducer를 하나로 묶어주는 메소드입니다.
// store에 저장되는 리듀서는 오직 1개입니다.
const rootReducer = combineReducers({
  counter
});

export default rootReducer;
 


2. 세부 reducer를 정의합니다.

// reducers/counter.js

// reducer가 많아지면 action상수가 중복될 수 있으니
// 액션이름 앞에 파일 이름을 넣습니다.
export const INCRESE = "COUNT/INCRESE";

export const increseCount = count => ({ type: INCRESE, count });

const initalState = {
  count: 0
};

const counter = (state = initalState, action) => {
  switch (action.type) {
    case INCRESE:
      return {
        ...state,
        count: action.count
      };

    // default를 쓰지 않으면 맨처음 state에 count값이 undefined가 나옵니다 꼭! default문을 넣으세요
    default:
      return state;
  }
};
 


app에 store 넣고, 만든 reducer 반영

// index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware, compose } from "redux";
import { Provider } from "react-redux";
import logger from "redux-logger";
import { composeWithDevTools } from "redux-devtools-extension";

import App from "./App";
import rootReducer from "./reducers";

// 배포 레벨에서는 리덕스 발동시 찍히는 logger를 사용하지 않습니다.
const enhancer =
  process.env.NODE_ENV === "production"
    ? compose(applyMiddleware())
    : composeWithDevTools(applyMiddleware(logger));

// 위에서 만든 reducer를 스토어 만들때 넣어줍니다
const store = createStore(rootReducer, enhancer);

ReactDOM.render(
  // 만든 store를 앱 상위에 넣어줍니다.
  <Provider store={store}>
    <App />
  </Provider>
  document.getElementById('root'),
);


컴포넌트에서 redux 사용하기

import { useSelector, useDispatch } from "react-redux";
import { increseCount } from "reducers/count";

// dispatch를 사용하기 위한 준비
const dispatch = useDispatch();

// store에 접근하여 state 가져오기
const { count } = useSelector(state => state.counter);

const increse = () => {
  // store에 있는 state 바꾸는 함수 실행
  dispatch(increseCount());
};

const Counter = () => {
  return (
    <div>
      {count}
      <button onClick={increse}>증가</button>
    </div>);
};

export default Counter;
 

  • 위 처럼 store에서 useDispatchuseSelector로 state와 함수 가져와서 적재적소에 호출해주면됩니다.


보완점**

  redux의 함수는 무조건 동기적으로 데이터가 흘러갑니다.그러나 웹은 언제나 비동기로 사용자 경험을 높이는 것이 중요합니다.그래서 나온것이 redux-saga입니다.redux을 사용하면서 redux-saga도 동시에 사용함으로 비동기의 유연함도 같이 가져갈 수 있습니다.



redux-toolkit? redux-persist?

  redux-toolkit이란 redux의 복잡한 이용방법을 좀 더 간단하게 쓸 수 있게 만든 라이브러리다. 과정은 간단히 단축이 되지만 redux를 완전히 이해하지 못하면 사용하는데 어려움을 겪는다.


 redux-persist란 localstorage 또는 session에 데이터를 저장해 캐싱하는데 도움을 주는 도구다. 이를 통해 우리는 회원정보를 저장해 로그인을 유지하는 등의 기능을 구현할 수 있다. 이에 대해서는 따로 다음 글에서 더 자세히 설명하겠다.redux-toolkit 사용법먼저 redux의 전반적인 데이터 흐름에 대해 살펴보겠다.


 사용자 flow에 따라 redux를 간단히 표현해보겠다.가장 먼저 사용자는 UI(=Component) 를 통해 원하는 요청을 보낸다.

우리는 우리가 원하는 요청을 하면 바로 컴퓨터가 이를 수행하는것처럼 느끼지만 이는 순간적으로 컴퓨터가 수많은 일을 엄청나게 빠르게 수행하기 때문에 그런것이다.

  1. UI(=Component)를 통한 사용자의 요청.

  2. 요청이 들어오는 순간 내부적으로 요청 데이터를 객체에 담고 이를 새로운 객체를 생성하는 함수(=actions)에 담는다. 이를 trigger라 한다.

  3. action은 실행되어 이전의 state와 동일한 값을 지니는 새로운 객체를 만든다. 이는 object가 얕은 복사시 화면이 렌더링 되지 않기 때문에 완전히 새로운 객체를 만들어 깊은 복사를 통해 화면이 새롭게 렌더링되도록 하기 위함이다. 하지만 이는 함수가 컴포넌트에서 실행되기 때문에 아직 렌더링은 되지 않는다. 이후의 과정을 거친 후 index.js(렌더링 파일)에서 다시 렌더링된다.

  4. useDispatch에 담아 컴포넌트에서 action을 통해 새롭게 만들어진 객체(전역 state)를 reducer로 보내준다.

  5. reducer는 store라는 객체 안에 있는 함수로 , actions를 통해 만들어진 새로운 state를 전달받아 (이때 아직은 요청을 적용하지 않은 상태이기 때문에 내부적 값은 기존과 동일하다) 요청 데이터를 입력하고 이를 리턴한다.

  6. 이렇게 반환 되어진 state는 store라는 객체에 저장된다.

  7. 이를 index.js에서 다시 렌더링 해준다(provider와 import를 모두 적용했다는 전제)


  • store.js 살펴보기 (object type의 전역 state)

    image

  • Slice 파일 살펴보기

    image

  • Input Component 살펴보기

    image