ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Redux] 공식문서 파먹기(1) - Essentials - Redux Overview and Concepts
    JavaScript 2023. 5. 18. 22:39

    본 포스팅은 리덕스 공식문서를 번역하여 정리했습니다.

     

    리덕스 개요 및 개념

     

    리덕스란?

    리덕스는 action 으로 앱의 상태를 관리하는 패턴이자 라이브러리 입니다.

    앱에서 사용하는 상태를 하나의 저장소에 저장하고 예측가능한 방식으로만 상태를 업데이트하는 패턴을 사용합니다.

     

    리덕스를 사용해야하는 이유

    리덕스에서 제공하는 기능을 이용하면 앱의 전체 상태를 관리하고 상태가 언제, 어떻게, 왜 업데이트 되는지 이로인해서 앱의 로직이 어떻게 작동하는지 이해할 수 있습니다.

     

    예측가능한 코드를 작성해서 예상대로 앱이 동작하도록 확신 할 수 있습니다.

     

    리덕스를 언제 사용해야하나?

    • 앱의 전체에서 전역적으로 사용하는 상태가 있는경우
    • 앱의 상태가 시간의 따라 자주 업데이트 되는 경우
    • 해당 상태를 변경하는 로직이 복잡한 경우
    • 앱의 큰 사이즈의 코드베이스를 여러 사람이 작업하는 경우

    위의 경우에 리덕스를 사용하는게 좋습니다.

     

     

    리덕스의 용어와 주요 개념

    상태관리(state management)

    간단한 예제코드를 같이 보겠습니다.

    function Counter() {
      // State: a counter value
      const [counter, setCounter] = useState(0)
    
      // Action: code that causes an update to the state when something happens
      const increment = () => {
        setCounter(prevCounter => prevCounter + 1)
      }
    
      // View: the UI definition
      return (
        <div>
          Value: {counter} <button onClick={increment}>Increment</button>
        </div>
      )
    }

     

    버튼을 클릭하면 숫자가 증가하는 간단한 리액트 소스입니다.

     

    위의 소스는 다음과 같이 구성된 독립된 앱입니다.

    • 상태: 앱의 구동하는 실체입니다
    • 뷰: 현재 상태를 기반으로한 UI의 설명
    • 액션: 사용자의 입력에 반응하는 이벤트 트리거입니다

    위는 단방향 데이터 흐름의 간단한 예시입니다.

    • 상태는 특정 시점의 앱의 상태를 설명해줍니다
    • 뷰는 현재의 상태를 기반으로 렌더링(업데이트)됩니다
    • 어떤 일(액션)이 일어나면 이 액션을 기반으로한 상태가 업데이트 됩니다.
    • 상태가 업데이트되면 현재의 상태(변경된 상태)를 기반으로 다시 렌더링(업데이트) 합니다

     

    그러나 만약 위의 구조에서 상태를 여러가지 컴포넌트에서 사용해야하는 경우, 특히 해당 컴포넌트가 앱의 다른 부분에 있는 경우에는 단순성이 무너져 관리가 어려워집니다. 상태를 상위 컴포넌트로 올려서 해결이 가능하지만 항상 그런 경우가 좋은 방법이 아닐때가 있습니다.

     

    위의 문제를 해결하기위한 방법 중 하나는, 공유하고있는 상태를 최상위의 스토어에 담아 관리할 수 있습니다. 그러면 컴포넌트의 위치와 상관없이 핸들링하기 수월해집니다.

     

    상태관리와 연관된 개념을 정의 및 분류하여 상태간의 독립성을 유지하고, 유지보수성을 높힐 수 있습니다.

     

    앱의 전역상태를 이용해 해당 상태를 업데이트할때 따라야할 어떤 패턴을 정의하고 이 패턴을 통해 코드를 예측가능하게 하는것이 리덕스의 기본 개념입니다.

     

    리덕스와 많이 사용하는 UI 라이브러리인 React의 핵심 원칙은 상태를 기반으로한 UI업데이트 입니다.

    상태를 기반으로한 앱을 구상할때 먼저 생각해야 할 점은 앱의 작동방식을 설명하는 상태를 구상하는것 입니다.

    업데이트 해야할 데이터가 적을수록 적은 상태로 UI를 설명하는것이 좋습니다.

     

    불변성(immutability)

    Mutable은 변경 가능 Immutable은 불변 이라는 뜻 입니다.

     

    const obj = { a:1 , b: 2 };
    obj.a = 3;
    obj.a; // 3
    
    const arr = ['a', 'b'];
    arr.push('c');
    arr; // ['a', 'b', 'c'];

    자바스크립트의 배열과 객체는 기본적으로 변경이 가능합니다.

     

    이는 배열 혹은 객체가 참조하고있는 메모리주소의 참조이지만 변경할 경우 메모리주소값 자체가 변경이됩니다.

     

    값을 불변으로 관리하려면 원본을 복사 후 복사본의 값을 변경해야합니다.

     

    const obj = {
      a: {
        c: 3
      },
      b: 2
    }
    
    const obj2 = {
      // copy obj
      ...obj,
      // overwrite a
      a: {
        // copy obj.a
        ...obj.a,
        // overwrite c
        c: 42
      }
    }
    
    obj2; //
    {
        a: {
            c: 42
        },
        b: 2
    }
    
    const arr = ['a', 'b']
    // Create a new copy of arr, with "c" appended to the end
    const arr2 = arr.concat('c')
    arr2; // ['a', 'b', 'c']
    
    // or, we can make a copy of the original array:
    const arr3 = arr.slice()
    // and mutate the copy:
    arr3.push('c')
    
    arr3; // ['a', 'b', 'c']

     

    원본을 복사하는 방법은 다양합니다. 자주 사용되는 스프레드 연산자(...) Array.prototype의 slice, concat 등의 방법이 있습니다.

     

    리덕스는 모든 상태 업데이트가 불변으로 이루어지길 기대합니다.

    [불변을 지키면서 상태를 업데이트해야 리덕스의 동작이 우리가 예상하는 방향으로 동작한다는 의미입니다]

     

     

    리덕스가 불변을 유지해야하는 이유는 이러합니다

    • UI가 최신 값을 표시하도록 업데이트 되지않는 등의 버그가 발생합니다
    • 상태가 업데이트된 이유와 방법을 이해하기 어렵습니다
    • 테스트 작성하기가 어려워집니다
    • time-travel 디버깅을 올바르게 사용하기 어렵습니다
    • 리덕스의 의도된 패턴에 위배됩니다

     

    용어

    Action

    액션은 'type' 프로퍼티(필드)를 가진 일반 객체입니다.

    액션은 위에서 설명했는데요 유저의 인터렉션에 의해 발생하는 상태의 업데이트를 트리거합니다.

     

    const addTodoAction = {
      type: 'todos/todoAdded',
      payload: 'Buy milk'
    }

     

    type 필드는 액션을 발생시키고 이 액션이 어떤 일을 해야하는지 정의하는 문자열입니다.

    그리고 두 번째 필드에는 일반적으로 payload 라는 이름으로 액션 객체에서 할 일을 추가적으로 정의할 수 있습니다.

     

    액션은 앱의 요구사항을 기반으로 상태를 정의했듯 같은 방식으로 어떤 액션을 취할지 설계해야합니다.

     

     

    Action Creator

    액션 크리에이터는 액션 객체를 생성하고 반환하는 함수입니다. 매 번 직접 작성할 필요를 줄여줍니다.

     

    const addTodos = text => (
    	return {
        	type: 'actionType',
            payload: text
        }
    )

     

    Reducer

    리덕스에는 초기에 설정한 루트 리듀서 하나만 존재합니다. 하나의 루트 리듀서는 모든 액션을 디스패치해서 새로운 상태를 반환합니다.

     

    리듀서는 현재 상태와 액션 객체를 인자로받아서 필요한 경우 상태를 업데이트하는 방법을 작성, 새 상태를 반환해주는 함수입니다.

    리듀서는 인자로 들어온 액션의 type에 따라 이벤트를 처리하는 함수입니다.

     

    리듀서는 아래의 규칙을 반드시 따라야합니다.

    • 인자로받은 상태, 액션을 기반으로한 새 상태를 반환해야합니다
    • 기존 상태의 불변성을 지켜야하기때문에 원본의 상태를 복사 후 복사본을 변경하여 업데이트해야합니다
    • 사이드이펙트를 발생시키지 않아야합니다(순수함수여야합니다)

     

    사이드 이펙트의 예를 들어보자면

    • 콘솔에 값을 로깅
    • 파일 저장
    • 비동기 타이머(setTimeout, setInterval)
    • HTTP 요청
    • 함수스코프 외부의 값을 수정
    • Math.random(), Date.now()와 같은 난수, 고유한 임의의 ID값 생성

    이 있습니다.

     

    리듀서가 순수함수여야 하는 이유는 아래와 같습니다

    • 리덕스는 같은 액션이 들어왔을때 어떤 동작을 할지 예측이 가능해야합니다 만약 사이드이펙트가 발생한다면 이 예측이 무너집니다
    • 리듀서 함수가 인자 외의 값을 같이 수정한다면 우리가 예상한 UI의 업데이트가 예상과 다르게 업데이트되지않는 일이 발생할 수 있습니다
    • 리덕스의 DevTools들의 몇 가지는 리듀서를 의존하기 때문입니다

     

    리듀서의 내부는 일반적으로 동일한 단계를 수행합니다

    • 리듀서에 액션의 타입이 있는지 확인해야합니다 있다면 상태의 복사본을 만들고 새 상태로 업데이트 하고 반환합니다
    • 없다면 기존의 상태를 반환합니다

     

    const initialState = { value: 0 }
    const actions = [
      { type: 'counter/increment' },
      { type: 'counter/increment' },
      { type: 'counter/increment' }
    ]
    
    function counterReducer(state = initialState, action) {
      // 리듀서에 액션의 타입이 있는지 체크
      if (action.type === 'counter/increment') {
        // 그렇다면 원본 상태를 복사
        return {
          ...state,
          // 복사본을 업데이트
          value: state.value + 1
        }
      }
      // 리듀서에 액션의 타입이 없기때문에 현재 상태 반환
      return state
    }
    
    const finalResult = actions.reduce(counterReducer, initialState);
    console.log(finalResult) // 3

     

    리듀서는 기본적으로 액션의 내부를 체크해서 리듀서 로직에서 액션의 타입을 체크 후 원하는 동작을 구성해야합니다.

    if else 혹은 switch를 보편적으로 사용합니다.

     

    위의 코드는 Array.prototype.reduce와 리듀서를 이용해 상태를 업데이트했습니다.

     

    Store

    스토어는 앱의 모든 상태를 담고있는 하나의 저장소입니다.

    리듀서를 전달하여 생성하고 getState()를 통해 현재의 상태를 반환받을 수 있습니다.

     

    import { configureStore } from '@reduxjs/toolkit'
    
    const store = configureStore({ reducer: counterReducer })
    
    console.log(store.getState()); // {value: 0}

     

    스토어의 상태는 자바스크립트의 객체, 배열 외의 데이터형은 들어올 수 없습니다.

    반드시 객체, 배열의 형태로 작성해야합니다.

     

    Dispatch

    스토어에는 디스패치 메서드가있습니다. 스토어에 저장된 상태를 업데이트하는 유일한 방법디스패치를 호출하고 액션을 전달해야합니다.

    디스패치가 호출되면 생성할때 전달한 리듀서가 호출되고 상태를 업데이트합니다.

     

    디스패치는 앱의 상태를 업데이트해주는 이벤트 트리거하고 생각하면됩니다.

    javascript의 이벤트 리스너를 생각해보면 element.addEventListener(eventType, callback) 으로 정의합니다.

    디스패치에 전달하는 액션객체의 타입이 addEventListener의 eventType과 동일하다고 생각할 수 있습니다.

     

    store.dispatch({ type: 'counter/increment' })
    
    console.log(store.getState()); // { value: 1 }
    
    const increment = () => {
      return {
        type: 'counter/increment'
      }
    }
    
    store.dispatch(increment())
    
    console.log(store.getState()); // {value: 2}

     

    dispatch에 액션객체를 전달하여 상태를 업데이트합니다.

     

     

    Selector

    셀렉터는 스토어의 특정한 값(상태)를 조회하는 함수입니다.

    같은 상태의 값을 여러 컴포넌트에서 사용해야한다면 여러번 작성할 필요없이 셀렉터를 이용할 수 있습니다.

     

    const selectCountValue = store => store.value;
    
    const countValue = selectCountValue(store.getState());
    
    countValue; // store.value

     

    Redux Data Flow

    위에서 데이터의 흐름(상태, 뷰, 액션)을 설명했습니다.

    이 부분을 리덕스에 대입하면 조금 더 많은 과정을 거칩니다.

     

    초기설정(initial)

    • 앱의 저장소를 루트 리듀서 함수를 이용해 생성합니다
    • 저장소는 리듀서를 한 번 호출하고 반환된 값을 초기 상태로 저장합니다
    • 뷰(UI)가 처음 렌더링될때 UI컴포넌트는 리덕스의 저장소의 현재 상태에 접근하여 현재 상태를 기반으로 렌더링할 내용을 결정합니다
      향후 저장소에 업데이트를 구독(subscribe)하여 상태가 변경되는것을 감지할 수 있습니다

    업데이트

    • 만약 유저의 인터렉션으로 어떤 트리거가 발생합니다
    • 앱의 코드는 미리 정의했던 type객체를 이용해 dispatch에 액션을 전달합니다
    • 저장소에 전달된 디스패치는 리듀서를 호출합니다
    • 리듀서는 호출된 액션의 객체를 기반으로 새로운 상태를 업데이트 후 반환합니다
    • 저장소는 구독하고있는 컴포넌트에 스토어의 상태가 업데이트됨을 알립니다.
    • 저장소의 데이터가 필요한 각각의 컴포넌트는 필요한 상태의 일부가 변경됐는지 확인합니다
    • 데이터가 변경됐다면 각 컴포넌트는 새로운 상태를 기반으로 UI를 업데이트(리렌더링)합니다.

     

    댓글

Designed by Tistory.