Vuex

Vuex은 더 이상 선호되는 상태 관리 경로로 간주되어서는 안 되며 현재 레거시 단계에 있습니다. 이는 기존 Vuex 스토어에 추가하는 것이 허용되지만 시간이 지남에 따라 스토어 크기를 줄이고 최종적으로 VueX를 완전히 이동(migrating away from VueX entirely)하는 것이 강력히 권장됨을 의미합니다. 어떠한 새로운 Vuex 스토어를 애플리케이션에 추가하기 전에, 먼저 그것을 추가할 Vue 애플리케이션이 Apollo를 사용하지 않는지 확인하십시오. VuexApollo는 절대로 필요하지 않은 경우를 제외하고는 결합해서는 안 됩니다. Apollo 기반 애플리케이션을 구축하는 방법에 대한 더 많은 지침은 GraphQL 문서를 참고하십시오.

이 페이지에 포함된 정보는 공식 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

액션은 애플리케이션에서 스토어로 데이터를 보내는 정보의 페이로드입니다.

액션은 일반적으로 타입페이로드로 구성되며, 발생한 일을 설명합니다. 변이(Mutations)와 달리, 액션은 비동기 작업을 포함할 수 있으므로 우리는 항상 액션에서 비동기 로직을 처리해야 합니다.

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

  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하는 것뿐입니다.

대부분의 변이는 commit을 사용하여 작업에서 호출됩니다. 비동기 작업이 없으면, mapMutations 헬퍼를 사용하여 컴포넌트에서 변이를 호출할 수 있습니다.

컴포넌트에서 변이를 커밋하는 예제에 대한 Vuex 문서를 참조하십시오.

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

요청이 발생하면 사용자에게 로딩 상태를 표시하고 싶어합니다.

로딩 상태를 토글하는 변이를 만드는 대신 다음을 수행해야 합니다:

  1. REQUEST_SOMETHING 유형의 변이를 만들어 로딩 상태를 토글합니다.
  2. RECEIVE_SOMETHING_SUCCESS 유형의 변이를 만들어 성공 콜백을 처리합니다.
  3. RECEIVE_SOMETHING_ERROR 유형의 변이를 만들어 오류 콜백을 처리합니다.
  4. 요청을 만들고 언급된 경우 변이를 커밋하는 fetchSomething 액션
    1. 응용 프로그램이 GET 요청 이외의 작업을 수행하는 경우 다음을 사용할 수 있습니다:
      • POST: createSomething
      • PUT: updateSomething
      • DELETE: deleteSomething

결과적으로, 구성 요소에서 fetchNamespace 액션을 디스패치하고 REQUEST_NAMESPACE, RECEIVE_NAMESPACE_SUCCESS, RECEIVE_NAMESPACE_ERROR 변이를 커밋하는 책임이 있습니다.

이전에는 fetchNamespace 액션에서 액션을 디스패치했지만 변이를 커밋하는 방식으로 변경되었으므로 이전 코드베이스의 구 버전에서 다른 패턴을 찾을 수 있습니다. 그러나 새로운 Vuex 저장소를 작성할 때마다 새로운 패턴을 활용하도록 권장합니다.

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

  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에서 참조된다는 점을 고려해야 합니다.

이러한 방식으로 작성된 변이는 유지 관리하기 어렵고 오류가 발생하기 쉬우므로 다음과 같이 변이를 작성해야 합니다.

// 좋음
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를 사용하여 수행할 수 있습니다:

// 반려동물이 있는 사용자 모두 가져오기
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 구현에서 변이 유형에 대한 상수 사용이 자주 있는 패턴입니다. > 이를 통해 상수를 린터(linter)와 같은 도구를 활용할 수 있으며 > 모든 상수를 단일 파일에 넣으면 동료들이 응용 프로그램 전체에서 가능한 변이를 한 눈에 확인할 수 있습니다.

export const ADD_USER = 'ADD_USER';

상태 저장소 초기화

보통 Vuex 저장소는 action을 사용하기 전에 초기 상태가 필요합니다. 종종 이에는 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 앱에 데이터를 제공할 때도 동일한 적용이 됩니다. 이에 대한 이유는 이 토론에서 설명되어 있습니다.

상태 저장소와의 통신

<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',
    };

    // store에 데이터 넣기
    await store.dispatch('addUser', user);

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

일부 테스트 파일은 여전히 @vue/test-utilslocalVue.use(Vuex)에서 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: {
    /**
     * @param {(string[]|Object[])} list - vuex 상태 키와 일치하는 문자열 목록 또는 목록 객체
     * @param {string} list[].key - vuex 상태에 있는 키와 일치하는 키
     * @param {string} list[].getter - 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: {
    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,
    ),
  }
}