VUE 의 라이프 사이클 (매우 디테일)

Zain·2025년 4월 5일
post-thumbnail
<ChildComponent :ref="ChildComponentRef" />

자식 컴포넌트를 ref로 참조해 해당 컴포넌트의 DOM에 접근하려 했지만,
부모 컴포넌트에서 접근할 수 없었다.
이 문제를 계기로 Vue의 라이프사이클을 다시 한번 정리해보려 한다. (사실 라이프 사이클과는 별개임)


ref로 자식 컴포넌트에 접근했는데 DOM에 접근할 수 없는 이유

<script setup>을 사용하는 컴포넌트는 기본적으로 닫혀 있습니다. 즉, 템플릿 참조 또는 $parent 체인을 통해 검색>되는 컴포넌트의 공개 인스턴스는 <script setup> 내부에서 선언된 바인딩을 노출하지 않습니다.

vue 공식 문서를 보면 defineExpose()에 대한 설명으로 위와 같이 나와있음
즉. Vue 3의 보안 정책으로

  • 내부 ref, data, DOM 등은 기본적으로 private 취급
  • defineExpose()로 노출한 것만 public처럼 외부에서 접근 가능

vue의 생명주기


공식 문서에서도 이 부분은 충분히 직관적으로 설명되어 있음.

1️⃣ 브라우저가 HTML 요청 → index.html 로딩

public/index.html이 브라우저에 의해 먼저 로드됨
이 안에 <div id="app"></div>와 <script type="module" src="/src/app.js"> 같은 초기 진입점이 있음

2️⃣ app.js 호출

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)
app.use(router)
app.mount('#app')
  • App.vue는 루트 컴포넌트
  • #app에 컴포넌트를 마운트하여 Vue의 제어가 시작됨

3️⃣ 루트 컴포넌트(App.vue) 렌더링

// App.vue
<template>
  <router-view />
</template>

router-view는 현재 URL에 매칭되는 라우트 객체에 등록된 컴포넌트를 실제로 화면에 렌더링하는 영역

4️⃣ router-view 렌더링

vue-router는 현재 URL을 기준으로 라우트를 매칭해서 router-view에 해당 컴포넌트를 동적으로 렌더링함

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes = [
  {
    path: '/',
    component: HomeView
  },
  {
    path: '/about',
    component: () => import('../views/AboutView.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

5️⃣ 컴포넌트 렌더링

<!-- 예시: HomeView.vue -->
<template>
  <div>{{ message }}</div>
</template>

<script setup>
import { ref, onBeforeMount, onMounted } from 'vue'

const message = ref('Hello!')
const CONSTANT_STATE = 'test'
  
console.log('script mounted!')
  
onBeforeMount(() => {
  console.log('before mounted!')
})
  
onMounted(() => {
  console.log('mounted!')
})
  
</script>

<style scoped>
div {
  color: blue;
}
</style>

초기 마운트

1. <script setup> 실행

  • Vue는 컴포넌트를 사용할 때 가장 먼저
  • 이때 ref 선언, 상수 선언, import 구문 등이 바로 실행됨.
import { ref, onBeforeMount, onMounted } from 'vue'

const message = ref('Hello!')
const CONSTANT_STATE = 'test'
console.log('script mounted!')

출력

script mounted!

2. 가상 DOM(VNode) 생성 준비

템플릿(<template>) 컴파일됨
setup() 함수에서 선언한 변수들이 템플릿에서 사용될 수 있도록 바인딩
아직 실제 DOM에 그려진 것은 아님 (가상DOM 형태)

3. onBeforeMount() 실행

실제 DOM에 컴포넌트가 붙기 직전 (가상 DOM은 준비된 상태)

// DOM에 접근 불가
onBeforeMount(() => {
  console.log('before mounted!')
})

4. DOM 렌더링 + 마운트 완료

  • 가상 DOM이 실제 DOM에 렌더링됨
  • 컴포넌트가 브라우저 화면에 보이기 시작

onMounted 호출

  • DOM이 모두 렌더링된 이후 호출됨
  • DOM에 접근 가능
onMounted(() => {
  console.log('onMounted - 이제 DOM 있음!');
});

업데이트 사이클

반응형 상태 변경시

1. onBeforeUpdate()

  • DOM 업데이트 직전에 호출
    → 아직 화면은 이전 상태

2. DOM 업데이트

  • 변경된 값 기반으로 화면 갱신
    가상 DOM -> 실제 DOM patching (새로운 DOM 상태가 화면에 반영된 시점)

3. onUpdated()

  • DOM이 갱신된 후 호출
    → 새로운 화면 상태에서 DOM 접근 가능

watch() 와 nextTick

watch는 반응형 변수의 값이 바뀔 때 바로 실행 (onBeforeUpdate 보다 먼저 호출)

DOM이 완전히 생성된 이후, nextTick() 호출을 원한다면?(초기 마운트 or 업데이트 사이클)

호출 시점의 컨텍스트 이후에 실행되기 때문에 onMounted(), onUpdated() 내부에서 호출해주면 됨
-> 두 훅 모두 돔이 랜더링 된 이후에 호출

많이들 오해하는 영역

nextTick()이 항상 DOM 렌더링 이후 호출된다고 생각하는 경우가 많음
정확히는 Vue의 현재 반응형 업데이트 및 렌더링 큐 작업이 끝난 다음 틱에서 실행됨
따라서 호출 시점의 컨텍스트 이후에 실행됨, 반드시 DOM이 렌더링된 이후라는 보장은 컨텍스트에 따라 달라질 수 있음


컴포넌트가 제거되는 시점

ex) PageA에서 router.push('/page-b')로 이동할 때

1. onBeforeUnmount()

  • 컴포넌트가 DOM에서 제거되기 직전 호출됨

2. onUnmounted()

  • 실제로 DOM에서 완전히 제거된 이후 호출됨

라이프 사이클 한눈에 확인하기

브라우저 HTML 요청  
→ index.html 로드  
→ app.js 실행  
→ createApp(App).mount('#app')  
→ 루트 컴포넌트 렌더링  
→ <script setup> 실행  
→ ref / 상수 선언  
→ setup() 실행  
→ 템플릿 컴파일 (가상 DOM 생성)  
→ onBeforeMount()  
→ 실제 DOM 렌더링  
→ onMounted() ← (이 시점부터 DOM 접근 가능)  
→ 반응형 데이터 변경  
→ onBeforeUpdate()  
→ DOM 업데이트  
→ onUpdated()  
→ router.push('/다른-페이지')  
→ onBeforeUnmount()  
→ onUnmounted()
profile
Vue, Laravel | TS, JS, PHP, MySQL

0개의 댓글