React 스터디 6주차 - redux

Yunes·2023년 9월 19일
0

리액트스터디

목록 보기
12/18
post-thumbnail

Redux 시작

Redux : 자바스크립트 앱을 위한 예측 가능한 상태 저장소

Single Source of Truth 단 하나의 진실의 원천

redux 는 하나의 상태를 갖는다. 이때의 상태는 객체를 말한다. 하나의 객체에 애플리케이션에 필요한 모든 상태를 넣는 방식이다. 이로써 앱의 복잡도를 낮출 수 있다.

redux 의 상태는 외부로부터 격리되어 있다. 이때 상태로의 접근 및 수정은 dispatcher 혹은 reducer 를 통해서만 가능하다. 데이터를 가져가는 것도 getState 를 통해서만 가능하다.

이렇게 외부로부터 데이터를 직접적으로 제어할 수 없도록 하여 데이터가 예기치 않게 수정되는 것을 막아 앱을 보다 예측 가능하게 만들 수 있다.

만약 상태가 변경되면 해당 상태를 사용하는 앱에 변경사실을 알린다.

이런 과정을 통해 UNDO 와 REDO 를 쉽게 할 수 있다.

hot module reloading


redux 핵심용어

lunit.gitbook.io/redux/basics/actions

action

액션은 앱에서 스토어로 보내는 데이터 묶음이다. 이들이 스토어의 유일한 정보원이 되며 store.dispatch() 를 통해 액션을 보낼 수 있다.

const ADD_TODO = 'ADD_TODO'

{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

액션은 자바스크립트 객체이며 어떤 형태의 액션이 실행될지에 대한 type 속성을 반드시 가져야 한다. 보통 타입은 문자열 상수로 정의한다. 혹은 타입들을 아래와 같이 별도의 모듈 단위로 관리할 수도 있다.

import { ADD_TODO, REMOVE_TODO } from '../actionTypes'

액션 생산자

액션 생산자는 액션을 만드는 함수이며 액션과는 다르다.
실제로 액션을 보내기 위해서는 결과값을 dispatch() 함수로 넘겨야 한다.

function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

dispatch(addTodo(text))

혹은 아래와 같이 자동으로 액션을 보내주는 바인드된 액션 생산자를 만들수도 있다.

const boundAddTodo = (text) => dispatch(addTodo(text));

boundAddTodo(text);
// actions.js

/*
 * 액션 타입
 */

export const ADD_TODO = 'ADD_TODO'
export const COMPLETE_TODO = 'COMPLETE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

/*
 * 다른 상수
 */

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

/*
 * 액션 생산자
 */

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function completeTodo(index) {
  return { type: COMPLETE_TODO, index }
}

export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

reducer

액션은 무언가 동작이 발생한다는 것에 대해 기술하나 그 결과적으로 앱의 상태가 어떻게 바뀌는지는 특정하지 않는다. 이런 일을 reducer 가 하게 된다.

리듀서는 이전 상태와 액션을 받아서 다음 상태를 반환하는 순수함수다.

(previousState, action) => newState

이 형태의 함수를 Array.prototype.reduce(reducer, ?initialValue) 로 넘길 것이라서 리듀서라고 부른다. 이때 리듀서는 순수하게 유지하는 것이 매우 중요하므로 리듀서 내에서 인수 변경 / API 호출, 라우팅 전환같은 사이드 이펙트 / Date.now() 같은 순수하지 않은 함수 호출 등을 하면 절대 안된다.

import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
}

function todoApp(state = initialState, action) {
  // 지금은 아무 액션도 다루지 않고
  // 주어진 상태를 그대로 반환합니다.
  return state
}

이때 visibilityFilter 에 따라 다른 todo 를 보이고 싶기 때문에 코드를 다음과 같이 switch case 문으로 수정한다.

import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
}

function todoApp(state = initialState, action) {
  switch (action.type) {
  case SET_VISIBILITY_FILTER:
    return Object.assign({}, state, {
      visibilityFilter: action.filter
    });
  default:
    return state
  }
}

주의점

  • 위의 예시코드는 배열을 상태로 갖는 케이스였기에 원본 state 를 변경하는 것과 같은 사이드 이펙트를 피하기 위해 원본은 그대로 두고 Object.assign() 을 통해 복사본을 만들어 반환하거나 아니면 ES7 의 object spread syntax 의 { ...state, ...newState } 를 사용하는 방식을 사용해야 한다.
  • Object.asign() 이 ES6 일부이나 대부분의 브라우저에서 구현되지 않았으니 폴리필을 사용하거나 Babel 플러그인이나 _.assign() 같은 다른 라이브러리의 헬퍼를 사용해야 한다고 한다.
  • can i use 사이트를 찾아보니 이젠 왠만한 브라우저에서는 지원이 되고 있는것 같으니 그냥 사용해도 될 것 같다.

보일러 플레이트 코드

  • 최소한의 변경으로 여러 곳에서 재사용되며 반복적으로 비슷한 형태를 띄는 코드
  • 어원 : 1890년대에 광고나 칼럼과 같이 계속 사용되는 텍스트 인쇄판은 강철로 찍기 시작했고 이를 Boilerplate 라고 불렀다.
import { VisibilityFilters } from './actions'

const initialState = {
  visibilityFilter: VisibilityFilters.SHOW_ALL,
  todos: []
}

function todoApp(state = initialState, action) {
  switch (action.type) {
  case SET_VISIBILITY_FILTER:
    return Object.assign({}, state, {
      visibilityFilter: action.filter
    });
  case ADD_TODO:
    return Object.assign({}, state, {
      todos: [...state.todos, {
        text: action.text,  
        completed: false
      }]
    });  
  case COMPLETE_TODO:
  return Object.assign({}, state, {
    todos: [
      ...state.todos.slice(0, action.index),
      Object.assign({}, state.todos[action.index], {
        completed: true
      }),
      ...state.todos.slice(action.index + 1)
    ]
  });
  default:
    return state
  }
}

위의 코드에서 todos 와 visibilityFilter 가 서로 독립적이니 서로 분리해줄 수 있다.

import { combineReducers } from 'redux';
import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions';
const { SHOW_ALL } = VisibilityFilters;

function visibilityFilter(state = SHOW_ALL, action) {
  switch (action.type) {
  case SET_VISIBILITY_FILTER:
    return action.filter;
  default:
    return state;
  }
}

function todos(state = [], action) {
  switch (action.type) {
  case ADD_TODO:
    return [...state, {
      text: action.text,
      completed: false
    }];
  case COMPLETE_TODO:
    return [
      ...state.slice(0, action.index),
      Object.assign({}, state[action.index], {
        completed: true
      }),
      ...state.slice(action.index + 1)
    ];
  default:
    return state;
  }
}

const todoApp = combineReducers({
  visibilityFilter,
  todos
});

export default todoApp;

combineReducers

여러 리듀서를 하나로 합쳐준다.

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
});

// 위와 동일

function reducer(state, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  };
}

store

action : 무슨 일이 일어날지에 대해
reducer : 액션에 따라 상태를 수정
store : action, reducer 를 함께 가져오는 객체

store 가 하는일

  • 애플리케이션의 상태 저장
  • getState() 를 통해 상태에 접근
  • dispatch(action) 을 통해 상태를 수정
  • subscribe(listener) 를 통해 리스너 등록

redux 애플리케이션에서 하나의 스토어만 가질 수 있으니 로직을 나누고 싶다면 하나의 스토어에 여러 리듀서를 결합하여 사용할 수 있다.

import { createStore } from 'redux';
import todoApp from './reducers';

let store = createStore(todoApp);
// let store = createStore(todoApp, window.STATE_FROM_SERVER);

스토어엔 createStore 를 통해 첫 번째 인자로 리듀서를 전달하고 두번째 인자로 초기 상태를 지정해줄 수도 있다.

import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from './actions';

// 초기 상태를 기록한다.
console.log(store.getState());

// 상태가 바뀔때마다 기록한다.
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

// 액션들을 보낸다.
store.dispatch(addTodo('Learn about actions'));
store.dispatch(addTodo('Learn about reducers'));
store.dispatch(addTodo('Learn about store'));
store.dispatch(completeTodo(0));
store.dispatch(completeTodo(1));
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED));

// 상태 변경을 더 이상 받아보지 않는다.
unsubscribe();

데이터 흐름

  1. store.dispatch(action) 호출
 { type: 'LIKE_ARTICLE', articleId: 42 };
 { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Megan' } };
 { type: 'ADD_TODO', text: 'Read the Redux docs.'};
  1. Redux 스토어가 리듀서 함수들을 호출
 // 애플리케이션의 현재 상태(할일 목록과 선택된 필터)
 let previousState = {
   visibleTodoFilter: 'SHOW_ALL',
   todos: [{
     text: 'Read the docs.',
     complete: false
   }]
 };

 // 실행되는 액션(할일 추가)
 let action = {
   type: 'ADD_TODO',
   text: 'Understand the flow.'
 };

 // 리듀서가 다음 상태를 반환함
 let nextState = todoApp(previousState, action);

리듀서는 다음 상태를 계산하는 순수함수이니 몇번을 실행하거나 언제 실행하든 상관없이 항상 같은 출력이 나와야 한다.

  1. 루트 리듀서가 각 리듀서의 출력을 합쳐 하나의 상태 트리로 만든다.
 function todos(state = [], action) {
   // Somehow calculate it...
   return nextState;
 }

 function visibleTodoFilter(state = 'SHOW_ALL', action) {
   // Somehow calculate it...
   return nextState;
 }

 let todoApp = combineReducers({
   todos,
   visibleTodoFilter
 });
  1. Redux 스토어가 루트 리듀서에 의해 반환된 상태 트리를 저장한다.

store.subscribe(listener) 를 통해 등록된 모든 리스터가 불러내지고 이들은 현재 상태를 얻기 위해 store.getState() 를 호출한다.

profile
미래의 나를 만들어나가는 한 개발자의 블로그입니다.

0개의 댓글