단어장 리팩터링 - 1
intro
이전에 만들었던 단어장 토이 프로젝트를 봤을 때 비효율적인 부분이 많다고 생각하여 리팩터링 하려고 한다.
현재 프로젝트는 무분별하게 pinia store를 이용하고 있다고 생각한다.
불필요한 pinia의 사용을 줄이고 Vue의 props와 emits를 이용해 코드를 리팩터링 하는 것이 목표다.
우선 단어장의 단어를 클릭했을 때 단어의 디테일을 모달 창으로 띄우는 코드의 리팩터링이다.
리팩터링 전의 코드에서는 WordList.vue
에서 WordCard.vue
에 단어의 정보들을 전달하고(prop), 해당 단어를 클릭하면, 커스텀 이벤트 @wordDetail
을 WordList.vue
에서 감지해, pinia 저장소에 해당 단어를 전달해 모달을 여는 방식이었다.
이 방식에서 pinia 저장소를 사용하여 오히려 더 복잡해졌다고 생각한다. pinia 저장소를 사용하지 않고, WordCard.vue
에서 prop으로 전달받은 단어의 정보들을 이용해 단어의 세부 정보 모달을 직접 띄우는 방식으로 변경하였다.
리팩터링 후 코드
WordCardV2.vue
<script setup>
import { Icon } from '@iconify/vue';
import { useMainStore } from '../stores/Main';
import { ref, computed } from 'vue';
import ModalWordDetailV2 from './ModalWordDetailV2.vue';
const props = defineProps({
word: String,
means: String,
check: Boolean,
time: String,
index: Number,
});
const mainStore = useMainStore();
const means = computed(() => props.means.split(','));
const openWordDetail = ref(false);
function toggleDetailOpen() {
openWordDetail.value = !openWordDetail.value;
}
function deleteWord() {
toggleDetailOpen();
mainStore.wordDelete(props.word);
}
function checkBtnClick() {
mainStore.wordCheck(props.word, !props.check, props.index);
}
</script>
<template>
<div class="flex flex-col">
<div class="flex gap-x-1">
<button>
<Icon
v-if="props.check === false"
class="flex items-center"
@click="checkBtnClick"
icon="carbon:checkbox"
width="37"
height="37"
></Icon>
<Icon
v-else
class="flex items-center"
@click="checkBtnClick"
icon="carbon:checkbox-checked"
width="37"
height="37"
></Icon>
</button>
<span
class="card_word"
:class="{ wordcheck_active: props.check }"
@click="toggleDetailOpen"
>{{ props.word }}</span
>
</div>
<div :class="{ wordcheck_active: props.check }">
<li v-for="mean in means" :key="mean" class="card_content">
{{ mean }}
</li>
</div>
<Teleport to="body">
<ModalWordDetailV2
v-if="openWordDetail"
:word="props.word"
:means="means"
:time="props.time"
@close="toggleDetailOpen"
@delete="deleteWord"
/>
</Teleport>
</div>
</template>
<style scoped>
.wordcheck_active {
@apply opacity-60 line-through decoration-2;
}
</style>
WordCard.vue
를 리팩터링한 코드
- ModalWordDetail에 전달하기 위해
props
에 단어가 추가된 시간 정보인time
을 추가 props
의 속성 중means
만 문자열 배열로 조작되어야 해서computed
를 이용했고 나머지 속성들은 조작 필요 없어 단순한 값 그대로 사용하기에 ref로 감싸지 않았다. props는 부모 속성이 업데이트되면 자식으로 흐르기 때문에 그대로 사용한다.- 변수
openWordDetail
은 단어를 클릭했을 때 모달 창을 열거나 닫기 위한 상태 변수다. - 함수
toggleDetailOpen
은 모달 오픈 상태인openWordDetail
의 값을 반전시켜 단어를 클릭하면 모달을 화면에 띄우고, 모달을 종료하는 명령이 오면toggleDetailOpen
을 호출해 모달을 닫는다. - 커스텀 이벤트
close
: 자식 컴포넌트인 모달 창에서 close 버튼이나 나가기 아이콘 눌렀을 때 발생하는 이벤트를 감지하면,toggleDetailOpen
을 호출해 모달을 닫는다. - 커스텀 이벤트
delete
: 자식 컴포넌트인 모달 창에서 delete 버튼을 눌렀을 때 동작하는 이벤트를 감지해 mainStore에서 단어 삭제 기능을 호출한다.
단어 삭제 명령을 해당 컴포넌트에서 호출하도록 했다. 해당 단어 컴포넌트의 모달에서 직접 삭제 명령을 호출하는 것보다 모달에서의 삭제 이벤트 발생을 감지해 WordCard.vue
에서 삭제 책임을 갖는게 더 적합하다고 생각했다.
WordList.vue
<template>
<!-- ... -->
<WordCardV2
:word="word.word"
:means="word.means"
:check="word.check"
:time="word.time"
:index="index"
/>
<!-- ... -->
<template>
커스텀 이벤트 trashCanWordDetail
를 제거했기에 WordCard 컴포넌트의 이벤트 감지 또한 제거한다. 또한 WordCard에서 means를 computed
이용해 조작하기에 그대로 전달한다.
<script setup>
import TheModal from './TheModal.vue';
const props = defineProps({
word: String,
means: Array,
time: String,
});
const emits = defineEmits(['close', 'delete']);
function closeDetailWord() {
emits('close');
}
function deleteWord() {
emits('delete');
}
</script>
<template>
<TheModal @clickCloseIcon="closeDetailWord">
<template #word>
{{ props.word }}
</template>
<template #means>
<li v-for="mean in props.means" :key="mean" class="modal_means">
{{ mean }}
</li>
</template>
<template #footer>
<div class="flex justify-end modal_timetext">{{ props.time }}</div>
<div class="flex gap-x-4">
<button
@click="closeDetailWord"
class="modal_btn bg-neutral-400 hover:bg-neutral-500"
>
cancel
</button>
<button
@click="deleteWord"
class="modal_btn bg-rose-400 hover:bg-rose-600"
>
delete
</button>
</div>
</template>
</TheModal>
</template>
이전에는 pinia store인 detailWordStore에서 단어의 정보들을 받아 화면에 화면에 렌더링 했지만, 중앙 스토어 이용은 불필요하다고 생각했기에 제거했다.
리팩터링 후, 해당 Modal에서는 props
을 전달받은 값들을 화면에 렌더링하고, emits
를 통해 커스텀 이벤트를 선언하여 부모 컴포넌트인 WordCard에서 이벤트 발생 감지에 따라 특정 기능들을 동작하게 했다.
ModalWordDetailV2.vue
의 뼈대인 TheModal
컴포넌트에서도 같은 방식으로 커스텀 이벤트 clickCloseIcon
을 이용해 모달 닫기 명령을 TheModal => ModalWordDetailV2 => WordCardV2로 이동해 모달을 닫는다.
쓰레기통 단어인 WordTrashCan에 관련된 컴포넌트들도 같은 방식으로 리팩터링했다.
마무리
props
와 emits
을 통해 추가적인 흐름이 생겼지만, WordCard.vue
가 특정 단어에 대한 책임을 담당한다. 이러한 구조는 pinia 스토어를 사용한 방식보다 훨씬 직관적이고 간단하다고 생각한다.
이다음에는 단어를 입력했을 때 나오는 모달들에 대한 코드들을 리팩터링 할 예정이다.