#3 Vue 이론

엽토군·2024년 5월 17일
0

To Do Wild 개발기

목록 보기
3/3

시리즈 소개

오프라인 PWA를 목표로 하는 To Do 웹앱 프로젝트 To Do Wild 를 개발하며 배운 것들을 적는 시리즈입니다.

상태 관리

Pinia 등을 꼭 써야 하는가?

결론부터 말하자면 그렇지 않다. 문서에도 써 있는 이야기다.

저장소에는 ... 여러 곳에서 사용되는 데이터가 포함되어야 합니다. 예를 들어, GNB에 표시되는 사용자 정보 ... 데이터가 있습니다.

반면에 컴포넌트에서 호스팅할 수 있는 로컬 데이터를 스토어에 포함하는 것을 피해야 합니다. ...

내가 만들고 있는 것은 투두앱이다.

  • 컴포넌트끼리의 결속도가 매우 높다. <TodoForm> 따로 쓰고 <TodoItem> 따로 쓸 일이 없다.
  • 데이터/상태 변경 시나리오가 매우 단순하다. 폼에서 부모로 갔다가 리스트로 가고 리스트에서 부모로 갔다가 리스트로 온다. 데이터가 어디서 무슨 험한 꼴 보고 돌아올지 내가 어떻게 아느냐는 식의 컴포넌트를 구현할 필요가 없다.

이리하여, 여기서 전역 상태 관리 라이브러리를 채용하는 것은 오버킬이라고 보았다.

그리고 지금 돌이켜 보면, 나는 그저 컴포넌트간 양방향 바인딩을 하고 싶었을 뿐이었던 것 같다.
그 방법을 공식문서에서 찾아내기까지 이상하게 오래 걸렸고, 그걸 알아내고 나서는 별 문제가 없어졌다.

컴포넌트에 붙이는 v-model

v-model은 예컨대 <input> 같은 일반 엘리먼트에 붙일 수 있다.
그러면 예컨대 <TodoItem> 같은 컴포넌트에는 붙일 수 없을까?
이렇게만 놓고 보면 서로 별 차이 없어 보이는데 안될 게 뭐냐 싶어진다.
그런데 놀랍게도, Vue 3.4 이전까지는 이게 안 돼서, 아래와 같은 삽질을 해서 구현을 했다고 한다.

<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

안 그래도 컴포넌트의 v-model API를 알기 전까지는 나도 저 삽질을 할 각오를 잔뜩 다지고 있었다.
문서를 아무리 읽어보아도 그 수밖에 없기 때문이다.

  • 데이터 자체가 상위 컴포넌트로 "상신되는" 일은 없다. 데이터는 항상 아래로 흐른다.
  • 상위 컴포넌트로 상신할 수 있는 것은 이벤트뿐이다.
  • 상위 컴포넌트가 이벤트를 처리하는 과정에서 데이터가 변경된다면, 하위 컴포넌트의 데이터도 "변경될" 것이다.

이 개념을 우아하게 숨긴 것이 v-model 디렉티브라고 생각된다.
API 문서에 따르면 디렉티브가 맞다. 가이드 문서에서는 명토 박아서 디렉티브라고 하지는 않고 일종의 매크로라는 식의 요상한 설명이 붙어있지만...

API 기초

플러그인

잠시 실무로 돌아와서... Indexed DB를 쓰긴 써야 되겠는데, Indexed DB 문서를 읽어보자면, 아무래도 어딘가에서 한 번은 "저장소를 생성하고 구성"해야 하는 모양이다.

하는건 하면 되는데 이걸 어디서 어떻게 하지???

  • main.jscreateApp() 전후 아무데나 구겨넣어도 될거 같은데?
  • index.html<head> 태그 안에 구겨넣어도 될거 같은데??
  • App.vue<script setup>에서 해도 될거 같은데???

셋 다 그닥 좋은 아이디어는 아니다... 넘 jQuery스럽달까...
굳이 좋은 아이디어를 짜내어보자면 대략 이렇게 요약될 것이다.

  1. Vue App이 사용할 자원이므로, App의 초기화 단계에서 수행하고 싶다.
    • 바꿔 말하자면, Vue App이 뜨지 못하는 불의의 상황에서 이 작업만 덜렁 수행되는 일은 막아도 좋을 것이다.
  2. App에 이런저런 사건이 너무 많이 발생하기 전에, 적당히 앞서서 미리 수행하고 싶다.
    • 구체적으로는, 컴포넌트들이 마운트돼 들어오기 전에 미리 수행돼 있었으면 좋겠다.

이리하여, Indexed DB 저장소 초기화를 수행할 위치로 플러그인을 선택했다.

// main.js 일부
import { createApp } from 'vue'
import IndexedDB from './plugins/IndexedDB'

const app = createApp(App)
app.use(IndexedDB)
// plugins/IndexedDB.ts
import { App } from "vue"
import TodoRepo from "../repositories/todoRepo.ts"

export default {
  install: (app: App) => {
    const repo = new TodoRepo()
    app.provide('repo', repo)
  }
}

TodoRepo 클래스에 관해서는 다음 글에서 좀더 자세히.
너무 지나치게 창의적(??)인 리포지토리라서 아무래도 굉장히 창피할 거 같으니, 어차피 창피할 예정인 다음 글에서 한꺼번에 창피해지기로 한다.

API 심화

define 접두사

Vue 튜토리얼 백번 읽고 거의 그대로 따라하면서도 '이게 왜 (안) 되지...?' 하면서 엉거주춤 흉내만 내다가, 오늘 웬만큼 기능 구현이 끝나고 나서야 약간 깨달아지는 바가 있어서 메모.

defineModel 함수는 왜 하필 define을 붙여서 defineModel이라고 부를까?
상위 템플릿에서 <Foo x-model="어쩌구" />로 쓸 때, 여기서의 "x-model"이 뭔지 이게 어떻게 작동해야 되는 건지를 정의("define")하기 때문에 "define Model"이라고 부른다.

다른 것도 비슷하다. 대충 다음 표로 정리된다.

~를 정의하지 않으면상위 템플릿에서 ~를 쓸 수 없다.
defineModel('foo')x-model:foo="..."
defineEmits(['foo'])@foo="..."
defineProps(['foo']):foo="..."

정리해놓고 보니 이런데서 초보 티가 팍팍 난다. 자세한 것은 관련 API 문서를 더 읽어보자.

profile
5년차 PHP 개발자입니다.

0개의 댓글