Vue 3 + TypeScript 모범 사례: 2025년 엔터프라이즈 프로젝트 아키텍처 가이드

지난달 팀에서 기술 스택 선정 회의를 열었는데, 프론트엔드 개발자들이 상태 관리, 디렉터리 구조, TS 설정을 두고 한참 논쟁했지만 결론을 내리지 못했습니다. 저도 처음 Vue 3 + TypeScript를 설정할 때 tsconfig.json을 20번 넘게 고쳤고, 다른 컴퓨터에서는 또 오류가 났습니다.
이 글은 우리 팀이 수많은 시행착오를 거쳐 정리한 구성입니다. 최적의 해답이라고 할 수는 없지만, 적어도 중대형 프로젝트 세 곳에서 운영하는 동안 큰 문제는 없었습니다. 새 프로젝트를 시작하거나 기존 프로젝트의 아키텍처를 개선하려는 분들이 시행착오를 줄이는 데 도움이 되길 바랍니다.
2025년 Vue 3 기술 스택 선정 가이드
먼저 기술 스택 선정부터 이야기해 보겠습니다. Vue 3가 출시된 지도 몇 년이 지났고, 2025년에 무엇을 사용해야 하는지는 이제 상당히 명확해졌습니다.
현재 우리 팀의 표준 조합은 Vite + Vue 3 + TypeScript + Pinia + Vue Router 4입니다. 이미 많은 분이 이 조합을 사용하고 있겠지만, 왜 이 도구들을 선택했는지 설명해 보겠습니다.
Vite는 길게 설명할 필요가 없습니다. 개발 경험이 Webpack보다 훨씬 좋고, 핫 업데이트도 거의 즉시 적용됩니다. Vue 3.6은 아직 alpha 단계지만 Evan You가 Vue.js Nation 2025 발표에서 공개한 수치는 상당히 놀랍습니다. Vapor Mode는 100밀리초 안에 컴포넌트 10만 개를 마운트할 수 있습니다. 아직 프로덕션에서 사용할 단계는 아니지만 방향은 옳다고 봅니다.
특히 Pinia를 이야기하고 싶습니다. 솔직히 Pinia의 API 설계를 처음 봤을 때 든 생각은 “상태 관리는 원래 이렇게 생겨야 하는구나”였습니다. 비교해 보겠습니다.
// Vuex 방식(복잡함)
const store = createStore({
state: () => ({ count: 0 }),
mutations: {
increment(state) { state.count++ }
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => commit('increment'), 1000)
}
}
})
// Pinia 방식(깔끔함)
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => count.value++
const incrementAsync = () => setTimeout(increment, 1000)
return { count, increment, incrementAsync }
})
Pinia는 크기가 약 1.5KB에 불과하고 Vuex 5 개발은 사실상 정체되어 있습니다. 프로젝트가 이미 Vuex에 깊이 의존하고 있고 마이그레이션 계획도 없는 경우가 아니라면, 새 프로젝트에서 굳이 Vuex를 사용할 필요는 없습니다.
프로젝트 디렉터리 구조 설계
모든 컴포넌트를 무작정 components 아래에 쌓아 두었다가 세 달 뒤에는 자신조차 파일을 찾지 못하는 프로젝트를 너무 많이 봤습니다.
작은 프로젝트라면 디렉터리 구조를 어떻게 짜도 큰 문제가 없지만, 프로젝트가 커지면 규칙이 없는 구조는 재앙이 됩니다. 우리 팀도 여러 방식을 시도한 끝에 다음 구조로 정착했습니다.
src/
├── api/ # API 인터페이스 계층
│ ├── modules/ # 비즈니스 모듈별 분리
│ │ ├── user.ts
│ │ └── order.ts
│ └── index.ts
├── assets/ # 정적 리소스
│ ├── images/
│ └── styles/
├── components/ # 전역 공용 컴포넌트
│ ├── base/ # 기본 컴포넌트(Button, Input 등)
│ └── business/ # 공용 비즈니스 컴포넌트
├── composables/ # 조합 함수
│ ├── useAuth.ts
│ └── useRequest.ts
├── layouts/ # 페이지 레이아웃 컴포넌트
├── router/ # 라우트 설정
│ ├── modules/ # 라우트 모듈
│ └── index.ts
├── stores/ # Pinia 상태 관리
│ ├── modules/
│ └── index.ts
├── types/ # 전역 타입 정의
│ ├── api.d.ts
│ └── global.d.ts
├── utils/ # 유틸리티 함수
├── views/ # 페이지 컴포넌트
│ ├── user/
│ └── order/
├── App.vue
└── main.ts
여기에는 몇 가지 핵심 포인트가 있습니다.
composables와 utils의 차이: composables에는 useAuth, useRequest처럼 반응형 로직을 포함한 조합 함수를 둡니다. utils에는 formatDate, debounce 같은 순수 유틸리티 함수를 둡니다. 이 둘을 섞는 경우가 많은데, 나중에 유지보수가 상당히 힘들어집니다.
파일 유형이 아닌 기능 도메인별 분리: api, stores, views 내부도 비즈니스 모듈별로 한 단계 더 나눕니다. 그러면 사용자 관련 항목은 모두 user 폴더에 있어 파일을 논리적으로 찾기 쉽습니다.
types 별도 분리: 전역 타입 정의는 types 디렉터리에 두고, 컴포넌트 내부 타입은 컴포넌트 파일 안에 작성할 수 있습니다. 모든 타입을 하나의 types 파일에 몰아넣으면 오히려 더 복잡해집니다.
TypeScript 타입 정의 모범 사례
솔직히 TS의 복잡한 타입 활용은 진입 장벽이 꽤 높지만, 다음 몇 가지 상황만 익혀도 대부분의 요구 사항을 처리할 수 있습니다.
먼저 tsconfig.json에서 다음 설정은 반드시 활성화해야 합니다.
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
strict: true는 필수입니다. 처음에는 오류가 많이 나더라도 장기적으로는 확실히 가치가 있습니다. moduleResolution: "bundler"는 비교적 새로운 옵션이며 Vite와 함께 사용할 때 경험이 더 좋습니다.
다음은 Vue 컴포넌트의 타입 정의입니다. 처음 defineProps<Props>() 문법을 봤을 때는 제네릭이 어떻게 전달되는지 몰라 몇 초 동안 멈칫했습니다. 사실 이것은 Vue 컴파일러가 처리해 주는 기능입니다.
<script setup lang="ts">
// Props 타입 정의
interface Props {
title: string
count?: number
items: string[]
}
const props = defineProps<Props>()
// 기본값이 있는 Props
const propsWithDefaults = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
// Emits 타입 정의
interface Emits {
(e: 'update', value: string): void
(e: 'delete', id: number): void
}
const emit = defineEmits<Emits>()
// 또는 더 간결한 방식 사용(Vue 3.3+)
const emit2 = defineEmits<{
update: [value: string]
delete: [id: number]
}>()
</script>
또 하나 자주 문제가 되는 부분은 .vue 파일의 타입 인식입니다. IDE에서 모듈을 찾을 수 없다는 오류가 난다면 src 디렉터리에 env.d.ts 또는 shims-vue.d.ts를 만들어야 합니다.
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
Pinia 상태 관리 실전
Vuex의 mutations에 익숙하다면 Pinia에서 state를 처음 직접 수정할 때 뭔가 규칙을 어기는 듯한 느낌이 듭니다. 나중에 생각해 보니 mutations 규칙은 사실 Flux 아키텍처에서 남은 부담이었고, Pinia는 이를 과감하게 없앤 것입니다.
우리 팀은 Composition API 방식으로 Store를 정의하는 것을 권장합니다.
// stores/modules/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { getUserInfo, login } from '@/api/modules/user'
export const useUserStore = defineStore('user', () => {
// state
const token = ref<string>('')
const userInfo = ref<UserInfo | null>(null)
// getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => userInfo.value?.name ?? '게스트')
// actions
const setToken = (newToken: string) => {
token.value = newToken
localStorage.setItem('token', newToken)
}
const fetchUserInfo = async () => {
try {
const res = await getUserInfo()
userInfo.value = res.data
} catch (error) {
console.error('사용자 정보 조회 실패', error)
}
}
const logout = () => {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return {
token,
userInfo,
isLoggedIn,
userName,
setToken,
fetchUserInfo,
logout
}
})
사용할 때 주의할 점이 하나 있습니다. 구조 분해하면 반응성을 잃게 되므로 storeToRefs()를 사용해야 합니다.
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// 잘못된 방식: 구조 분해 후 반응성을 잃음
const { userName, isLoggedIn } = userStore
// 올바른 방식
const { userName, isLoggedIn } = storeToRefs(userStore)
// actions는 일반 함수이므로 직접 구조 분해 가능
const { logout, fetchUserInfo } = userStore
영속 저장에는 pinia-plugin-persistedstate 플러그인을 권장합니다. 설정도 간단합니다.
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// store에서 활성화
export const useUserStore = defineStore('user', () => {
// ...
}, {
persist: true // 또는 세부 옵션 설정
})
Vue Router 4 타입 안전 라우팅 설정
라우트 가드를 많이 작성하다 보면 모든 로직을 beforeEach에 넣고 싶어집니다. 예전에 한 프로젝트에서는 beforeEach 코드가 거의 200줄에 달했고, 권한 검사, 분석 추적, 제목 설정을 모두 몰아넣었습니다. 나중에 여러 가드로 분리하고 나서야 유지보수가 조금 수월해졌습니다.
먼저 라우트 설정의 타입 정의를 살펴보겠습니다.
// router/index.ts
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
// 라우트 meta 타입 확장
declare module 'vue-router' {
interface RouteMeta {
title?: string
requiresAuth?: boolean
roles?: string[]
}
}
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '홈',
requiresAuth: false
}
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: {
title: '대시보드',
requiresAuth: true,
roles: ['admin', 'editor']
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
권한 검사 가드는 다음과 같이 작성할 수 있습니다.
// router/guards/auth.ts
import type { Router } from 'vue-router'
import { useUserStore } from '@/stores/modules/user'
export function setupAuthGuard(router: Router) {
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 로그인이 필요 없는 페이지는 바로 통과
if (!to.meta.requiresAuth) {
next()
return
}
// 로그인하지 않았다면 로그인 페이지로 이동
if (!userStore.isLoggedIn) {
next({ path: '/login', query: { redirect: to.fullPath } })
return
}
// 권한 검사
const { roles } = to.meta
if (roles && roles.length > 0) {
const hasRole = roles.some(role => userStore.userInfo?.roles?.includes(role))
if (!hasRole) {
next({ path: '/403' })
return
}
}
next()
})
}
라우트 모듈화는 비즈니스 도메인별로 여러 파일로 나눈 뒤 index.ts에서 병합하는 방식을 권장합니다.
// router/modules/user.ts
export const userRoutes: RouteRecordRaw[] = [
{
path: '/user',
name: 'User',
component: () => import('@/layouts/BasicLayout.vue'),
children: [
{
path: 'profile',
name: 'UserProfile',
component: () => import('@/views/user/profile.vue')
}
]
}
]
코드 규칙과 엔지니어링 설정
ESLint 9가 flat config로 바뀐 뒤 기존 설정을 마이그레이션하는 데 꼬박 오후 한나절이 걸렸습니다. 이전 .eslintrc 설정은 더 이상 사용할 수 없으며, 새 설정은 다음과 같습니다.
// eslint.config.js
import js from '@eslint/js'
import vue from 'eslint-plugin-vue'
import typescript from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import vueParser from 'vue-eslint-parser'
export default [
js.configs.recommended,
...vue.configs['flat/recommended'],
{
files: ['**/*.{ts,tsx,vue}'],
languageOptions: {
parser: vueParser,
parserOptions: {
parser: tsParser,
sourceType: 'module'
}
},
plugins: {
'@typescript-eslint': typescript
},
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn'
}
}
]
Prettier와 ESLint가 충돌하는 문제는 eslint-config-prettier로 해결할 수 있습니다. 이 패키지는 ESLint에서 Prettier와 충돌하는 규칙을 비활성화합니다.
unplugin-auto-import는 Vue, Vue Router, Pinia의 API를 자동으로 임포트하여 수많은 import 문을 생략하게 해 주는 유용한 도구입니다.
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({
plugins: [
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
eslintrc: {
enabled: true
}
})
]
})
설정하고 나면 코드에서 ref(), computed()를 직접 작성할 수 있고 수동 import가 필요 없습니다.
Git 커밋 규칙에는 husky + lint-staged를 사용하여 커밋 전에 lint 검사를 자동으로 실행합니다.
// package.json
{
"scripts": {
"prepare": "husky install",
"lint": "eslint . --fix",
"lint-staged": "lint-staged"
},
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix", "prettier --write"]
}
}
마무리하며
여기까지 쓰고 보니 처음 Vue 3를 접했을 때 어찌할 바를 몰랐던 기억이 떠오릅니다. 당시에는 Composition API가 막 나왔고, 커뮤니티에서는 이를 사용할지 Options API를 고수할지를 두고 논쟁이 이어졌습니다. 튜토리얼 품질도 제각각이라 시행착오가 일상이었습니다.
이 구성이 모든 프로젝트에 적합한 것은 아닙니다. 작은 프로젝트라면 상태 관리에 provide/inject만 사용해도 충분하고 디렉터리 구조도 이렇게 세분화할 필요가 없습니다. 하지만 중대형 엔터프라이즈 프로젝트에서 세 명 이상이 협업한다면 이 아키텍처가 상당한 수고를 덜어 줄 것입니다.
기술 스택 선정에 만능 해결책은 없습니다. 가장 중요한 것은 팀이 합의하고 규칙을 만들며 꾸준히 지키는 것입니다. 이 글이 참고가 되어 시행착오를 줄이는 데 도움이 되길 바랍니다.
여러분의 팀은 Vue 3 프로젝트를 어떻게 구성하고 있나요? 좋은 실전 경험이 있다면 댓글로 공유해 주세요.
Vue 3 + TypeScript 엔터프라이즈 프로젝트 아키텍처 구축 전체 과정
프로젝트 초기화부터 코드 규칙까지 Vite, Pinia, Vue Router, ESLint 등의 모범 사례를 포함한 전체 설정 절차
Estimated time: PT4H
-
1
Step 1: 프로젝트 초기화와 기술 스택 선정
Vite 프로젝트 생성: -
2
Step 2: 프로젝트 디렉터리 구조 설계
디렉터리 구조를 기능 도메인별로 분리: -
3
Step 3: TypeScript 타입 정의 설정
tsconfig.json 핵심 설정: -
4
Step 4: Pinia 상태 관리 설정
Composition API 방식으로 Store 정의 권장: -
5
Step 5: const userInfo = ref<UserInfo
null>(null); -
6
Step 6: Vue Router 4 타입 안전 라우팅 설정
라우트 meta 타입 확장: -
7
Step 7: ESLint 9와 코드 규칙 설정
ESLint 9에서는 flat config 사용:
FAQ
Pinia와 Vuex의 차이점은 무엇이며, 왜 Pinia를 권장하나요?
API 설계 비교:
Vuex는 state, mutations, actions의 3단 구조가 필요합니다.
const store = createStore({
state: () => ({ count: 0 }),
mutations: { increment(state) { state.count++ } },
actions: { incrementAsync({ commit }) { setTimeout(() => commit('increment'), 1000) } }
})
Pinia는 Composition API 방식이라 더 간결합니다.
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const increment = () => count.value++;
const incrementAsync = () => setTimeout(increment, 1000);
return { count, increment, incrementAsync };
})
Pinia의 장점:
• mutations가 필요 없음(state를 직접 수정)
• 더 나은 TypeScript 지원
• 더 작은 크기
• 더 직관적인 API
프로젝트가 이미 Vuex에 깊이 의존하고 있고 마이그레이션 계획도 없는 경우가 아니라면, 새 프로젝트에서 굳이 Vuex를 사용할 필요는 없습니다.
Vue 3 프로젝트 디렉터리 구조는 어떻게 설계해야 하나요? composables와 utils의 차이는 무엇인가요?
• src/api/modules/(user.ts, order.ts처럼 비즈니스 모듈별 분리)
• src/components/base/와 business/(기본 컴포넌트와 공용 비즈니스 컴포넌트)
• src/composables/(useAuth, useRequest 같은 반응형 로직을 포함한 조합 함수)
• src/utils/(formatDate, debounce 같은 순수 유틸리티 함수)
• src/stores/modules/(비즈니스 모듈별 Pinia 상태 관리)
• src/views/(user/, order/처럼 비즈니스 모듈별 페이지 컴포넌트)
• src/types/(전역 타입 정의)
composables와 utils의 차이:
• composables에는 useAuth, useRequest 같은 반응형 로직을 포함한 조합 함수를 둡니다.
• utils에는 formatDate, debounce 같은 순수 유틸리티 함수를 둡니다.
• 이 둘을 섞으면 나중에 유지보수가 상당히 힘들어집니다.
기능 도메인별 분리의 장점:
• api, stores, views 내부도 비즈니스 모듈별로 한 단계 더 나눕니다.
• 그러면 사용자 관련 항목은 모두 user 폴더에 있어 파일을 논리적으로 찾기 쉽습니다.
Vue 3에서 TypeScript는 어떻게 설정하며, 핵심 포인트는 무엇인가요?
• strict: true 활성화(필수입니다. 처음에는 오류가 많이 나더라도 장기적으로는 확실히 가치가 있습니다.)
• moduleResolution: 'bundler'(Vite와 함께 사용할 때 경험이 더 좋으며 비교적 새로운 옵션입니다.)
• paths에 @/* 별칭 설정
Vue 컴포넌트 타입 정의:
• defineProps<Props>() 제네릭 문법 사용:
interface Props { title: string; count?: number; items: string[] }
const props = defineProps<Props>()
• 기본값은 withDefaults(defineProps<Props>(), { count: 0, items: () => [] }) 사용
• Emits 타입은 defineEmits<Emits>() 또는 더 간결한 방식 사용:
const emit2 = defineEmits<{ update: [value: string]; delete: [id: number] }>()
.vue 파일 타입 인식:
src 디렉터리에 env.d.ts 또는 shims-vue.d.ts를 만듭니다.
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
IDE에서 모듈을 찾을 수 없다는 오류가 나면 대개 이 파일이 제대로 설정되지 않은 경우입니다.
Pinia 상태 관리는 어떻게 사용하며, 주의할 점은 무엇인가요?
defineStore('user', () => {
const token = ref<string>('');
const userInfo = ref<UserInfo | null>(null);
const isLoggedIn = computed(() => !!token.value);
const fetchUserInfo = async () => {
try {
const res = await getUserInfo();
userInfo.value = res.data;
} catch (error) {
console.error('사용자 정보 조회 실패', error);
}
};
return { token, userInfo, isLoggedIn, fetchUserInfo };
})
사용 시 주의 사항:
• 반응성을 유지하며 구조 분해하려면 storeToRefs()를 사용합니다: const { userName, isLoggedIn } = storeToRefs(userStore)
• actions는 일반 함수이므로 직접 구조 분해할 수 있습니다: const { logout, fetchUserInfo } = userStore
영속 저장:
• pinia-plugin-persistedstate 설치
• main.ts에서 pinia.use(piniaPluginPersistedstate) 호출
• store에서 persist: true를 활성화하거나 세부 옵션 설정
Vuex의 mutations에 익숙하다면 Pinia에서 state를 처음 직접 수정할 때 뭔가 잘못하는 듯한 느낌이 듭니다. 하지만 mutations 규칙은 사실 Flux 아키텍처에서 남은 부담이며, Pinia는 이를 과감하게 없앴습니다.
ESLint 9의 flat config는 어떻게 설정하며 이전 버전과 무엇이 다른가요?
새 설정 방식:
• eslint.config.js 생성
• js.configs.recommended와 vue.configs['flat/recommended'] 사용
• files: ['**/*.{ts,tsx,vue}'] 설정
• languageOptions.parser를 vue-eslint-parser로 설정
• parserOptions.parser를 @typescript-eslint/parser로 설정
• plugins에 @typescript-eslint 설정
• rules에 세부 규칙 설정('vue/multi-word-component-names': 'off', '@typescript-eslint/no-unused-vars': 'warn')
Prettier 통합:
• eslint-config-prettier로 ESLint와 Prettier 충돌 해결
• 이 패키지는 ESLint에서 Prettier와 충돌하는 규칙을 비활성화합니다.
자동 임포트:
• unplugin-auto-import 설치
• vite.config.ts에서 AutoImport({ imports: ['vue', 'vue-router', 'pinia'], dts: 'src/auto-imports.d.ts' }) 설정
• 설정 후 코드에서 ref(), computed()를 직접 작성해도 수동 import가 필요 없습니다.
Git 커밋 규칙:
• husky + lint-staged 설정
• package.json에 lint-staged 설정("*.{js,ts,vue}": ["eslint --fix", "prettier --write"])
• 커밋 전에 lint 검사 자동 실행
Vue Router 4에서 타입 안전 라우팅과 권한 가드는 어떻게 설정하나요?
declare module 'vue-router' {
interface RouteMeta {
title?: string;
requiresAuth?: boolean;
roles?: string[];
}
}
라우트 설정:
• RouteRecordRaw[] 타입 사용
• meta에 title, requiresAuth, roles 등 정의
권한 검사 가드:
• router/guards/auth.ts에 setupAuthGuard 함수 생성
• router.beforeEach에서 to.meta.requiresAuth 확인(로그인이 필요 없는 페이지는 바로 통과)
• 로그인하지 않았다면 로그인 페이지로 이동: next({ path: '/login', query: { redirect: to.fullPath } })
• roles 권한 검사: const hasRole = roles.some(role => userStore.userInfo?.roles?.includes(role))
• 권한이 부족하면 403으로 이동
라우트 모듈화:
• 비즈니스 도메인별로 여러 파일로 분리(router/modules/user.ts)
• index.ts에서 병합
라우트 가드를 많이 작성하다 보면 모든 로직을 beforeEach에 넣고 싶어집니다. 예전에 한 프로젝트에서는 beforeEach 코드가 거의 200줄에 달했고, 권한 검사, 분석 추적, 제목 설정을 모두 몰아넣었습니다. 나중에 여러 가드로 분리하고 나서야 유지보수가 조금 수월해졌습니다.
4분 읽기 · 게시일: 2025년 11월 24일 · 수정일: 2026년 9월 8일
프론트엔드 프레임워크
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
React 19 폼에 아직도 코드 30줄? Actions로 간소화하고 성능은 40% 향상
React 19의 Actions, use() Hook, Compiler 등 6가지 핵심 기능을 깊이 있게 살펴봅니다. 실전 코드 비교를 통해 폼 처리를 간소화하고 성능을 최적화하며 Server Components를 활용하는 방법을 소개합니다. 일주일간 직접 사용해 본 경험을 바탕으로 빠른 시작을 돕습니다.
5편 중 3편
다음
프런트엔드 성능 최적화 실전: Core Web Vitals 만점 공략
Core Web Vitals의 세 가지 핵심 지표(LCP/INP/CLS)를 체계적으로 설명하고 이미지 최적화, 코드 분할, 지연 로딩 등 10가지 이상의 실전 방법을 소개합니다. 2주 안에 60점에서 90점 이상으로 높인 사례와 전체 최적화 체크리스트, 주의 사항까지 담았습니다.
5편 중 5편



댓글
GitHub로 로그인하여 댓글을 남기세요