Vuex
Vuex는 더 이상 선호되는 리포지터리 관리 경로로 고려되지 않아 현재 유산 단계에 있습니다. 이는 기존 Vuex
리포지터리에 추가하는 것이 허용되지만, 시간이 지날수록 리포지터리 크기를 줄이고 드디어 VueX를 완전히 이동하는 것을 강력히 권장하는 것을 의미합니다. 애플리케이션에 새로운 Vuex
리포지터리를 추가하기 전에, 먼저 계획 중인 Vue
애플리케이션이 Apollo를 사용하지 않는지 확인하십시오. Vuex
와 Apollo
는 절대적으로 필요하지 않는 이상 결합해서는 안 됩니다. Apollo
기반 애플리케이션을 구축하는 방법에 대한 추가 지침은 GraphQL 문서를 참고해주세요.
이 페이지에 포함된 정보는 공식 Vuex 문서에서 보다 자세히 설명되어 있습니다.
관심사의 분리
Vuex는 상태, 게터, 뮤테이션, 액션, 및 모듈로 구성됩니다.
사용자가 작업을 선택하면 dispatch
해야 합니다. 이 작업은 상태를 변경하는 뮤테이션을 commit
합니다. 작업 자체는 상태를 업데이트하지 않습니다. 상태를 업데이트해야 하는 것은 뮤테이션뿐입니다.
파일 구조
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
액션은 애플리케이션에서 리포지터리로 데이터를 보내는 정보의 페이로드입니다.
액션은 보통 type
과 payload
로 구성되며, 발생한 일을 설명합니다. 뮤테이션과 달리, 액션에는 비동기 작업을 포함할 수 있으므로 항상 액션에서 비동기 논리를 처리해야 합니다.
이 파일에서는 사용자 디렉터리을 처리하기 위해 뮤테이션을 호출하는 액션을 작성합니다:
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: '오류가 발생했습니다' })
});
}
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 문서의 컴포넌트에서 뮤테이션 커밋하기를 참조하세요.
네이밍 패턴: REQUEST
및 RECEIVE
네임스페이스
요청을 보내면 사용자에게 로딩 상태를 표시하고 싶을 때가 많습니다.
로딩 상태를 토글하기 위해 뮤테이션을 만들지 말고, 다음을 해야 합니다:
- 로딩 상태를 토글하는
REQUEST_SOMETHING
유형의 뮤테이션 - 성공 콜백을 처리하는
RECEIVE_SOMETHING_SUCCESS
유형의 뮤테이션 - 오류 콜백을 처리하는
RECEIVE_SOMETHING_ERROR
유형의 뮤테이션 - 요청을 만들고 언급된 경우 뮤테이션을 커밋하는
fetchSomething
액션- 애플리케이션이
GET
요청 이외의 작업을 수행하는 경우 다음을 사용할 수 있습니다:-
POST
:createSomething
-
PUT
:updateSomething
-
DELETE
:deleteSomething
-
- 애플리케이션이
이러한 패턴을 따르면 다음이 보장됩니다:
- 모든 애플리케이션이 동일한 패턴을 따르므로 누구나 코드를 유지보수하기 쉬워집니다.
- 애플리케이션의 모든 데이터가 동일한 수명주기 패턴을 따릅니다.
- 유닛 테스트가 더 쉬워집니다.
복잡한 상태 업데이트
가끔, 특히 상태가 복잡할 때는 돌연변이가 업데이트해야 하는 내용을 정확히 탐색하기가 정말 어렵습니다.
이상적으로는 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
속성은 반응적이지 않을 것입니다.
- 새로운
-
item
이items
에 의해 참조된다는 것을 기억해야 합니다.
이러한 돌연변이는 유지 관리하기 어렵고 오류가 발생할 가능성이 더 많습니다. 대신 다음과 같이 돌연변이를 작성해야 합니다:
// 좋음
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
가끔 상태에 기반한 파생 상태를 얻어야 할 수도 있습니다. 특정 속성을 필터링하는 것과 같은 작업을 위해 게터를 사용할 수도 있습니다. 이는 computed props 작동 방식에 따라 결과를 캐싱합니다.
// 반려동물이 있는 사용자 모두 가져오기
export const getUsersWithPets = (state, getters) => {
return state.users.filter(user => user.pet !== undefined);
};
컴포넌트에서 게터에 액세스하려면 mapGetters
도우미를 사용하세요:
import { mapGetters } from 'vuex';
{
computed: {
...mapGetters([
'getUsersWithPets',
]),
},
};
mutation_types.js
Vuex mutations 설명서에 따르면: > 다양한 Flux 구현에서 돌연변이 유형에 대한 상수 사용이 흔히 볼 수 있는 패턴입니다. 이를 통해 상수를 단일 파일에 모아 도구(린터와 같은)를 활용할 수 있으며, 협업자들이 전체 응용 프로그램에서 가능한 돌연변이를 한눈에 볼 수 있습니다.
export const ADD_USER = 'ADD_USER';
상태 리포지터리의 초기화
action
을 사용하기 전에 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)
});
};
그리고 상태 함수는 이 초기 데이터를 매개변수로 받아들여 상태의 생성 함수에 이 데이터를 전달할 수 있습니다:
// in 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
객체에 이를 통합할 수 있습니다:
// in store/state.js
export default ({
projectId,
documentationPath,
anOptionalProperty = true
}) => ({
projectId,
documentationPath,
anOptionalProperty,
// 여기에 다른 상태 속성
});
왜 단순히 …초기 상태를 퍼뜨리지 않을까요?
안목이 높은 독자는 위의 예에서 몇 줄의 코드를 줄일 수 있다는 기회를 볼 수 있습니다.
// 이렇게는 하지 마세요!
export default initialState => ({
...initialState,
// 여기에 다른 상태 속성
});
우리는 frontend 코드베이스를 검색하고 발견하기 위한 능력을 향상시키기 위해 이러한 패턴을 피하기로 결정했습니다. Vue 앱에 데이터 제공할 때도 마찬가지입니다. 이에 대한 이유는 이 논의에서 설명되어 있습니다:
someStateKey
가 상태에 사용된다 가정해 보세요.el.dataset
에서만 제공되었다면 직접적으로 그것을 찾을 수 없을 수 있습니다. 대신some_state_key
를 위해 grep해야 합니다. 역도 마찬가지입니다.some_state_key
를 사용하는 것은 어디에서 찾아야 할지 궁금해할 수 있지만, 실제로someStateKey
를 위해 grep해야 할 수 있습니다.
상태와의 통신
<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">
Loading...
</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 테스트 관련 사항
VueX 문서를 참조하여 Actions, Getters 및 Mutations을 테스트합니다.
스토어가 필요한 컴포넌트 테스트
작은 컴포넌트는 데이터에 액세스하기 위해 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',
};
// store에 데이터 채우기
await store.dispatch('addUser', user);
expect(wrapper.text()).toContain(user.name);
});
});
일부 테스트 파일은 여전히 @vue/test-utils
및 localVue.use(Vuex)
에서 deprecated createLocalVue
함수를 사용할 수 있습니다. 이는 불필요하며 가능한 경우 피해야 하거나 제거해야 합니다.
양방향 데이터 바인딩
Vuex에 양식 데이터를 저장할 때 저장된 값 업데이트가 때로는 필요합니다. 스토어를 직접 변경해서는 안 되며, 대신 액션을 사용해야 합니다.
우리의 코드에서 v-model
을 사용하려면 다음과 같이 계산된 속성을 만들어야 합니다.
export default {
computed: {
someValue: {
get() {
return this.$store.state.someValue;
},
set(value) {
this.$store.dispatch("setSomeValue", value);
}
}
}
};
대안으로 mapState
와 mapActions
을 사용할 수도 있습니다:
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: {
/**
* @param {(string[]|Object[])} list - 상태 키에 대응하는 문자열 디렉터리 또는 디렉터리 객체
* @param {string} list[].key - vuex state에 있는 키와 일치하는 키
* @param {string} list[].getter - getter의 이름, 사용하지 않으려면 비워두세요
* @param {string} list[].updateFn - 액션의 이름, 기본 액션을 사용하려면 비워두세요
* @param {string} defaultUpdateFn - 디스패치할 기본 함수
* @param {string|function} root - (선택사항) 키 값이 포함된 상태를 찾을 state의 키
* @returns {Object} 생성된 모든 계산된 속성을 담은 사전
*/
...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,
),
}
}