상태 관리

Created
May 16, 2024
Tags
React

앞서 전역 데이터를 효율적으로 관리할 수 있게 해주는 리액트 훅, useContext에 대해서 배웠다. 컴포넌트 간에 상태를 공유할 때 사용되는데, 컴포넌트 내에서만 상태를 관리할 수 있다. 반면에 앞으로 배울 리덕스(Redux)는 전역 상태를 리덕스 저장소(Redux Store)에 저장해서 관리한다. 모든 컴포넌트가 저장소에 접근할 수 있다. 이제 리덕스에 대해서 배워보자.

Redux

리덕스는 모든 상태를 하나의 저장소에서 관리하는 JavaScript 애플리케이션의 상태 관리를 위한 라이브러리이다.

메타(구 페이스북)가 리액트를 처음 발표했을 때 플럭스(Flux)라고 부르는 앱 설계 규격을 함께 발표했었다. 플럭스는 앱 수준 상태, 즉 여러 컴포넌트가 공유하는 상태를 리액트 방식으로 구현하는 방식이다. 이후에 플럭스 설계 규칙을 준수하는 오픈소스 라이브러리가 등장했는데, 리덕스는 그중에서 가장 많이 사용되는 패키지이다.

액션(Action)

어떤 동작이 발생했는지를 나타내는 객체이다. 이 동작은 사용자 입력, 네트워크 응답, 타이머 등과 같은 다양한 이벤트에 의해 트리거될 수 있다. dispatch의 파라미터로 전달된다. 액션은 아래와 같은 프로퍼티를 포함한다.

  • type: 액션의 종류를 식별하는 문자열 또는 심볼이다. ADD_CARD, CHANGE_PASSWORD 같은 값이 될 수 있다.
  • payload: 액션과 관련된 추가적인 데이터를 포함하는 필드이다. 선택적이며 액션의 유형에 따라 달라질 수 있다. 예를 들어, ADD_CARD 액션의 경우 카드를 추가하는 일의 내용이 payload로 전달될 수 있다.
{
	type: 'ADD_CARD',
	payload: {
		uuid: string,
		title: string,
		paragraphs: string
	}
}

액션 생성 함수(Action Creator)

액션 객체를 반환하는 함수이다. 필수적으로 사용하는 것은 아닌데 컴포넌트에서 쉽게 액션을 발생시키기 위해서 만드는 경우가 대부분이다. 그래서 보통 export를 통해 다른 파일에서 불러와 사용한다.

export const addCard = (payload: Card): AddCardAction => ({
  type: 'ADD_CARD',
  payload
})

리듀서(Reducer)

리덕스에서 상태를 변경하는 데 사용되는 순수 함수이다. 따라서, 이전 상태를 변경하지 않고 새로운 상태를 반환해야 한다.

현재 상태와 액션을 입력 받아 새로운 함수를 반환한다. 즉, 기존 상태를 전달 받은 액션을 참고해서 새로운 상태(객체)를 반환한다. 상태 변경 로직을 정의하고, 불변성을 유지하여 예측 가능한 상태 관리를 도와준다.


export const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_CARD':
      return [action.payload, ...state]
    case 'REMOVE_CARD':
      return state.filter(card => card.uuid !== action.payload)
    default:
	    return state;
  }
}

스토어(Store)

리덕스 저장소는 애플리케이션의 상태를 보유하는 객체이다. 모든 상태는 이 단일 저장소에 저장된다. 리덕스 스토어는 상태의 변경을 감시하고 있다가 변경 시 구독된 컴포넌트에 상태 업데이트를 알린다.

  • 애플리케이션의 상태를 저장한다.
  • getState()를 통해 상태에 접근할 수 있게 해준다.
  • dispatch(action)을 통해 상태를 수정할 수 있게 해준다.
  • subscribe(listener)를 통해 리스너를 등록한다.

디스패치(Dispatch)

액션을 발생시켜 스토어로 전달하는 것을 의미한다. 액션이 디스패치되면 리듀서가 호출되어 상태가 업데이트된다.

const addCard = useCallback(() => {
    dispatch(addCard(cards));
}, [dispatch])

구독(Subscribe)

구독은 스토어의 상태 변화를 감지하고 처리하기 위해 사용된다. 스토어의 subscribe 메서드를 호출하여 구독을 등록하면, 상태가 변경될 때마다 등록된 콜백 함수가 호출된다.

리덕스 미들웨어(Redux Middleware)

미들웨어, 중간에서 조정과 중개의 역할을 수행하는 소프트웨어 계층을 말한다. 그렇다면 리덕스 미들웨어란 이렇게 정의할 수 있겠다. 리덕스 스토어에 액션이 디스패치되기 전과 후에 중간에서 특정 동작을 가로채고, 수정하거나 로깅하거나 비동기 작업 등을 처리하는 역할이다. 이를 통해 애플리케이션의 동작은 변경하고 확장할 수 있다.

  1. 액션 로깅: 액션이 디스패치될 때 미들웨어에서 로깅을 수행하여 액션의 내용을 기록한다. 이를 통해 디버깅과 모니터링을 용이하게 할 수 있다.
  2. 비동기 작업 처리: 비동기 작업은 미들웨어를 통해 처리될 수 있다. 예를 들어, 네트워크 요청을 보내고 응답을 기다리는 동안 애플리케이션이 차단되지 않도록 할 수 있다.
  3. 액션 변형: 미들웨어를 사용하여 액션을 가로채고 수정할 수 있다. 이를 통해 액션을 조작하거나 특정 조건에 따라 다른 액션을 디스패치할 수 있다.
  4. 비동기 작업 결과 처리: 비동기 작업이 완료되면 해당 작업의 결과에 따라 새로운 액션을 디스패치하여 상태를 업데이트할 수 있다.
const myMiddleware = store => next => action => {
  // 미들웨어 내에서 수행할 작업들
  
  // 액션을 가로채기 전에 수행할 작업
  
  // 다음 미들웨어나 리듀서에 액션 전달
  const result = next(action);
  
  // 액션을 처리한 후에 수행할 작업
  
  // 옵셔널: 다음 미들웨어나 리듀서에 전달된 결과 반환
  return result;
}

export default myMiddleware;
  • 미들웨어는 함수를 반환하는 함수를 반환하는 함수이다.
  • 첫 번째 함수: store는 리덕스 스토어 인스턴스로 dispatch, getState, subscribe 같은 내장 함수들이 있다. 리덕스 스토어를 인수로 받아 두 번째 함수를 반환한다.
  • 두 번째 함수: next는 액션을 다음 미들웨어에게 전달하는 함수로 store.dispatch와 비슷한 역할을 한다. next(action)을 호출하면 그 다음 처리해야할 미들웨어에게 액션을 넘겨주고, 만약 미들웨어가 없다면 리듀서에게 액션을 넘겨준다. 만약 미들웨어 내부에서 next를 사용하지 않으면 액션이 리듀서에게 전달되지 않는다. 즉 액션이 무시되는 것이다.
  • 세 번째 함수: action은 디스패치되어 현재 처리하고 있는 액션을 가리킨다. 실제로 액션을 가로채고 처리하는 함수이다. 이 함수는 액션을 인수로 받아서 작업을 수행한 후 다음 미들웨어나 리듀서에 액션을 전달한다.

미들웨어를 왜 쓰는가?

먼저 리덕스 동작 과정을 살펴보자.

image

액션 객체 생성 → 디스패치가 액션 발생을 스토어에게 알림 → 리듀서가 정해진 로직에 따라 액션 처리 → 리듀서가 새로운 상태값 반환 → 스토어가 새로운 상태를 저장

리덕스는 이렇게 동기적으로 작동하고, 이 과정은 순식간에 실행된다. 개발자는 필요에 따라 디스패치된 액션을 스토어로 전달하기 전에 로깅, 비동기 작업 처리, 액션 취소 및 변형 등의 작업을 해야할 때가 있다. 그렇다고 시간을 딜레이 시키는 동작이나 비동기 작업을 억지로 넣는다면 에러가 발생해 정상적으로 작동하지 않는다.

리덕스 미들웨어는 이러한 비동기 작업을 처리하고 상태 관리를 보다 쉽게 할 수 있도록 도와준다.

Redux Thunk

리덕스 미들웨어로 가장 많이 사용되는 패키지는 redux-thunk 패키지이다.

아래 함수는 Redux Thunk를 사용하여 비동기 로딩 상태를 처리하는 데 사용하기 위한 액션 생성 함수이다. 이 함수는 주어진 시간 동안 로딩 상태를 활성화한 다음, 해당 시간이 지난 후에 로딩 상태를 비활성화한다.

export const doTimedLoading =

  (duration: number = 3 * 1000) =>
  
  (dispatch: Dispatch) => {
  
    dispatch(setLoading(true));
    const timerId = setTimeout(() => {
      clearTimeout(timerId);
      dispatch(setLoading(false));
    }, duration);
    
  };
  • 파라미터
    • duration: 로딩 상태를 유지할 시간을 지정했다. 3초로.
  • 반환된 함수
    • Redux Thunk 패턴을 사용하여 함수를 반환한다.
    • 이 반환된 함수는 dispatch 함수를 인수로 받는다. 이 함수는 스토어에 액션을 디스패치하기 위해 사용된다.
  • 구현 내용
    • 반환된 함수 내에서는 먼저 setLoading(true) 액션을 디스패치하여 로딩 상태를 활성화한다.
    • setTimeout을 사용하여 3초가 지나면 setLoading(false) 액션을 디스패치하여 로딩 상태를 비활성화한다.
    • clearTimeout을 사용하여 타이머를 취소한다.
image

React Query

Zustand

Recoil & Jotai