je개발 회고

[ je 개발 회고 ] L 프로젝트 (6) - Store 리셋이 왜 안될까

Je-chan 2024. 3. 6. 23:30

1. Store Reset

1-1) Pinia 사용

  Vue3 프레임워크를 사용할 때 사용하는 중앙 상태 관리 라이브러리는 Pinia 다. Pinia 는 두 가지 방식으로 코드를 작성할 수 있다. 첫 번째는, Optional API 처럼(혹은 이전 중앙 상태 관리 라이브러리였던 Vuex 처럼) 사용하는 방법과 Composition API 처럼 사용하는 방법이다. 

 

  아래의 코드가 Optional API(혹은 Vuex) 처럼 사용하는 방법이다.

import { defineStore } from 'pinia'

export const useStore = defineStore('storeId', {
  // arrow function recommended for full type inference
  state: () => {
    return {
      // all these properties will have their type inferred automatically
      count: 0,
      name: 'Eduardo',
      isAdmin: true,
      items: [],
      hasChanged: true,
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    // since we rely on `this`, we cannot use an arrow function
    increment() {
      this.count++
    },
    randomizeCounter() {
      this.count = Math.round(100 * Math.random())
    },
  },
})

  

  state 는 상태, getters 는 상태의 값을 추적해서 계산되는 값(useMemo 와 비슷하다), actions 는 상태를 조작하는 함수다. Vue 를 사용하다 보면, 객체 지향이라는 느낌이 다분히 들 때가 많은데 대표적으로 이런 코드 스타일을 볼 때다. 그리고, 이렇게 코드를 작성하는 것이 Vue 를 오래 사용한 사람이라면 익숙할 것이다.

 

  반면, Composition API 처럼 작성하는 것은 다음과 같다. dlfm

import { defineStore } from 'pinia'
import {ref, computed } from 'vue'

export const useStore = defineStore('storeId', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  
  const increment = () => { 
    count.value++
  }
  
  const randomizeCounter = () => {
    count.value = Math.round(100 * Math.random())
  }
  
  
  
  const name = ref("Eduardo")
  const isAdmin = ref(true)
  const items = ref([])
  const hasChanged = ref(true)

  return {
    count,
    doubleCount,
    increment,
    randomizeCounter,
    
    name,
    isAdmin,
    items,
    hasChanged
  }
})

 

  좀더, 연관 있는 것끼리 코드를 묶을 수 있고, 우리가 Composition API 로 코드를 작성하고 있는지라 코드 스타일이 비슷해서 이런 Setup Store 방식을 사용하고 있다.

 

1-2) $reset 을 지원하지 않는다..?

  그런데 이렇게 Setup Store 를 사용하니 발생하는 문제가 있었다. 

 

https://pinia.vuejs.kr/core-concepts/state#resetting-the-state

 

Vue 3의 공식 Store | Vue.js를 위한 직관적인 스토어

직관적이고 타입 안전한 가벼운 그리고 유연한 Vue 스토어

pinia.vuejs.kr

  

  흠... 모든 상태를 reset 하는 $reset 메서드를 사용할 수 없다. 우리가 만드는 방식에 의하면 이 $reset 은 필수 불가결한 메서드인데... 이것을 사용할 수 없다니.. 이 말은 곧, 모든 상태마다 reset 하는 함수를 만들어서 $reset 함수 안에서 다 호출해야 한다는 것이다. 그당시 코드를 모방하면 아래와 같다. (전 포스팅에서도 언급했든 우리는 useState 를 만들어서 사용하고 있다)

// utils
export const useState = <T>(initState: T): [ShallowRef<T>, (newState: T) => void ] => {
  const state = shallowRef<T>(initState)
  const setState = (newState: T) => {
    state.value = newState
  }

  return [state, setState]
}

// store
import { defineStore } from 'pinia'
import { useState } from '../utils'

export const useStore = defineStore('storeId', () => {
  const [count, setCount] = useState(0)
  const doubleCount = computed(() => count.value * 2)
  const increment = () => { 
    setCount(count.value + 1)
  }
  const randomizeCounter = () => {
    setCount(Math.round(100 * Math.random()))
  }
  const resetCount = () => setCount(0)
  
  const [name, setName] = useState("Eduardo")
  const resetName = () => setName("Eduardo")
  
  const [isAdmin, setIsAdmin] = useState(true)
  const resetIsAdmin = () => setIsAdmin(true)
  
  const [items, setItems] = useState([])
  const resetItems = () => setItems([])
  
  const $reset = () => {
    resetCount()
    resetName()
    resetIsAdmin()
    resetItems()
  }
  
  return {
  	...
  }
})

 

  이건 너무 비효율적이라 판단했다. 실제로 하나의 Store 를 이렇게 하드하게 reset 을 만들고 적용해봤는데 이렇게 개발하면 안 되겠다고 판단했다. 때문에 최대한 코드를 바꾸지 않고 수정할 방법을 모색해서 구현했다.

 

1-3) 최대한 효율적으로 $reset 만들기

  이 당시, 나는 모던 자바스크립트 DeepDive 에서 함수 파트를 열심히 읽고 있는 중이었는데, 클로저에 굉장히 큰 감명을 받은 상태였다. 그래서 이 문제를 클로저로 해결해봤다.

 

// useResetTool.ts
import { useState as originUseState } from '../utils'

export const useResetTool = () => {
  const resetFuncQueue: Array<() => void> = [];

  /**
   * 인자로 받은 reset 함수를 Queue 쌓는 함수
   * @param resetFuncArgs : State 를 초기화 하는 reset 함수
   */
  const useResetInject = (...resetFuncArgs: Array<() => void>) => {
    resetFuncArgs.forEach((resetFunc) => resetFuncQueue.push(resetFunc));
  };


  /**
   * useState 와 사용 방법이 리턴 값 제외 모두 동일하며 자동으로 resetFuncQueue 에 reset 함수를 등록
   * @param initState : 상태 초기값이자, reset 할 때 상태에 들어갈 값
   * @returns [state, setState, resetState]
   */
  const useState = <T>(
    initState: T
  ): [ShallowRef<T>, (newState: T) => void, () => void] => {
    const [state, setState] = originUseState(initState);

    const resetState = () => setState(initState);

    useResetInject(resetState);

    return [state, setState, resetState];
  };

  /**
   * resetFuncQueue 에 들어가 있는 모든 reset 함수를 실행
   */
  const $reset = () => {
    resetFuncQueue.forEach((resetFunc) => resetFunc());
  };

  return {
    useResetInject,
    useState,
    $reset
  };
};

 

  이렇게 만들면 useState 로 만든 상태들은 모두, reset 할 수 있는 함수들이 resetFuncQueue 안에 들어간다.  useResetInject 는 resetFuncQueue 에 reset 함수를 넣는 기능을 한다. 혹여나 useState 를 사용하지 않는 상태가 있을 수 있어 만들어 둔 함수다. 이제 위의 예시로 든 Store 의 상태를 reset 해보자

import { defineStore } from 'pinia'
import { useResetTool } from '../utils'

export const useStore = defineStore('storeId', () => {
  const { useState, $reset } = useResetTool()

  const [count, setCount] = useState(0)
  const doubleCount = computed(() => count.value * 2)
  const increment = () => { 
    setCount(count.value + 1)
  }
  const randomizeCounter = () => {
    setCount(Math.round(100 * Math.random()))
  }
  
  const [name, setName] = useState("Eduardo")
  
  const [isAdmin, setIsAdmin] = useState(true)
  
  const [items, setItems] = useState([])
 
  
  return {
  	...
  }
})

 

  끝이다. 실제로 기존 코드에서 useResetTool 을 import 하고 호출한 것밖에 더 없다. 실제로 useResetTool 은 useState 말고도 더 많은 상태 관련 함수들이 들어간다(직접 커스터마이징한 useFetch, usePolling 등) 그렇기에, 모든 상태를 reset 할 수 있도록 도와주면서 실제로 지금까지 만든 코드에서 추가되는 코드가 별로 없어서 굉장히 유용하게 사용하고 있다.