[React] Redux 기초

seungyeon·2021년 4월 5일
6

React

목록 보기
2/2
post-thumbnail

Redux 란?

리덕스(Redux)는 리액트 생태계에서 가장 사용률이 높은 상태관리 라이브러리이다. 자바스크립트 앱을 위한 상태 컨테이너라고 생각하면 된다. React와 같은 component환경이 아니어도 값의 변화를 저장하고 관리하기 위해 리덕스를 사용할 수 있다. 즉, 리액트만을 위한 Library는 아니다. 리액트 뿐만 아니라 Augular, jQuery, vanilla JavaScript 등 다양한 framework와 작동되게 설계되었다.

리덕스는 Flux 구조*를 기반으로 생성되었기 때문에 단방향으로 일관적으로 동작하고, 서로 다른 환경(서버, 클라이언트, 네이티브)에서 작동하며, 테스트하기 쉬운 앱을 작성하도록 도와준다.

Flux 구조 *

Flux 패턴은 action이 발생하면 dispatcher에 의해 store에 변경된 사항이 저장되고, 그 저장된 사항에 의해 view가 변경되는 단방향 패턴을 보이고 있다.
여기서 dispatcher란 어플리케이션에서 발생한 action들을 정리해주는 역할을 하며 Store란 어플리케이션의 데이터들이 저장되는 장소이다.

이러한 Flux 패턴의 가장 큰 장점은 개발 흐름이 단방향으로 흐르기 때문에 훨씬 파악하기 쉽고 코드의 흐름이 예측 가능(Predictable)하다는 것이다.

+ 리덕스 구조


+ 더 알아보기: Flux 구조

Flux는 Facebook에서 만든 client-side web applications을 구축할 때 사용하는 application architecture(앱 구조), design pattern(디자인 패턴)이다.

MVC (Model–View–Controlle)구조 의 단점을 보완할 목적으로 개발된 Flux는 대규모 프로젝트에서 너무 복잡해지는 MVC구조의 단점을 보완하는 단방향 데이터 흐름(unidirectional data flow)의 구조이다.

리덕스는 Flux에 영감을 받아 개발되었지만 몇 가지 중요한 차이점이 존재한다.

  • Flux와 달리 리덕스는 dispatcher라는 개념이 존재하지 않는다.
  • 리덕스는 다수의 store도 존재하지 않는다. 대신 리덕스는 하나의 root에 하나의 store만이 존재한다.
  • 순수함수(pure functions)에 의존한다. (state의 불변성)

차이점은 있지만 Flux와 리덕스의 구조는 매우 유사하다. 결국 리덕스는 Flux 패턴을 좀 더 쉽고 정돈된 형태로 쓸 수 있게 도와주는 라이브러리라고 볼 수 있다.


왜 redux를 사용하는가? (부제: 상태관리 라이브러리의 필요성)

프로젝트의 규모가 작은 경우에는 컴포넌트 구조가 단순하고 관리해야 할 state도 많지 않기 때문에 굳이 리덕스와 같은 상태관리 라이브러리를 사용하지 않아도 된다.
하지만 프로젝트의 규모가 커질수록 컴포넌트의 수가 늘어나고 구조가 복잡해지면서 관리해야 하는 state 도 늘어나게 된다.

이 경우 기존의 state 전달 방식은 props 를 통해 이루어지기 때문에, state drilling** 과 같은 문제가 발생한다.

  • state Drilling - 상위 컴포넌트의 state 를 하위 컴포넌트로 계속해서 전달해 목표 컴포넌트까지 props 를 통해 전달하는 행위

이러한 props 를 통한 state 전달 방식은 로직상의 큰 문제는 없지만, 코드의 가독성이 떨어지고 해당 컴포넌트에서 사용하지 않는 (전달만을 위해 존재하는) props 들이 많이 생기면서 비효율적인 데이터 흐름이 발생하게 된다. 상위 컴포넌트에서 props의 이름을 변경할 경우 해당 props가 거쳐간 모든 컴포넌트에서 props 이름을 수정해주어야 하기 때문에 코드 관리 측면에서도 좋지 않다.

Redux를 사용하면 ...

  • 리덕스를 사용하면 하나의 store를 통해 모든 state상태 관리 로직을 저장, 유지할 수 있게 되며 원하는 Component로만 data를 전달할 수 있다.

  • 리덕스를 사용하면 컴포넌트들의 상태 관련 로직들을 다른 파일들로 분리시켜서 더욱 효율적으로 관리 할 수 있고, 컴포넌트끼리 상태를 공유하게 될 때 여러 컴포넌트를 거치지 않고도 손쉽게 상태 값을 전달할 수 있다.
  • 리덕스는 한 방향으로만 동작하기 때문에 데이터의 흐름을 예측하기 쉽다.
  • 리덕스의 미들웨어라는 기능을 통하면 비동기 작업, 로깅 등의 확장적인 작업들을 더욱 쉽게 할 수도 있다.
  • 리덕스는 시간여행형 디버거와 결합된 실시간 코드 수정과 같은 훌륭한 개발자 경험도 제공한다.

Redux의 3가지 원칙

리덕스에서 반드시 지켜져야 할 3가지 규칙이 있다.

1. 하나의 애플리케이션 안에는 하나의 스토어(store)만 존재한다.

리덕스에서는 하나의 App에는 하나의 스토어만 두어 여러 개의 스토어를 구독하여 발생할 수 있는 혼란을 피한다. dispatcher 동작 간에 하나의 스토어 상태를 구독하는데, 해당 스토어가 어딘지 찾기 어려워 진다.
그로인해 생기는 혼란을 찾아내 수정하는 것도 어려워 진다.

때문에, 여러가지 reducer를 조합하여 하나의 store 로 생성한다. combineReducers 메서드를 사용해 여러개의 reducer를 하나의 store 로 구성할 수 있다.

특정 업데이트가 너무 빈번하게 일어나거나, 애플리케이션의 특정 부분을 완전히 분리시키게 될 때 여러개의 스토어를 만들 수도 있지만 권장 사항이 아니며, 여러개의 스토어가 존재하는 경우에는 리덕스 개발 도구를 사용하지 못한다.

2. 상태(state)는 읽기전용(read-only)이다.

리덕스에서도 리액트와 마찬가지로 기존의 상태는 건들이지 않고 새로운 상태 객체를 생성하여 상태를 업데이트 해준다. 이를 위해 spread-syntax(...)나 concat, Object.assign 등을 사용한다.

  • 이렇게 상태의 불변성을 유지시켜주면 나중에 개발자 도구를 통해서 뒤로 돌릴 수도 있고 다시 앞으로 돌릴 수도 있다.
  • 리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경 되는 것을 감지하기 위하여 shallow equality 검사를 하기 때문이다. 이를 통하여 객체의 변화를 감지할 때 객체의 깊숙한 안쪽까지 비교를 하는 것이 아니라 겉핥기 식으로 비교를 하여 좋은 성능을 유지할 수 있다.

3. 리듀서는 "순수함수"여야 한다.

순수함수는 동일한 인풋이라면 언제나 동일한 아웃풋이 있어야 한다. 즉, 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야한다.

리듀서 함수는 파라미터로 stateaction 객체를 받는다. 이때, 리듀서 함수는 인자로 받아온 state는 변경하지 않고, action을 통해 변경한 새로운 state 객체를 만들어서 반환해야한다.

순수하지 않은 작업들

new Date(), Math.random(), axios.get() 와 같은 일부 작업들은 실행 할 때마다 다른 결과값이 나타날 수 있다. 이런 작업들은 순수하지 않은 작업이므로, 리듀서 함수의 바깥에서 처리해주어야 한다. 이러한 작업들을 처리해주기 위해 리덕스 미들웨어를 사용한다.


Redux 기본 개념

"Action 객체는 Dispatch 메서드에 전달되고, Dispatch는 Reducer를 호출해서 새로운 state를 생성한다."

Action(액션)

state 가 업데이트될 때, 어떻게 업데이트 할지를 정의해주는 객체.
쉽게 말해 액션이란 어플리케이션에서 일어나는 모든 사건들 중에서 상태 변화가 필요한 사건들을 말한다.
Redux에서는 어떤 사건이 발생하여 state 값 변경이 필요하면 action 을 발생시킨 후, 이 객체를 dispatch() 함수의 인자로 넘겨준다. 그러면 dispatch() 함수가 Reducer 함수를 호출해 Reducer 함수를 실행시켜 새로운 state를 생성한다.

  • action 객체는 type을 필수로 가지고 있어야한다(그 외의 값은 자유롭게 추가해주면 된다). action의 type은 일반적으로 문자열 상수로 정의된다.
const ADD_TODO = 'ADD_TODO' // action의 type을 정의
  • 정의된 action type은 action creators(액션 생성자 함수)를 통해 사용된다.
const ADD_TODO = 'ADD_TODO'

const addTodo = (text) => {
  return {
    type: ADD_TODO,
    text
  }
}

액션 생성자 함수(Action creator)

액션 생성함수를 만들어서 사용하는 이유는 나중에 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함이다. 그래서 보통 함수 앞에 export 키워드를 붙여서 다른 파일에서 불러와서 사용한다.

export const addTodo = (text) => {
  return {
    type: ADD_TODO,
    text
  }
}
  • 리덕스 사용 시 액션 생성함수를 사용이 필수는 아니다. 액션을 발생시킬 때마다 직접 액션 객체를 작성해도 된다.

Reducer(리듀서)

reducer는 두 가지 인자를 받는데, 첫 번째는 이전 상태 정보(state)가 들어오고 두 번째 인자는 아까 위에서 발생한 액션 객체(action)가 들어온다. 리듀서 함수가 상태를 업데이트하면 그에 따라서 render가 다시 일어나 화면이 바뀌게 된다.
리듀서는 받아온 현재 stateaction을 적용한 새로운 state 를 리턴해주는 함수다.

  • 리듀서는 stateaction 객체를 파라미터로 받아온다.
  • state가 객체나 배열일 경우 스프레드 연산자를 사용하거나 concat 메소드를 사용하는 식으로 원래의 배열이나 객체(state)를 수정하지 않아야 한다는 점에 주의하자!

리듀서 함수에서는 actiontype에 따라 변화된 state를 반환하게 된다.

const itemReducer = (state = initialState, action) {
  switch (action.type) {
    case ADD_TO_CART:
      return Object.assign({}, state, {cartItems: [...state.cartItems, action.payload]});
    case REMOVE_FROM_CART:
      const filtered = state.cartItems.filter(el => el.itemId !== action.payload.itemId);
      return { ...state, cartItems: filtered };
    default:
      return state;
  }
};
  • useReducer 에서와 달리, 리덕스의 리듀서에서는 default: 부분에 기존 state를 그대로 반환하도록 작성해야 한다.
    + useReducer 에선 일반적으로 default: 부분에 throw new Error('Unhandled Action') 과 같이 에러를 발생시키도록 처리하는게 일반적이다.

Store(스토어)

스토어는 어플리케이션의 상태가 보관되는 하나의 저장소이다. 어플리케이션의 모든 상태는 하나의 스토어에서 관리된다.
스토어 안에는 현재의 앱 상태state와 리듀서reducer, 추가적인 내장 함수들(dispatch, subscribe 등)이 들어있다.

import { createStore } from 'redux';
import rootReducer from '../reducers/index';

const store = createStore(rootReducer);

이처럼 store을 생성하고 reducer을 연결하여 어플리케이션에 연결하게 된다.

useSelector

const items = useSelector((store) => store.cartReducer);

useSelector를 통해 스토어의 특정 state를 가져올 수 있다.


리덕스로 구현한 간단한 쇼핑몰 어플리케이션에서 Add to Cart 버튼 클릭 시 Shopping Cart의 뱃지의 숫자가 늘어나게 되는 과정의 구동 방식을 정리해보면 아래와 같다.


이어서 공부할 것

  • Redux와 미들웨어(thunk, saga)
  • 리덕스 미들웨어(Redux Middleware) 만들기

Reference

0개의 댓글