jomoo.dev 컴포넌트 디렉터리 리팩터링
intro
이번 컴포넌트 디렉터리 리팩터링으로 jomoo.dev 프로젝트의 리팩터링을 마치려 한다. 컴포넌트 디렉터리의 리팩터링은 복잡한 컴포넌트가 없어 큰 어려움 없어 보인다. 컴포넌트나 함수의 이름을 좀 더 적절하게 변경하거나, 가독성 향상을 위한 코드의 구조 변경을 중점으로 리팩터링하겠다.
리팩터링 전 components 디렉터리
리팩터링 전 components 디렉터리, 어떻게 리팩터링할지 간단하게 설명하겠다.
우선 components/content 디렉터리에 있는 ProseCode.vue
컴포넌트는 nuxt-content의 내장된 Prose component를 덮어쓰기 위한 컴포넌트다. ProseCode.vue
는 작성한 글(content)에서 코드 블록을 다루는 컴포넌트로, 코드 블록 안에 있는 코드 복사 기능을 추가하기 위해 생성했던 컴포넌트다. 해당 컴포넌트는 변경할 부분이 없다.
CategoriesMenu.vue
컴포너트는 jomoo.dev 리팩터링-1에서 추가했던 컴포넌트로 화면 넓이가 md 사이즈보다(768px
미만) 작을 때 category를 보여주는 컴포넌트다. 이 컴포넌트 또한 변경할 부분은 없어 보인다.
ContentCard.vue
컴포넌트는 category/detail/index 페이지에서 detail 관련 글들을 카드 형태로 보여주기 위해 사용되는 컴포넌트다.IndexContentCard.vue
컴포넌트는 메인 페이지에서 작성한 글들을 카드 형태로 보여주기 위해 사용되는 컴포넌트다.
일단 ContentCard.vue
와 IndexContentCard.vue
의 어디서 사용되는지 이름이 헷갈린다. 컴포넌트 이름을 좀 더 직관적으로 변경하겠다.
listIcon.vue
md 사이즈보다 작을 때 기본 레이아웃 default.vue
에서 사용하는 아이콘 컴포넌트다. 컴포넌트의 이름이 파스칼 케이스가 아니고, 현재 다른 아이콘들은 nuxt-icon을 이용하고 있다.listIcon.vue
는 nuxt-icon을 이용하기 전에 만들었던 컴포넌트다. 꼭 필요한 아이콘 컴포넌트가 아니기에, 해당 컴포넌트 파일은 삭제하고 nuxt-icon을 이용하겠다.
NoteSideBar.vue
, ProjectsSideBar.vue
컴포넌트들은 sideBar.vue
에서 현재 경로에 따라 동적으로 사용되는 컴포넌트들이다. 두 개의 컴포넌트를 사용할 필요없이 현재 경로에 맞게 데이터만 변경하는 식으로 리팩터링 하는 게 좋을 거 같다.
PostMoveCard.vue
는 category/detail/post 페이지에서 이전 글이나 다음 글에 대한 정보를 보여주면서, 보여주는 글에 대한 링크 역할을 하는 컴포넌트다. 이전 글 jomoo.dev 리팩터링-3에서 함께 리팩터링했기에 컴포넌트의 이름만 변경하려 한다.
ContendCard.vue
와 IndexContentCard.vue
리팩터링
처음에는 ContendCard.vue
와 IndexContentCard.vue
컴포넌트의 이름만 직관적으로 변경하려 했었다. 하지만 두 컴포넌트가 렌더링하는 정보는 같기에 하나의 컴포넌트로 통합하기로 했다.
리팩터링 결과 PostCard.vue
export const POST_CARD_SIZE = {
small: 'small',
wide: 'wide',
};
<script setup>
import { POST_CARD_SIZE } from '~/constants/postCardSize';
const props = defineProps({
path: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
date: {
type: String,
required: true,
},
size: {
type: String,
required: true,
},
});
const isPostCardSizeWide = computed(() => props.size === POST_CARD_SIZE.wide);
const containerClass = computed(() => {
if (isPostCardSizeWide.value) {
return 'border-b mb-2 max-h-[150px] min-h-[150px] px-1';
}
return 'border-2 border-stone-500 hover:border-4 hover:px-[18px] hover:py-[10px] min-h-[16rem] h-64 max-h-64 rounded-xl px-5';
});
const titleClass = computed(() => (isPostCardSizeWide.value ? 'md:text-3xl' : 'py-2.5'));
const descriptionClass = computed(() => {
if (isPostCardSizeWide.value) {
return 'text-lg truncate py-2.5';
}
return 'text-sm md:text-base';
});
</script>
<template>
<div
class="cursor-pointer flex flex-col py-3"
:class="containerClass"
@click="navigateTo(props.path)"
>
<div class="text-2xl font-bold truncate" :class="titleClass">
{{ props.title }}
</div>
<div class="text-zinc-600 flex-1" :class="descriptionClass">
{{ props.description }}
</div>
<div class="flex text-sm text-zinc-400" :class="{ 'justify-end': !isPostCardSizeWide }">
{{ props.date }}
</div>
</div>
</template>
이전 ContendCard.vue
와 IndexContentCard.vue
컴포넌트를 하나의 컴포넌트로 합친 PostCard.vue
컴포넌트
우선 작성한 글에 대한 정보를 담는 카드이므로, ContentCard 보다는 PostCard가 더 적합하다고 생각해 이름을 변경했다.
해당 컴포넌트는 메인 페이지와 각 category detail index 페이지에서 이용하는 컴포넌트다. 두 페이지에서 렌더링할 정보는 글의 제목, 설명, 작성한 날짜로 동일하며 css 값만 차이가 있다.
css 값만 다르게 하여 하나의 컴포넌트로 사용하기 위해 PostCard 컴포넌트의 사이즈를 상수로 저장해 props.size
로 전달받은 값과 비교하여 동적으로 PostCard의 css 스타일을 결정한다.
현재 PostCard의 사이즈는 small
과 wide
두 개다. small
은 메인 페이지에서, wide
는 category detail index page에서 사용하는 사이즈다.
계산된 속성 isPostCardSizeWide
는 렌더링할 PostCard가 wide
인지 확인한다. 해당 값을 이용해, 필요한 tailwind css class 들을 반환하여, 동적으로 클래스 바인딩을 통해 스타일링한다.
PostCard.vue
에서 고민했던 점
PostCard.vue
구현에 있어 <slot>
과 동적 클래스 바인딩 중 어떤 방식이 좋을까 고민이 있었다.
처음에는 위 코드처럼 클래스 바인딩을 이용해 전달받은 사이즈에 따라 동적으로 스타일링했었다. 이 방법은 PostCard 컴포넌트를 이용하는 곳에서 원하는 사이즈만 전달하면 되기에 이용하는 곳(페이지 또는 상위 컴포넌트)에서 직관적이면서 간단하게 사용할 수 있다.
다른 방법으로, PostCard 컴포넌트의 특정 부분을 <slot>
으로 구분해, PostCard 컴포넌트를 이용하는 곳에서 <slot>
에 태그를 넣어 사용하는 방법도 있다. 이 방법은 이용하는 곳에서 필요한 코드를 삽입하기에 컴포넌트의 재사용성이 엄청나게 향상된다. 이후 프로젝트에서 새로운 사이즈의 카드가 필요해도 필요한 곳에서 <slot>
직접 에 삽입하기에 아무 문제가 없다. 또한 nuxt-ui 프로젝트의 코드에서도 <slot>
을 많이 사용하는 것을 보니 점점 이 방법이 좋아 보였다.
어떤 방법이 더 좋을까 계속 고민하다가, <slot>
을 과하게 사용하면 컴포넌트의 의미가 흐려진다고 생각이 들었다. 재사용하기 위해 코드를 분리했는데 컴포넌트에서 구역만 나누고, 페이지나 상위 컴포넌트에게 나머지 일을 다 맡겨 버리면 컴포넌트가 굳이 필요할까?
복잡한 구조가 필요하거나 특정 부분만 다른 정보를 렌더링하려 할 때, <slot>
을 이용하는 것이 좋다고 생각한다.
PostCard 컴포넌트는 제목(header), 내용(body), 날짜(footer) 세 개로 나뉘는 간단한 구조이며, 같은 정보만을 렌더링한다. 단지, css class만 다를 뿐이다. 따라서 <slot>
으로 재사용성을 늘리는 것보다, 동적으로 클래스 바인딩을 이용해 스타일링 하는 직관적인 방법이 더 좋은 거 같다.
컴포넌트에서 과도한 재사용성은 장점만 있는 것은 아니라고 생각한다.
listIcon.vue
리팩터링
listIcon.vue
컴포넌트를 삭제하고, 대신 nuxt-icon을 이용하도록 변경했다.
default.vue
<template>
<!-- 변경 전 -->
<div class="cursor-pointer" @click="toggleCategoriesMenu">
<ListIcon />
</div>
<!-- -->
</template>
<template>
<!-- 변경 후 -->
<div class="cursor-pointer" @click="toggleCategoriesMenu">
<Icon name="mingcute:menu-line" size="32" />
</div>
<!-- -->
</template>
NoteSideBar.vue
와 ProjectsSideBar.vue
리팩터링
두 컴포넌트를 모두 지웠다.constants/categoriesDetail
상수 파일의 고정된 정보를 sideBar.vue
레이아웃에서 사용해 현재 경로에 따라 다른 category details 항목들을 렌더링하도록 변경했다.
레이아웃 sideBar.vue
변경
변경 전
<script setup>
const ProjectsSideBar = resolveComponent('ProjectsSideBar');
const NoteSideBar = resolveComponent('NoteSideBar');
const route = useRoute();
const { category } = route.params;
const sideBar = { projects: ProjectsSideBar, note: NoteSideBar };
</script>
<template>
<!-- 생략 -->
<div
class="col-span-10 md:col-span-2 flex md:sticky top-[7rem] lg:top-[8.6rem] overflow-y-auto max-h-[calc(100vh-220px)] justify-between"
>
<component :is="sideBar[category]" />
</div>
<!-- 생략 -->
</template>
변경 후
<script setup>
import { CATEGORIES_DETAILS } from '~/constants/categoriesDetail';
const route = useRoute();
const { category, detail } = route.params;
const categoryDetailItems = CATEGORIES_DETAILS[category].map((categoryDetail) => {
const active = categoryDetail.detail === detail;
return { ...categoryDetail, active };
});
</script>
<template>
<!-- 생략 -->
<div
class="col-span-10 md:col-span-2 flex md:sticky top-[7rem] lg:top-[8.6rem] overflow-y-auto max-h-[calc(100vh-220px)] justify-between"
>
<nav>
<ul class="font-bold flex gap-x-4 md:block">
<li v-for="{ path, text, active } in categoryDetailItems" :key="path">
<NuxtLink :to="path">
<div
class="hover:text-emerald-500 py-1.5"
:class="active ? 'text-emerald-600' : 'text-black'"
>
{{ text }}
</div>
</NuxtLink>
</li>
</ul>
</nav>
</div>
<!-- 생략 -->
</template>
이전에는 현재 경로의 category를 이용해 ProjectsSideBar.vue
나 NoteSideBar.vue
컴포넌트를 선택해 렌더링하는 방식이었다. 리팩터링 후에는 두 개의 컴포넌트를 사용하지 않는다.
constants/categoriesDetail
에 위치한 category detail 관련 정보와 현재 경로의 category, detail을 조합한 결과를 sideBar.vue
레이아웃에서 직접 렌더링하도록 변경했다.
조합한 결과인, 현재 category의 detail 관련 정보 categoryDetailItems
는 { path, text, active }
형식의 객체를 담고 있는 배열이다.
path
: 해당 category detail index 페이지의 주소text
: category detail을 나타내는 문자active
: 현재 경로의 category detail가 일치하는지 확인하는 boolean 타입의 값이다.
active
속성을 이용해 현재 경로와 일치하는 detail 항목만 다른 css 클래스를 갖게 하여, 방문 중인 detail 항목을 구분한다.
리팩터링 후 components 디렉터리
ContentCard.vue
,IndexContentCard.vue
통합 =>PostCard.vue
listIcon.vue
삭제NoteSideBar.vue
,ProjectsSideBar.vue
삭제PostMoveCard.vue
이름 변경 =>PostNavigationCard.vue
다이어그램
메인 페이지 index.vue
와 각 카테고리 디테일 index 페이지 category/detail.index.vue
에서 PostCard.vue
컴포넌트를 사용하고 있으며, PostCard 컴포넌트의 사이즈인 small
, wide
를 전달해 동적으로 스타일링하고 있다.
default.vue
레이아웃에서 CategoriesMenu.vue
컴포넌트를 사용한다.
각 카테고리 디테일 포스트 페이지 category/detail/post.vue
에서 주변 글에 대한 정보와 네비게이션 역할을 하는 컴포넌트 PostNavigationCard.vue
를 사용하며, before
나 after
를 전달해 이전 글인지 다음 글인지에 대한 방향을 정한다.
경로 이동
추가로, 경로 이동에 있어 useRouter에 이동할 경로를 push 하는 방식을 이용했었는데, nuxt 홈페이지에서 navegateTo
를 이용한 방법을 권고해서 변경했다.
push(): Programmatically navigate to a new URL by pushing an entry in the history stack. It is recommended to use navigateTo instead.
replace(): Programmatically navigate to a new URL by replacing the current entry in the routes history stack. It is recommended to use navigateTo instead.
마무리
리팩터링 자체는 간단했는데 '어떤 방법이 더 좋을까'에 대한 고민이 계속되어 오래 걸렸다. 어떤 방법이든 무조건 제일 좋다는 없고, 장단점이 있기에 선택하기 참 어렵다. 잘하는 사람한테 이럴 때는 어떤 방법이 더 좋을지 질문하고 싶다.
이것으로 jomoodev 블로그 프로젝트의 리팩토링을 마치겠다. 다음부터는 메인 페이지 변경과 새로운 기능을 추가하려고 한다.