Vuex
Vuex는 더 이상 선호되는 리포지터리 관리 경로로 간주되어서는 안 되며 현재 유산 단계에 있습니다. 이는 기존 Vuex
리포지터리에 추가하는 것이 허용되지만 리포지터리 크기를 시간이 지남에 따라 줄이고 최종적으로 VueX를 완전히 이동하는 것이 강력히 권장됨을 의미합니다. 응용 프로그램에 새 Vuex
리포지터리를 추가하기 전에 먼저 추가할 Vue
응용 프로그램이 Apollo를 사용하지 않는지 확인하십시오. Vuex
와 Apollo
를 절대적으로 필요하지 않은 경우에만 결합해야 합니다. Apollo
기반 응용 프로그램을 구축하는 방법에 대한 자세한 지침은 GraphQL 문서를 참고하세요.
이 페이지에 포함된 정보는 공식 Vuex 문서에서 자세히 설명되어 있습니다.
관심 분리
Vuex는 상태(State), 게터(Getters), 뮤테이션(Mutations), 액션(Actions) 및 모듈로 구성됩니다.
사용자가 액션을 선택하면 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
액션은 응용 프로그램에서 리포지터리로 데이터를 보내는 정보의 페이로드입니다.
액션은 일반적으로 유형
및 페이로드
로 구성되며 발생한 일을 설명합니다. 뮤테이션과 달리 액션에는 비동기 작업이 포함될 수 있으므로 항상 액션에서 비동기 논리를 처리해야 합니다.
이 파일에서 사용자 디렉터리을 처리하는 뮤테이션을 호출하는 액션을 작성합니다:
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 문서의 컴포넌트에서 뮤테이션 커밋하기를 참조하세요.
네이밍 패턴: 요청
및 받음
네임스페이스
요청을 수행할 때 종종 사용자에게 로딩 상태를 표시하려고 합니다.
로딩 상태를 토글하는 뮤테이션을 만들지 않고 다음과 같이 해야합니다:
-
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,
}
]
});
다음과 같이 뮤테이션을 작성하는 것이 유지 관리하기가 더 쉽습니다:
// Good
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가 작동하는 방식 때문에 가능합니다. 이는 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';
리포지터리 상태 초기화
일반적으로 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)
});
};
그런 다음 리포지터리 함수는 이러한 데이터를 상태 생성 함수에 전달할 수 있습니다.
// 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,
// 이 외의 상태 속성들 여기에
});
우리는 프런트엔드 코드베이스를 검색하고 찾는 능력을 향상시키기 위해 이러한 패턴을 피하기로 결정했습니다. 이와 관련한 이유는 여기 논의에서 설명되어 있습니다.
리포지터리와 통신
<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 관련 테스트
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);
});
});
일부 테스트 파일은 여전히 중지된 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 - 뷰엑스 상태에 있는 키와 일치하는 키
* @param {string} list[].getter - 게터의 이름, 사용하지 않으려면 비워둡니다.
* @param {string} list[].updateFn - 액션의 이름, 기본 동작을 사용하려면 비워둡니다.
* @param {string} defaultUpdateFn - 전송하는 기본 함수
* @param {string|function} root - 키 상태로 상태 객체를 검색하는 데 사용, 생략 가능
* @returns {Object} 생성된 모든 계산된 속성들을 가진 사전
*/
...mapComputed(
[
'baz',
{ key: 'bar', updateFn: 'updateBar' },
{ key: 'foo', getter: 'getFoo' },
],
'updateAll',
),
}
}
mapComputed
는 매개변수에서 상태 값을 가져와 업데이트할 때 올바른 작업을 전송하는 적절한 계산된 속성들을 생성합니다.
키의 root
가 1단계 이상까지 더 깊다면 함수를 사용하여 관련 상태 객체를 검색할 수 있습니다.
예를 들어 다음과 같은 리포지터리가 있는 경우:
// 기능이 없는 리포지터리이며 예제 문맥만 제시하기 위해 사용됩니다.
export default {
state: {
foo: {
qux: {
baz: '',
bar: '',
foo: '',
},
},
actions: {
updateAll() {...},
updateBar() {...},
},
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,
),
}
}