Redux 기초

한태동·2024년 10월 27일

Reduxaction이라고 불리는 이벤트를 사용하여 애플리케이션 상태를 관리하고 업데이트하기 위한 패턴이자 라이브러리이다 . 애플리케이션 전체에서 필요로 하는 상태를 중앙에서 저장하고, 상태가 예측 가능한 방식으로 업데이트될 수 있도록 규칙을 제공한다.

Redux는 애플리케이션의 여러 부분에서 필요한 전역상태를 관리하는 데 도움을 준다.

Redux는 공유 상태 관리를 처리하는 데 도움이 되지만, 단점도 있다. 학습해야 할 개념이 많고 작성해야 할 코드가 더 많다. 또한 코드에 약간의 간접성을 추가하며, 특정 규칙을 따르도록 요구한다.

Redux가 더 유용한 경우는 다음과 같다:

  • 애플리케이션에서 여러 곳에서 필요한 많은 양의 상태가 있을 때
  • 애플리케이션 상태가 시간에 따라 자주 업데이트될 때
  • 해당 상태를 업데이트하는 로직이 복잡할 때
  • 중간에서 대형 규모의 코드베이스가 있고, 여러 사람이 함께 작업할 가능성이 있을 때

리덕스 라이브러리와 툴

  1. React-Redux:

    Redux는 자주 React와 함께 사용된다. React-Redux는 Redux 스토어와 상호작용하여 상태를 읽고 스토어를 업데이트하는 액션을 디스패치할 수 있게 해주는 공식 패키지이다.

  2. Redux Toolkit

    Redux Toolkit은 Redux 로직을 작성하는 데 권장되는 접근 방식이다. Redux 애플리케이션을 구축하는 데 필수적인 패키지와 기능을 포함하고 있다. Redux Toolkit은 대부분의 Redux 작업을 간소화하고 일반적인 실수를 방지하며 Redux 애플리케이션을 쉽게 작성할 수 있도록 돕는다.

  3. Redux DevTools Extension

    Redux DevTools Extension은 Redux 스토어의 상태 변화 기록을 시간 순으로 보여줍니다. 이를 통해 애플리케이션을 효과적으로 디버깅할 수 있으며, "타임 트래블 디버깅"과 같은 강력한 디버깅 기법도 사용할 수 있습니다.

리덕스 컨셉과 기초

다양한 컴포넌트에서 상태가 공유되지 않는 작은 프로젝트에서 리덕스를 사용할 필요는 없다. 하지만 다른 곳에 위치하는 다양한 컴포넌트들이 똑같은 상태를 참조하고 변화시킨다면 코드는 매우 복잡해질 뿐더러, 원하지 않게 상태를 변화시키는 실수를 범할 가능성이 높아진다. 이때 리덕스는 빛을 발한다.

상태를 공유하는 컴포넌트를 부모 컴포넌트에 끌어올림으로써 어느정도 문제를 해결할 수 있지만, 이는 props drilling으로 이어져 코드를 더욱 복잡해보이게할 뿐만 아니라 부모 컴포넌트가 자식 컴포넌트에 종속적이게 될 수밖에 없기 때문에 유지/보수에 안 좋을 수밖에 없다.

따라서 리덕스는 공유 상태를 컴포넌트에서 분리하여, 컴포넌트 트리의 외부에 위치시킴으로써 독립적으로 관리하는 접근법을 사용한다. 이를 통해 컴포넌트 트리는 하나의 큰 가 되어 트리 내 어디에 있든 상관없이 모든 컴포넌트가 상태에 접근하거나 액션을 트리거할 수 있게 된다.

이것이 Redux의 기본 아이디어이다. 애플리케이션의 전역 상태를 컴포넌트 트리와 분리된 하나의 위치에 모아두고, 상태를 업데이트할 때 코드가 예측 가능하도록 특정 패턴을 따르게 하는 방식인 것이다.

Immutability

Redux는 모든 상태 업데이트가 불변하게(immutably) 수행되기를 기대한다.이는 변화의 예측 가능성과 안정성을 보장하기 위해서이다. 상태가 불변하게 유지되면, 이전 상태와 새로운 상태를 비교하는 것이 쉬워지며, 상태 변경의 추적이 명확해진다.

JavaScript 객체와 배열은 기본적으로 mutable(변경 가능)하다. 객체를 생성하면 해당 속성의 내용을 변경할 수 있고, 배열을 생성하면 그 내용을 변경할 수 있다.

const obj = { a: 1, b: 2 }
// 여전히 동일한 객체이지만, 내용이 변경되었습니다.
obj.b = 3

const arr = ['a', 'b']
// 마찬가지로, 이 배열의 내용을 변경할 수 있습니다.
arr.push('c')
arr[1] = 'd'

메모리 내에서 동일한 객체나 배열을 참조하고 있지만, 객체 내부의 내용은 변경된 것이다.

값을 불변(immutable)하게 업데이트하려면, 기존 객체나 배열을 복사한 후 복사본을 수정해야 한다.

이 작업은 JavaScript의 배열,객체의 스프레드 연산자와 원본 배열을 변경하지 않고 새로운 배열을 반환하는 배열 메서드를 사용하여 수동으로 수행할 수 있다.

const obj = {
  a: {
    // obj.a.c를 안전하게 업데이트하려면 각 부분을 복사해야 합니다.
    c: 3
  },
  b: 2
}

const obj2 = {
  // obj를 복사합니다.
  ...obj,
  // a를 덮어씁니다.
  a: {
    // obj.a를 복사합니다.
    ...obj.a,
    // c를 덮어씁니다.
    c: 42
  }
}

const arr = ['a', 'b']
// 새로운 배열을 생성하고 "c"를 끝에 추가합니다.
const arr2 = arr.concat('c')

// 또는, 원본 배열의 복사본을 만들 수 있습니다.
const arr3 = arr.slice()
// 그리고 복사본을 변경합니다.
arr3.push('c')

Terminology

1. Actions (액션)

액션은 type 필드를 가진 객체로써 애플리케이션에서 발생한 일을 설명하는 이벤트이다.

type 필드는 액션에 설명적인 이름을 부여하는 string이어야 하며, 예를 들어 "todos/todoAdded"와 같은 형식이다. 일반적으로 type 문자열을 도메인/이벤트이름 형식으로 작성하며, 첫 부분은 액션이 속하는 기능이나 카테고리를, 두 번째 부분은 구체적인 이벤트를 나타낸다.

액션은 발생한 일에 대한 추가 정보를 담는 다른 필드를 가질 수도 있다. 관례상, 이 정보를 payload라는 필드에 넣는다.

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

2. Action Creators (액션 생성자)

액션 생성자는 액션 객체를 생성하고 반환하는 함수이다. 위의 예시처럼 액션 객체를 직접 작성하기 보다 이 생성자를 통해 작성하는 경우가 일반적이다.

const addTodo = text => {
  return {
    type: 'todos/todoAdded',
    payload: text
  }
}

이렇게 함으로써 액션 객체를 일관되게 생성할 수 있으며, 코드의 유지보수성과 재사용성이 향상된다.

3. Reducer

리듀서는 현재 상태와 액션 객체를 받아 상태를 업데이트할지 결정하고, 새로운 상태를 반환하는 함수이다. (state, action) => newState. 즉, 리듀서는 인수로 받은 액션(이벤트) 타입에 따라 이벤트를 처리하는 이벤트 리스너라고 생각하면 된다.

리듀서는 다음과 같은 특정 규칙을 항상 따라야 한다:

💡
  • stateaction 인수를 기반으로 새 상태 값을 계산해야 한다.
  • 기존 상태를 수정할 수 없다. 대신, 기존 상태를 복사하고 복사본의 값을 변경하여 불변성을 유지한 상태로 업데이트를 해야 한다.
  • 비동기 로직을 실행하거나, 랜덤 값을 계산하는 등, 다른 부작용을 일으키면 안된다.

다음은 리듀서가 따라야 할 단계를 보여주는 간단한 리듀서 예제이다:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // 리듀서가 이 액션을 처리해야 하는지 확인합니다.
  if (action.type === 'counter/increment') {
    // 그렇다면 `state`를 복사합니다.
    return {
      ...state,
      // 복사본을 새로운 값으로 업데이트합니다.
      value: state.value + 1
    }
  }
  // 그렇지 않으면 기존 상태를 변경하지 않고 반환합니다.
  return state
}

리듀서는 if/else, switch, 반복문 등 모든 종류의 로직을 사용하여 새 상태가 무엇이 되어야 하는지를 결정할 수 있다.

4. store

Redux가 관리하는 상태들는 store라는 객체에 저장된다.

스토어는 reducer를 전달하여 생성되며, 현재 상태 값을 반환하는 getState라는 메서드를 가지고 있다:

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

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

이렇게 하면 store 객체를 통해 애플리케이션의 상태를 관리하고 접근할 수 있습니다.

5. dispatch

Redux 스토어에는 dispatch라는 메서드가 있다. 상태를 업데이트하는 유일한 방법은 store.dispatch()를 호출하고 액션 객체를 전달하는 것이다. 그러면 스토어는 리듀서 함수를 실행하고 새 상태 값을 저장하며, getState()를 호출하여 업데이트된 값을 가져올 수 있다:

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}

이렇게 하면 액션을 일관되게 디스패치할 수 있어 코드의 유지 보수성을 높일 수 있다.

6. selector

선택자는 스토어 상태 값에서 특정 정보를 추출하는 함수이다. 애플리케이션이 커지면, 애플리케이션의 다양한 부분이 동일한 데이터를 읽어야 할 때 반복되는 로직을 피하는 데 도움이 된다.

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

이렇게 선택자를 사용하면 상태에서 특정 데이터를 일관되게 추출할 수 있어 코드의 가독성과 유지 보수성을 향상시킬 수 있습니다.

Redux 애플리케이션의 data flow

단방향 데이터 흐름은 애플리케이션을 업데이트하는 일련의 단계이다:

  1. 상태(state)는 특정 시점에서 애플리케이션의 상태를 나타낸다.
  2. UI는 해당 상태를 기반으로 렌더링된다.
  3. 사용자가 버튼을 클릭하는 등의 이벤트가 발생하면, 발생한 일을 기준으로 상태가 업데이트된다.
  4. UI는 새로운 상태를 기반으로 다시 렌더링된다.

Redux에서 이 단계를 좀 더 구체적으로 나눌 수 있다:

초기 설정

import { configureStore } from '@reduxjs/toolkit'

// 초기 상태를 정의합니다.
const initialState = { value: 0 }

// 루트 리듀서를 정의합니다.
function rootReducer(state = initialState, action) {
  switch (action.type) {
    case 'counter/increment':
      return { ...state, value: state.value + 1 }
    default:
      return state
  }
}

// configureStore 함수를 사용하여 스토어를 생성합니다.
const store = configureStore({
  reducer: rootReducer
})

console.log(store.getState())
// { value: 0 }
  • 루트 리듀서 함수로 Redux 스토어가 생성된다.
  • 스토어는 루트 리듀서를 한 번 호출하고 반환된 값을 초기 상태로 저장한다.
  • UI가 처음 렌더링될 때, UI 컴포넌트들은 Redux 스토어의 현재 상태에 접근하고 해당 데이터를 기반으로 무엇을 렌더링할지 결정한다. 또한, 이후 스토어 업데이트가 발생할 때 알림을 받을 수 있도록 구독(subscribe)한다.

업데이트

// 컴포넌트 정의
function Counter() {
  // useDispatch 훅을 통해 디스패치 함수 가져오기
  const dispatch = useDispatch()
  // useSelector 훅을 통해 현재 상태 값 선택
  const count = useSelector((state) => state.value)

  // 버튼 클릭 시 'counter/increment' 액션을 디스패치
  const handleIncrement = () => {
    dispatch({ type: 'counter/increment' })
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  )
}
  1. 애플리케이션 내에서 버튼 클릭과 같은 이벤트가 발생한다.
  2. 애플리케이션 코드가 Redux 스토어에 액션을 디스패치한다. 위 코드에서는 dispatch({type: 'counter/increment'}) 가 디스패치이다.
  3. 스토어는 이전 상태와 현재 액션을 가지고 리듀서 함수를 다시 실행하여, 반환된 값을 새로운 상태로 저장한다.
  4. 스토어는 상태가 업데이트되었음을 구독 중인 모든 UI 컴포넌트에 알린다.
  5. 각 UI 컴포넌트는 필요한 상태가 변경되었는지 확인한다.
  6. 데이터가 변경되었음을 확인한 컴포넌트는 새 데이터를 사용하여 다시 렌더링되고, 화면에 표시된 내용이 업데이트된다.

정리

💡
  • Redux는 전역 애플리케이션 상태를 관리하는 라이브러리이다.
  • Redux는 일반적으로 React와 Redux를 통합하기 위한 React-Redux 라이브러리와 함께 사용된다.
  • Redux Toolkit은 Redux 로직을 작성하는 데 권장되는 방법이다.
  • Redux는 "단방향 데이터 흐름" 애플리케이션 구조를 사용한다.
  • 상태(state)는 특정 시점에서 애플리케이션의 상태를 설명하며, UI는 그 상태에 따라 렌더링된다.

애플리케이션 내에서 무언가가 발생하면:

  1. UI에서 액션을 디스패치한다.
  2. 스토어는 리듀서를 실행하며, 발생한 일에 따라 상태를 업데이트한다.
  3. 스토어는 상태가 변경되었음을 UI에 알린다.
  4. UI는 새로운 상태에 따라 다시 렌더링된다.

Redux는 여러 유형의 코드를 사용합니다:

  • Actions: type 필드를 가진 단순 객체로, 애플리케이션에서 "무슨 일이 일어났는지"를 설명한다.
  • Reducers: 이전 상태와 액션을 기반으로 새로운 상태 값을 계산하는 함수이다.
  • Redux Store: 액션이 디스패치될 때마다 루트 리듀서를 실행하여 상태를 관리한다.
날짜: 2024년 10월 27일

0개의 댓글