(TIL) Redux - RTK : 아, 서순..

김동우·2021년 10월 28일
1

Redux

목록 보기
2/2
post-thumbnail

1.

반갑습니다.

어제는 Redux -> RTK의 순서가 아닌 RTK -> Redux 순서로 공부하는 글이었습니다.

그렇기에 오늘은 약간의 코드와 다량의 개념들로 글이 이루어질 것 같습니다.

지난 글에서는 코드를 보며

  • Provider component
  • Store
    • configureStore()
  • Action
  • Reducer
  • dispatch

등에 대한 개념을 RTK를 통해 알아보았습니다.

그런데 해당 방식은 서순이 맞지 않기도 하고, 놓치는 부분이 약간씩 생기는 것 같아 Redux의 개념을 RTK로 이해해보는 식으로 변경해볼까 합니다.

이번 글은 기존의 Redux에서 왜 RTK를 사용하는지? 그리고 어떤 개념들이 RTK를 사용하게 만들었는지 체크해보겠습니다.

그럼 시작해보겠습니다.

2. Redux

보통 가장 처음 생각하고 넘어갔어야 할 3가지 원칙을 어제 글에서는 적지 않았었습니다.

그런데 공부하다보니 대부분의 포스팅, 도서, 강의에서 강조하는 내용이 3가지가 있어 적고 시작하겠습니다.

약간 복무신조 느낌이네요.

  1. Single source of truth : 스토어는 단 하나만 존재해야 한다.
  2. Read-only state : 상태는 불변 그 자체다.
  3. Changes from pure functions : 순수 함수로만 변경해야 한다.

하나씩 생각해보죠.

SSOT(Single source of truth)

Redux는 단 하나의 상태 객체, Store를 구성할 것을 강조합니다.

  1. 먼저 앱의 확장성을 고려했을 때, 하나의 자바스크립트 객체를 활용하는 것이 Client - Server 요청 처리를 단순화하기 때문입니다.

  2. 그리고 이전의 객체가 저장되어 있다면 redo - undo를 쉽게 구현할 수 있기 때문입니다.

  3. 디버깅에 용이하기 때문입니다.

자, 보통의 개발자가 좋아하는 키워드 하나로 표현할 수 있겠죠?

분명 "유지보수"에 용이할 수 있다. 가 포인트입니다.

Read-only, Immutable

변경에 있어 하나의 가능성만을 남겨두고, 단방향 플로우를 구현해야 하는 이유.

디버깅에 용이하다. 유지보수에 도움이 된다. 프로젝트 안정성을 보장한다.

하나의 컨벤션을 준수하는 것으로 수많은 오류를 회피할 수 있습니다.

그런데 이런 이유 말고 왜 불변이라는 표현을 사용해야만 할까요?

슬슬 감이 오시듯,

상태는 Redux 뿐 아니라, React도 강조하고 있는 개념인 불변성을 가진 객체로 관리되어야 합니다.

그건 바로 변경 탐지의 이유때문입니다.

React가 상태, props의 변경을 어떻게 체크하는지 생각해보면 왜 불변성을 유지해야 하는지도 알 수 있습니다.

ReactShallow compare를 통해 Rendering 최적화를 이뤄냈으니, 한 번 해당 키워드 글을 읽어보면 좋습니다.

Change from pure function

순수함수? 쉽게 볼 수 있는 키워드는 아닙니다.

그런데 크게 신경써야 할 부분은 바로 이 부분입니다.

하나의 스토어, 불변객체인 State 관리, 그보다 난해한 것은 내가 작성한 Reducer가 순수함수가 맞을까? 하는 고민이 가장 어려울 것 같습니다.

순수함수는 I-O(Input - Output)이 명확해야 합니다.

어떠한 Side-Effect도 존재하지 않는 함수를 순수함수라 명합니다.

예를 들어보면,

지난 프로젝트에서 다루었던 dayjs() 의 유틸함수 중 일부는 순수함수가 아닙니다.

현재 시점의 Date() 객체의 data를 다루는 함수의 경우 다양한 상황에서 버그가 발생할 수 있습니다.

대표적 예시로 Github Actions에서의 test 환경은 KST 환경과 다른 UTC 환경입니다.

그럴 경우 Date() 객체의 data는 한국 시각이 아니니, I-O 자체가 틀어져버리는 이슈가 발생하게 됩니다.

그러나 반대로

특정 date String을 입력으로 받아 milliSec로 변경해주는 로직의 경우 Side-Effect가 존재하지 않습니다.

변경("2021-10-28"); // return 대충 밀리초(12321313213)

이런 함수가 바로 순수함수입니다.

즉, 앞으로의 상태변경 로직은 순수함수로만 작성되어야 한다. 정도만 이해하면 될 것 같습니다.

Why Redux?

Redux를 알아보았을 때, Store 하나로 프로젝트 전체의 상태를 관리한다는 것은 때로 매우 까다로울 수 있으며, 때로는 프로젝트 내부의 Cost가 높아질 수도 있습니다.

거기다 불변객체가 비교에는 용이하지만, re-Render가 적은 경우 객체 단순 변경이 어쩌면 더 좋은 퍼포먼스를 자랑할수도 있습니다.

순수함수를 고려하는 것 또한 개발자의 입장으로써 쉬운 일은 아닐 수 있죠.

그럼에도 Redux는 확장을 고려할 때 사용해야 하는 하나의 솔루션으로 인정받았습니다.

실제로 인턴십 중에도 Redux를 실무에 사용하고 있다는 것을 듣기도 했고, 지금의 회사도 Redux를 사용하고 있습니다.

그래서인지 Redux를 사용할 때는 모두 입을 모아 "더 고민해보고, 다시 고민해라." 라는 말을 하기도 합니다.

하지만 상태관리 이슈에 부딪혀본 저와 같은 사람들이라면 이제는 도전해볼 때가 되었습니다.

가보죠.

Flow

먼저, Redux의 동작흐름은 어제 말씀드린것처럼

이런 식으로 구성되어 있습니다.

이를 하나씩 생각해보면

Action

Action은 특정 상태에 대한 하나의 정의입니다.

통상적으로 객체 형태이며, type 프로퍼티를 필수적으로 갖는 JS Plain Object입니다.

Middleware

Reducer 실행 전, 거쳐가는 하나의 레이어 개념입니다.

Middleware에는 보통 Reducer의 예외처리를 하거나, 상태값을 디버깅하는 로직을 추가할 수 있습니다.

그 외에도 Action에 대한 비동기 처리를 가능하게 한다던가(redux-thunk), 특정 Action을 기반으로 동작하는 하나의 chaining(redux-saga)을 구현할수도 있습니다.

라이브러리를 주로 활용하는데, 위 두 예시가 자주 사용되는 라이브러리입니다.

Middleware의 경우 function compose로 구현되는데, 이는 클로저 개념을 응용하는 방식입니다.

주로

const middleware = store => next => action => next(action)

의 형태를 가집니다.

어제 까보았던 RTK의 store API를 적용해서 생각하면,

const middleware = ({getState, dispatch}) => next => action => next(action)

으로 디테일하게 표현할 수 있습니다.

Reducer

(prevState, action) 둘을 전달받아 newState를 반환하는 하나의 function입니다.

const reducer = (prev, action) => new

의 구조로 이루어져 있다고 생각할 수 있습니다.

Redux는 Reducer를 위한 라이브러리입니다.

그러니, Reducer를 얼마나 잘 작성하고, 잘 다룰 수 있느냐가 사실상 관건이 아닐까 생각합니다.

Store

단일 JS object입니다.

프로젝트 전체의 상태를 보유하기도 하고, 경우에 따라 일부만 보유할 수도 있습니다.

어제 보았던 store의 내부에는 다양한 property가 존재하는데, 이를 store API라고 표현하기도 합니다.

모든 상태의 집합체라고 생각할 수 있습니다.

또한 값을 직접 수정할 수 없기에 dispatch 메서드를 지원합니다.

dispatchreducer 내부 등에서 호출할 경우 store는 쌓인 순서, queue에 담아두는 것과 같은 순서로 처리합니다.

View

UI단, interaction을 접수받는 유일무이한 창구입니다.

저는 항상 View도 단방향 통신을 한다고 말하는데, 유저의 눈과 손을 분리해서 생각하기에 View도 하나의 순환구조가 형성된다고 생각합니다.

눈에 영향을 끼치고, 손에게 영향을 받으며, 흐름에 맞게 다른 시스템 구조의 누군가에게 영향을 끼치게 됩니다.

특정 Action의 Trigger이기도 합니다.

RTK, ReduxToolKit

이제 Redux를 어느정도 알아봤으니, RTK에 대해 얘기해봅시다.

Redux ToolkitRedux의 다양한 기능을 보다 쉽게 사용할 수 있는 메서드를 지원합니다.

공식문서에도 잔뜩 있기도 하고, 구체적인 내용은 적지 않겠습니다.

그런데 새로운 개념 하나 둘 정도는 확실히 짚고 넘어가봅시다.

Slice? AsyncThunk? what the...

분명 template을 하나 만들면 이상한 키워드들이 좀 있습니다.

AsyncThunk라던가 혹은 Slice가 그 예시가 될겁니다.

어쩌면 어제 적었던 configureStore()도 그 예시중에 포함됩니다.

그런데 AsyncThunkconfigureStore 정도는 이름만 봐도 느낌이 있는데, Slice? 이건 정말 모르겠습니다.

그러니 한 번 짚고 넘어가봅시다.

Slice

SliceReducer + Action 입니다.

?

왜 이렇게 되어있는지 의문이시겠지만, 분명 그런 개념이라고 명시되어 있습니다.

Slice를 통해 어제 Redux 개념을 공부하려다보니 분명 구멍이 생겼을 정도로 가히 파격적인 개념입니다.

다시 어제의 코드를 가져와봅시다.

const initialState: CounterState = {
  value: 0,
  status: "idle",
};

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = "loading";
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = "idle";
        state.value += action.payload;
      });
  },
});

이제 눈에 확 들어오는 친구들이 몇 있습니다.

  • initialState : 초기 상태값입니다. 따로 할당된 값은 블럭 위에 있습니다.

  • reducers : Action type을 key로 갖는 reducer function의 집합체입니다.

  • extraReducers : pending, fulfilled, reject 등의 비동기 Promise 상태에 따라 변경되는 새로운 State를 반환하는 reducer 입니다. 예외처리기입니다.

이정도면 Slice는 참 간단히 이해할 수 있겠습니다.

근데 incrementAsync는 어디에 있을까요?

createAsyncThunk

여기 있습니다.

// counterAPI.ts
export function fetchCount(amount = 1) {
  return new Promise<{ data: number }>((resolve) =>
    setTimeout(() => resolve({ data: amount }), 500)
  );
}

...

// counterSlice.ts
export const incrementAsync = createAsyncThunk("counter/fetchCount", async (amount: number) => {
  const response = await fetchCount(amount);
  return response.data;
});

이게 뭐시여... 싶으시겠지만, RTK를 활용한 비동기 처리 로직 사용법의 예시입니다.

axios를 쓰면 fetch가 어색하듯, createAsyncThunk도 어색합니다.

그러나 해당 요청결과를 extraReducer 내부에서 당연히 필터할 수 있고, 기존 custom hooks 를 활용한 로직 결과 반환을 Slice 파일 내에서 처리할 수 있다는 소소한 장점도 생깁니다.

그러니 thunkAction도 유용하게 사용해봅시다.

RTK middleware?

Middleware layer의 예시를 RTK식으로 표현할 수도 있습니다.

export const incrementIfOdd = 
  (amount: number): AppThunk =>
    (dispatch, getState) => {
      const currentValue = selectCount(getState());
      if (currentValue % 2 === 1) {
      dispatch(incrementByAmount(amount));
      }
  };

홀수일 때 더하는 로직인데, 익숙한 표현이 하나 보입니다.

그 전에 AppThunkcustom type 입니다.

실제로는 TunkAction type을 대입한 것과 같은 구조기에 return type(dispatch, getState) => {} 와 같습니다.

(dispatch, getState) => {}

확실히 Redux middleware와 유사한 구성이죠?

또한 currentValue의 경우 prevState에 해당합니다.

dispatch(action())도 보이네요.

생각보다 이제 보이는게 좀 있는 것 같습니다.

마치며

Redux -> RTK 구조로 공부하는게 어제보다 효율도 잘 나오고, 개념 이해도 쉬웠던 것 같습니다.

둘 다 사용할거니까 어차피 둘 다 하긴 했어야 합니다.

그런데 로드맵이 있다면 Redux 개념 -> RTK 순서가 훨씬 도움이 되었던 것 같네요.

그럼 오늘은 이정도로 마치겠습니다. 읽어주셔서 감사합니다.

0개의 댓글