Vuex

Vuex는 더 이상 선호되는 리포지터리 관리 경로로 간주되어서는 안 되며 현재 유산 단계에 있습니다. 이는 기존 Vuex 리포지터리에 추가하는 것이 허용되지만 리포지터리 크기를 시간이 지남에 따라 줄이고 최종적으로 VueX를 완전히 이동하는 것이 강력히 권장됨을 의미합니다. 응용 프로그램에 새 Vuex 리포지터리를 추가하기 전에 먼저 추가할 Vue 응용 프로그램이 Apollo를 사용하지 않는지 확인하십시오. VuexApollo를 절대적으로 필요하지 않은 경우에만 결합해야 합니다. 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 문서의 컴포넌트에서 뮤테이션 커밋하기를 참조하세요.

네이밍 패턴: 요청받음 네임스페이스

요청을 수행할 때 종종 사용자에게 로딩 상태를 표시하려고 합니다.

로딩 상태를 토글하는 뮤테이션을 만들지 않고 다음과 같이 해야합니다:

  1. REQUEST_SOMETHING 유형의 뮤테이션으로 로딩 상태를 토글합니다.
  2. RECEIVE_SOMETHING_SUCCESS 유형의 뮤테이션으로 성공적인 콜백을 처리합니다.
  3. RECEIVE_SOMETHING_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,
      }
    ]
});

다음과 같이 뮤테이션을 작성하는 것이 유지 관리하기가 더 쉽습니다:

// 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);
      }
    }
  }
};

또한 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 - 상수와 매치되는 문자열 디렉터리 또는 매치 객체 디렉터리
     * @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,
    ),
  }
}