Composition API RFC Migration - ChoDragon9/posts GitHub Wiki


title: Composition API ๊ฒฝํ—˜ ์ •๋ฆฌ sidebar: auto

์„ธํŒ…

์„ค์น˜

npm install @vue/composition-api

ํ”Œ๋Ÿฌ๊ทธ์ธ

import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'

Vue.use(VueCompositionApi)

์˜์กด์„ฑ

@vue/composition-api๊ฐ€ ๋ณ€๊ฒฝ ๋˜์—ˆ์„ ๋•Œ ์ด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋Š” API ๋ณ€๊ฒฝ/์‚ญ์ œ์— ๋Œ€ํ•œ ๊ถŒํ•œ์ด ์—†๋‹ค. ๊ทธ๋ž˜์„œ ์—…๋ฐ์ดํŠธ๊ฐ€ ๋˜๋Š” ์ˆœ๊ฐ„ ํ”„๋กœ์ ํŠธ์— ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ์ „ํŒŒํ•˜๊ฒŒ ๋œ๋‹ค. ์ด๋Ÿฐ ๋ถ€๋ถ„์„ ๋‚˜๋Š” ์™ธ๋ถ€ ์˜์กด์„ฑ์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

์™ธ๋ถ€ ์˜์กด์„ฑ์€ ์ง์ ‘์ ์œผ๋กœ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒํ•œ์ด ์—†์Œ์œผ๋กœ ์˜์กด์„ฑ์ด ๊ฐ•ํ•˜๋‹ค. ์ด๋ฅผ ์•ฝํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ•์€ ์ค‘๊ฐ„์— ๋ฌด์–ธ๊ฐ€๋ฅผ ๊ปด๋„ฃ๋Š” ๊ฒƒ์ด๋‹ค.

ํ•ด๊ฒฐ๋ฐฉ์•ˆ

์™ธ๋ถ€ ์˜์กด์„ฑ์„ ์ตœ์†Œํ™” ํ•˜๋ฉด์„œ Composition API๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋Œ€์•ˆ์œผ๋กœ Wrapper ํŒจํ„ด์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

Vue Wrapper

๋‹ค์Œ๊ณผ ๊ฐ™์ด @vue/composition-api์—์„œ ์‚ฌ์šฉํ•  ๊ธฐ๋Šฅ๋งŒ ์ถ”์ถœํ•˜๊ณ  exportํ•œ๋‹ค.

export {
  defineComponent,
  onMounted,
  onBeforeMount,
  ref,
  reactive,
  toRefs,
  computed,
  watch
} from '@vue/composition-api'
Vue Wrapper Consumer
<template>
</template>

<script lang="ts">
import { defineComponent, ref } from '~/vue-wrapper'

export default defineComponent({
  props: {
    id: String,
    label: String,
    disabled: Boolean,
    checked: Boolean
  },
  setup(props, context) {
    const isChecked = ref(props.checked)
    const toggleCheckBox = (): void => {
      isChecked.value = !isChecked.value
      context.emit('on-change', isChecked.value)
    }

    return { isChecked, toggleCheckBox }
  }
})
</script>

์ผ๋ฐ˜์ ์œผ๋กœ Custom Component => Vue ์ด๋Ÿฐ ํ˜•ํƒœ๋กœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค. ๋งŒ์•ฝ์— 100๊ฐœ ์ด์ƒ์˜ Custom Component๊ฐ€ ์žˆ๋‹ค๋ฉด Vue ์—…๊ทธ๋ ˆ์ด๋“œ ์‹œ ๋ชจ๋“  Custom Component์— ๋Œ€ํ•œ ์ˆ˜๋™ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•˜๋‹ค.

Wrapper ํŒจํ„ด์„ ์ ์šฉํ•˜๋ฉด ์•ž์„œ ์–ธ๊ธ‰ํ•œ ํ˜•ํƒœ๋Š” Custom Component => Vue Wrapper => Vue ์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝ๋œ๋‹ค. ๊ทธ๋ž˜์„œ Vue์— ๋ณ€๊ฒฝ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ Custom Component๊ฐ€ ์•„๋‹Œ Vue Wrapper๋งŒ ์ˆ˜์ •ํ•˜๋ฉด ๋œ๋‹ค.

ํŠนํžˆ Wrapper ํŒจํ„ด๋ฅผ ์ ์šฉํ–ˆ์„ ๋•Œ, Vue์—์„œ API์˜ ์ด๋ฆ„์ด ๋ณ€๊ฒฝ ๋˜์—ˆ์„ ๋•Œ IDE์˜ Refactor ๊ธฐ๋Šฅ์„ ํ†ตํ•ด Function ์ด๋ฆ„์„ ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋งค์šฐ ํŽธ๋ฆฌํ•˜๋‹ค.

API Reference

Composition API๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

<template>
  <div>
    <div>{{ count }} {{ plusOne }}</div>
    <div>{{ obj.foo }}</div>
    <div>{{ firstName }} {{ lastName }}</div>
    <div ref="templateRef"></div>
    <a @click.prevent="increment">INCREMENT</a>
    
    <my-button></my-button>
  </div>
</template>

<script>
import {
  defineComponent,
  ref,
  reactive,
  computed,
  toRefs,
  watch,
  onMounted,
  onBeforeMount,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from '@vue/composition-api'
import MyButton from './my-button.vue'

export default defineComponent({
  components: {
    MyButton
  },
  props: {
    name: String
  },
  setup(props, context) {
    console.log('[LIFECYCLE] beforeCreate, created')
    console.log(`name is: ${props.name}`)

    const count = ref(0)
    const plusOne = computed(() => count.value + 1)
    const obj = reactive({ foo: 'bar ' })
    const state = reactive({
      firstName: 'Peter',
      lastName: 'Cho'
    })
    const templateRef = ref(null)

    const increment = () => {
      count.value++
      context.emit('on-change', count.value)
    }

    watch(() => {
      console.log(`count is ${count.value}`)
    })

    onBeforeMount(() => {
      console.log('[LIFECYCLE] beforeMount')
    })
    onMounted(() => {
      console.log('[LIFECYCLE] mounted')
    })
    onBeforeUpdate(() => {
      console.log('[LIFECYCLE] beforeUpdate')
    })
    onUpdated(() => {
      console.log('[LIFECYCLE] updated')
    })
    onBeforeUnmount(() => {
      console.log('[LIFECYCLE] beforeDestroy')
    })
    onUnmounted(() => {
      console.log('[LIFECYCLE] destroyed')
    })

    return {
      count,
      plusOne,
      obj,
      ...toRefs(state),
      templateRef,
      increment
    }
  }
})
</script>

ํ”Œ๋Ÿฌ๊ทธ์ธ

Vue Options API์—์„œ ํ”Œ๋Ÿฌ๊ทธ์ธ ์‚ฌ์šฉ ์‹œ, this.$router ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ–ˆ๋‹ค. Composition API์—์„œ๋Š” setup(props, context)์˜ ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ ์ „๋‹ฌ๋˜๋Š” context๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. context.root.$router ํ˜•ํƒœ๋กœ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

Props

๋Ÿฐํƒ€์ž„๊ณผ ์ปดํŒŒ์ผํƒ€์ž„์˜ ํƒ€์ž… ์ผ์น˜

props๋ฅผ required๋กœ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด Optional๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค.

export default defineComponent({
  props: {
    id: String, // string | undefined
  }
})

required๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•„์ˆ˜ ํƒ€์ž…์œผ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค.

export default defineComponent({
  props: {
    id: {
      type: String,
      required: true
    }, // string
  }
})

Props์˜ any ํƒ€์ž… ์ •์˜

  • (์ปดํŒŒ์ผ ํƒ€์ž„) TypeScript๋กœ props ํƒ€์ž… ์ •์˜ ์‹œ, any ๋˜๋Š” unknown์ด ํ•„์š”ํ•  ๋•Œ๊ฐ€ ์žˆ๋‹ค.
  • (๋Ÿฐํƒ€์ž„) ์ด๋•Œ props์˜ ํƒ€์ž…์€ null๋กœ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค.

::: tip Composition API ๊ณต์‹ ๋ฌธ์„œ์—๋Š” undefined์™€ null๋กœ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ๊ฐ€์ด๋“œํ•˜๋Š”๋ฐ, TypeScript๋กœ ์ •์˜๋œ Declaration ํŒŒ์ผ์—์„œ๋Š” null๋งŒ ํ—ˆ์šฉํ•œ๋‹ค. :::

interface CommonSelectOption {
  value: unknown;
}

export default defineComponent({
  props: {
    value: null,
  },
  // ... ์ƒ๋žต
})

State

ref์™€ reactive

ref๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ๊ฐ’์— ์ ‘๊ทผํ•  ๋•Œ .value๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋‚˜๋Š” ref๋ฅผ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ ๋ถˆํŽธํ•จ์„ ๋Š๊ผˆ๋‹ค.

๊ทธ๋ž˜์„œ ํŠน๋ณ„ํ•œ ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹Œ ์ด์ƒ ref ๋Œ€์‹ ์— reactive๋กœ ์ƒํƒœ๋ฅผ ์ •์˜ํ•˜๊ณ , reactive๋กœ ์ •์˜ํ•œ ๋ณ€์ˆ˜๋ฅผ returnํ•  ๋•Œ๋Š”toRefs๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ฒฝํ—˜์ ์œผ๋กœ ์ข‹์•˜๋‹ค.

AS IS (ref)
setup(props, context) {
  const inputText = ref('')
  const isFocused = ref(false)

  const onChange = (): void => {
    context.emit('on-change', inputText.value)
  }
  const onFocus = (): void => {
    isFocused.value = true // value์— ํ• ๋‹น
  }

  const onBlur = (): void => {
    isFocused.value = false // value์— ํ• ๋‹น
  }

  onBeforeMount(() => {
    inputText.value = props.value // value์— ํ• ๋‹น
  })

  return {
    inputText,
    isFocused,
    onChange,
    onFocus,
    onBlur
  }
}
TO BE (reactive)
setup(props, context) {
  const state = reactive({
    inputText: '',
    isFocused: false
  })

  const onChange = (): void => {
    context.emit('on-change', state.inputText)
  }
  const onFocus = (): void => {
    state.isFocused = true // state์˜ property๋ฅผ ํ†ตํ•ด ๊ฐ’์„ ํ• ๋‹นํ•œ๋‹ค.
  }

  const onBlur = (): void => {
    state.isFocused = false // state์˜ property์— ๊ฐ’์„ ํ• ๋‹นํ•œ๋‹ค.
  }

  onBeforeMount(() => {
    state.inputText = props.value // state์˜ property์— ๊ฐ’์„ ํ• ๋‹นํ•œ๋‹ค.
  })

  return {
    ...toRefs(state), // toRefs๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ref๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
    onChange,
    onFocus,
    onBlur
  }
}

ํƒ€์ž… ์ •์˜

๋‚˜๋Š” reactive์— Generic์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊น”๋”ํ•˜๋‹ค๊ณ  ๋Š๊ผˆ๋‹ค.

interface Pagination {
  currentPage: number
  totalPage: number
}
const state = reactive<Pagination>({
  currentPage: 1,
  totalPage: 0
})

TemplateRef

<template>๋‚ด์—์„œ <div ref="box">์™€ ๊ฐ™์ด TemplateRef๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ํ•ญ์ƒ ref๋กœ ์ •์˜ํ•œ state๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. reactive๋กœ ์ •์˜ํ•œ state๋Š” ์ •์ƒ์ ์œผ๋กœ ์ฐธ์กฐ๋˜์ง€ ์•Š๋Š”๋‹ค.

Vuex

useStore

Options API์™€ Class-based API์—์„œ๋Š” this ์ปจํ…์ŠคํŠธ๊ฐ€ ์กด์žฌ ํ•˜๋ฏ€๋กœ this.$store๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Composition API๋Š” this ์ปจํ…์ŠคํŠธ๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— setup(props, context)์˜ ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ์ „๋‹ฌ๋˜๋Š” context.root.$store๋ฅผ ํ†ตํ•˜์—ฌ Vuex๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

useXXX ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค๋ฅธ ์ฝ”๋“œ๋“ค๊ณผ ์ž˜ ์–ด์šธ๋ฆฌ๋ฆฌ๋ผ ์ƒ๊ฐํ•œ๋‹ค.

import {SetupContext} from '@vue/composition-api';

export const useStore = (context: SetupContext) => {
  const {
    root: {
      $store: {
        state,
        getters,
        commit,
        dispatch,
      },
    },
  } = context;
  return {
    state,
    getters,
    commit,
    dispatch,
  };
};

state๋Š” reactive์™€ ref๋ฅผ ํ†ตํ•ด ๋ฐ˜์‘ํ˜• ์ƒํƒœ๋ฅผ ๋งŒ๋“ค์–ด๋„ mutation์„ ๊ฐ์ง€ํ•˜์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์— state๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

export default defineComponent({
  setup(props, context) {
    const { dispatch, state } = useStore(context)

    const fetchCheckLogin = (): Promise<void> => {
      return dispatch('auth/fetchCheckLogin')
    }
    const fetchLogout = (): Promise<void> => {
      return dispatch('auth/fetchLogout')
    }
    const isLogin = (): boolean => {
      return state?.auth?.auth?.success
    }
    const toAuthTitle = (): string => {
      return isLogin() ? '๋กœ๊ทธ์•„์›ƒ' : '๋กœ๊ทธ์ธ'
    }
    const logout = (): void => {
      fetchLogout()
    }

    onMounted(() => {
      fetchCheckLogin()
    })

    return {
      toAuthTitle,
      logout
    }
  }
})

state ์ด๋ฆ„ ์ถฉ๋Œ

reactive๋กœ ์ •์˜ํ•œ ๋ฐ˜์‘ํ˜• ์ƒํƒœ์™€ useStore๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉํ•˜๋Š” ์Šคํ† ์–ด ์ƒํƒœ์˜ ๋ณ€์ˆ˜๋ช…์„ state๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค. setup() ๋‚ด๋ถ€์— ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์ถฉ๋Œ์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณ€์ˆ˜๋ช…์˜ ๋ณ€๊ฒฝ์ด ํ•„์š”ํ•˜๋‹ค.

store๋ฅผ ๋„ค์ž„์ŠคํŽ˜์ด์Šค๋กœ ๊ฐ€์ง€๋Š” ๊ฒƒ๋„ ํ•˜๋‚˜์˜ ๋Œ€์•ˆ์ด๋‹ค.

setup(props, context) {
  const store = useStore(context) // store.state๋กœ ์ ‘๊ทผ
}

state ์ ‘๊ทผ

๋ชจ๋“ˆ ํ˜•ํƒœ์ธ state๋Š” ์‚ฌ์šฉ ์‹œ ๊นŠ์€ ์ ‘๊ทผ์ด ํ•„์š”ํ•˜๋‹ค. computed๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๊ธฐ์กด ์˜ต์…˜ API ์ฒ˜๋Ÿผ ์‚ฌ์šฉ๊ฐ€๋Šฅํ•˜๋‹ค.

setup(props, context) {
  const store = useStore(context)
  const state = reactive({
    auth: computed(() => store.state.authModule.auth)
  })
}

useAction

์‚ฌ์šฉ์ธก์—์„œ ๋ชจ๋“ˆ๋ช…๊ณผ ์•ก์…˜๋ช…์€ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์ด๊ธฐ ๋•Œ๋ฌธ์— ๋Ÿฐํƒ€์ž„์—์„œ๋งŒ ์ •์ƒ ์ž‘๋™์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. useAction๋Š” ์ปดํŒŒ์ผํƒ€์ž„์— ๋ชจ๋“ˆ๋ช…๊ณผ ์•ก์…˜๋ช…์ด ์ •์ƒ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜์—ˆ์Œ์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๋งŒ๋“ค์–ด์กŒ๋‹ค.

ModuleActions์—์„œ key๋Š” ๋ชจ๋“ˆ๋ช…, value๋Š” ์•ก์…˜๋ช…์œผ๋กœ ํƒ€์ž…์„ ์ •์˜ํ–ˆ๋‹ค.

  • ๋ชจ๋“ˆ๋ช…๊ณผ ํŒŒ์ผ๋ช…์ด ๋™์ผํ•˜๋„๋ก ํ•˜๋“œ์ฝ”๋”ฉํ•ด์•ผ ํ•œ๋‹ค.
  • ์•ก์…˜๋ช…์€ keyof typeof T ํ˜•ํƒœ๋กœ ํƒ€์ž…์ด ์ •์˜๋˜๊ธฐ ๋•Œ๋ฌธ์— ์•ก์…˜์ด ์ถ”๊ฐ€๋˜๋ฉด ์ž๋™์œผ๋กœ ๋ฐ˜์˜๋œ๋‹ค.

๋จผ์ € ๋ชจ๋“ˆ๋ช…์— ๋Œ€ํ•ด ํƒ€์ž… ์ฒดํฌํ•œ ๋’ค, ํ•ด๋‹น ๋ชจ๋“ˆ์˜ ์•ก์…˜๋ช…์„ ํƒ€์ž… ์ฒดํฌํ•œ๋‹ค. ํ•˜๋‚˜๋ผ๋„ ์ •์˜๋œ ๋ช…์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉด ์ปดํŒŒ์ผ ํƒ€์ž… ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

/use/useStoreAction.ts
import { Dispatch } from 'vuex'
import { actions } from '~/store/auth'
import notice from '~/store/notice'

interface ModuleActions {
  auth: keyof typeof actions
  notice: keyof typeof notice.actions
}

type ActionHandle<Keys extends string> = {
  [key in Keys]: (payload?: any) => Promise<any>
}

export const useStoreAction = (dispatch: Dispatch) => {
  function useAction<T extends keyof ModuleActions>(
    moduleName: T,
    actions: ModuleActions[T][]
  ): ActionHandle<ModuleActions[T]> {
    return Object.assign(
      {},
      ...actions.map((action) => {
        return {
          [action]: (payload) => dispatch(`${moduleName}/${action}`, payload)
        }
      })
    )
  }

  return { useAction }
}

์‚ฌ์šฉ ์ธก์€ mapActions์™€ ์œ ์‚ฌํ•œ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•œ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋Š” ๋ชจ๋“ˆ๋ช…, ๋‘ ๋ฒˆ์งธ ์ธ์ž๋Š” ์•ก์…˜๋ช…์„ ๋ฐฐ์—ด๋กœ ์‚ฌ์šฉํ•˜๋ฉฐ return type์€ ๊ฐ์ฒด์ด๊ธฐ ๋•Œ๋ฌธ์— Destructuring ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

const { dispatch } = useStore(context)
const { useAction } = useStoreAction(dispatch)
const { fetchCheckLogin, fetchLogout } = useAction('auth', [
  'fetchCheckLogin',
  'fetchLogout'
])

useGetter

useAction๊ณผ ๊ฐ™์€ ์ด์œ ๋กœ ๋งŒ๋“ค์–ด์ง„ Getter๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ํ•จ์ˆ˜์ด๋‹ค.

/use/useStoreGetter.ts
import { getters } from '~/store/auth'

interface ModuleGetters {
  auth: keyof typeof getters
}

type Getters<Keys extends string> = {
  [key in Keys]: () => any
}

export const useStoreGetter = (getters: object) => {
  function useGetter<T extends keyof ModuleGetters>(
    moduleName: T,
    moduleGetters: ModuleGetters[T][]
  ): Getters<ModuleGetters[T]> {
    return Object.assign(
      {},
      ...moduleGetters.map((name) => {
        return {
          [name]: (): any => getters[`${moduleName}/${name}`]
        }
      })
    )
  }

  return { useGetter }
}

getter๋Š” ์ธ์ž๋ฅผ ๋ฐ›์ง€ ์•Š๋Š” ํ•จ์ˆ˜๋กœ ๋งŒ๋“ค์–ด์ง„๋‹ค.

const { getters } = useStore(context)
const { useGetter } = useStoreGetter(getters)
const { isLogin } = useGetter('auth', ['isLogin'])
const toAuthTitle = (): string => {
  return isLogin() ? '๋กœ๊ทธ์•„์›ƒ' : '๋กœ๊ทธ์ธ'
}

useAction, useGetter ์ด์ 

ํƒ€์ž… ์ถ”๋ก 

Store ๋ชจ๋“ˆ์— ์ •์˜๋œ Action๊ณผ Getter์˜ ํƒ€์ž…์„ ์ถ”๋ก ํ•  ์ˆ˜ ์žˆ๋‹ค.

IDE ์ง€์›

์ •์˜๋ถ€๋ฅผ ์ฐพ์•„์ฃผ๋Š” ๊ธฐ๋Šฅ(Navigate to declaration)๊ณผ ์ด๋ฆ„๋ณ€๊ฒฝ ๊ธฐ๋Šฅ(Rename ๋˜๋Š” Refactor)์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ธฐ์กด์˜ Vuex๋Š” ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ๋•Œ ์ œํ•œ์ด ์žˆ์—ˆ๋‹ค.

์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ

๋‘ ํ•จ์ˆ˜๋Š” setup()์˜ Context์— ์˜์กดํ•˜์ง€ ์•Š๋Š”๋‹ค. useStoreAction๋Š” Dispatch์— ์˜์กดํ•˜๊ณ , useStoreGetter๋Š” object ํƒ€์ž…์— ์˜์กดํ•œ๋‹ค. ์ฆ‰, Middleware์—์„œ ์žฌ์‚ฌ์šฉ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ค€๋‹ค.

import { useStoreGetter } from '~/use/useStoreGetter'
import { useStoreAction } from '~/use/useStoreAction'

export default ({ store: { dispatch, getters } }) => {
  const { useAction } = useStoreAction(dispatch)
  const { useGetter } = useStoreGetter(getters)
  const { fetchCheckLogin, fetchLogout } = useAction('auth', [
    'fetchCheckLogin',
    'fetchLogout'
  ])
  const { isLogin } = useGetter('auth', ['isLogin'])

  return Promise.all([fetchCheckLogin(), fetchLogout()]).then(() =>
    console.log(isLogin())
  )
}

useStorePlugin

ActionTree๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  Plugin์„ ์‚ฌ์šฉํ•œ ์ผ€์ด์Šค

Store์— ActionTree ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋ฉด Action๋ช…([key: string])์„ ์˜ค์ง string ์œผ๋กœ ์ถ”๋ก ํ•œ๋‹ค. ์ด์— ๋Œ€ํ•œ ์˜ํ–ฅ์œผ๋กœ useAction์€ ์˜๋„์™€ ๋‹ค๋ฅด๊ฒŒ ๋™์ž‘ํ•œ๋‹ค.

/vuex/types/index.d.ts
export interface ActionTree<S, R> {
  [key: string]: Action<S, R>;
}

๋งŒ์•ฝ์— ActionTree๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ Store๋‚ด์—์„œ this๋ฅผ ํ†ตํ•ด ํ”Œ๋Ÿฌ๊ทธ์ธ์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋‹ค. ๊ทธ๋ž˜์„œ ๊ณ ์•ˆํ•ด๋‚ธ ๋ฐฉ๋ฒ•์ด useStorePlugin์ด๋ฉฐ ์ด๋ฅผ ํ†ตํ•ด ํ”Œ๋Ÿฌ๊ทธ์ธ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค.

/use/useStorePlugin.ts
import { Store } from 'vuex'

export const useStorePlugin = (store: any) => {
  const { $axios } = store as Store<any>
  return { $axios }
}
/store/auth.ts
export const actions = {
  fetchCheckLogin(context): Promise<void> {
    const { $axios } = useStorePlugin(this)
    return $axios.$get('/check_login')
  },
}

useStoreAction, useStoreGetter ๊ฐ„์†Œํ™”

์•ž์„œ ์†Œ๊ฐœํ•œ ์ฝ”๋“œ๋ฅผ ํ†ตํ•˜์—ฌ ๊ฐœ๋ฐœ์„ ์ง„ํ–‰ ํ–ˆ์„ ๋•Œ ์ฝ”๋“œ๋ฒ ์ด์Šค๊ฐ€ ๋Œ€ํญ๊นŒ์ง„ ์•„๋‹ˆ๊ณ ์†Œํญ ์ฆ๊ฐ€ํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๊ธฐ์กด์— ์‚ฌ์šฉ ์ค‘์ด๋˜ ์ฝ”๋“œ์— ๋Œ€ํ•ด ๋™์ž‘์€ ๋™์ผํ•˜๋˜ ์ฝ”๋“œ๋ฒ ์ด์Šค๋ฅผ ๊ฐ„์†Œํ™”์‹œํ‚ค๋Š” ๋ฐฉ์•ˆ์ด ํ•„์š”ํ–ˆ๋‹ค.

์šฐ์„  useStoreAction ์‚ฌ์šฉ๋ถ€๋ฅผ ๊ฐœ์„ ํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค๊ณ  ๋Š๊ผˆ๋‹ค. useStoreAction ์‚ฌ์šฉ๋ถ€๋ฅผ ๋ณด๋ฉด ๋ฌด์–ธ๊ฐ€ ๋งŽ์ด ์ž‘์„ฑํ•ด์ค˜์•ผ ํ•˜๋Š” ๊ฒŒ ๋„ˆ๋ฌด ๋งŽ๋‹ค. ํ˜„์žฌ useStoreAction ์‚ฌ์šฉ๋ถ€์—๋Š” 1) ์‚ฌ์šฉํ•  ๋ชจ๋“ˆ, 2) ์‚ฌ์šฉํ•  ํ•จ์ˆ˜, 3) ์‚ฌ์šฉํ•  ํ•จ์ˆ˜ ํ•ด์ฒด์˜ ์ฝ”๋“œ ๋“ฑ์ด ๊ธฐ์ˆ  ๋˜๊ณ  ์žˆ๋‹ค.

useStoreAction์˜ ๋ชฉ์ ์€ ํƒ€์ž… ์ถ”๋ก ์„ ํ†ตํ•œ ์„ ์–ธ๋ถ€ ์ถ”์ ์— ๋Œ€ํ•œ ๋น„์ค‘์ด ํฌ๊ธฐ ๋•Œ๋ฌธ์— ํƒ€์ž… ์ถ”๋ก  ๋ถ€๋ถ„ ์ด์™ธ์— ๋Œ€ํ•œ ๊ฐœ์„ ์ด ํ•„์š”ํ•˜๋‹ค.

๊ฐœ์„ ํ•œ ๋ฐฉํ–ฅ์€ ๋‹จ์ˆœํžˆ 1) ์‚ฌ์šฉํ•  ๋ชจ๋“ˆ๋งŒ ๊ธฐ์ˆ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

- const { fetchCheckLogin, fetchLogout } = useAction('auth', [
-   'fetchCheckLogin',
-   'fetchLogout'
- ])
+ const authActions = useAction('auth')
/use/useStoreAction.ts

useStoreAction๋Š” ๋ชจ๋“ˆ๋ช…์„ ์ธ์ž๋กœ ๋ฐ›๊ณ  ์•ก์…˜์„ ๋ชจ๋‘ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

const actionMap = new Map([
  ['auth', Object.keys(actions)],
  ['notice', Object.keys(notice.actions)]
]);

export const useStoreAction = (dispatch: Dispatch) => {
  function useAction<T extends keyof ModuleActions>(
    moduleName: T,
    actions?: ModuleActions[T][]
  ): ActionHandle<ModuleActions[T]> {
    const keys = actions || actionMap.get(moduleName) || [];
    return Object.assign(
      {},
      ...keys.map((action) => {
        return {
          [action]: (payload) => dispatch(`${moduleName}/${action}`, payload)
        }
      })
    )
  }

  return { useAction }
};

Nuxt

v2.12.0๋ถ€ํ„ฐ ์ƒˆ๋กœ์šด fetch ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ ์šฉ๋œ๋‹ค. fetch(context){} ํ˜•ํƒœ์˜€๋‹ค๋ฉด fetch(){} ํ˜•ํƒœ๋กœ ๋ฐ”๋€๋‹ค. middleware๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์„ ๊ถŒ์žฅํ•˜๋ฉฐ, this๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ฐ”๋€๋‹ค.

composition api์—์„œ fetch ๊ด€๋ จ ๋ผ์ดํ”„ ์‚ฌ์ดํด์ด ์—†์Œ

  • middleware๋กœ ์‚ฌ์šฉํ•  ๊ฒƒ์„ ๊ถŒ์žฅํ•จ

fetch, layout, middleware ํƒ€์ž… ๋ฏธ์ง€์›

  • defineComponent ํ•จ์ˆ˜์— fetch, layout, middleware ๋ฏธ์ง€์›
  • ํƒ€์ž… ํ™•์žฅ์œผ๋กœ ํ•ด๊ฒฐํ•  ๊ฒƒ์„ ๊ถŒ์žฅํ•จ
pages/index.ts
  • fetch๋Š” ํ•ญ์ƒ Promise<void> ํƒ€์ž…์œผ๋กœ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•จ
export default defineComponent({
  fetch(context): Promise<void> {
  }
})
types/vue-shim.d.ts
import Vue from 'vue'
import { Context, Middleware } from '@nuxt/types'

declare module '*.vue' {
  export default Vue
}

declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    fetch?(ctx: Context): Promise<void> | void
    layout?: string | ((ctx: Context) => string)
    middleware?: Middleware | Middleware[]
  }
}

Type

reactive ํ•„๋“œ์˜ ํ•„๋“œ ํƒ€์ž… ๋ฌธ์ œ

#261, #614 ๋ฒ„์ „ ์—…๊ทธ๋ ˆ์ด๋“œ ํ›„ ๋ฐ˜์˜๋  ๊ฒƒ์œผ๋กœ ๋ณด์ž„

reactive์˜ ํ•„๋“œ๋กœ ์ง€์ •๋œ ํƒ€์ž…์ด ๊ฐ์ฒด์ผ ๋•Œ, ํ•„๋“œ์˜ ๋‚ด๋ถ€์— ํ•„๋“œ๊ฐ€ ์กด์žฌํ•˜๊ฒŒ ๋œ๋‹ค. ์ž‘์„ฑ ์˜๋„๋Š” ํ•ด๋‹น ํ•„๋“œ์˜ ํƒ€์ž…์ด์ง€๋งŒ ํ˜„์žฌ ํƒ€์ž… ์ถ”๋ก ์œผ๋ก  UnwrapRef์ด ๋œ๋‹ค.

enum Axis {
  One = 1,
  Two = 2
}
interface MyInterfaceInInterface {
  x: Axis // number or 1 | 2๋Š” ๋™์ž‘ํ•จ
  y: Axis
}

interface MyInterface {
  field: MyInterfaceInInterface
}

export default defineComponent({
  setup() {
    const myState = reactive<MyInterface>({
      field: {
        x: 1,
        y: 2
      }
    })
    const add = (x: number, y: number) => x + y
    add(myState.field.x, myState.field.y)
    // Type Error 
  }
})
Argument type UnwrapRef3 is not assignable to parameter type number
Type UnwrapRef3 is not assignable to type number

reactive ํƒ€์ž…

reactive์— ์„ ์–ธํ•˜๋Š” ํƒ€์ž…์€ ์˜๋„ํ•œ ๋Œ€๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ์ผ€์ด์Šค๊ฐ€ ์กด์žฌํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด ์…€๋ ‰ํŠธ ๋ฐ•์Šค์—์„œ ์•„์ดํ…œ์„ ์„ ํƒํ•˜๊ณ , BackEnd API๋กœ ์š”์ฒญํ•˜๋Š” ์‚ฌ๋ก€์ด๋‹ค.

/types/my-component.ts
interface Options {
  id: number
  value: string
}
interface State {
  options: Options[]
  selected: Options | null
}
interface RequestBody {
  selectedId: number
}
/components/my-component.ts
const state = reactive<State>({
  options: [],
  selected: null
})

const changeSelectedOption = (option) => {
  state.selected = option
}

const onSave = () => {
  if (state.selected === null) {
    return
  }
  const body: RequestBody = {
    selectedId: state.selected.id
  }
}

๋ณด๊ธฐ์—๋Š” ์ •์ƒ์ ์ธ ์ฝ”๋“œ๋กœ ๋ณด์ด์ง€๋งŒ state.selected.id์—์„œ TS2339: Property 'id' does not exist on type 'string'. ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

์˜๋„ํ•œ ๋Œ€๋กœ ์‹คํ–‰๋˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„  Typecasting(as Type)์„ ํ•ด์•ผ ํ•œ๋‹ค.

// Not Cool
const state = reactive<State>({
  options: [],
  selected: null
})

// Cool
const state = reactive({
  options: [],
  selected: null
}) as State

์ปจ๋ฒค์…˜

์ปดํฌ๋„ŒํŠธ Input/Output ์ •์˜

์šฉ์–ด์ •์˜

  • Input: ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ์ธก์—์„œ ๋Œ€์ƒ ์ปดํฌ๋„ŒํŠธ์— ์ฃผ์ž…ํ•  ๊ฐ’
  • Output: ๋Œ€์ƒ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ์ธก์— ์ „๋‹ฌํ•  ๊ฐ’

Vue ์ปดํฌ๋„ŒํŠธ๋Š” Input ์—ญํ• ์„ ํ•˜๋Š” Props๋Š” ์„ค์ • ๋ ˆ๋ฒจ๋กœ ์ •์˜ํ•œ๋‹ค. ํ•˜์ง€๋งŒ Emit๋Š” ์„ค์ • ๋ ˆ๋ฒจ๋กœ ์ •์˜๋˜์ง€ ์•Š๊ณ , ํ•„์š”ํ•  ๋•Œ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ํ˜•ํƒœ๋กœ ๊ตฌ์„ฑ๋œ๋‹ค. ์ปดํฌ๋„ŒํŠธ์˜ ์ŠคํŒฉ์„ ์‰ฝ๊ฒŒ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ•ด๋‹น ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

Composition API์˜ ๊ฒฝ์šฐ ์ตœ์ƒ๋‹จ์— ์ถ”๊ฐ€๋˜๋ฉด ์‰ฝ๊ฒŒ ํŒŒ์•… ๊ฐ€๋Šฅํ• ๊ฑฐ๋ผ ์ƒ๊ฐํ•œ๋‹ค.

setup (props: Props, context) {
  const emit = {
    change: state => context.emit('change', {...state.surveyForm}),
    changeImage: file => context.emit('change-image', file),
  };

์ปดํฌ๋„ŒํŠธ ํ•จ์ˆ˜๋ณ„ ์—ญํ•  ๋ถ„๋ฆฌ

์ปดํฌ๋„ŒํŠธ์— ์ •์˜๋˜๋Š” ํ•จ์ˆ˜์˜ ์—ญํ• ์€ ์ƒํƒœ๋ณ€๊ฒฝ, ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ, ํ—ฌํผ ํ•จ์ˆ˜๊ฐ€ ์žˆ๋‹ค. ๊ฐ ํ•จ์ˆ˜๋Š” ๊ฐ™์€ ๋ ˆ๋ฒจ๋กœ ์ •์˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์‹œ๊ฐ์  ๊ตฌ๋ถ„์ด ์ž˜์•ˆ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

๋˜ํ•œ setup ํ•จ์ˆ˜ ๋‚ด๋ถ€์— ํ•จ์ˆ˜ ์ถ”๊ฐ€ ํ›„ <template>์— ์‚ฌ์šฉํ•˜๋ ค๋ฉด ํ• ๋‹น ๋ฐ˜ํ™˜๊ฐ’์— ์ถ”๊ฐ€๊ฐ€ ํ•„์š”ํ•˜๋‹ค. ๊ฐ์ฒด์— ๋‹ด์•„ ์ „๋‹ฌํ•˜๋ฉด ๋ฒˆ๊ฑฐ๋กœ์›€๊ณผ ๋ฐ˜ํ™˜๊ฐ’์„ ๊ฐ„์†Œํ™” ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค.

๊ฒฐ๋ก ์ ์œผ๋กœ Vuex์˜ ๋„ค์ด๋ฐ์„ ๋”ฐ๋ผ์„œ ์ƒํƒœ๋ณ€๊ฒฝ์€ mutation, ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋Š” action์œผ๋กœ ์ •์˜๋˜๋ฉด ์–ด๋–จ๊ฐ€ ์ƒ๊ฐํ•ด๋ดค๋‹ค.

// AS IS
setup() {
  const state = {}
  const onChange = () => {}
  const onClick = () => {}
  return {state, onChange, onClick}
}

// TO BE
setup() {
  const state = {}
  const actions = {
    onChange: () => {},
    onClick: () => {}
  }
  return {state, actions}
}
โš ๏ธ **GitHub.com Fallback** โš ๏ธ