jomoo.dev 리팩터링 - 2

2024년 1월 6일

intro

이번 리팩터링은 pinia store 포스트 상태를 다루는 postData.js를 리팩터링하려 한다. 중복되는 코드가 너무 많고, 기능 대비 코드가 너무 길다고 생각한다. 이것들을 최대한 없애는 것이 목표다.

리팩터링 전 코드

postDate.js

store/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

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.jspostStore.js로 이름을 변경했으며, 세부 카테고리를 detail로 명명하고, detail을 배열로 만들어 배열 메서드 map을 이용해 작성한 글들을 갖고 오는 중복 코드를 제거하려 했었다.
하지만 예상했던 결과와는 다르게 나왔다.

store/postStore.js
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

store/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

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이 아닌 경우에 유용하다고 한다.
작성한 글들을 가지고 오는 작업은 전역적으로 한 번만 실행하는 작업이므로 적합한 기능이라 생각해 추가했다.

image 이번 리팩터링은 간단하지만, 습관화를 위해 다이어그램을 만들었다.
app.vue의 setup 단계에서 postStore의 setPosts()를 호출해 작성한 글들을 가지고 와서 totalPosts에 저장해 관리하며, 이후 각 컴포넌트와 페이지에서 totalPosts를 이용해 작성한 글들을 렌더링한다.

마무리

보기 싫었던 코드를 드디어 제거해 마음이 한결 가벼워졌다. 이번 리팩터링은 간단했지만, promise, async, await 같은 알아야 하는 지식에 부족함을 많이 느꼈다. 기초 공부가 귀찮아도 꼭 하자.
다음에는 page 관련 중복 제거를 목표로 리팩터링하려 한다.


댓글