Vuex

DEPRECATED

Vuex는 GitLab에서 사용이 중단되었으며 새로운 Vuex 스토어를 생성하지 말아야 합니다. 기존의 Vuex 스토어는 유지할 수 있지만 Vuex를 완전히 이탈하는 것을 강력히 권장합니다.

이 페이지에 포함된 나머지 정보는 공식 Vuex 문서에서 보다 자세히 설명되어 있습니다.

관심사의 분리

Vuex는 상태(State), 게터(Getters), 뮤테이션(Mutations), 액션(Actions), 모듈(Modules)으로 구성됩니다.

사용자가 작업을 선택하면 dispatch해야 합니다. 이 액션은 상태를 변경하는 뮤테이션을 commits합니다. 액션 자체는 상태를 업데이트하지 않으며, 상태를 업데이트하는 것은 뮤테이션만 해야 합니다.

파일 구조

GitLab에서 Vuex를 사용할 때, 가독성을 높이기 위해 이러한 관심사를 서로 다른 파일로 분리하세요:

└── store
  ├── index.js          # 모듈을 조립하고 스토어를 내보내는 곳
  ├── actions.js        # 액션
  ├── mutations.js      # 뮤테이션
  ├── getters.js        # 게터
  ├── state.js          # 상태
  └── mutation_types.js # 뮤테이션 유형

다음 예제는 사용자를 목록화하고 상태에 추가하는 응용 프로그램을 보여줍니다. (더 복잡한 예제 구현에 대해서는 이 저장소에 저장된 보안 응용 프로그램을 확인하세요).

index.js

이것은 우리 스토어의 진입점입니다. 다음을 가이드로 사용할 수 있습니다:

// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';

export const createStore = () =>
  new Vuex.Store({
    actions,
    getters,
    mutations,
    state,
  });

state.js

코드를 작성하기 전에 처음 해야 할 일은 상태를 설계하는 것입니다.

보통 우리는 HAML에서 Vue 응용 프로그램으로 데이터를 제공해야 합니다. 더 나은 액세스를 위해 상태에 저장합니다.

export default () => ({
  endpoint: null,

  isLoading: false,
  error: null,

  isAddingUser: false,
  errorAddingUser: false,

  users: [],
});

state 속성에 액세스

구성 요소에서 mapState를 사용하여 상태 속성에 액세스할 수 있습니다.

actions.js

액션은 응용 프로그램에서 스토어로 데이터를 보내는 정보의 페이로드입니다.

액션은 일반적으로 typepayload로 구성되며, 발생한 일을 설명합니다. 뮤테이션과는 달리 액션에는 비동기 작업이 포함될 수 있습니다. 따라서 항상 액션에서 비동기 논리를 처리해야 합니다.

이 파일에서는 사용자 목록을 처리하는 뮤테이션을 호출하는 액션을 작성합니다:

import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { createAlert } from '~/alert';

export const fetchUsers = ({ state, dispatch }) => {
  commit(types.REQUEST_USERS);

  axios.get(state.endpoint)
    .then(({ data }) => commit(types.RECEIVE_USERS_SUCCESS, data))
    .catch((error) => {
      commit(types.RECEIVE_USERS_ERROR, error)
      createAlert({ message: 'There was an error' })
    });
}

export const addUser = ({ state, dispatch }, user) => {
  commit(types.REQUEST_ADD_USER);

  axios.post(state.endpoint, user)
    .then(({ data }) => commit(types.RECEIVE_ADD_USER_SUCCESS, data))
    .catch((error) => commit(types.REQUEST_ADD_USER_ERROR, error));
}

액션 디스패치

구성 요소에서 액션을 디스패치하려면 mapActions 도우미를 사용하세요:

import { mapActions } from 'vuex';

{
  methods: {
    ...mapActions([
      'addUser',
    ]),
    onClickUser(user) {
      this.addUser(user);
    },
  },
};

mutations.js

뮤테이션은 스토어로 보낸 액션에 응답하여 응용 프로그램 상태가 어떻게 변경되는지를 지정합니다. Vuex 스토어에서 상태를 변경하는 유일한 방법은 뮤테이션을 커밋하는 것입니다.

대부분의 뮤테이션은 commit을 사용하여 액션에서 커밋됩니다. 비동기 작업이 없는 경우 mapMutations 도우미를 사용하여 구성 요소에서 뮤테이션을 호출할 수 있습니다.

구성 요소에서 뮤테이션을 커밋하는 예제는 Vuex 문서의 컴포넌트에서 뮤테이션 커밋하기를 참조하세요.

네이밍 패턴: REQUESTRECEIVE 네임스페이스

요청을 보내면 사용자에게 로딩 상태를 표시하는 것이 일반적입니다.

로딩 상태를 토글하는 뮤테이션을 생성하는 대신, 다음을 수행해야 합니다:

  1. 로딩 상태를 토글하는 유형이 REQUEST_뭔가인 뮤테이션
  2. 성공 콜백을 처리하는 유형이 RECEIVE_뭔가_SUCCESS인 뮤테이션
  3. 오류 콜백을 처리하는 유형이 RECEIVE_뭔가_ERROR인 뮤테이션
  4. 요청을 수행하고 언급된 경우 뮤테이션을 커밋하는 fetchSomething 액션
    1. 응용 프로그램이 GET 요청 이외의 작업을 수행하는 경우 다음을 참고하세요:
      • POST: createSomething
      • PUT: updateSomething
      • DELETE: deleteSomething

이 패턴을 따르면 다음을 보장할 수 있습니다:

  1. 모든 응용 프로그램이 동일한 패턴을 따르므로 코드를 유지하는 데 누구나 쉽습니다.
  2. 응용 프로그램의 모든 데이터가 동일한 라이프사이클 패턴을 따릅니다.
  3. 단위 테스트가 쉬워집니다.

복잡한 상태 업데이트

특히 상태가 복잡한 경우, 뮤테이션이 업데이트해야 하는 상태를 정확하게 탐색하는 것이 매우 어려울 수 있습니다. 이상적으로 vuex 상태는 가능한 한 정규화/분리되어야 하지만 항상 해당되는 것은 아닙니다.

뮤테이션 자체에서 선택한 변경된 상태 부분을 선택하고 뮤테이션에서 직접 상태를 변경하는 것이 코드를 읽고 유지하는 데 훨씬 쉽습니다.

다음과 같은 상태가 제공될 때:

export default () => ({
  items: [
    {
      id: 1,
      name: 'my_issue',
      closed: false,
    },
    {
      id: 2,
      name: 'another_issue',
      closed: false,
    }
  ]
});

다음과 같이 뮤테이션을 작성하는 것은 유혹이 될 수 있습니다.

// 안 좋은 방법
export default {
  [types.MARK_AS_CLOSED](state, item) {
    Object.assign(item, {closed: true})
  }
}

이 접근 방식은 작동하지만 여러 의존성이 있습니다.

  • 구성 요소/액션에서 item을 올바르게 선택해야 합니다.
  • item 속성이 이미 closed 상태로 선언되어 있어야 합니다.
    • confidential 속성이 추가되면 반응성이 없을 수 있습니다.
  • itemitems에 의해 참조됩니다.

이러한 지정된 item이 포함된 상태가 고정되므로 이 접근 방식은 유지하기 어렵고 오류가 발생할 수 있습니다. 대신 다음과 같이 뮤테이션을 작성해야 합니다:

// 좋은 방법
export default {
  [types.MARK_AS_CLOSED](state, itemId) {
    const item = state.items.find(x => x.id === itemId);

    if (!item) {
      return;
    }

    Vue.set(item, 'closed', true);
  },
};

이 접근 방식은 다음과 같은 이유로 더 나은 접근 방식입니다.

  • 뮤테이션에서 상태를 선택하고 업데이트하므로 유지보수하기가 더 쉽습니다.
  • 올바른 itemId가 전달되면 상태가 올바르게 업데이트됩니다.
  • 초기 상태와 결합되는 것을 피하기 위해 새 item을 생성하므로 반응성 문제가 없습니다.

이렇게 작성된 뮤테이션은 유지하기가 더 쉽습니다. 또한 반응성 시스템의 한계로 인한 오류를 피할 수 있습니다.

getters.js

가끔씩 스토어 상태를 기반으로 파생된 상태를 얻어야 할 수도 있는데, 특정 속성에 대해 필터링하는 것과 같이 그럴 때가 있습니다. 계산된 속성이 작동하는 방식으로 인해, getter를 사용하면 종속성에 따라 결과를 캐시합니다. 이는 다음과 같은 getters를 통해 수행됩니다:

// 모든 애완동물을 가진 사용자 가져오기
export const getUsersWithPets = (state, getters) => {
  return state.users.filter(user => user.pet !== undefined);
};

구성 요소에서 getter에 액세스하려면 mapGetters 도우미를 사용하세요:

import { mapGetters } from 'vuex';

{
  computed: {
    ...mapGetters([
      'getUsersWithPets',
    ]),
  },
};

mutation_types.js

Vuex 뮤테이션 문서에서:

여러 Flux 구현에서 변이 유형에 대한 상수를 사용하는 패턴을 자주 볼 수 있습니다. 이로써 린터와 같은 도구를 활용할 수 있게 하며, 모든 상수를 단일 파일에 두면 동료들이 전체 애플리케이션에서 가능한 변이 항목을 한눈에 볼 수 있습니다.

export const ADD_USER = 'ADD_USER';

스토어 상태 초기화

액션을 사용하기 전에 Vuex 스토어에 초기 상태가 필요한 것이 일반적입니다. 종종 API 엔드포인트, 문서 URL 또는 ID와 같은 데이터가 포함될 수 있습니다.

이 초기 상태를 설정하려면 Vue 구성요소를 마운트할 때 스토어 생성 함수에 매개변수로 전달하면 됩니다:

// Vue 앱의 초기화 스크립트에서 (예: mount_show.js)

import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { createStore } from './stores';
import AwesomeVueApp from './components/awesome_vue_app.vue'

Vue.use(Vuex);

export default () => {
  const el = document.getElementById('js-awesome-vue-app');

  return new Vue({
    el,
    name: 'AwesomeVueRoot',
    store: createStore(el.dataset),
    render: h => h(AwesomeVueApp)
  });
};

스토어 함수는 이 초기 데이터를 상태 생성 함수로 전달할 수 있습니다:

// store/index.js

import * as actions from './actions';
import mutations from './mutations';
import createState from './state';

export default initialState => ({
  actions,
  mutations,
  state: createState(initialState),
});

그리고 상태 함수는 이 초기 데이터를 매개변수로 허용하고 반환되는 state 객체에 이를 삽입할 수 있습니다:

// store/state.js

export default ({
  projectId,
  documentationPath,
  anOptionalProperty = true
}) => ({
  projectId,
  documentationPath,
  anOptionalProperty,

  // 여기에 다른 상태 속성
});

초기 상태를 그대로 전파하는 것이 왜 아닐까요?

좀 더 생각하는 독자는 위에서 몇 줄의 코드를 줄일 수 있는 기회를 볼 수 있습니다:

// 이렇게 하지 마세요!

export default initialState => ({
  ...initialState,

  // 여기에 다른 상태 속성
});

저희는 프론트엔드 코드베이스를 발견하고 검색하는 능력을 향상시키기 위해 이 패턴을 피하기로 하였습니다. Vue 앱에 데이터를 제공하는 것과 관련한 이유는 이 논의에서 설명되어 있습니다:

someStateKey가 스토어 상태에서 사용된다고 가정해 봅시다. el.dataset에서 가져온 경우 직접 해당 데이터를 찾지 못할 수 있습니다. 대신에 some_state_key를 검색해야 합니다. 역으로, Rails 템플릿에서 사용하는지 궁금할 수도 있지만, 당신은 someStateKey를 위해 검색해야 합니다.

스토어와 통신

<script>
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';

export default {
  computed: {
    ...mapGetters([
      'getUsersWithPets'
    ]),
    ...mapState([
      'isLoading',
      'users',
      'error',
    ]),
  },
  methods: {
    ...mapActions([
      'fetchUsers',
      'addUser',
    ]),
    onClickAddUser(data) {
      this.addUser(data);
    }
  },
  created() {
    this.fetchUsers()
  }
}
</script>
<template>
  <ul>
    <li v-if="isLoading">
      로딩 ...
    </li>
    <li v-else-if="error">
      {{ error }}
    </li>
    <template v-else>
      <li
        v-for="user in users"
        :key="user.id"
      >
        {{ user }}
      </li>
    </template>
  </ul>
</template>

Vuex 테스트

Vuex 테스트 관련 사항

Actions, Getters 및 Mutations에 대한 테스트에 대한 Vuex 문서를 참조하세요.

스토어가 필요한 컴포넌트 테스트

작은 컴포넌트는 데이터에 액세스하기 위해 store 속성을 사용할 수 있습니다. 따라서 해당 컴포넌트의 유닛 테스트를 작성하려면 스토어를 포함하고 올바른 상태를 제공해야 합니다:

//component_spec.js
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import { createStore } from './store';
import Component from './component.vue'

Vue.use(Vuex);

describe('component', () => {
  let store;
  let wrapper;

  const createComponent = () => {
    store = createStore();

    wrapper = mount(Component, {
      store,
    });
  };

  beforeEach(() => {
    createComponent();
  });

  it('should show a user', async () => {
    const user = {
      name: 'Foo',
      age: '30',
    };

    // 스토어에 데이터 채우기
    await store.dispatch('addUser', user);

    expect(wrapper.text()).toContain(user.name);
  });
});

일부 테스트 파일에서는 여전히 @vue/test-utils에서 Deprecated createLocalVue 함수를 사용할 수 있습니다. 이는 불필요하며 가능한 경우 피하거나 제거해야 합니다.

양방향 데이터 바인딩

폼 데이터를 Vuex에 저장할 때, 저장된 값을 업데이트해야 할 때가 있습니다. 스토어를 직접 변경해서는 안 되며, 대신 액션을 사용해야 합니다. 우리 코드에서 v-model을 사용하려면, 다음과 같이 계산된 속성을 만들어야 합니다:

export default {
  computed: {
    someValue: {
      get() {
        return this.$store.state.someValue;
      },
      set(value) {
        this.$store.dispatch("setSomeValue", value);
      }
    }
  }
};

다른 방법으로 mapStatemapActions을 사용할 수도 있습니다:

export default {
  computed: {
    ...mapState(['someValue']),
    localSomeValue: {
      get() {
        return this.someValue;
      },
      set(value) {
        this.setSomeValue(value)
      }
    }
  },
  methods: {
    ...mapActions(['setSomeValue'])
  }
};

이러한 속성을 몇 개 추가하면 복잡해지고, 코드가 반복되어 테스트할 내용이 많아집니다. 이를 간소화하기 위해 ~/vuex_shared/bindings.js에 도우미가 있습니다.

도우미는 다음과 같이 사용할 수 있습니다:

// 이 스토어는 기능이 없어 예시를 설명하기 위해 사용됩니다
export default {
  state: {
    baz: '',
    bar: '',
    foo: ''
  },
  actions: {
    updateBar() {...},
    updateAll() {...},
  },
  getters: {
    getFoo() {...},
  }
}
import { mapComputed } from '~/vuex_shared/bindings'
export default {
  computed: {
    ...mapComputed(
      [
        'baz',
        { key: 'bar', updateFn: 'updateBar' },
        { key: 'foo', getter: 'getFoo' },
      ],
      'updateAll',
    ),
  }
}

mapComputed은 데이터를 스토어에서 가져오고 업데이트될 때 올바른 액션을 디스패치하는 적절한 계산된 속성을 생성합니다.

키의 ‘root’가 한 수준 이상 깊은 경우, 해당 상태 객체를 검색하기 위해 함수를 사용할 수 있습니다.

예를 들어, 다음과 같은 스토어가 있다면:

// 이 스토어는 기능이 없어 예시를 설명하기 위해 사용됩니다
export default {
  state: {
    foo: {
      qux: {
        baz: '',
        bar: '',
        foo: '',
      },
    },
  },
  actions: {
    updateBar() {...},
    updateAll() {...},
  },
  getters: {
    getFoo() {...},
  }
}

‘root’는 다음과 같을 수 있습니다:

import { mapComputed } from '~/vuex_shared/bindings'
export default {
  computed: {
    ...mapComputed(
      [
        'baz',
        { key: 'bar', updateFn: 'updateBar' },
        { key: 'foo', getter: 'getFoo' },
      ],
      'updateAll',
      (state) => state.foo.qux,
    ),
  }
}