본문 바로가기

React/상태관리하기

Redux 톺아보기 0 - ( 핵심 컨셉, 적절한 사용, 개념 및 용어 )

새로운 기술을 접하고 적용할 때 해당 기술을 잘 사용하는 방법 중 하나라고 생각하는 기술의 핵심 컨셉 이해하기.

 

기술의 핵심 컨셉을 이용하고 사용한다면, 과하게 사용하는 일을 피하거나 필요한 순간 적절히 사용할 수 있지 않을까 싶다. 

 

 


[ 그래서 Redux 핵심 컨셉이 뭔데? ]

 

 

리덕스 핵심 컨셉을 공식 문서를 통해 살펴보자.

 

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

 

우리의 애플리케이션의 state가 위처럼 평범한 오브젝트 형태로 정의되어 있다고 생각해 보자. 예를 들어 투두 앱의 상태는 위의 예시 오브젝트의 형태로 만들어질 것이다.

 

이 객체는 setter가 없다는 것을 제외하고는 "model"과 비슷하다. 이렇게 되면 다른 부분의 코드에서는 이 state를 임의적으로 변경할 수 없고, 이는 재생산이 어렵다는 버그를 생성할 수 있다.

 

state 내의 무언가를 변경하기 위해서는 액션을 디스패치 해야 한다. 액션(action)은 어떠한 작업을 할지 설명하는 자바스크립트 순수 객체이다. 아래의 몇 가지 예시를 살펴보자.

 

{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

 

모든 변경 사항을 액션으로 만들어 놓으면 앱에서 어떤 일이 일어나고 있는지에 대해 쉽게 알 수 있다. 어떤 것이 변경되면, 우리는 이게 왜 변경됐는지 알 수 있다. 액션들은 변경 사항에 대한 빵부스러기라고 할 수 있다.

 

function visibilityFilter(state = 'SHOW_ALL', action) {
  if (action.type === 'SET_VISIBILITY_FILTER') {
    return action.filter
  } else {
    return state
  }
}

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map((todo, index) =>
        action.index === index
          ? { text: todo.text, completed: !todo.completed }
          : todo
      )
    default:
      return state
  }
}

 

상태와 액션을 한데 묶기 위해선, 우리는 리듀서라는 함수를 작성해야 한다. 다시 말하지만 엄청나게 대단할 것은 없다. 리듀서는 그저 상태와 액션을 인자로 하고, 애플리케이션의 다음 단계를 리턴해주는 함수일 뿐이다. 큰 애플리케이션을 위해 이런 함수를 작성하는 것은 어렵기 때문에 상태의 부분 부분을 관리하도록 함수를 작게 작성해야 한다.

 

function todoApp(state = {}, action) {
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  }
}

 

그리고 리듀서를 하나 더 작성하여, 위의 두 리듀서를 각각 해당하는 상태 키들에 대해 호출함으로써 애플리케이션의 전체 상태를 관리하도록 한다.

 

이것이 기본적으로 리덕스의 전체 개념이다.

 

 


 

[ 그래서 Redux가 뭔데..? ]

 

 

라이브러리를 사용하기 전 해당 라이브러리가 어떤 기능을 하고 사용하면 어떤 이점이 있는지 파악하고 사용하는 것이 중요하다. 때문에 Redux를 사용하면 어떤 이점이 있는지 살펴보고 가면 좋을 것 같다.

 

Redux는 자바스크립트 앱을 위한 예측 가능한 상태 컨테이너로 일관적으로 동작하고, 서로 다른 환경(서버, 클라이언트, 네이티브)에서 작동하고, 테스트하기 쉬운 앱을 작성하도록 도와준다. 또한 React와 같은 View 라이브러리와 함께 사용이 가능하다. 

 

🤔 현재까지는 Redux 사용 이점이 와닿지 않지만, 문서를 조금 더 살펴보면 알 수 있기를 기대해 본다.

 

 

 


[그래서 Redux를 무조건 사용해야 할까?]

 

 

당연히 아니다. 리덕스는 상태를 관리하기에 좋은 도구이지만 사용할 상황인지 적절히 따져 보아야 한다. 단지 누군가가 사용한다고, 사용하라고 했다는 이유만으로 리덕스를 사용해서는 안된다.

 

공식 문서에서는 리덕스 사용하기 적절한 때를 알기 위한 몇 가지 제안이 나와있다.

  • 계속해서 바뀌는 상당한 양의 데이터가 있다.
  • 상태를 위한 단 하나의 근원이 필요하다.
  • 최상위 컴포넌트가 모든 상태를 가지고 있는 것은 더 이상 적절하지 않다.

 

위의 제안을 보고도 여전히 사용하지 않아도 되는 상황이 확 와닿지 않는다. 대부분의 애플리케이션이 상태를 최상위 컴포넌트에 모아두면 문제가 될 것이고, 서버에서 데이터를 받아오는 경우 진실한 정보의 원천인 리덕스 store에 넣어 데이터를 업데이트해 가면서 사용을 할 것이니까.

때문에 조금 더 공식 문서를 살펴보면 아래와 같다.

 

만들고 있는 애플리케이션의 종류, 해결해야 하는 문제의 종류, 직면한 문제를 가장 잘 해결할 수 있는 도구를 이해하는 것이 중요하다.

 

리덕스는 상태 관리를 처리하는데 도움이 되지만 다른 도구와 마찬가지로 장단점이 존재한다.

 

코드를 작성하는 가장 짧거나 빠른 방법으로 설계되지 않았으며, 배워야 할 개념과 작성해야 할 코드가 너무 많다. 또한 간접적인 정보를 추가하고 특정 제한 사항을 따르도록 요청해야 한다는 단점이 있다.

 

반대로 다음의 경우 리덕스는 유용하다.

  • 앱의 여러 위치에서 필요한 많은 양의 애플리케이션 상태가 있다.
  • 앱 상태가 자주 업데이트 된다.
  • 해당 상태를 업데이트하는 로직이 복잡할 수 있다.
  • 앱에 중간 또는 큰 규모의 코드베이스가 있으며 많은 사람들이 작업할 수 있다.
  • 시간이 지남에 따라 해당 상태가 어떻게 업데이트되는지 확인해야 한다.

 

 


[ Redux 용어 및 개념]

 

  • State
function Counter() {
  // State: a counter value
  const [counter, setCounter] = useState(0)

  // Action: 어떤일이 발생할 때 State를 업데이트하는 코드
  const increment = () => {
    setCounter(prevCounter => prevCounter + 1)
  }

  // View: UI 정의
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}

 

작은 리액트 카운터 컴포넌트부터 살펴보자. 컴포넌트의 State의 숫자를 추적하고 버튼을 클릭하면 숫자가 증가한다.

 

해당 애플리케이션은 다음 부분으로 구성된 독립형 애플리케이션이다.

  • 애플리케이션을 구동하는 진실의 원천인 State
  • 현재 State에 기반한 UI의 선언적 설명인 View
  • 액션, 사용자 입력에 따라 앱에서 발생하는 이벤트, 상태에서 업데이트 트리거

 

이러한 애플리케이션은 "단방향 데이터 흐름"의 작은 예이다.

  • 상태는 특정 시점의 애플리케이션 State를 설명한다.
  • UI는 해당 State를 기반으로 렌더링 된다.
  • 어떤 일(예 : 사용자가 버튼을 클릭하는 경우)이 발생하면 발생한 상황에 따른 State가 업데이트된다.
  • 새로운 State에 따라 UI가 다시 렌더링 된다.

 

그러나 동일한 State를 공유하고 사용해야 하는 컴포넌트가 있는 경우, 특히 해당 컴포넌트가 응용 프로그램의 다른 부분에 있는 경우 단순성이 무너질 수 있다. 때때로 이러한 문제는 부모 컴포넌트로 State 끌어올리기를 사용하여 해결할 수 있지만 항상 도움이 되는 것은 아니다.

 

이를 해결하는 한 가지 방법은 컴포넌트에서 공유하고자 하는 State를 추출하여 구성 컴포넌트 트리의 외부의 중앙 위치에 배치하는 것이다. 이를 통해 컴포넌트 트리는 큰 View가 되어 모든 컴포넌트는 트리의 위치에 관계없이 State에 접근하거나 액션을 트리거할 수 있다.

 

State 관리와 관련된 개념을 정의 및 분리하는 View와 State 간의 독립성을 유지하는 규칙을 적용함으로써 우리는 코드에 더 많은 구조와 유지 관리성을 제공할 수 있다. 

 

이것이 리덕스의 기본 아이디어이다. 애플리케이션의 전역 상태를 포함하는 단일 중앙 집중식 저장소와 코드를 예측할 수 있도록 해당 State를 업데이트할 때 따라야 할 특정 패턴이다.

 

 

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

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

 

자바스크립트 객체와 배열은 모두 변경이 가능하다. 객체를 만들면 해당 필드의 내용을 변경할 수 있다. 물론 배열을 만들면 배열의 내용도 변경할 수 있다. 메모리의 동일한 객체 또는 배열을 참조하여 내용을 변경했기 때문이다.

 

 

const obj = {
  a: {
    // obj.a.c를 안전하게 업데이트하려면 각 조각을 복사해야 한다.
    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']
// 끝에 "c"가 추가된 새 arr 복사본 만들기
const arr2 = arr.concat('c')

// 또는 원본 배열의 복사본을 만들 수 있다.
const arr3 = arr.slice()
// 그리고 본사본을 변화시킨다.
arr3.push('c')

값을 불변으로 업데이트하려면 코드에서 기존 객체 또는 배열의 복사본을 만든 다음 해당 복사본을 수정해야 한다. 원래 배열을 변경하는 대신 배열의 새 복사본을 반환하는 배열 메서드뿐만 아니라 자바스크립트의 배열 또는 객체 스프레드 연산자를 사용하여 수동으로 위의 작업을 할 수 있다.

 

이렇게 복사본을 만들어 내용을 변경하는 모든 State 업데이트가 불변으로 수행되어야 한다.

 

 

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

 

액션은 필드가 있는 일반 자바스크립트 객체이다.

 

todos / todoAdded 처럼 액션은 애플리케이션에서 발생한 일을 설명하는 이벤트로 생각하면 된다. 필드의 type은 액션에 설명이 포함된 이름을 제공하는 문자열이어야 한다. 

 

일반적으로 domain / eventName과 같은 유형의 문자열을 사용한다. 여기서 첫 번째 부분은 이 액션이 속한 기능 또는 범주이고 두 번째는 발생한 특정 액션이다.

 

 

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

 

액션 생성자는 액션 객체를 생성하고 반환하는 함수이다. 일반적으로 매번 액션 객체를 직접 작성할 필요가 없다.

 

 

 

  • Reducer
(state, action) => newState

 

리듀서는 현재 State와 액션 객체를 수신하고 필요한 경우 상태를 업데이트하는 방법을 결정하고 새 State를 반환하는 함수이다. 리듀서는 전달받은 액션에 따라 이벤트를 처리하는 이벤트 리스너라고 생각하면 된다.

 

리듀서는 상항 아래의 규칙을 따라야 한다.

  • State 및 액션 인수를 기반으로 새로운 State 값만 계산해야 한다.
  • 기존 State를 수정할 수 없다. 대신 기존 값을 복사하고 복사된 값을 변경하여 불변 업데이트를 수행해야 한다.
  • 비동기 논리를 수행하거나 임의의 값을 계산하거나 다른 부작용을 일으키지 말아야 한다.

 

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // 리듀서가 액션에 관심이 있는지 확인
  if (action.type === 'counter/increment') {
    // 만약 관심이 있다면 State를 복사한다.
    return {
      ...state,
      // 복사본을 새 값으로 업데이트 한다.
      value: state.value + 1
    }
  }
  // 그렇지 않다면 기존 State를 변경하지 않고 반환한다.
  return state
}

 

또한 리듀서 함수 내부의 로직은 일반적으로 동일한 일련의 단계를 따른다.

  • 리듀서가 액션에 관심이 있는지 확인한다. ➡️ 만약 그렇다면 State의 복사본을 만들고 새 값으로 복사본을 업데이트한 다음 반환한다.
  • 그렇지 않다면 기존 State를 반환한다.

 

 

 

Redux 애플리케이션 데이터

 

앞에서 애플리케이션을 업데이트하는 일련의 단계를 설명하는 "단방향 데이터 흐름"에 대해 이야기했다.

  • State는 특정 시점의 애플리케이션 State를 설명한다.
  • UI는 해당 State를 기반으로 렌더링 된다.
  • 어떤 일(예 : 사용자가 버튼을 클릭하는 경우)이 발생하면 발생한 상황에 따른 State가 업데이트된다.
  • 새로운 State에 따라 UI가 다시 렌더링 된다.

 

 

특히 리덕스의 경우 다음 단계를 더 자세히 나눌 수 있다.

  • 초기 설정
    • 리덕스 스토어는 루트 리듀서 함수를 사용하여 생성된다.
    • 스토어는 루트 리듀서를 한 번 호출하고 반환 값을 초기 State 값으로 저장한다.
    • UI가 처음 렌더링되면 UI 구성 요소는 리덕스 스토어의 현재 State에 접근하고 해당 데이터를 사용하여 무엇을 렌더링 할지 결정한다. 또한 향후 스토어 업데이트를 구독하여 State가 변경되었는지 알 수 있다.
  • 업데이트
    • 사용자가 버튼을 클릭하는 것과 같이 애플리케이션에서 어떤 일이 발생한다.
    • 애플리케이션 코드는 다음과 같이 리덕스 스토어에 액션을 전달한다. ➡️ dispatch({ type : "counter / increment " })
    • 스토어는 이전 State와 현재 액션으로 리듀서 함수를 다시 실행하고 반환하는 값을 State에 저장한다.
    • 스토어는 등록된 UI의 모든 부분에 스토어가 업데이트되었음을 알린다.
    • 스토어의 데이터가 필요한 각 UI 컴포넌트는 필요한 State의 일 부분이 변경되었는지 확인한다.
    • 변경된 데이터를 보는 각 컴포넌트는 새 데이터로 강제로 다시 렌더링 하므로 화면에 표시되는 내용을 업데이트할 수 있다.

 

'React > 상태관리하기' 카테고리의 다른 글

MobX 톺아보기 0 - 컨셉, 원칙  (0) 2023.02.13
Redux 톺아보기 2 - (비동기 로직)  (0) 2023.02.11
Redux 톺아보기 1 - (redux-toolkit)  (0) 2023.02.10
Redux toolkit 사용  (0) 2022.11.08
Redux React에 적용하기  (0) 2022.11.08