⚙️ Vue 작동 원리와 구조 이해 - 앱 실행부터 반응형 렌더링까지
Vue 기초 시리즈 > 3️⃣ Vue 작동 원리와 구조 이해 > 1️⃣ Vue 앱 실행 흐름 완전 이해
- 1. Vue 앱의 실행 순서 개요
- 2. main.js (또는 main.ts) 역할
- 3. App.vue의 구조와 루트 컴포넌트
- 4. 마운트란 무엇인가?
- 5. 데이터 변경 → DOM 갱신까지의 반응형 흐름
- 6. 실무 구성 팁 및 주의사항
- 🔚 마무리 및 다음 글 안내
✅ Vue 앱의 실행 흐름 (SPA, Vue 3 + Router + Axios + Store + Interceptor 기준)
Vue 앱은 크게 아래 순서로 실행됩니다:
- main.js에서 Vue 앱 인스턴스를 생성, App.vue를 루트 컴포넌트로 지정, Vue 내부가 컴포넌트를 분석하고 DOM에 mount
- 이후 안에서든 밖에서든 데이터가 바뀌면 Virtual DOM → 실제 DOM 갱신
1. main.js 또는 main.ts 진입
const app = createApp(App); // 앱 인스턴스 생성
app.use(router); // 라우터 등록
app.use(store); // Vuex 등록
app.use(interceptors); // axios interceptor 등록 (mitt 등 포함)
app.mount('#app'); // DOM에 마운트
2. App.vue 렌더링 시작
<template>
<Popup ref="popupRef" />
<router-view />
</template>
<router-view />
는 현재 경로에 맞는 컴포넌트를 표시함- 즉, 라우터에서 경로를 먼저 판단해야 렌더링 가능
3. router/index.js
→ 경로 분석 시작
router.beforeEach((to, from, next) => {
// 로그인 여부 체크
// 권한 조건 충족 여부 판단
// next() 또는 next('/login') 등 분기
});
- 여기서
localStorage
토큰 검사, 인증 API 호출 등이 들어감 - 권한이 부족하면
/login
이나/403
으로 강제 이동
router는 화면 진입 전에 작동
- 사용자가 어떤 URL로 이동하려 할 때
- 즉, <router-link> 클릭하거나 this.$router.push() 호출하거나 직접 주소 입력했을 때
- → 이때 router.beforeEach()가 실행돼서 인증 여부, 권한 체크 등을 선행 처리함
router.beforeEach((to, from, next) => {
if (!isLoggedIn && to.meta.requiresAuth) {
next('/login');
} else {
next(); // 라우팅 승인
}
});
4. 라우팅 승인 → router-view
가 실제 컴포넌트를 렌더링
- 예:
/main
→Main.vue
렌더링 - 이때부터 각 컴포넌트의
created
,mounted
훅 실행
5. Main.vue
또는 다른 페이지에서 API 요청 발생
this.$api.get('/api/v1/user') // 이때 axios가 interceptors 통과
6. axios 요청 → interceptor 작동
axios.interceptors.request.use(config => {
// 요청 직전: 토큰 추가, 로깅 등
return config;
});
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
mitt.emit('auth-expired');
}
return Promise.reject(error);
}
);
mitt + interceptor는 API 요청 시점에 작동
- 예: axios.get('/api/v1/user') 호출 시
- 이 요청이 보내지기 직전과 응답을 받은 직후에 interceptor가 개입
- 그리고 에러 발생 시나 특정 응답 상태일 때 mitt.emit()으로 전역 이벤트 발생
axios.interceptors.response.use(
res => res,
err => {
if (err.response?.status === 401) {
mitt.emit('auth-expired'); // 예: 토큰 만료 처리
}
return Promise.reject(err);
}
);
7. API 결과에 따라 상태 변경 (store 활용)
store.commit('SET_USER', userData); // 상태 반영
- 상태 변경되면 연관된 컴포넌트 자동으로 re-render (반응형)
8. 화면이 자동 갱신됨 (Virtual DOM → DOM)
watch
,computed
등이 반응형으로 작동- 사용자의 액션에 따라 다시 router 이동 or API 호출 반복
📌 정리: Vue 앱 실행 순서 전체 흐름
1. main.js 진입
└ createApp → app.use(plugin) → app.mount()
2. App.vue 진입
└ <router-view /> 에 따라 route 컴포넌트 결정
3. router.beforeEach() 실행
└ 로그인 여부/권한 체크 후 페이지 접근 결정
4. 페이지 컴포넌트 렌더링 (created/mounted 실행)
5. API 요청 발생 → axios interceptor 작동
└ token 추가, 응답 에러 핸들링, mitt 이벤트 발생 등
6. 응답 받아 상태(store) 변경 → 화면 자동 갱신
7. 사용자 액션 → router 이동 or API 재요청 반복
✅ main.js 또는 main.ts의 역할
// 기본 구조 예시 (Vite 기준)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App)
.use(router)
.mount('#app')
createApp()으로 앱을 만들고, mount()로 실제 DOM(#app)에 연결합니다.
import { createApp, nextTick } from "vue/dist/vue.esm-bundler";
import App from "@/App.vue";
import api from "@/common/axios/api";
import apiInterceptors from "@/common/axios/interceptors";
import queue from "@/common/axios/apiCommon"; // api 요청목록
import { utils } from "@/common/utils";
/** 플러그인 import
* app.use()로 등록되는 전역 기능
*/
import store from "@/common/store"; // store/index.js (vuex 라이브러리)
import Popup from "@/common/utils/popup"; // popup/index.js 내부구조는 install(app) 메서드를 포함한 Vue 플러그인 객체로 App.vue에서 <Popup ref=popupRef"/>로 UI렌더링 해야 함
import routes from "@/routes"; // routes/index.js (vue-router 라이브러리)
import mitts from "mitt"; // vue 제공 기본 에미터
import TitleHeader from "@/components/template/TitleHeader.vue"; // <TitleHeader/> 처럼 사용할 컴포넌트로 등록 (.vue경로까지)
import "./assets/main.css"; // 공통 css
import Content from "./components/template/Content.vue"; // 컴포넌트 요소
...
import { ref } from "vue";
const app = createApp(App); //들어갈 수 있는 vue파일은 형식이 정해져있음 export default {...
...
//가져온 요소
const mitt = mitts(); // 에미터를 반환하는 함수를 리턴하는 이벤트 버스 라이브러리
apiInterceptors(mitt); // 해당 함수를 실행할 때 가로채는 api
app.config.globalProperties.$api = api; // 전역 앱객체의 속성으로 설정
app.config.globalProperties.$queue = queue;
app.config.globalProperties.$emitter = mitt;
app.config.globalProperties.$utils = utils;
const popupRef = ref(null);
// app.config.globalProperties.$popup = (opts) => {
// if (popupRef.value) {
// // Object.assign(popupRef.value, opts);
// Object.entries(opts).forEach(([key, val]) => {
// popupRef.value[key] = val; // ✅ props로 넘기지 말고 직접 할당
// });
// popupRef.value._resolve = opts._resolve; // ✅ 반드시 필요
// // ✅ show 값을 맨 마지막에 true로 세팅해야 화면 렌더링 순서가 맞아짐
// popupRef.value.show = opts.type === "search";
// popupRef.value.msgShow = opts.type !== "search";
// console.error("✅popupRef가 연결");
// } else {
// console.error("popupRef가 연결되지 않음");
// }
// };
/**
* 직접 별도파일로 빼서 provide할수도 있겠지만 여기서는 바로 앱 객체에 등록함, 유틸처럼 재사용이나 복잡한 로직의 경우 분리하는 케이스필요하면 그땐 분리하는게 좋음.
*/
const popupHandler = ({ type = "search" || "alert" || "confirm" || "fnAlert", style = { width: null, height: null }, componentType = null, msg = null, props = null, ok = null, cancel = null }) => {
return new Promise((resolve) => {
nextTick(() => {
app.config.globalProperties.$popup({
type,
msg,
style,
componentType,
props,
ok, // ✅ 기존 콜백도 여전히 넘겨줌
// ok: (...args) => resolve({ ok: true, args }),
cancel: (...args) => resolve({ ok: false, args }), //사용자의 취소도 정상적인 흐름이기 때문에 resolve로 처리함. resolve로만 처리하면 사용할 때 .then(({ok})={if(ok){...} else {...} }); then하나로 처리할 수 있음. reject로 작성하면 catch도 반드시 써야하기 때문에 로직이 분산될 수 있음. 실제 오류는 reject로 처리하는 것이 맞음.
_resolve: resolve,
});
});
});
};
/**
* 팝업을 원하는 컴포넌트 내에서 호출(inject)하기 위해 주입
* 업은 다른 요소와 달리 실제 화면에 그려져야 하는 것이기 때문에 <Popup /> Dom에 선언 위치가 필요함 (UI 분리. <template>이나 App.vue에서 1번만 선언)
* provide/inject 방식으로 Promise처리가 가능
*/
app.provide("popupHandler", popupHandler);
app.provide("$api", queue);
app.provide("api", queue);
app.component("TitleHeader", TitleHeader);
app.component("ButtonArea", ButtonArea);
app.component("SearchArea", SearchArea);
app.component("ContextMenu", ContextMenu);
app.component("Content", Content);
app.component("Loading", Loading);
app.component("DynamicSelect", DynamicSelect);
app.component("DynamicInputBox", DynamicInputBox);
/**
* 플러그인 등록
* 앱초기 실행 흐름
* 순서대로 초기화가 되기 때문에 나중에 등록한 플러그인은 앞 플러그인의 결과나 context에 의존합니다.
* router가 먼저 등록되어 라우터 관련 전역설정 (beforeEach, afterEach)가 가장 먼저 적용되고
* store는 그 다음이라서, 라우터가 스토어에 접근하려면 이 순서가 맞아야 함. app.use(MyPlugin) 을 router보다 먼저 실행하면서 app.config.globalProperties.$router 같은건 undefined가 되어버림
* router/index.js에서는 this.$store를 쓸 수 없고 import store 방식으로 가능
*/
app.use(routes); // ✅ 여기서 라우터를 등록. → rounter.beforeEach() 실행 → next() 승인 아래줄 실행
app.use(store); // (Vuex 등 상태관리 등록)
app.use(Popup);
app.mount("#app"); // App.vue를 root로 연결 → app.mount('#app') → <router-view />에 라우트 컴포넌트 표시
// nextTick(() => {
// const rootInstance = app._instance?.proxy;
// if (rootInstance?.$refs?.popupRef) {
// popupRef.value = rootInstance.$refs.popupRef;
// // console.log("✅ popupRef 연결 완료!2", popupRef.value);
// } else {
// console.warn("❌ popupRef 연결 실패 (nextTick)2");
// }
// });
✅ App.vue = 루트 컴포넌트, 최상위 레이아웃
모든 Vue 앱의 시작점은 App.vue입니다. 여기서 전역 템플릿, 라우터 뷰, 공통 컴포넌트 등을 선언합니다.
(공통 컴포넌트 : 전역 Popup, Loading, Modal, 알림창 같은 항상 표시되어야 할 컴포넌트 )
<template>
<popup ref="popupRef"/>
<Loading />
<router-view />
</template>
<script setup>
// 상태 관리, 전역 컴포넌트 import 등
</script>
✅ 마운트란?
mount()는 Vue 가상 DOM 구조를 실제 DOM에 연결하는 과정입니다.
- Vue는 내부적으로 DOM 구조를 추상화한 Virtual DOM을 만듭니다.
- .mount('#app')이 호출되면, 해당 요소를 기준으로 HTML이 실제로 렌더링됩니다.
✅ 데이터가 변경되면 어떻게 화면이 바뀌나?
Vue는 Proxy 기반의 반응형 시스템을 통해 상태가 바뀌면 자동으로 DOM을 갱신합니다.
import { ref } from 'vue';
const count = ref(0);
function increment() {
count.value++; // 여기서 value가 바뀌면 DOM 자동 갱신
}
변경 → Virtual DOM 재계산 → 실제 DOM 최소 갱신 구조입니다.
✅ 실무 구성 팁
- main.js에는 router, store, 전역 컴포넌트 등록 등 핵심 설정만 남길 것
- App.vue에는 router-view만 두고, 레이아웃/타이틀 등은 별도 컴포넌트로 분리
- 마운트 전후 처리:
onMounted()
또는mounted()
활용
🔚 다음 글 예고
다음 글에서는 Vue 컴포넌트의 라이프사이클에 대해 다룹니다.
각 단계(setup, created, mounted 등)가 어떤 타이밍에 실행되는지, 어떤 용도로 써야 하는지를 완전히 정리해드립니다.
👉 다음 글: Vue 컴포넌트 라이프사이클 완전정리
'framework_library > vue' 카테고리의 다른 글
⚙️ Vue.js 렌더링 흐름 - 템플릿과 DOM 업데이트 구조 이해하기 (0) | 2025.05.01 |
---|---|
⚙️ Vue 컴포넌트 라이프사이클 완전 정복 (0) | 2025.05.01 |
🌿 Vue 템플릿 조건부 렌더링과 반복 처리 패턴 (0) | 2025.05.01 |
🌿 Vue.js 컴포넌트 슬롯(Slot)의 개념과 활용법 (0) | 2025.05.01 |
🌿 컴포넌트 간 상태 공유 - Provide / Inject 완전정복 (1) | 2025.05.01 |
댓글