20241225

Vue3 + Typescript + Vite + ESLint + Prettier + Pinia + Router + Tailwind

프론트엔드 팀 새 프로젝트 시작할 때 쓰는 셋업 스택 정리. Vue3 + TS + Vite + ESLint + Prettier + Pinia + Vue Router + Tailwind. 실제로 회사에서 이 조합으로 두 개 프로젝트 돌리고 있고 가장 큰 장점은 Vite 개발서버 속도와 Pinia 코드 간결함이다. Vuex 쓰다가 Pinia 가면 못 돌아온다. Vue 3.5 / Vite 6 기준.

왜 이 조합인지

  • Vite: HMR 체감이 다르다. 큰 프로젝트에서 webpack dev server 대비 압도적
  • Vue3: Composition API + <script setup> 이 주력. 리액트 훅이랑 생각하는 방향이 비슷해서 프론트 리소스 이동도 쉬움
  • TypeScript: 솔직히 팀에 2명 이상 되면 필수
  • ESLint / Prettier: lint는 논리, 포맷은 Prettier. 영역 분리가 명확해짐
  • Pinia: Vuex 대체. 타입 추론 훨씬 강함 (Vuex 5 프로젝트 사실상 Pinia로 수렴했다고 보면 됨)
  • Vue Router: SPA 라우팅 표준
  • Tailwind: 클래스 이름 싸움에서 해방. 처음엔 HTML이 지저분해 보이는데 2주 쓰면 돌아갈 수가 없음

프로젝트 생성

$ npm create vite@latest 프로젝트명 --template vue-ts
$ cd 프로젝트명
$ npm install
$ npm run dev

@types/node 설치

vite.config.ts에서 path 모듈 쓸 때 타입 에러 나는 거 막기 위함.

$ npm install --save-dev @types/node

vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import * as path from "path";

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: { "@": path.resolve(__dirname, "./src") },
  },
});

tsconfig.json

ViTe의 alias와 TS의 paths를 동일하게 맞춰줘야 에디터에서 경고 안 뜬다. 이거 두 개 동기화 안 맞으면 자동완성은 되는데 빌드 에러나는 짜증 상황이 생김.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

ESLint + Prettier

$ npm install -D prettier
$ npm install -D eslint
$ npm install -D eslint-plugin-vue
$ npm install -D eslint-config-prettier
$ npm install -D @vue/eslint-config-typescript

.eslintrc.js

중요한 건 'prettier'를 extends 맨 뒤에 넣어야 한다는 것. 앞의 규칙들 중 포맷 관련 규칙을 다 꺼버리는 역할이라서 순서가 중요함. 이거 순서 잘못 해서 저장할 때마다 포맷이 왔다갔다 하면 하루 날린다.

module.exports = {
  env: {
    node: true
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'prettier'
  ],
  rules: [
    // override or add rules settings here
  ]
};

Pinia

$ npm install pinia
import { createPinia } from 'pinia';

const pinia = createPinia();

...use(pinia);

Pinia의 진짜 매력은 store를 setup 함수처럼 쓸 수 있다는 것. defineStore('auth', () => { const user = ref(null); ... return { user }; }) 이렇게. Vuex의 state/mutations/actions 세 영역 다닐 필요 없어짐.

Vue Router 4

import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";

const routes: Array<RouteRecordRaw> = [
  { path: "/", component: () => import("../views/Home.vue") },
  { path: "/about", component: () => import("../views/About.vue") }
];

export default createRouter({
  history: createWebHistory(),
  routes,
});

컴포넌트를 () => import(...) 동적 import로 선언하면 자동으로 route-level code splitting 된다. 빌드 사이즈 신경쓴다면 처음부터 이렇게.

import router from "./router";
...use(router);

Tailwind CSS

$ npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
$ npx tailwindcss init -p
import 'tailwindcss/tailwind.css';

이 셋업으로 실제 겪은 이슈 몇 가지

초반에 Vite 환경에서 process.env 직접 쓰다가 undefined 만나고 당황함. Vite는 import.meta.env 쓰는 게 맞음. 환경변수명도 VITE_ 접두어 붙여야 노출된다.

그리고 IDE는 VS Code + Volar 확장 (이제 공식 이름은 Vue - Official)을 쓰면 된다. TS 검사 속도를 위해 Take Over Mode 켜두는 걸 추천. 기본 TypeScript 확장을 끄고 Volar가 전담하는 방식.