만족

[React] redux에서 swr로 옮긴 썰 본문

[React] redux에서 swr로 옮긴 썰

FrontEnd/React Satisfaction 2022. 9. 10. 02:46

배경

현재 redux와 swr를 동시에 사용 중이다.

 

redux는 설정 값이나 검색 기록 등의 로컬에서만 다루는 상태들을 관리하는데 사용하고 있고,

swr에는 서버로부터 받아온 데이터(상태)를 관리하는데 사용하고 있다.

 

기존에는 swr를 사용하지 않고 redux만을 사용해 모든 전역 상태를 관리했었지만,

캐시 관리 등의 문제로 서버에서 받아오는 데이터는 swr로 이관한 상태였다.

 

그러나 redux의 높은 복잡성과 swr의 단순함을 보면서

로컬 값도 swr로 관리하면 안될까 하는 생각을 하게 되었다.

 

redux의 라이프사이클에서 알 수 있듯 보면 redux에서 새로운 상태를 다루고자 할 때

새로운 state를 정의하고 그 상태의 변화를 일으킬 때 필요한 action과 reducer를 만들고 store에 통합해야 한다.

 

명확한 대상 상태의 초기값, 예측 가능한 상태 변화라는 장점이 있지만

새로 추가할때는 큰 번거로움을 수반한다.

 

function useUser (id) {
  const { data, error } = useSWR(`/api/user/${id}`, fetcher)

  return {
    user: data,
    isLoading: !error && !data,
    isError: error
  }
}

SWR은 redux와는 조금 다른 목적의 상태 관리 라이브러리지만,

상태값을 구분할 id(key)와 그 값을 결정할 function(fetcher)로 구성된다.

 

값을 갱신할 때는 useSWR이 리턴하는 mutate를 사용할 수 있고,

이 mutate를 래핑해 함수를 생성함으로써 redux의 action/reducer처럼 다음 상태를 예측 가능하게 할 수도 있다.

 

예시: redux로 작성된 기존의 상태 관리 코드

실제 내 프로젝트의 코드를 보여주면 좋겠지만,

이 값이 뭐고 왜 이렇게 변해야 하고 가타부타 설명하기는 씹덕같기 떄문에

redux 공식 홈페이지의 Tutorial에서 사용된 Counter를 기준으로 설명할 것이다.

 

https://redux-toolkit.js.org/tutorials/quick-start

 

Quick Start | Redux Toolkit

 

redux-toolkit.js.org

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state) => state.counter.value)`
export const selectCount = (state) => state.counter.value

export default counterSlice.reducer

물론 이건 @reduxtjs/toolkit이 사용되어 내가 사용중이던 생 redux보단 간단해지긴 했다.

 

정리해보면 counter라는 이름의 전역 상태는 

{value: 0}이라는 초기값을 갖고,

increment, decrement, incrementByAmount라는 액션/리듀서를 갖는다.

 

아무튼 reducers object아래의 increment/decrement/incrementByAmount 함수를 보면 알 수 있듯

우리는 각 액션이 호출되었을 때 다음에 상태가 어떻게 변화할 지 명확히 알 수 있다.

 

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'

export function Counter() {
  const count = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

이것을 사용할 때는 useSelector로 상태를 불러오고, 

useDispatch를 이용해 액션을 발생시킴으로써 상태를 변화시킨다.

 

나는 구분자, 초기값, 액션/리듀서를 만드는 createSlice에서만 끝났으면 좋겠다고 생각했지만

selectCount로 셀렉터를, counterSlice.action로 액션을 내보내는 동작과

이것을 사용할 컴포넌트에서는 추가로 useSelector와 useDispatcher를 사용해야 한다는 점이 마음에 들지 않았다.

 

게다가 이 코드에서는 나타나지 않지만, store를 통합하고 루트 컴포넌트를 Provider로 감싸는 과정이 추가로 필요하다.

 

나는 이것을 swr로 옮겨 보일러플레이트가 적은 전역 상태 관리를 진행했다.

 

예시: SWR로 동일한 동작 작성

// useCounter.js

import useSWR from "swr";

//초기 상태값
let state = {
  value: 0
};

//hook형태로 리턴
export const useCounter = () => {
  //data는 현재 상태값
  //mutate는 상태값 갱신
  const { data, mutate } = useSWR("counter", () => state);

  //상태 변화 유발
  const increment = () => {
    state = {
      ...state,
      value: state.value + 1
    };
    mutate(state);
  };
  const decrement = () => {
    state = {
      ...state,
      value: state.value - 1
    };
    mutate(state);
  };
  const incrementByAmount = amount => {
    state = {
      ...state,
      value: state.value + amount
    };
    mutate(state);
  };

  return {
    //첫 호출시 초기값은 undefined로 세팅되었다가 이후 state값으로 설정되므로
    //undefined일때 state로 값을 설정해준다
    counter: data ?? state,

    increment,
    decrement,
    incrementByAmount
  };
};
// lab.js

import React from "react";
import { useCounter } from "../../hooks/useCounter";

const Lab = () => {
  const { counter, increment, decrement, incrementByAmount } = useCounter();
  return (
    <div>
      Counter: {counter.value}
      <br />
      <br />
      <span onClick={increment}>increment</span>
      <br />
      <span onClick={decrement}>decrement</span>
      <br />
      <span onClick={() => incrementByAmount(10)}>incrementByAmount(10)</span>
    </div>
  );
};

export default Lab;

 

redux로 구현했던 counter를 swr로 구현해봤다.

 

이 포스트에서는 swr의 사용법을 다루지는 않을 것이므로 공식 튜토리얼을 참조하기 바란다.

https://swr.vercel.app/ko/docs/getting-started

 

 

정상적으로 작동한다.

 

아 물론 전역적으로 이 설정값을 사용해서 swr에서 말하는 캐시(여기서는 state가 되겠다)가 revalidate되지 않게 만든다.

 

공식 사용 용도는 네트워크로부터 내려받은 데이터를 일정 시간 캐싱하고 자동으로 갱신하는 것이므로,

기본 옵션은 redux처럼 일반적인 상태 관리에 사용하기에 부적합하기 때문이다.

 

이거 맞나..?

원래 사용 용도에는 벗어난 방법이긴 하다.

 

그러나 내가 생각하기에 사용상의 문제가 없고, 코드도 딱히 더러워지는 편이 아니기 때문에 별 문제가 되지 않는다고 본다.

 

오이 사서 그걸 먹든 얼굴에 붙이든 사용자 마음이다.

 

사용 용도에 맞춰야만 한다면 mobx를 쓰는게 나을 것이다.

 

redux는 그럼 언제 쓸건가?

redux도 훌륭한 상태 관리 도구다.

 

특히 nextjs를 사용할 때 스토어를 하나 두고 서버사이드 렌더링 과정에서 스토어를 미리 조작해 클라이언트로 스토어 값을 내려줄 수도 있다.

 

그러나 내 프로젝트의 경우 CSR로만 동작하고 있기 때문에 redux가 제공하는 장점을 충분히 누리지 못한다고 생각했기 때문에 한번 떠오르는 아이디어를 적용해봤다.

 

 



Comments