jomoo.dev 리팩터링 - 2
intro
이번 리팩터링은 pinia store 포스트 상태를 다루는 postData.js
를 리팩터링하려 한다. 중복되는 코드가 너무 많고, 기능 대비 코드가 너무 길다고 생각한다. 이것들을 최대한 없애는 것이 목표다.
리팩터링 전 코드
postDate.js
import { defineStore } from 'pinia';
export const usePostDataStore = defineStore('post', {
state: () => ({
programmersPosts: [],
programmersPostsIdx: {},
algorithmsPosts: [],
algorithmsPostsIdx: {},
loltrPosts: [],
loltrPostsIdx: {},
vocaPosts: [],
vocaPostsIdx: {},
jomoodevPosts: [],
jomoodevPostsIdx: {},
wootecoPosts: [],
wootecoPostsIdx: {},
jsPosts: [],
jsPostsIdx: {},
}),
actions: {
postSet(post) {
const posts = [...post];
const postsIdx = {};
posts.forEach((post, idx) => {
const { title } = post;
postsIdx[title] = idx;
});
return { posts, postsIdx };
},
programmersPostUpdate(programmers) {
const { posts, postsIdx } = this.postSet(programmers);
this.programmersPosts = [...posts];
this.programmersPostsIdx = { ...postsIdx };
},
algorithmsPostsUpdate(algorithms) {
const { posts, postsIdx } = this.postSet(algorithms);
this.algorithmsPosts = [...posts];
this.algorithmsPostsIdx = { ...postsIdx };
},
loltrPostsUpdate(loltr) {
const { posts, postsIdx } = this.postSet(loltr);
this.loltrPosts = [...posts];
this.loltrPostsIdx = { ...postsIdx };
},
vocaPostsUpdate(voca) {
const { posts, postsIdx } = this.postSet(voca);
this.vocaPosts = [...posts];
this.vocaPostsIdx = { ...postsIdx };
},
jomoodevPostsUpdate(jomoodev) {
const { posts, postsIdx } = this.postSet(jomoodev);
this.jomoodevPosts = [...posts];
this.jomoodevPostsIdx = { ...postsIdx };
},
wootecoPostsUptae(wooteco) {
const { posts, postsIdx } = this.postSet(wooteco);
this.wootecoPosts = [...posts];
this.wootecoPostsIdx = { ...postsIdx };
},
jsPostsUpdate(js) {
const { posts, postsIdx } = this.postSet(js);
this.jsPosts = [...posts];
this.jsPostsIdx = { ...postsIdx };
},
},
});
app.vue
<script setup>
...
async function getPosts(title, detail) {
const posts = await queryContent(title, detail)
.only(['title', '_path', 'description', 'date'])
.find();
return posts.reverse();
}
const postsProgrammers = await getPosts('note', 'programmers');
const postsAlgorithms = await getPosts('note', 'algorithms');
const postsLottr = await getPosts('projects', 'loltr');
const postsVoca = await getPosts('projects', 'vocabularynote');
const postsJomoodev = await getPosts('projects', 'jomoodev');
const postsWooteco = await getPosts('note', 'wooteco');
const postsJs = await getPosts('note', 'js');
postStore.programmersPostUpdate(postsProgrammers);
postStore.algorithmsPostsUpdate(postsAlgorithms);
postStore.loltrPostsUpdate(postsLottr);
postStore.vocaPostsUpdate(postsVoca);
postStore.jomoodevPostsUpdate(postsJomoodev);
postStore.wootecoPostsUptae(postsWooteco);
postStore.jsPostsUpdate(postsJs);
...
</script>
새로운 항목의 글을 작성하려고 할 때마다 스토어에 새로운 상태를 만들고, 상태를 다루는 함수들을 일일이 추가했으며, app.vue
에서도 관련 코드들을 추가했었다. 정말 비효율적인 방법이다.
이전부터 해당 프로젝트에서 가장 리팩터링하고 싶었던 부분이었지만, 리팩터링을 미루고 미뤄 항목이 많아 져 코드가 엄청 길어졌다.
일단 항목들을 이터레이터 형식으로 변경해 중복 코드를 제거할 수 있을 거 같다.
리팩터링
postStore.js
pinia store postDate.js
를 postStore.js
로 이름을 변경했으며,
세부 카테고리를 detail
로 명명하고, detail
을 배열로 만들어 배열 메서드 map을 이용해 작성한 글들을 갖고 오는 중복 코드를 제거하려 했었다.
하지만 예상했던 결과와는 다르게 나왔다.
async function setPost() {
const queryPost = async ({ category, detail }) => {
const post = await queryContent(category, detail)
.only(['title', '_path', 'description', 'date'])
.find();
return post;
};
console.log(CATEGORIES_DETAILS.map(queryPost));
}
출력
[Promise { <pending> }, Promise { <pending> }, Promise { <pending> }]
map이 반환되는 시점에 값이 결정되지 않아 <pending>
상태로 반환되는 거였다. await
사용 의미 없이 map이 멈추지 않고 다음 이터러블 값을 순회하기 때문이다.
즉, await
는 Promise 배열을 기다려주지 않고 <pending>
상태로 반환한다.
이를 해결하기 위해서 Promise.all(iterable)
메서드를 이용했다. Promise.all
은 이터러블 객체의 모든 promise를 병렬로 처리한다.Promise.all
은 순서가 보장되지 않지만, 모든 포스트를 갖고 오는 작업에는 순서가 보장될 필요가 없어 문제없다.
리팩터링 전에는 detail
당 하나의 상태로 관리했었다. 이는 확장성에 있어 불리하다고 생각해 { detail1: detail1Posts, detail2: detail2Posts }
형식으로 변경하기로 했다.
데이터 형식을 변경하려 하니 새로운 문제가 발생했다. map을 이용하면 새로운 배열이 반환되어 객체 상태로 바꾸기 위해 데이터 형식을 한 번 더 변경시켜야 한다. 한 번에 이 과정을 끝내고 싶어, reduce나 for ... of를 이용하려 했지만,
nuxt A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function. This is probably not a Nuxt bug. Find out more at
https://nuxt.com/docs/guide/concepts/auto-imports#vue-and-nuxt-composables
.
이런 오류가 발생했다. Nuxt 인스턴스가 생성되기 전이나 파괴된 후 Nuxt 인스턴스에 접근할 때 주로 발생한다고 하는데, nuxt-content의 queryContent
컴포저블 사용에서 오류가 발생하는 거 같다. 여기저기 검색하고, chatGPT 선생님께 여쭤보고, 코드도 만지작거렸지만, 좀 더 공부가 필요한 부분인 거 같다...
나중에 다시 도전해 보기로 했고, 일단은 Promise.all + map으로 promise 값들을 처리하고, reduce를 이용해 객체 형태로 바꿨다.
postStore.js
import { defineStore } from 'pinia';
import { CATEGORIES_DETAILS } from '~~/constants/categoriesDetail';
export const usePostStore = defineStore('post', () => {
const totalPosts = ref({});
async function setPost() {
const queryPost = async ({ category, detail }) => {
const post = await queryContent(category, detail)
.only(['title', '_path', 'description', 'date'])
.find();
return { key: detail, data: post.reverse() };
};
const posts = await Promise.all(CATEGORIES_DETAILS.map(queryPost));
totalPosts.value = posts.reduce((acc, { key, data }) => {
acc[key] = data;
return acc;
}, {});
}
function pickPosts(detail) {
return totalPosts.value[detail];
}
return {
totalPosts,
setPost,
pickPost,
};
});
리팩터링 후 post 스토어totalPosts
가 모든 post 들을 저장해 공유하며, App.vue
에서 setup 단계에서 setPost()
를 호출해 작성한 글들을 가지고 온다.
카테고리 디테일 detail
을 key로 하여 totalPosts
에 저장하고 있으며, detail
을 매개변수로 받아 detail posts를 반환하는 함수 pickPosts
를 이용해 각 페이지에서 이용하게 했다. 이후 페이지, 컴포넌트들을 리팩터링할 방식을 결정한 후 변경할 예정이다.
app.vue
<script setup>
import { usePostStore } from '~~/store/postStore';
const postStore = usePostStore();
await callOnce(postStore.setPost);
</script>
길었던 app.vue
의 <script setup>
을 중복 코드를 다 제거해 간단하게 만들었다.
이전에 없었던 함수 callOnce
는 nuxt 3.9 에서 새롭게 추가된 utils API로 SSR 또는 CSR 중에 지정된 함수 또는 코드 블록을 한 번 실행하는 함수다.
이벤트 로깅이나 전역 상태 설정과 같이 한 번만 실행해야 하는 코드에 유용하다고 돼 있다. SSR에서 함수를 호출할 때, 서버와 클라이언트에서 각각 함수를 호출하는데, 이를 hydration이라 한다. callOnce
는 클라이언트에서 함수를 다시 호출하는 것을 회피해, hydration이 아닌 경우에 유용하다고 한다.
작성한 글들을 가지고 오는 작업은 전역적으로 한 번만 실행하는 작업이므로 적합한 기능이라 생각해 추가했다.
이번 리팩터링은 간단하지만, 습관화를 위해 다이어그램을 만들었다.
app.vue의 setup 단계에서 postStore의 setPosts()
를 호출해 작성한 글들을 가지고 와서 totalPosts
에 저장해 관리하며, 이후 각 컴포넌트와 페이지에서 totalPosts
를 이용해 작성한 글들을 렌더링한다.
마무리
보기 싫었던 코드를 드디어 제거해 마음이 한결 가벼워졌다. 이번 리팩터링은 간단했지만, promise, async, await 같은 알아야 하는 지식에 부족함을 많이 느꼈다. 기초 공부가 귀찮아도 꼭 하자.
다음에는 page 관련 중복 제거를 목표로 리팩터링하려 한다.