본문 바로가기

💻 개발자/✈️ React

[React] Basics in Redux without React (한글 번역)

반응형

Redux를 배우기 위해 참고하기 좋은 레퍼런스가 있어 한글 번역 작업을 했다.

Redux에 대한 기초가 궁금하신 분들에게 조금이라도 도움이 되었으면 좋겠다.

원문은 아래에 링크로 첨부한다.

 

https://www.robinwieruch.de/react-redux-tutorial#what-is-redux

 

React Redux Tutorial for Beginners [2019] - RWieruch

A complete React Redux tutorial for beginners: Learn how to build React Redux applications from scratch by following this step by step implementation of an example application ...

www.robinwieruch.de

 

BASICS IN REDUX WITHOUT REACT

 

Redux 공식 웹사이트에서 "리덕스는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너이다"라고 말한다. 리덕스는 독립적으로 사용이 가능하고 React와 Angular와 같은 라이브러리와 연결하여 자바스크립트 앱 안의 상태를 관리를 위해 사용할 수 있다. 

 

리덕스는 Flux 아키텍처에서 약간의 규칙을 차용했지만 모든 것을 차용하지는 않았다. 리덕스는 실제 상태 업데이트를 위해 캡슐화된 정보, 즉 Actions를 가지고 있다. 또한 상태를 저장하기 위해 Store을 가지고 있다. 그러나 Store은 단일 객체이다. 즉, Redux는 여러 개의 Store을 가지는 Flux 아키텍처와는 다르게 오직 하나만의 Store를 가진다는 것이다. 게다가, single Dispatcher 또한 없다. 대신에 Redux는 여러 개의 Reducer을 사용한다. 기본적으로 Reducer은 Action으로부터 정보를 가져오고, Store에 저장된 이전 상태와 함께 새로운 상태를 "reduce"한다. View는 Store안에 있는 상태가 변경될 때, 그 Store를 참조함으로써 변경(act)될 수 있다.

 

View -> Action -> Reducer(s) -> Store -> View

 

그래서 왜 Redux라고 부를까? 왜냐하면 리덕스는 Reducer 과 Flux 두 개의 단어가 조합되어 있기 때문이다. 리덕스의 추상적인 모습은 상상이 될 것이다. 그 상태(state)는 더 이상 View 안에만 있는 것이 아니다. 상태(state)는 단지 View와 연결되어 있을 뿐이다. 그럼 연결되어 있다는 것은 어떤 의미일까? 단방향 데이터 흐름(unidirectional data flow)의 일부이기 때문에 두 끝에서 연결된다는 의미이다. 하나의 끝은 상태를 최종적으로 업데이트하는 작업을 트리거(trigger)하는 역할을 하고, 두 번째 끝은 Store로 부터 상태를 받아오는 역할을 한다.

 

그러므로 View는 상태(state)가 변화하는 것에 따라서 업데이트 될 수 있으면서 상태 변경을 트리거(trigger)할 수도 있다. 이번 경우에는 View가 React가 될 수 있지만 Redux는 다른 라이브러리와 함께 사용할 수 있고 독립적으로 사용할 수 있다. 결국 Redux는 상태 관리 컨테이너(state management container) 일 뿐이다.

 

Actions(s)

Redux의 action은 자바스크립트 객체이다. action은 type과 선택적인(optional) payload를 가진다. type은 종종 action type으로 언급된다. type은 문자열 값인 반면에 payload는 문자열(string)에서 객체(object)까지 어떤 형태나 가능하다.

 

처음에 Redux를 알기 위한 playground는 Todo 어플리케이션이 될 것이다. 예를 들어, 이 어플리케이션에서 다음과 같은 action을 사용하여 새로운 todo 아이템을 추가할 수 있을 것이다.

 

{
  type: 'TODO_ADD',
  todo: { id: '0', name: 'learn redux', completed: false },
}

 

Redux에서 action을 실행하는 것을 dispatching 이라고 한다. 당신은 Redux store 안에 있는 상태(store)를 바꾸기 위해서 action을 발송(dispatch)시킬 수 있다. 당신이 상태(state)를 변경시키길 원한다면 오직 단 한 가지 방법, action을 발송(dispatch) 해야 한다. action을 dispatch 시키는 것은 당신의 View에서 촉발될 수 있다. 이것은 HTML 버튼을 클릭하는 것만큼이나 간단하다. 게다가 Redux action에서 payload는 의무적인 것도 아니다. 당신은 payload를 제외시키고 오직 action type만 포함시킨 actions를 정의할 수 있다. 결국 action이 dispatch 되면 이것은 모두 reducers를 통과하게 될 것이다.

 

Reducer(s)

Reducer은 단방향 데이터 흐름 체인(chain)의 다음 부분이다. View는 모든 reducer을 통과하는 action type과 선택적인 payload를 가진 action 객체를 발송(dispatch)한다. reducer이란 무엇일까? reducer은 순수 함수(pure function)이다. 순수 함수는 input 값이 같다면 항상 같은 output을 생산한다. 이것은 부수 효과(side-effects)가 없다. 즉 오직 입력 / 출력 작업일 뿐이다. reducer은 state와 action 두 개의 input을 가진다. state는 항상 Redux store의 전역 상태 객체(global state object)이다. action은 type과 optional payload가 dispatch된 action이다. reducer은 이전 상태와 새로 들어온 action을 새로운 상태로 명명하는 것을 말한다. 

 

(prevState, action) => newState

 

reducer은 부수 효과(side-effects)가 없는 순수 함수라는 함수형 프로그래밍 원칙과는 별개로 불변의(immutable) 데이터 구조도 포함한다. 이것은 항상 들어오는 prevState 객체를 변경하지 않고 newState 객체를 반환(return)해야한다는 것을 의미한다. 즉, Todo 애플리케이션의 상태(state)가 todo 목록인 아래와 같은 reducer은 허용되지 않는 reducer function이다. (다음은 허용되지 않는다는 뜻)

 

function(state, action) {
  state.push(action.todo);
  return state;
}

// 사용이 안되는 reducer 함수

 

배열 push method는 새로운 상태 객체를 반환하는 대신에 이전 상태를 변경한다. 다음은 허용 가능한 예이다. 왜냐하면 이전 상태를 온전히 지키면서 새로운 상태를 반환하기 때문이다. (다음은 허용된다는 뜻)

 

function reducer(state, action) {
  return state.concat(action.todo);
}

// 사용해도 되는 reducer 함수

 

자바스크립트 내장 함수인 concat을 사용함으로써, 상태와 todos의 목록이 또 다른(새로운 배열) 아이템으로 연결된다. 이 아이템은 action으로부터 새로 추가된 todo이다. 당신은 "이제 불변성(immutability)을 만족하는가?" 라고 궁금증을 표할 수 있다. 결론적으로 만족한다. 왜냐하면 concat은 이전의 배열을 수정하지 않고 항상 새로운 배열을 반환하기 때문이다. 위의 데이터 구조는 불변성을 유지한다.

 

하지만 action type은 어떨까? 지금, 새로운 상태를 생성하기 위해 오직 payload만 사용되고 action type은 무시되었다. 그래서 당신은 action type에 관해 무엇을 할 수 있을까? 기본적으로 action 객체가 reducers에 도착했을 때, action type이 평가된다. reducer가 action type을 관심을 가질 때 새로운 상태를 생성한다. 그렇지 않으면 단순히 이전 상태를 반환한다. 자바스크립트에서 switch case는 다른 action type을 평가하는데 도움을 준다. 그렇게 하지 않으면 이전 상태를 default 값으로 반환한다.

 

당신의 Todo 어플리케이션에 Todo를 완료 / 미완료로 toggle 하는 두 번째 action과 action type이 있다고 상상해보자. payload에서 필요한 유일한 정보는 상태에서 Todo를 식별하는 식별자뿐이다. (식별자는 id 값을 말하는 것 같음)

 

{
  type: 'TODO_TOGGLE',
  todo: { id : '0' },
}

 

reducer(s)은 이제 TODO_ADD와 TODO_TOGGLE 두 가지 action을 수행해야 한다. switch case 문을 사용함으로써, 당신은 다른 케이스들로 분기시킬 수 있다. 만약 위와 같이 switch case문이 없다면 변경되지 않은 상태를 default 값으로 반환한다.

 

function reducer(state, action) {
  switch(action.type) {
    case 'TODO_ADD' : {
      // do something and return new state
    }
    case 'TODO_TOGGLE' : {
      // do something and return new state
    }
    default : return state;
  }
}

 

이 튜토리얼은 이미 TODO_ADD action type과 그 기능에 대해 논의했다. 이것은 이전의 todo 아이템 리스트에 새로운 todo 아이템을 간단하게 연결시키는 것이다. 그럼 TODO_TOGGLE의 기능은 무엇일까?

 

function reducer(state, action) {
  switch(action.type) {
    case 'TODO_ADD' : {
      return state.concat(action.todo);
    }
    case 'TODO_TOGGLE' : {
      return state.map(todo =>
        todo.id === action.todo.id
          ? Object.assign({}, todo, { completed: !todo.completed })
          : todo
      );
    }
    default : return state;
  }
}

 

이번 예시에서 자바스크립트 내장 함수 map은 state와 todo의 리스트를 매핑하는 데 사용되었고, 온전한 todo를 반환하거나 todo.completed가 toggle 된 todo를 반환한다. toggle 된 todo는 id property를 통해 식별되었다. 자바스크립트 내장 함수 map은 항상 새로운 배열을 반환한다. 이전 상태는 변경되지 않으므로 todo 상태는 변하지 않으며 새로운 상태로 반환될 수 있다.

 

하지만 toggle된 todo가 변경되지 않았는가? 하지만 그 생각은 틀렸다. 왜냐하면 Object.assign() 은 이전 객체를 변화시키지 않고 새로운 객체를 반환하기 때문이다. Object.assign() 은 주어진 모든 객체들을 전자에서 후자로 서로 합쳐준다. 만약 전자의 객체와 후자의 객체에서 같은 property를 사용하고 있다면, 후자의 property가 사용될 것이다. 즉, (위의 예시에서) 업데이트된 todo 아이템의 completed property 는 이전 todo item의 상태를 무효로 한다.(이전 상태를 변경시키지 않는다는 뜻)

 

actions와 reducer 그리고 기본 자바스크립트의 연관성을 기록해라. 지금까지 Redux 라이브러리와 연관된 함수는 없었다. 숨겨진 라이브러리 magic 같은 것은 없다. 단지 함수형 프로그래밍 원칙을 염두한 자바스크립트 일 뿐이다.

 

알아두면 좋은 최근의 reducer에 관한 유용한 정보가 있다. 이것은 크기가 커져서 유지 관리하기가 어렵다. 괜찮은 reducers를 유지하기 위하여, 순수 함수처럼 서로 다른 switch case branches를 밖으로 끄집어낼 수 있다.

 

function reducer(state, action) {
  switch(action.type) {
    case 'TODO_ADD' : {
      return applyAddTodo(state, action);
    }
    case 'TODO_TOGGLE' : {
      return applyToggleTodo(state, action);
    }
    default : return state;
  }
}

function applyAddTodo(state, action) {
  return state.concat(action.todo);
}

function applyToggleTodo(state, action) {
  return state.map(todo => 
    todo.id === action.todo.id
      ? Object.assign({}, todo, { completed: !todo.completed })
      : todo
  );
}

 

마침내 Todo 어플리케이션이 두 개의 action과 하나의 reducer을 가졌다. Redux setup에서 마지막 부분은 Store이다.

 

Redux Store

지금까지 보면, Todo 어플리케이션은 상태 업데이트를 촉발(trigger)하는 방법(action(s))과 이전 상태와 action을 새로운 상태로 합치는 방법(reducer(s))을 가지고 있다. 그러나 어느 누구도 이 부분들을 함께 합칠 책임은 없다.

 

  • 어디서 action을 trigger 해야하는가?
  • 누가 reducer에 action을 넘겨주는가?
  • 나의 View에 업데이트된 상태를 합치기 위해 이것(업데이트된 상태)을 어디서 얻을 수 있는가?

 

이것이 바로 Redux Store이다. store에는 하나의 전역 상태 객체가 있다. 다수의 store은 없고 다수의 상태(state) 또한 없다. store은 어플리케이션에서 오직 하나의 인스턴스이다. 게다가 이것은 당신이 Redux를 사용하면서 마주치는 첫 번째 라이브러리 의존성이다. 그러므로 import 문을 사용해서 Redux 라이브러리에서 store 객체를 생성하는 기능을 가져온다 (npm install --save redux를 사용해 설치하고 난 뒤에).

 

import { createStore } from 'redux';

 

이제 당신은 이를 사용하여 단일 인스턴스 store를 만들 수 있다. createStore 함수는 의무적으로 하나의 인자를 가지는데 이는 reducer이다. todo 아이템을 더하고 완료하는 섹션에서 이미 reducer을 정의했다.

 

const store = createStore(reducer);

 

또한 createStore은 두 번째 선택적인 인자를 가지는데 이는 초기 상태(initial state)이다. Todo 어플리케이션의 경우에는 reducer은 todo의 목록에서 상태로서 작동한다. todo 목록 아이템들은 빈 배열이나 todo들로 미리 채워진 배열로 초기화되어야 한다. 만약 초기화되지 않았다면, 정의되지 않은 인자(undefined argument)에서 작동되기 때문에 reducer은 실패할 것이다.

 

const store = createStore(reducer, []);

 

이번 튜토리얼 뒷부분에서 Redux와 함께 React를 사용할 때 Redux에서 상태를 초기화하는 다른 방법도 보게 될 것이다. 그런 다음 store 대신에 reducer를 사용하여 보다 세분화된 수준에서 상태를 초기화한다.

 

이제 reducer에 대해 알고 있는 store 인스턴스가 있다. Redux setup이 끝났다. 그러나, store와 상호작용하는 필수적인 부분이 빠졌다. 당신은 상태를 변경시키기 위해 action을 dispatch 하고, store에서 상태를 가져오고, store에서 상태의 업데이트를 수신하기를 원한다. 

 

첫 번째로 어떻게 action을 dispatch 할 수 있을까?

 

store.dispatch({
  type: 'TODO_ADD',
  todo: { id: '0', name: 'learn redux', completed: false },
});

 

두 번째로 어떻게 store로부터 전역 상태를 가져올 수 있을까?

 

store.getState();

 

세 번째로 어떻게 업데이트를 수신(또는 수신 해제) 하기 위해 store를 구독(또는 구독 취소)하는 방법은 무엇일까?

 

const unsubscribe = store.subscribe(() => {
  console.log(store.getState());
});

unsubscribe();

 

이것이 전부다. Redux store은 상태에 접근하고, 상태를 업데이트하고, 업데이트를 수신하기 위해 오직 slim API만 가지고 있다. 이것은 Redux를 성공적으로 만들 수 있는 필수 제약 조건 중 하나이다. 

반응형