Vuex에서의 마이그레이션
Vuex는 GitLab에서 사라지고 있습니다, 기존의 Vuex 스토어가 있는 경우 마이그레이션을 강력히 고려해야 합니다.
왜?
우리는 GraphQL API를 모든 사용자 지향적 기능의 주요 선택사항으로 정의했습니다. GraphQL이 사용되는 경우 Apollo Client도 함께 사용될 것으로 안전하게 가정할 수 있습니다. 우리는 Apollo과 Vuex를 함께 사용하고 싶지 않습니다, 그래서 우리는 REST API에서 GraphQL로 이동함에 따라 VueX 스토어 수가 자연스럽게 감소할 것입니다.
이 섹션에서는 기존의 VueX 스토어를 순수한 Vue와 Apollo로 마이그레이션하거나 VueX에 덜 의존하는 방법에 대한 지침과 방법을 제공합니다.
어떻게?
마이그레이션을 진행하기 전에 선호하는 상태 관리자 솔루션을 선택하세요.
- Pinia를 사용할 계획이라면(파일럿 단계), 이 가이드를 따르세요.
- 모든 상태 관리에 Apollo Client를 사용할 계획이라면, 아래 가이드를 따르세요(#migrate-to-vue-managed-state-and-apollo-client).
Vue에서 관리되는 상태와 Apollo Client로 마이그레이션
전체적으로, 우리가 변경할 복잡성을 이해하고 싶습니다. 때로는 전역 상태에 저장할 가치 있는 속성이 몇 개만 있는 경우가 있고, 때로는 모든 속성을 순수한 Vue
로 추출해도 안전한 경우가 있습니다. VueX
속성은 일반적으로 다음 중 하나의 범주로 들어갑니다:
- 정적 속성
- 반응성이 있는 가변 속성
- Getter
- API 데이터
따라서 첫 번째 단계는 현재 VueX 상태를 읽고 각 속성의 범주를 결정하는 것입니다.
고수준에서 각 범주를 해당되는 비-VueX 코드 패턴과 매핑할 수 있습니다:
- 정적 속성: Vue API의 제공/주입.
- 반응성이 있는 가변 속성: Vue 이벤트 및 프롭, Apollo Client.
- Getter: 유틸리티 함수, Apollo
update
후크, 계산된 속성. - API 데이터: Apollo Client.
예제를 통해 진행해 봅시다. 각 섹션에서는 이 상태를 참조하며 천천히 전체적인 마이그레이션을 진행합니다:
// state.js 즉, 우리의 스토어
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
blobPath,
summaryEndpoint,
suiteEndpoint,
testReports: {},
selectedSuiteIndex: null,
isLoading: false,
errorMessage: null,
limit : 10,
pageInfo: {
page: 1,
perPage: 20,
},
});
정적 값 마이그레이션 방법
가장 쉽게 마이그레이션할 수 있는 값의 유형은 정적 값입니다:
- 클라이언트 측 상수: 정적 값이 클라이언트 측 상수인 경우, 이 값을 상태 속성이나 메서드에서 쉽게 액세스할 수 있도록 스토어에 구현된 것일 수 있습니다. 그러나 이러한 값은 필요할 때
constants.js
파일에 추가하고 필요할 때 가져오는 것이 일반적으로 더 좋은 방법입니다. - Rails 주입된 데이터 세트: Vue 앱에 제공해야 하는 값입니다. 이 값들은 정적이므로 VueX 스토어에 추가하는 것은 필요하지 않으며, 대신
provide/inject
Vue API를 통해 쉽게 처리할 수 있습니다. 이는 VueX 오버헤드 없이 동등한 방법입니다. 이것은 오직 우리 컴포넌트를 마운트하는 최상위 JS 파일 내에서만 주입되어야 합니다.
예제를 살펴보면 두 속성에 이미 Endpoint
가 있는 것을 볼 수 있으며, 이는 아마도 Rails 데이터 세트에서 가져온 것임을 의미합니다. 이를 확인하기 위해 코드베이스에서 이러한 속성을 검색하고 정의된 위치를 확인할 것입니다. 또한 blobPath
도 정적 속성이며, 여기에 비교적 명확하지는 않지만 pageInfo
도 사실은 상수입니다! 이것은 절대 수정되지 않으며 getter에서만 사용되는 기본 값으로 사용됩니다.
// state.js 즉, 우리의 스토어
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
limit
blobPath, // 정적 - 데이터 세트
summaryEndpoint, // 정적 - 데이터 세트
suiteEndpoint, // 정적 - 데이터 세트
testReports: {},
selectedSuiteIndex: null,
isLoading: false,
errorMessage: null,
pageInfo: { // 정적 - 상수
page: 1, // 정적 - 상수
perPage: 20, // 정적 - 상수
},
});
반응성이 있는 가변 값을 마이그레이션하는 방법
이러한 값은 많은 다른 컴포넌트에서 사용될 때 특히 유용합니다. 따라서 각 속성이 몇 번 읽히고 쓰이는지, 이들이 얼마나 멀리 떨어져 있는지를 먼저 평가할 수 있습니다. 읽히는 횟수가 적고 서로 가까이 위치하는 경우, 이러한 속성들은 네이티브 Vue 프롭과 이벤트를 이용해 쉽게 제거될 수 있습니다.
간단한 읽기/쓰기 값
예제로 돌아가서, selectedSuiteIndex
는 한 컴포넌트에서만 사용되며, 한 번에 getter에서만 사용됩니다. 게다가 이 getter도 한 번만 사용됩니다! 이 로직을 Vue로 변환하는 것은 매우 쉽습니다. 이는 컴포넌트 인스턴스의 data
속성이 될 수 있기 때문입니다. getter의 경우 계산된 속성을 사용하거나, 컴포넌트에서 해당 항목을 반환하는 메서드를 사용할 수 있습니다. 왜냐하면 여기서 인덱스에 액세스할 수 있기 때문입니다. 이 예제에서 VueX 스토어가 모든 것을 하나의 컴포넌트 내에 추가하여 실제로 많은 추상화를 추가하여 애플리케이션을 복잡하게 만든 것을 볼 수 있습니다.
다행히도, 우리의 예제에서 모든 속성이 한 컴포넌트 내에 존재할 수 있습니다. 그러나 이것이 불가능한 경우도 있습니다. 그럴 때는 Vue 이벤트와 프롭을 사용하여 형제 컴포넌트 간 통신에 사용할 수 있습니다. 상태를 알아야 하는 상위 컴포넌트 내에 데이터를 저장하고, 하위 컴포넌트가 컴포넌트에 값을 쓰고자 할 때, 새 값과 함께 이벤트를 $emit
하여 상위 컴포넌트를 업데이트하게 할 수 있습니다. 그러면 모든 하위 컴포넌트에 프롭을 전달하여 모든 형제 컴포넌트의 인스턴스가 같은 데이터를 공유할 수 있습니다.
때로는 이벤트와 프롭이 매우 까다롭게 느껴질 수 있습니다. 특히 매우 깊은 컴포넌트 트리에서는 그렇지만, 이것은 주로 불편 문제이며, 아키텍처적 결함이나 해결해야 할 문제가 아닌 것에 대해 알아두는 것이 매우 중요합니다. 심지어 매우 깊게 중첩된 상태에서도 프롭 전달은 상당히 합당한 패턴입니다.
공유 읽기/쓰기 값
여러 컴포넌트에서 사용되는 속성이며 읽기와 쓰기가 매우 많거나 멀리 떨어져 있어 Vue 프롭과 이벤트가 좋지 않은 해결책으로 보이는 경우. 대신 Apollo 클라이언트 측 리졸버를 사용합니다. 이 섹션에서는 Apollo Client에 대한 지식이 필요하므로 필요한 경우 상세 정보를 확인하세요.
먼저 Vue 앱을 VueApollo
를 사용하도록 설정해야 합니다. 그런 다음 스토어를 생성할 때 Apollo Client에 resolvers
및 typedefs
를 전달합니다:
import { resolvers } from "./graphql/settings.js"
import typeDefs from './graphql/typedefs.graphql';
...
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({
resolvers, // 곧 작성
{ typeDefs }, // 곧 생성할 것입니다
}),
});
예제를 위해서 우리의 필드를 app.status
라고 부르겠습니다. 여기에 필요한 것은 @client
지시문을 사용하는 쿼리 및 뮤테이션을 정의하는 것입니다. 지금바로 만들어 봅시다:
// get_app_status.query.graphql
query getAppStatus {
app @client {
status
}
}
// update_app_status.mutation.graphql
mutation updateAppStatus($appStatus: String) {
updateAppStatus(appStatus: $appStatus) @client
}
스키마에 존재하지 않는 필드에 대해서는 typeDefs
를 설정해야 합니다. 예를 들어:
// typedefs.graphql
type TestReportApp {
status: String!
}
extend type Query {
app: TestReportApp
}
이제 리졸버를 작성하여 해당 뮤테이션을 사용하여 필드를 업데이트할 수 있게 해야 합니다:
// settings.js
export const resolvers = {
Mutation: {
// appStatus는 우리의 뮤테이션에 대한 인수입니다
updateAppStatus: (_, { appStatus }, { cache }) => {
cache.writeQuery({
query: getAppStatus,
data: {
app: {
__typename: 'TestReportApp',
status: appStatus,
},
},
});
},
}
}
쿼리 작성에 대해서는 추가 지침 없이도 작동합니다. 왜냐하면 어떤 Object
와 같은 동작을 하기 때문입니다. 왜냐하면 app { status }
를 쿼리하는 것이 app.status
를 쿼리하는 것과 동일하기 때문입니다. 그러나 이 필드의 기본 writeQuery
를 작성해주어야 하거나 우리의 필드가 가질 첫 번째 값이 무엇인지 정의하기 위해 우리의 cacheConfig
에 대한 typePolicies
를 설정할 수 있습니다.
그래서 이제 이 값에서 읽을 때 우리의 로컬 쿼리를 사용할 수 있습니다. 업데이트가 필요할 때에는 뮤테이션을 호출하고 새 값의 인수로 전달할 수 있습니다.
네트워크 관련 값
isLoading
및 errorMessage
와 같은 값들은 네트워크 요청 상태에 연결되어 있습니다. 이들은 읽기/쓰기 속성입니다만, Apollo Client의 내부 기능으로 쉽게 대체될 수 있습니다. 추가 작업 없이요.
javascript
// state.js 또는 우리의 스토어
export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) => ({
blobPath, // 정적 - 데이터셋
summaryEndpoint, // 정적 - 데이터셋
suiteEndpoint, // 정적 - 데이터셋
testReports: {},
selectedSuiteIndex: null, // 가변 -> 데이터 속성
isLoading: false, // 가변 -> 네트워크에 연결됨
errorMessage: null, // 가변 -> 네트워크에 연결됨
pageInfo: { // 정적 - 상수
page: 1, // 정적 - 상수
perPage: 20, // 정적 - 상수
},
});
게터 이주 방법
게터는 각각의 경우에 따라 검토되어야 하지만, 일반적인 지침은 우리가 게터 내에서 사용했던 상태 값을 인수로 취하고, 원하는 값을 반환하는 순수한 JavaScript 유틸 함수를 쉽게 작성할 수 있다는 것입니다. 다음 게터를 고려해 보세요:
javascript
// getters.js
export const getSelectedSuite = (state) =>
state.testReports?.test_suites?.[state.selectedSuiteIndex] || {};
여기서 우리가 하는 일은 두 상태 값을 참조하는 것인데, 두 값 모두 함수의 인수로 전환될 수 있습니다:
javascript
// new_utils.js
export const getSelectedSuite = (testReports, selectedSuiteIndex) =>
testReports?.test_suites?.[selectedSuiteIndex] || {};
이 새로운 유틸은 이전처럼 가져다 사용하고, 직접 컴포넌트 내에서 사용할 수 있습니다. 게터의 대부분의 사양은 유틸로 손쉽게 이주될 수 있습니다. 왜냐하면 로직은 보존되기 때문입니다.
API 데이터 이주 방법
마지막으로, 우리의 마지막 속성은 testReports
로 axios
를 통해 API로부터 가져옵니다. 우리는 순수한 REST 어플리케이션 안에 있다고 가정하고, 아직은 GraphQL 데이터를 사용할 수 없다고 가정합니다:
```javascript
// actions.js
export const fetchSummary = ({ state, commit, dispatch }) => {
dispatch(‘toggleLoading’);
return axios
.get(state.summaryEndpoint)
.then(({ data }) => {
commit(types.SET_SUMMARY, data);
})
.catch(() => {
createAlert({
message: s__(‘TestReports|There was an error fetching the summary.’),
});
})
.finally(() => {
dispatch(‘toggleLoading’);
});
};
```
이곳에는 두 가지 옵션이 있습니다. 이 액션이 한 번만 사용된다면, actions.js
파일에서 모든 코드를 가져와서 state
관련 코드를 모두 제거하고 data
속성을 사용하는 것을 우리가 막는 것은 없습니다. 이 경우에는 isLoading
및 errorMessages
는 한 번만 사용되므로 둘 다 함께 살아 있을 것입니다.
이 함수가 여러 번 재사용된다면 (또는 재사용 계획이 있다면), 그 때 Apollo Client를 사용하여 네트워크 호출 및 캐싱을 잘 활용할 수 있습니다. 이 섹션에서는 Apollo Client 지식과 설정 방법을 알고 있다고 가정하지만, 경우에 따라 GraphQL 문서를 참조하십시오.
로컬 GraphQL 쿼리 ( @client
지시문 사용)를 사용하여 데이터를 수신하려는 방식을 구성하고, 클라이언트 측 리졸버를 사용하여 해당 쿼리를 어떻게 해결해야 하는지 Apollo Client에 알려줄 수 있습니다. 브라우저 네트워크 탭에서 REST 호출을 보고 케이스에 적합한 구조를 결정할 수 있습니다. 우리의 예에서는 다음과 같이 쿼리를 작성할 수 있습니다:
graphql
query getTestReportSummary($fullPath: ID!, $iid: ID!, endpoint: String!) {
project(fullPath: $fullPath){
id,
pipeline(iid: $iid){
id,
testReportSummary(endpoint: $endpoint) @client {
testSuites{
nodes{
name
totalTime,
# 여기에는 더 많은 필드가 있지만, 예제에 필요하지 않습니다
}
}
}
}
}
}
여기서의 구조는 우리가 원하는 대로 어떻게든 작성할 수 있는 임의의 것입니다. 이 REST 호출의 구조와는 다르게 project.pipeline.testReportSummary
를 건너뛰는 것이 유혹스러울 수 있습니다. 그러나 GraphQL API와 호환되도록 쿼리 구조를 만든다면, 나중에 GraphQL
로 전환하기로 결정하더라도 쿼리를 수정할 필요가 없고, 오히려 @client
지시문을 제거하기만 하면 될 뿐입니다. 이는 결과적으로 이미 요약 정보를 가지고 있다는 것을 Apollo Client가 알고 있으므로 무료로 캐싱해 줍니다!
추가적으로, testReportSummary
필드에 endpoint
매개변수를 전달하고 있습니다. 이것은 순수한 GraphQL
에서는 필요하지 않지만, 저희의 리졸버는 나중에 REST
호출을 수행하기 위해 이 정보가 필요할 것입니다.
이제 클라이언트 측 리졸버를 작성해야 합니다. @client
지시문이 달린 필드를 사용할 때마다 이 리졸버가 실행되어 작업의 결과를 반환합니다. 이것은 기본적으로 Vuex
액션이 수행한 것과 동일한 역할을 합니다.
만약 우리의 GraphQL 호출이 testReportSummary
라는 데이터 속성에 저장되고 있다고 가정하면, 이 쿼리를 실행하는 모든 컴포넌트 내에서 isLoading
대신에 this.$apollo.queries.testReportSummary.lodaing
을 사용할 수 있습니다. 에러는 쿼리의 error
후크 내에서 처리할 수 있습니다.
마이그레이션 전략
이제 각 데이터 유형을 살펴본 후, VueX 기반 스토어에서 다른 스토어로의 전환을 계획하는 방법을 검토해 봅시다. VueX와 Apollo가 함께 존재하는 것을 피하려고 하므로, 두 스토어가 동시에 사용 가능한 시간이 적을수록 좋습니다. 이 중첩을 최소화하기 위해 우리는 Apollo 스토어를 추가하는 작업이 없는 모든 것을 스토어에서 제거하는 것으로 마이그레이션을 시작해야 합니다. 다음은 각각이 별도의 MR이 될 수 있는 지점입니다:
- 정적 값에서 마이그레이션 진행:
Rails
데이터 세트 및 클라이언트 측 상수를 제거하고, 대신provide/inject
및constants.js
파일을 사용합니다. - 간단한 읽기/쓰기 작업을 다음 중 하나로 대체합니다:
- 단일 컴포넌트인 경우
data
속성 및methods
를 사용합니다. - 로컬화된 그룹의 컴포넌트에서 공유되는 경우
props
및emits
를 사용합니다.
- 단일 컴포넌트인 경우
- 공유 읽기/쓰기 작업을 Apollo Client의
@client
지시문으로 대체합니다. - 네트워크 데이터를 Apollo Client로 대체하며, 가능한 경우 실제 GraphQL 호출 또는 REST 호출을 위해 클라이언트 측 리졸버를 사용합니다.
공유 읽기/쓰기 작업이나 네트워크 데이터를 신속하게 대체할 수 없는 경우(예: 1~2개의 단계에서), Apollo Client와 전용 기능 플래그 뒤에 있는 다른 Vue 컴포넌트를 만들고, 현재 VueX를 사용하는 컴포넌트에 legacy-
접두사를 붙입니다. 새로운 컴포넌트는 처음에 모든 기능을 구현할 수 없을 수 있지만, 우리가 MR을 만들 때마다 점진적으로 추가할 수 있습니다. 이렇게 하면 우리의 기존 컴포넌트는 스토어로 VueX만 사용하고, 새로운 컴포넌트는 오직 Apollo를 사용하게 됩니다. 새 컴포넌트가 모든 로직을 재구현한 후, 우리는 특성 플래그를 켜고, 그것이 예상대로 작동하는지 확인할 수 있습니다.