front-end/redux

[redux] concepts and data flow

Ash_O 2024. 2. 11. 16:19

출처 : https://ko.redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow

Background Concepts

state management

import react, { useState } from "React";

function Counter() {
  // state
  const [counter, setCounter] = useState(0);

  // action
  // code that causes an update to the state when something happens
  const increment = () => {
    setCounter((prevCounter) => prevCounter + 1);
  };

	// view
  return (
    <div>
      value: {counter} <button onClick={increment}>Increment</button>
    </div>
  );
}

 

구성요소

  1. state
  2. 앱을 구동하는 소스
  3. view
  4. 상태를 기반으로 한 UI의 선언적 설명
  5. action
  6. 사용자 입력에 기반한 앱에서 발생하는 이벤트 상태를 업데이트하는 트리거임

이는 단방향 데이터 흐름 예제이다.

  1. state는 어떤 특정 시점에 앱의 상태를 설명한다.
  2. 그 상태를 기반으로 UI가 렌더링된다.
  3. 무언가 발생하면, 그 상태가 그 동작에 기반되어 업데이트 된다
  4. 새로운 상태를 기반으로 UI가 다시 렌더링된다.

이는 데이터가 한 방향으로만 흐르는 패턴으로, 상태의 변경은 UI에 반영된다

그리고 사용자 상호작용은 새로운 상태를 만들어 다시 UI를 갱신하는 흐름이다.

 

 

하지만 여러 컴포넌트가 동일한 상태를 공유하고 사용해야 하는 경우는 이런 단순함이 붕괴될 수 있다.

state lifting up 같은 것으로 해결할 수는 있지만 늘 도움되는건 아니다

 

해결 방법으로는 컴포넌트에서 공유 상태를 추출하고, 컴포넌트 트리 외부에 중앙 집중식으로 두는 것이다

이렇게 하면 컴포넌트 트리가 View가 되어 컴포넌트가 트리에 어디에 있든 관계없이 어떤 컴포넌트든 상태에 접근하거나 action을 트리거 할 수 있다.

 

상태 관리와 관련된 개념을 정의하고 분리함으로써 뷰와 상태 간의 독립성을 유지하는 규칙을 강제하여 코드에 더 많은 구조와 유지 관리성을 부여할 수 있다.

 

즉, app의 전역 상태를 담을 단일 중앙 위치와 해당 상태를 업데이트할 때 따를 특정 패턴을 정의하여 코드를 예측가능하게 만든다.

Immutability

mutable : ‘changable’

immutable : never be changed

 

JS 객체와 배열은 모두 mutable이다.

만약 객체를 만들면, 그 필드의 내용을 바꿀수 있다.

그리고 배열의 경우도 바꿀수 있다.

const obj = { a: 1, b: 2 };
// still the same object outside, but the contents have changed
obj.b = 3;

const arr = ["a", "b"];
// In the same way, we can change the contents of this array
arr.push("c");
arr[1] = "d";

 

위 처럼 객체나 배열은 변경할수 있다.

메모리에서는 여전히 동일한 객체와 배열 참조인데, 객체 내부의 내용이 변경되었다.

값을 불변하게 업데이트하려면, 코드는 기존 객체/배열의 복사본을 만들고 그 복사본을 수정해야한다

 

JavaScript에서는 개열이나 객체의 전개 연산자와 메서드 등을 사용해서 위 작업을 할 수 있다.

const obj_ = { a: 1, b: 2 };
// still the same object outside, but the contents have changed
obj_.b = 3;

const arr_ = ["a", "b"];
// In the same way, we can change the contents of this arr_ay
arr_.push("c");
arr_[1] = "d";

const obj = {
  a: {
    // To safely update obj.a.c, we have to copy each piece
    c: 3,
  },
  b: 2,
};

const obj2 = {
  // copy obj
  ...obj,
  // overwrite a
  a: {
    // copy obj.a
    ...obj.a,
    // overwrite c
    c: 42,
  },
};

const arr = ["a", "b"];
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat("c");

// or, we can make a copy of the original array:
const arr3 = arr.slice();
// and mutate the copy:
arr3.push("c");

 

redux는 모든 상태 업데이트가 불변적으로 이뤄져야 한다고 가정한다


redux terminology

actions

액션은 type 필드를 갖는 일반적인 객체이다.

액션은 이벤트라고 생각해볼 수 있다.

  • type 필드는 todos/todoAdded 같은 액션에 설명적인 이름을 부여하는 문자열이어야 한다
  • 일반적으로 ‘domain/event’ 같은 형식으로 작성한다
    • domain : 액션이 속한 특징이나 범주
    • event : 구체적인 사건
  • 액션 객체에는 발생한 사건에 대한 추가 정보를 담은 다른 필드가 있을 수 있다.
  • 관례적으로 그 정보를 payload 필드에 넣는다
const addTodoAction = {
  type: "todos/todoAdded",
  payload: "Buy milk",
};

reducers

리듀서는 현재 상태와 액션 객체를 받고 필요한 경우 상태를 어떻게 업데이트할지 결정하고 새로운 상태를 반환하는 함수이다.

  • (state, action) => newState

리듀서는 받은 액션의 유형에 따라 처리하는 이벤트 리스너라고 생각해볼 수 있다

 

💡 리듀서라는 이름은 Array.reduce() 메서드에 전달되는 콜백 함수와 유사하여 이렇게 지어졌다.

 

리듀서의 규칙

  1. 상태와 액션 인자를 기반으로 새로운 상태 값을 계산해야한다.
  2. 기존 상태를 수정하는 것은 허용하지 않는다. 기존 상태를 복사하고 복사된 값을 변경해서 불변 업데이트를 진행해야 한다.
  3. 비동기 로직, 무작위 값 계산, side effect를 일으키는 행위는 허용하지 않는다.

리듀서 함수의 로직은 일반적으로 다음의 단계를 따른다.

  1. 이 리듀서가 액션에 관심있는지 확인한다
  2. 그렇다면, 상태의 복사본을 만들고 그 값을 새로운 값으로 업데이트한 다음 반환한다
  3. 그렇지 않다면 기존 상태를 변경하지 않고 그대로 반환한다.
const initialState = { value: 0 };

function counterReducer(state = initialState, action) {
  // Check to see if the reducer cares about this action
  if (action.type === "counter/incremented") {
    // If so, make a copy of `state`
    return {
      ...state,
      // and update the copy with the new value
      value: state.value + 1,
    };
  }
  // otherwise return the existing state unchanged
  return state;
}

 

리듀서는 새로운 상태를 결정하기 위해 내부에 어떤 종류의 로직이든 사용할 수 있다.

  • if/else
  • switch
  • 반복문

why called reducer?

Array.reduce() 메서드는 값을 하나씩 처리하고 최종 결과를 반환하는 배열을 만들 수 있다.

즉, 배열을 하나의 값으로 축소한다고 생각해볼 수 있다.

 

이 함수는 콜백을 인자로 받고, 배열의 각 항목에 대해 콜백을 한 번씩 호출한다.

콜백함수는 다음과 같은 인자를 갖는다.

  1. previousResult
  2. currentItem
const numbers = [2, 5, 8];

const addNumbers = (previousResult, currentItem) => {
  console.log({ previousResult, currentItem });
  return previousResult + currentItem;
};

const initialValue = 0;

const total = numbers.reduce(addNumbers, initialValue);
// {previousResult: 0, currentItem: 2}
// {previousResult: 2, currentItem: 5}
// {previousResult: 7, currentItem: 8}

console.log(total);
// 15

 

여기서, addNumbers “reduce callback” 함수가 스스로 아무 것도 추적할 필요가 없다는 것이다.

이는 previousResult와 currentItem 인자를 가져와서 얘네로 뭔가 수행하고 새로운 결과를 반환한다.

 

리덕스의 리듀서 함수는 이 “reduce callback” 함수와 같은 개념이다

이전 결과(state)와 현재 항목(action)을 가져와서 그 인수를 기반으로 새로운 상태 값을 결정하고 해당 새로운 상태를 반환한다.

 

redux 액션들의 배열을 만들고 reduce()를 호출하고 리듀서 함수를 전달하면 동일한 방식으로 최종 결과를 얻을 수 있다.

const actions = [{ type: "counter/incremented" }, { type: "counter/incremented" }, { type: "counter/incremented" }];

const initialState = { value: 0 };

function counterReducer(state = initialState, action) {
  // Check to see if the reducer cares about this action
  if (action.type === "counter/incremented") {
    // If so, make a copy of `state`
    return {
      ...state,
      // and update the copy with the new value
      value: state.value + 1,
    };
  }
  // otherwise return the existing state unchanged
  return state;
}

const finalResult = actions.reduce(counterReducer, initialState);
console.log(finalResult);
// {value: 3}

 

리덕스 리듀서는 액션들의 집합을 하나의 상태로 축소한다고 해볼 수 있다.

차이점은 Array.reduce()는 한 번에 처리되지만 redux는 실행중인 앱의 수명 동안 발생한다.

store

현재 리덕스 상태는 store라는 객체에 있다.

store는 reducer를 전달하여 생성되고, 현재 상태 값을 반환하는 getState 메서드를 가지고 있다.

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

dispatch

store 에는 dispatch 메서드가 있다.

상태를 업데이트하는 방법은 store.dispatch()를 호출하고 액션 객체를 전달하는 것이다.

스토어는 리듀서 함수를 실행하고 새로운 상태를 저장하며, getState()를 호출해서 업데이트된 값ㅇ르 가져올 수 있다.

store.dispatch({ type: "counter/incremented" });

console.log(store.getState());
// {value: 1}

 

액션을 디스패치 하는 것은 app에서 ‘이벤트를 트리거하는 것’으로 생각해볼 수 있다.

어떤 일이 발생했고, 그것을 스토어가 알아야 한다

리듀서는 이벤트 리스너처럼 작동해서, 관심있는 액션을 감지하면 상태를 업데이트한다.

 

selector

셀렉터는 스토어 상태 값에서 특정 정보를 추출하는 방법을 아는 함수들이다.

app이 커질수록 서로 다른 부분에서 동일한 데이터를 읽어야 하는 경우에 반복되는 로직을 피하는데 도움이 될 수 있다.

const selectCounterValue = (state) => state.value;

const currentValue = selectCounterValue(store.getState());
console.log(currentValue);

Core Concepts and Principles

Single Source of Truth

app의 전역 상태는 단일 스토어 내부의 객체로 저장된다.

특정 데이터는 여러 위치에 중복되지 않고 단일 위치에 존재해야 한다.

 

그럼, 앱의 상태가 변할 때 디버깅이나 검사가 더 쉬워지고, 전체 app과 상호 작용해야하는 로직을 중앙화하는 데 도움이 된다.

 

tip

  • 모든 앱의 상태가 redux 스토어에 들어가야 하는게 아니다.
  • 데이터 조각이 redux에 들어갈지 아니면 UI 구성요소에 들어가야 할지는 해당 데이터가 필요한 위치를 기반으로 결정해야 한다.

 

state is read-only

상태를 변경하는 유일한 방법 : action을 dispatch하는 것

이렇게 하면 UI가 실수로 데이터를 덮어쓰지 않고, 상태 업데이트가 왜 발생했는지 추적하기가 더 쉽다

액션은 평범한 JavaScript 객체라서 logging, 직렬화, 저장 및 디버깅이나 테스트 목적으로 replay할 수 있다.

 

changes are made with pure Reducer function

상태 트리를 어떻게 업데이트할지 지정하는건 리듀서 함수를 작성하는 것이다.

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

다른 함수와 마찬가지로 리듀서를 더 작은 함수로 나눌 수 있어서 작업을 돕거나 공통 작업에 대한 재사용 가능한 리듀서를 작성할 수 있다.

 

 

Redux Application Data Flow

단방향 데이터 흐름에 대하여

  • 상태는 특정 시점에서 앱의 상태를 설명한다
  • UI는 해당 상태를 기반으로 렌더링된다
  • 뭔가 발생하면, 상태는 발생한 내용에 기반해서 업데이트된다.
  • UI는 새로운 상태로 다시 렌더링된다.

redux의 경우 아래처럼 말해볼 수 있다.

  • initial setup
    • 루트 리듀서 함수로 redux 스토어를 생상한다
    • 스토어는 루트 리듀서를 한번 호출하고 반환을 초기 상태로 저장한다
    • UI가 처음 렌더링될 때, UI 컴포넌트들은 Redux 스토어의 현재 상태에 엑세스하고 해당 데이터로 어떤 것을 렌더링할 지 결정함
    • 그리고 상태가 변경되었는지 여부를 알 수 있도록 나중에 스토어 업데이트에 구독한다
  • update
    • 앱에서 무언가 발생한다 ( button click )
    • 앱 코드는 redux 스토어에 action을 dispatch한다 dispatch({type:’counter/incremented’})
    • 스토어는 이전 상태와 현재 액션으로 리듀서 함수를 다시 실행하고 반환 값을 새로운 상태로 지정한다
    • 스토어는 구독된 UI의 모든 부분에 스토어가 업데이트되었음을 알린다
    • 스토어에서 데이터가 필요한 각 UI 컴포넌트는 필요한 상태가 변경되었는지 확인한다.
    • 데이터가 변경된 각 컴포넌트는 새 데이터로 화면을 다시 렌더링하도록 강제하여 화면에 표시된 내용을 업데이트한다.

summary

redux는 다음 세가지 원칙으로 요약될 수 있다

  1. 전역 앱 상태는 단일 스토어에 유지된다
  2. 스토어 상태는 앱의 나머지 부분에 대해 읽기 전용이다
  3. 리듀서 함수는 액션에 응답해서 상태를 업데이트하는 데 사용된다

리덕스는 단방향 데이터 흐름 앱 구조를 사용한다

  1. 상태는 특정 시점에서 앱의 상태를 설명하고, UI는 해당 상태를 기반으로 렌더링된다.
  2. 앱에서 무언가 일어날 때
    • UI가 액션을 디스패치한다
    • 스토어가 리듀서를 실행하고, 발생한 내용을 기반으로 상태가 업데이트된다
    • 상태가 변경되었음을 스토어가 UI에 알린다
  3. UI는 새로운 상태를 기반으로 다시 렌더링된다.

'front-end > redux' 카테고리의 다른 글

[redux] 개요  (0) 2024.02.02