본문 바로가기
framework_library/vue

⚙️ Vue 작동 원리와 구조 이해 - 앱 실행부터 반응형 렌더링까지

by 죄니안죄니 2025. 5. 1.

⚙️ Vue 작동 원리와 구조 이해 - 앱 실행부터 반응형 렌더링까지

Vue 기초 시리즈 > 3️⃣ Vue 작동 원리와 구조 이해 > 1️⃣ Vue 앱 실행 흐름 완전 이해


✅  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가 실제 컴포넌트를 렌더링

  • 예: /mainMain.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 컴포넌트 라이프사이클 완전정리

댓글