Vue 3에서 공유 상태 관리하기

게시 됨: 2022-03-10
빠른 요약 ↬ 대규모 Vue 애플리케이션을 작성하는 것은 어려울 수 있습니다. Vue 3 애플리케이션에서 공유 상태를 사용하면 이러한 복잡성을 줄이는 솔루션이 될 수 있습니다. 상태를 해결하는 데에는 여러 가지 일반적인 솔루션이 있습니다. 이 기사에서는 팩토리, 공유 객체 및 Vuex 사용과 같은 접근 방식의 장단점에 대해 자세히 설명합니다. 또한 Vue 3에서 공유 상태를 사용하는 방식을 변경할 수 있는 Vuex 5의 기능도 보여드리겠습니다.

상태가 어려울 수 있습니다. 간단한 Vue 프로젝트를 시작할 때 특정 구성 요소에서 작업 상태를 유지하는 것이 간단할 수 있습니다.

 setup() { let books: Work[] = reactive([]); onMounted(async () => { // Call the API const response = await bookService.getScienceBooks(); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } }); return { books }; },

프로젝트가 데이터를 표시하는 단일 페이지(아마도 정렬 또는 필터링)인 경우 이는 매력적일 수 있습니다. 그러나 이 경우 이 구성 요소는 모든 요청에 ​​대해 데이터를 가져옵니다. 계속 유지하고 싶다면? 그것이 국가 관리가 작동하는 곳입니다. 네트워크 연결은 종종 비용이 많이 들고 때때로 신뢰할 수 없기 때문에 응용 프로그램을 탐색할 때 이 상태를 유지하는 것이 좋습니다.

또 다른 문제는 구성 요소 간의 통신입니다. 이벤트와 소품을 사용하여 직접적인 자식-부모와 통신할 수 있지만 각 보기/페이지가 독립적인 경우 오류 처리 및 사용 중 플래그와 같은 간단한 상황을 처리하는 것이 어려울 수 있습니다. 예를 들어 오류 및 로드 애니메이션을 표시하도록 최상위 컨트롤이 연결되어 있다고 가정해 보겠습니다.

 // App.vue <template> <div class="container mx-auto bg-gray-100 p-1"> <router-link to="/"><h1>Bookcase</h1></router-link> <div class="alert" v-if="error">{{ error }}</div> <div class="alert bg-gray-200 text-gray-900" v-if="isBusy"> Loading... </div> <router-view :key="$route.fullPath"></router-view> </div> </template>

이 상태를 처리하는 효과적인 방법이 없으면 발행/구독 시스템을 제안할 수 있지만 실제로 많은 경우 데이터 공유가 더 간단합니다. 공유 상태를 원하면 어떻게 해야 합니까? 이를 수행하는 몇 가지 일반적인 방법을 살펴보겠습니다.

참고 : 이 섹션의 코드는 GitHub에 있는 예제 프로젝트의 "메인" 분기에서 찾을 수 있습니다.

Vue 3의 공유 상태

Vue 3로 이동한 이후로 나는 완전히 Composition API를 사용하도록 마이그레이션했습니다. 이 기사에서는 TypeScript도 사용하고 있지만 보여드리는 예제에서는 필수 사항이 아닙니다. 원하는 방식으로 상태를 공유할 수 있지만 가장 일반적으로 사용되는 패턴을 찾는 몇 가지 기술을 보여 드리겠습니다. 각각의 장단점이 있으므로 여기에서 내가 말하는 것을 도그마로 받아들이지 마십시오.

기술에는 다음이 포함됩니다.

  • 공장,
  • 공유 싱글톤,
  • 뷰 4,
  • 뷰엑스 5.

참고 : Vuex 5는 이 기사를 작성하는 시점에서 RFC(Request for Comments) 단계에 있으므로 Vuex가 어디로 갈지 준비하고 싶지만 현재 이 옵션의 작동 버전이 없습니다.

파헤쳐보자…

공장

참고 : 이 섹션의 코드는 GitHub에 있는 예제 프로젝트의 "Factories" 분기에 있습니다.

팩토리 패턴은 관심 있는 상태의 인스턴스를 생성하는 것입니다. 이 패턴에서는 합성 API의 시작 함수와 매우 유사한 함수를 반환합니다. 범위를 만들고 찾고 있는 구성 요소를 빌드합니다. 예를 들어:

 export default function () { const books: Work[] = reactive([]); async function loadBooks(val: string) { const response = await bookService.getBooks(val, currentPage.value); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } } return { loadBooks, books }; }

다음과 같이 필요한 팩토리 생성 객체의 일부만 요청할 수 있습니다.

 // In Home.vue const { books, loadBooks } = BookFactory();

네트워크 요청이 발생할 때 표시할 isBusy 플래그를 추가하면 위의 코드는 변경되지 않지만 isBusy 를 표시할 위치를 결정할 수 있습니다.

 export default function () { const books: Work[] = reactive([]); const isBusy = ref(false); async function loadBooks(val: string) { isBusy.value = true; const response = await bookService.getBooks(val, currentPage.value); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } } return { loadBooks, books, isBusy }; }

다른 보기(vue?)에서는 나머지 공장이 어떻게 작동하는지 알 필요 없이 isBusy 플래그만 요청할 수 있습니다.

 // App.vue export default defineComponent({ setup() { const { isBusy } = BookFactory(); return { isBusy } }, })

그러나 문제를 발견했을 수 있습니다. 팩토리를 호출할 때마다 모든 객체의 새 인스턴스를 얻습니다. 공장에서 새 인스턴스를 반환하도록 하려는 경우가 있지만 우리의 경우 상태 공유에 대해 이야기하고 있으므로 생성을 공장 외부로 이동해야 합니다.

 const books: Work[] = reactive([]); const isBusy = ref(false); async function loadBooks(val: string) { isBusy.value = true; const response = await bookService.getBooks(val, currentPage.value); if (response.status === 200) { books.splice(0, books.length, ...response.data.works); } } export default function () { return { loadBooks, books, isBusy }; }

이제 팩토리는 공유 인스턴스를 제공하거나 원하는 경우 싱글톤을 제공합니다. 이 패턴이 작동하는 동안 매번 새 인스턴스를 생성하지 않는 함수를 반환하는 것은 혼란스러울 수 있습니다.

기본 객체가 const 로 표시되어 있기 때문에 이를 대체할 수 없어야 합니다(또한 싱글톤 특성을 깨뜨림). 따라서 이 코드는 다음과 같이 불평해야 합니다.

 // In Home.vue const { books, loadBooks } = BookFactory(); books = []; // Error, books is defined as const

따라서 변경 가능한 상태가 업데이트될 수 있는지 확인하는 것이 중요할 수 있습니다(예: 책을 할당하는 대신 books.splice() 사용).

이를 처리하는 또 다른 방법은 공유 인스턴스를 사용하는 것입니다.

점프 후 더! 아래에서 계속 읽기 ↓

공유 인스턴스

이 섹션의 코드는 GitHub에 있는 예제 프로젝트의 "SharedState" 분기에 있습니다.

상태를 공유하려는 경우 상태가 싱글톤이라는 사실을 분명히 하는 것이 좋습니다. 이 경우 정적 개체로 가져올 수 있습니다. 예를 들어 반응 객체로 가져올 수 있는 객체를 만들고 싶습니다.

 export default reactive({ books: new Array<Work>(), isBusy: false, async loadBooks() { this.isBusy = true; const response = await bookService.getBooks(this.currentTopic, this.currentPage); if (response.status === 200) { this.books.splice(0, this.books.length, ...response.data.works); } this.isBusy = false; } });

이 경우 객체를 가져오기만 하면 됩니다(이 예제에서는 저장소라고 함).

 // Home.vue import state from "@/state"; export default defineComponent({ setup() { // ... onMounted(async () => { if (state.books.length === 0) state.loadBooks(); }); return { state, bookTopics, }; }, });

그러면 상태에 바인딩하기 쉬워집니다.

 <!-- Home.vue --> <div class="grid grid-cols-4"> <div v-for="book in state.books" :key="book.key" class="border bg-white border-grey-500 m-1 p-1" > <router-link :to="{ name: 'book', params: { id: book.key } }"> <BookInfo :book="book" /> </router-link> </div>

다른 패턴과 마찬가지로 뷰 간에 이 인스턴스를 공유할 수 있다는 이점이 있습니다.

 // App.vue import state from "@/state"; export default defineComponent({ setup() { return { state }; }, })

그런 다음 이것은 동일한 객체( Home.vue 의 부모 또는 라우터의 다른 페이지인지 여부)에 바인딩할 수 있습니다.

 <!-- App.vue --> <div class="container mx-auto bg-gray-100 p-1"> <router-link to="/"><h1>Bookcase</h1></router-link> <div class="alert bg-gray-200 text-gray-900" v-if="state.isBusy">Loading...</div> <router-view :key="$route.fullPath"></router-view> </div>

팩토리 패턴을 사용하든 공유 인스턴스를 사용하든 둘 다 공통적인 문제가 있습니다. 바로 가변 상태입니다. 원하지 않는 바인딩이나 코드 변경 상태의 우발적인 부작용이 있을 수 있습니다. 여기에서 사용하는 것과 같은 간단한 예에서는 걱정할 만큼 복잡하지 않습니다. 그러나 점점 더 큰 앱을 빌드하면서 상태 변형에 대해 더 신중하게 생각하고 싶을 것입니다. Vuex가 구출에 올 ​​수 있는 곳입니다.

뷰엑스 4

이 섹션의 코드는 GitHub에 있는 예제 프로젝트의 "Vuex4" 분기에 있습니다.

Vuex는 Vue의 상태 관리자입니다. 별도의 프로젝트로 관리되지만 핵심 팀에서 구축했습니다. Vuex의 목적은 상태를 상태에 대해 수행하려는 작업과 분리하는 것입니다. 상태의 모든 변경은 Vuex를 거쳐야 하므로 더 복잡하지만 우발적인 상태 변경으로부터 보호할 수 있습니다.

Vuex의 아이디어는 상태 관리의 예측 가능한 흐름을 제공하는 것입니다. View는 Action으로 흐른 다음 Mutations를 사용하여 State를 변경하고 View를 업데이트합니다. 상태 변경의 흐름을 제한함으로써 애플리케이션의 상태를 변경하는 부작용을 줄여야 합니다. 따라서 더 큰 응용 프로그램을 더 쉽게 구축할 수 있습니다. Vuex에는 학습 곡선이 있지만 그 복잡성으로 예측 가능성을 얻을 수 있습니다.

또한 Vuex는 시간 여행이라는 기능을 포함하여 상태 관리와 함께 작동하는 개발 시간 도구(Vue 도구를 통해)를 지원합니다. 이를 통해 상태 기록을 보고 앞뒤로 이동하여 애플리케이션에 미치는 영향을 확인할 수 있습니다.

Vuex도 중요할 때가 있습니다.

Vue 3 프로젝트에 추가하려면 프로젝트에 패키지를 추가할 수 있습니다.

 > npm i vuex

또는 Vue CLI를 사용하여 추가할 수 있습니다.

 > vue add vuex

CLI를 사용하면 Vuex 스토어의 시작점이 생성됩니다. 그렇지 않으면 프로젝트에 수동으로 연결해야 합니다. 이것이 어떻게 작동하는지 살펴보겠습니다.

먼저 Vuex의 createStore 함수로 생성된 상태 객체가 필요합니다.

 import { createStore } from 'vuex' export default createStore({ state: {}, mutations: {}, actions: {}, getters: {} });

보시다시피 상점에는 여러 속성을 정의해야 합니다. 상태는 애플리케이션에 액세스 권한을 부여하려는 데이터의 목록일 뿐입니다.

 import { createStore } from 'vuex' export default createStore({ state: { books: [], isBusy: false }, mutations: {}, actions: {} });

상태는 ref 또는 반응 래퍼를 사용해서는 안 됩니다. 이 데이터는 공유 인스턴스 또는 팩토리에서 사용한 것과 동일한 종류의 공유 ​​데이터입니다. 이 저장소는 응용 프로그램의 단일 항목이므로 상태의 데이터도 공유됩니다.

다음으로 행동을 살펴보자. 작업은 상태와 관련된 활성화하려는 작업입니다. 예를 들어:

 actions: { async loadBooks(store) { const response = await bookService.getBooks(store.state.currentTopic, if (response.status === 200) { // ... } } },

상태 및 기타 작업을 가져올 수 있도록 작업이 저장소의 인스턴스로 전달됩니다. 일반적으로 필요한 부분만 분해합니다.

 actions: { async loadBooks({ state }) { const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { // ... } } },

마지막 부분은 돌연변이입니다. 돌연변이는 상태를 변경할 수 있는 함수입니다. 돌연변이만이 상태에 영향을 줄 수 있습니다. 따라서 이 예에서는 상태를 변경하는 돌연변이가 필요합니다.

 mutations: { setBusy: (state) => state.isBusy = true, clearBusy: (state) => state.isBusy = false, setBooks(state, books) { state.books.splice(0, state.books.length, ...books); } },

돌연변이 함수는 항상 상태 객체를 전달하므로 해당 상태를 변경할 수 있습니다. 처음 두 가지 예에서 명시적으로 상태를 설정하고 있음을 알 수 있습니다. 그러나 세 번째 예에서는 설정할 상태를 전달합니다. 돌연변이는 항상 두 개의 매개변수, 즉 상태와 돌연변이를 호출할 때의 인수를 취합니다.

돌연변이를 호출하려면 저장소에서 커밋 기능을 사용합니다. 우리의 경우, 나는 그것을 구조화에 추가할 것입니다:

 actions: { async loadBooks({ state, commit }) { commit("setBusy"); const response = await bookService.getBooks(state.currentTopic, if (response.status === 200) { commit("setBooks", response.data); } commit("clearBusy"); } },

여기에서 볼 수 있는 것은 커밋 에 작업 이름이 필요한 방식입니다. 이것을 단순히 마술 문자열을 사용하지 않도록 하는 트릭이 있지만 지금은 건너뛸 것입니다. 이러한 매직 스트링의 사용은 Vuex 사용의 한계 중 하나입니다.

커밋을 사용하는 것이 불필요한 래퍼처럼 보일 수 있지만 Vuex는 돌연변이 내부를 제외하고 상태를 변경하도록 허용하지 않으므로 커밋 을 통한 호출만 가능하다는 것을 기억하십시오.

또한 setBooks 에 대한 호출이 두 번째 인수를 취하는 것을 볼 수 있습니다. 이것은 돌연변이를 호출하는 두 번째 인수입니다. 더 많은 정보가 필요한 경우 단일 인수로 압축해야 합니다(현재 Vuex의 또 다른 제한 사항). 책 목록에 책을 삽입해야 한다고 가정하면 다음과 같이 호출할 수 있습니다.

 commit("insertBook", { book, place: 4 }); // object, tuple, etc.

그런 다음 필요한 조각으로 분해할 수 있습니다.

 mutations: { insertBook(state, { book, place }) => // ... }

이거 우아한데? 실제로는 아니지만 작동합니다.

이제 돌연변이 작업을 수행했으므로 코드에서 Vuex 저장소를 사용할 수 있어야 합니다. 실제로 매장에 가는 방법은 두 가지입니다. 먼저, 애플리케이션(예: main.ts/js)에 스토어를 등록하면 애플리케이션의 모든 위치에 액세스할 수 있는 중앙 집중식 스토어에 액세스할 수 있습니다.

 // main.ts import store from './store' createApp(App) .use(store) .use(router) .mount('#app')

이것은 Vuex를 추가하는 것이 아니라 만들고 있는 실제 저장소입니다. 이것이 추가되면 useStore 를 호출하여 store 객체를 얻을 수 있습니다:

 import { useStore } from "vuex"; export default defineComponent({ components: { BookInfo, }, setup() { const store = useStore(); const books = computed(() => store.state.books); // ...

이것은 잘 작동하지만 스토어를 직접 가져오는 것을 선호합니다.

 import store from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const books = computed(() => store.state.books); // ...

이제 상점 개체에 액세스할 수 있으므로 이를 어떻게 사용합니까? 상태의 경우 변경 사항이 바인딩에 전파되도록 계산된 함수로 래핑해야 합니다.

 export default defineComponent({ setup() { const books = computed(() => store.state.books); return { books }; }, });

작업을 호출하려면 디스패치 메서드를 호출해야 합니다.

 export default defineComponent({ setup() { const books = computed(() => store.state.books); onMounted(async () => await store.dispatch("loadBooks")); return { books }; }, });

작업에는 메서드 이름 뒤에 추가하는 매개 변수가 있을 수 있습니다. 마지막으로 상태를 변경하려면 Actions에서 했던 것처럼 commit을 호출해야 합니다. 예를 들어 저장소에 페이징 속성이 있고 commit 을 사용하여 상태를 변경할 수 있습니다.

 const incrementPage = () => store.commit("setPage", store.state.currentPage + 1); const decrementPage = () => store.commit("setPage", store.state.currentPage - 1);

다음과 같이 호출하면 오류가 발생합니다(상태를 수동으로 변경할 수 없기 때문에).

 const incrementPage = () => store.state.currentPage++; const decrementPage = () => store.state.currentPage--;

이것이 여기에서 진정한 힘입니다. 우리는 상태가 변경되는 위치를 제어하고 개발 라인에서 더 나아가 오류를 생성하는 부작용이 없도록 하고 싶습니다.

Vuex에서 움직이는 부분의 수에 압도될 수 있지만 더 크고 복잡한 프로젝트에서 상태를 관리하는 데 실제로 도움이 될 수 있습니다. 모든 경우에 필요하다고 말하지는 않겠지만 전반적으로 도움이 되는 대규모 프로젝트가 있을 것입니다.

Vuex 4의 가장 큰 문제는 TypeScript 프로젝트에서 Vuex 4로 작업할 때 많은 것이 필요하다는 것입니다. 개발 및 빌드에 도움이 되는 TypeScript 유형을 확실히 만들 수 있지만 많은 움직이는 부분이 필요합니다.

Vuex 5는 Vuex가 TypeScript(및 일반적으로 JavaScript 프로젝트)에서 작동하는 방식을 단순화하기 위한 것입니다. 다음에 출시되면 어떻게 작동하는지 봅시다.

뷰엑스 5

참고 : 이 섹션의 코드는 GitHub에 있는 예제 프로젝트의 "Vuex5" 분기에 있습니다.

이 기사의 시점에서 Vuex 5는 실제가 아닙니다. RFC(의견 요청)입니다. 계획이야. 토론의 출발점입니다. 따라서 여기서 설명할 내용 중 많은 부분이 다소 변경될 수 있습니다. 그러나 Vuex의 변경 사항에 대비하기 위해 이 변경 사항이 어디로 가는지 보여주고 싶었습니다. 이 때문에 이 예제와 연결된 코드는 빌드되지 않습니다.

Vuex가 작동하는 방식에 대한 기본 개념은 처음 시작된 이후로 다소 변경되지 않았습니다. Vue 3의 도입과 함께 Vuex 4는 대부분 Vuex가 새로운 프로젝트에서 작업할 수 있도록 만들어졌습니다. 하지만 팀은 Vuex의 실제 문제점을 살펴보고 해결하려고 합니다. 이를 위해 다음과 같은 몇 가지 중요한 변경을 계획하고 있습니다.

  • 더 이상 변이는 없습니다: 행동은 상태(그리고 아마도 누구라도)를 변이시킬 수 있습니다.
  • 더 나은 TypeScript 지원.
  • 더 나은 다중 저장소 기능.

어떻게 작동할까요? 스토어 생성부터 시작하겠습니다.

 export default createStore({ key: 'bookStore', state: () => ({ isBusy: false, books: new Array<Work>() }), actions: { async loadBooks() { try { this.isBusy = true; const response = await bookService.getBooks(); if (response.status === 200) { this.books = response.data.works; } } finally { this.isBusy = false; } } }, getters: { findBook(key: string): Work | undefined { return this.books.find(b => b.key === key); } } });

첫 번째 변경 사항은 이제 모든 상점에 자체 키가 필요하다는 것입니다. 이는 여러 상점을 검색할 수 있도록 하기 위한 것입니다. 다음으로 상태 객체가 이제 팩토리임을 알 수 있습니다(예: 구문 분석 시 생성되지 않은 함수에서 반환). 그리고 더 이상 돌연변이 섹션이 없습니다. 마지막으로 액션 내에서 this 포인터의 속성으로 상태에 액세스하는 것을 볼 수 있습니다. 더 이상 상태를 전달하고 작업을 수행할 필요가 없습니다. 이것은 개발을 단순화하는 데 도움이 될 뿐만 아니라 TypeScript의 유형을 더 쉽게 유추할 수 있습니다.

Vuex를 애플리케이션에 등록하려면 글로벌 스토어 대신 Vuex를 등록합니다.

 import { createVuex } from 'vuex' createApp(App) .use(createVuex()) .use(router) .mount('#app')

마지막으로 스토어를 사용하려면 스토어를 가져온 다음 인스턴스를 만듭니다.

 import bookStore from "@/store"; export default defineComponent({ components: { BookInfo, }, setup() { const store = bookStore(); // Generate the wrapper // ...

상점에서 반환되는 것은 공장을 몇 번이나 호출하든 상관없이 상점의 이 인스턴스를 반환하는 공장 객체입니다. 반환된 객체는 일급 시민(유형 정보 포함)으로 작업, 상태 및 getter가 있는 객체입니다.

 onMounted(async () => await store.loadBooks()); const incrementPage = () => store.currentPage++; const decrementPage = () => store.currentPage--;

여기서 볼 수 있는 것은 상태(예: currentPage )가 단순한 속성이라는 것입니다. 그리고 작업(예: loadBooks )은 단지 함수입니다. 여기 가게를 이용하고 있다는 사실이 부작용이다. Vuex 객체를 객체로 취급하고 작업을 계속할 수 있습니다. 이는 API의 상당한 개선입니다.

주목해야 할 또 다른 변경 사항은 Composition API와 유사한 구문을 사용하여 상점을 생성할 수도 있다는 것입니다.

 export default defineStore("another", () => { // State const isBusy = ref(false); const books = reactive(new Array≷Work>()); // Actions async function loadBooks() { try { this.isBusy = true; const response = await bookService.getBooks(this.currentTopic, this.currentPage); if (response.status === 200) { this.books = response.data.works; } } finally { this.isBusy = false; } } findBook(key: string): Work | undefined { return this.books.find(b => b.key === key); } // Getters const bookCount = computed(() => this.books.length); return { isBusy, books, loadBooks, findBook, bookCount } });

이를 통해 Composition API를 사용하여 보기를 수행하는 것처럼 Vuex 개체를 빌드할 수 있으며 틀림없이 더 간단합니다.

이 새로운 디자인의 한 가지 주요 단점은 상태의 비가변성을 잃게 된다는 것입니다. 이것을 활성화할 수 있는지에 대한 논의가 진행 중이지만(Vuex 4와 마찬가지로 개발 전용) 이것이 얼마나 중요한지에 대한 합의는 없습니다. 저는 개인적으로 이것이 Vuex의 주요 이점이라고 생각하지만 이것이 어떻게 작동하는지 봐야 합니다.

우리를 어디?

단일 페이지 애플리케이션에서 공유 상태를 관리하는 것은 대부분의 앱 개발에서 중요한 부분입니다. Vue에서 어떻게 진행할 것인지에 대한 게임 계획을 갖는 것은 솔루션을 설계하는 데 있어 중요한 단계입니다. 이 기사에서 Vuex 5에 대한 내용을 포함하여 공유 상태를 관리하기 위한 몇 가지 패턴을 보여 주었습니다. 이제 자신의 프로젝트에 대해 올바른 결정을 내릴 수 있는 지식을 갖게 되었기를 바랍니다.