Vue - computed lifecycle

2026. 8. 1. 20:43·알면좋은것들
반응형

Vue를 쓰다 보면 이상한 지점에 도달한다. 같은 결과를 만드는 방법이 셋이나 있다. 할 일 목록의 개수에 따라 문구를 바꾸는 일을, methods로도 할 수 있고 computed로도 할 수 있고 watch로도 할 수 있다. 화면에 찍히는 글자는 똑같다.

똑같아 보이는데 셋이 존재한다는 건, 보이지 않는 곳에서 서로 다른 일이 벌어지고 있다는 뜻이다. 어떤 건 매번 다시 계산하고 어떤 건 캐시를 쓰며, 어떤 건 값을 만들고 어떤 건 일을 시킨다.

이 글은 그 기준을 정리한 것이다. 그리고 같은 질문의 연장선에서 Vue가 언제 무엇을 하는지(라이프사이클)와 화면을 어떻게 다시 그리는지(Virtual DOM)까지 이어 다룬다. querySelector를 쓰지 말라는 조언의 이유가 여기서 나온다.

이 글은 [Vue 컴포넌트 데이터 흐름 — props와 emit]에서 이어진다. [DOM과 Event 완전 정리]에서 다룬 직접 DOM 조작이 8번 항목에서 다시 등장한다.

 

📖 이 글에서 다루는 내용

  1. computed — 템플릿에서 로직을 걷어낸다
  2. computed vs methods — 캐싱이 가른다
  3. computed를 쓸 때 조심할 것
  4. watch — 변화를 감시하고 일을 시킨다
  5. computed와 watch, 무엇을 쓸 것인가
  6. 라이프사이클 훅 — Vue가 언제 무엇을 하는가
  7. onMounted와 onUpdated
  8. Virtual DOM — Vue가 화면을 다시 그리는 방식
  9. 실제 DOM이 정말 필요할 때 — 템플릿 ref
  10. 자주 걸려 넘어지는 지점들
  11. 핵심 요약

 


1. computed — 템플릿에서 로직을 걷어낸다

computed 는 "계산된 속성"을 정의하는 함수다. 미리 계산해 둔 값을 쓰게 해서 템플릿의 표현식을 단순하게 만들고, 불필요한 반복 연산을 줄인다.

문제 상황부터 보자.

const todos = ref([
  { text: 'Vue 실습' },
  { text: '자격증 공부' },
  { text: 'TIL 작성' }
])
<p>{{ todos.length > 0 ? '아직 남았다' : '퇴근!' }}</p>

동작은 한다. 문제는 둘이다.

  • 템플릿이 복잡해진다. 화면 구조를 읽으려는데 계산 로직이 섞여 있다
  • 여러 곳에서 쓰면 그대로 반복된다. 조건을 바꾸려면 모든 자리를 찾아 고쳐야 한다

computed로 빼면 이렇게 된다.

import { ref, computed } from 'vue'

const restOfTodos = computed(() => {
  return todos.value.length > 0 ? '아직 남았다' : '퇴근!'
})
<p>{{ restOfTodos }}</p>

특징 세 가지

  • 반환값은 computed ref다. 일반 ref처럼 .value로 참조하고, 템플릿에서는 .value를 생략한다
  • 의존하는 반응형 데이터를 자동으로 추적한다. todos를 썼다는 사실을 Vue가 기억한다
  • 의존 데이터가 변경될 때만 재평가된다. todos가 그대로면 다시 계산하지 않는다

세 번째가 methods와 갈리는 지점이다.

 


2. computed vs methods — 캐싱이 가른다

같은 일을 메서드로도 만들 수 있다.

const getRestOfTodos = function () {
  return todos.value.length > 0 ? '아직 남았다' : '퇴근!'
}
<p>{{ getRestOfTodos() }}</p>

화면에 찍히는 결과는 완전히 같다. 결정적인 차이는 캐싱이다.

  computed method
캐싱 의존하는 반응형 데이터를 기준으로 캐시된다 없다
재실행 시점 의존 데이터가 변경된 경우에만 재평가 다시 렌더링될 때마다 항상 실행
실행 트리거 의존 데이터 변경 시 자동 호출해야 실행

같은 값을 화면 세 곳에서 쓴다고 하자. method는 렌더링할 때마다 함수를 세 번 실행한다. computed는 한 번 계산하고 나머지 두 번은 캐시된 결과를 즉시 반환한다. 그리고 todos가 바뀌지 않는 한, 다음 렌더링에서도 다시 계산하지 않는다.

💡 캐시(Cache)란
데이터나 결과를 일시적으로 저장해 두는 임시 저장소다. 같은 것을 다시 계산하지 않고 빠르게 꺼내 쓰기 위한 장치다. 브라우저가 방문했던 페이지의 이미지를 저장해 뒀다가 재접속 시 즉시 보여주는 것과 같은 원리다.

그럼 무조건 computed인가

아니다. 목적이 다르다.

  이럴 때 쓴다
computed 의존하는 데이터에 따라 결과가 바뀌는 값을 만들 때. 같은 값을 여러 곳에서 쓸 때
method 단순히 특정 동작을 수행할 때. 인자를 받아야 할 때. 데이터 의존과 무관하게 항상 같은 결과를 낼 때

버튼 클릭 핸들러를 computed로 만들 수는 없다. 그건 값이 아니라 동작이다. 그리고 [Vue 조건부·리스트 렌더링] 편에서 본 것처럼, 인자를 받아야 하는 경우(중첩 v-for 안에서 항목마다 다른 필터링)에도 computed로는 만들 수 없어 메서드를 쓴다.

➡️ 무조건 computed가 아니라, 목적과 상황에 맞게 조합한다.

 


3. computed를 쓸 때 조심할 것

① 반환값은 읽기 전용이다

computed의 반환값은 의존하는 데이터에서 파생된 값이고, 이미 계산이 끝난 상태다. 일종의 스냅샷(snapshot) 이며 의존 데이터가 바뀔 때만 새 스냅샷이 만들어진다.

따라서 읽기 전용으로 취급해야 하고 직접 변경해서는 안 된다. 새 값을 원한다면 의존하는 원본 데이터를 바꾼다.

restOfTodos.value = '퇴근!'   // 이러면 안 된다
todos.value.push(newTodo)     // 원본을 바꾸면 계산이 따라온다

② 원본 배열을 변경하지 않는다

reverse()와 sort()는 원본 배열을 바꾼다. [JavaScript 객체와 배열 완전 정리]에서 본 변화 메서드다.

return numbers.reverse()        // 원본이 뒤집힌다
return [...numbers].reverse()   // 복사본을 만들어 뒤집는다

computed 안에서 원본을 건드리면, 계산이 원본을 바꾸고 그 변경이 다시 계산을 유발하는 구조가 된다. 정렬 전에 복사하는 습관이 필요하다.

 


4. watch — 변화를 감시하고 일을 시킨다

watch 는 하나 이상의 반응형 데이터를 감시하다가, 변경되면 콜백을 호출한다.

watch(source, (newValue, oldValue) => {
  // 변경됐을 때 할 일
})
인자 설명
source 감시 대상 (반응형 변수, 값을 반환하는 함수 등)
콜백 source가 변경될 때 호출된다
└ newValue 바뀐 값
└ oldValue 바뀌기 전 값 (선택)
const count = ref(0)

watch(count, (newValue, oldValue) => {
  console.log(`newValue: ${newValue}, oldValue: ${oldValue}`)
})
newValue: 1, oldValue: 0
newValue: 2, oldValue: 1
newValue: 3, oldValue: 2

여러 대상을 함께 감시하기

배열로 넘기면 된다. 콜백의 인자도 배열로 들어오므로 구조 분해로 받는다.

watch([foo, bar], ([newFoo, newBar], [prevFoo, prevBar]) => {
  // 둘 중 하나라도 바뀌면 실행된다
})

💡 watch는 기본적으로 게으르다(lazy)
등록하는 시점에는 콜백이 실행되지 않고, 값이 바뀌어야 비로소 실행된다. 처음에 한 번 실행하고 시작하고 싶다면 세 번째 인자에 옵션을 준다.

watch(keyword, fetchResults, { immediate: true })

⚠️ 객체를 감시하면 newValue와 oldValue가 같아진다
객체나 배열의 내부가 바뀐 경우, 두 값 모두 같은 객체를 가리키는 주소라서 이전 값을 구분할 수 없다. [JavaScript 기초 문법 정리]의 참조 타입 이야기 그대로다. 이전 값을 비교해야 한다면 특정 속성만 감시하거나 직접 복사본을 남겨 둬야 한다.

 


5. computed와 watch, 무엇을 쓸 것인가

둘 다 반응형 데이터의 변화에 반응하지만 목적이 다르다.



  computed watch
동작 의존 데이터로부터 계산된 값을 반환한다 변화를 감지해 작업을 수행한다 (side effect)
목적 계산 결과를 캐싱해 재사용 데이터 변화에 따른 특정 작업
예시 연산된 길이, 필터링된 목록 API 호출, 로컬 스토리지 저장, 외부 라이브러리 연동

➡️ 값을 만들면 computed, 일을 시키면 watch.

그리고 공통점이 하나 있다. 둘 다 감시하는 원본 데이터를 직접 변경하지 않는다.

📌 원본 노트의 watch 활용 예시는 computed로 써야 하는 경우다
노트에는 입력값의 길이를 구하는 예제가 watch로 되어 있다.

const message = ref('')
const messageLength = ref(0)
watch(message, (newValue) => { messageLength.value = newValue.length })

동작은 한다. 하지만 이건 값을 만드는 일이므로 바로 다음 절의 기준("값을 만들면 computed")에 정면으로 어긋난다. computed로 쓰면 한 줄이고, 상태를 하나 덜 만든다.

const messageLength = computed(() => message.value.length)

ref 두 개를 손으로 동기화하는 코드는 대개 computed로 대체할 수 있다는 신호다. watch가 맞는 경우는 입력이 바뀔 때 서버에 검색 요청을 보내는 것처럼, 값이 아니라 바깥세상에 영향을 주는 일을 할 때다.

 


6. 라이프사이클 훅 — Vue가 언제 무엇을 하는가

라이프사이클 훅(Lifecycle Hooks) 은 Vue 인스턴스의 생애주기 중 특정 시점에 실행되는 함수다. Vue가 컴포넌트를 만들고, 화면에 붙이고, 갱신하고, 걷어내는 각 단계 사이사이에 개발자가 코드를 끼워 넣을 수 있게 열어 둔 자리다.

시점을 순서대로 놓으면 이렇다.

시점 훅 이때 무엇을 하나
인스턴스 생성 <script setup> 본문 상태와 함수를 선언한다. 아직 DOM은 없다
DOM 생성 완료 onMounted 서버에서 데이터 가져오기, DOM 요소 접근, 외부 라이브러리 초기화
데이터 변경 → DOM 갱신 후 onUpdated 갱신된 DOM에 대한 후처리
화면에서 제거되기 전 onUnmounted 타이머 해제, 이벤트 리스너 제거, 연결 정리

가장 많이 쓰는 것은 이 셋이고, 각 단계에는 onBeforeMount, onBeforeUpdate, onBeforeUnmount처럼 "직전" 버전도 있다.

⚠️ 훅은 반드시 동기적으로 등록해야 한다
Vue는 훅에 등록된 콜백을 어느 인스턴스의 것인지 자동으로 연결하는데, 이게 성립하려면 setup이 실행되는 흐름 안에서 훅이 호출되어야 한다. setTimeout 안이나 await 뒤에서 onMounted를 부르면 연결이 끊겨 동작하지 않는다.

 


7. onMounted와 onUpdated

onMounted — DOM이 준비된 뒤

컴포넌트의 초기 렌더링과 DOM 요소 생성이 완료된 후 실행된다.

import { onMounted } from 'vue'

onMounted(() => {
  console.log('mounted')
})

서버에서 초기 데이터를 받아오는 코드가 대개 여기 들어간다. [비동기와 AJAX 완전 정리]에서 본 axios.get()을 이 안에서 호출하는 것이 전형적인 패턴이다.

onUpdated — DOM이 갱신된 뒤

반응형 데이터의 변경으로 컴포넌트의 DOM이 업데이트된 후 실행된다.

import { ref, onUpdated } from 'vue'

const count = ref(0)

onUpdated(() => {
  console.log('DOM이 갱신되었습니다')
})

⚠️ onUpdated 안에서 반응형 데이터를 바꾸면 무한 루프가 될 수 있다
원본 노트의 예제가 정확히 그 형태다.

onUpdated(() => {
  message.value = 'updated!'   // message가 템플릿에 쓰이고 있다면
})

데이터를 바꾸면 → DOM이 갱신되고 → onUpdated가 다시 호출되고 → 또 데이터를 바꾸는 순환이 된다. 이 예제는 매번 같은 값을 대입해서 Vue가 변경으로 판단하지 않기에 멈추지만, 카운터를 증가시키는 식이었다면 브라우저가 멈춘다.
onUpdated는 갱신된 DOM을 읽는 용도로 쓰고, 상태를 바꿔야 한다면 watch를 쓴다.

onUnmounted — 뒷정리

컴포넌트가 화면에서 사라질 때 실행된다. onMounted에서 벌여 놓은 것을 여기서 걷는다.

import { onMounted, onUnmounted } from 'vue'

let timer

onMounted(() => {
  timer = setInterval(fetchData, 5000)
})

onUnmounted(() => {
  clearInterval(timer)   // 정리하지 않으면 컴포넌트가 사라져도 계속 돈다
})

정리하지 않은 타이머와 리스너는 메모리 누수의 대표적인 원인이다. onMounted와 onUnmounted는 한 쌍으로 생각하는 편이 좋다.

 


8. Virtual DOM — Vue가 화면을 다시 그리는 방식

Virtual DOM은 가상의 DOM을 메모리에 저장해 두고 실제 DOM과 동기화하는 프로그래밍 개념이다. Vue의 내부 렌더링 기술이다.



과정은 이렇다.

  1. 컴포넌트와 반응형 상태가 가상 DOM 트리로 컴파일된다
  2. 이 트리가 실제 DOM에 마운트된다
  3. 반응형 상태가 변경되면 새 가상 DOM 트리를 만든다
  4. 이전 트리와 비교(패치) 해서 달라진 부분만 실제 DOM에 적용한다

[Vue 조건부·리스트 렌더링] 편에서 key가 왜 필요한지 설명하며 나온 그 비교 과정이 4단계다. key는 이 비교에서 "이전의 이 항목과 새 목록의 저 항목이 같은 것인가"를 판단하는 이름표였다.

장점 설명
효율성 실제 DOM 조작을 최소화하고 변경된 부분만 갱신한다
반응성 데이터 변경을 감지해 UI를 자동으로 업데이트한다
추상화 개발자는 DOM 조작을 Vue에 맡기고 컴포넌트와 템플릿으로 UI를 구성한다

📌 "Virtual DOM이 더 빠르다"는 오해에 선을 그어 둔다
손으로 최적화한 직접 DOM 조작보다 항상 빠른 것은 아니다. 가상 트리를 만들고 비교하는 비용 자체가 있기 때문이다.
Virtual DOM의 진짜 가치는 "선언적으로 쓰면서도 충분히 좋은 성능을 얻는 것" 에 있다. 어느 요소를 어떻게 고칠지 매번 지시하지 않고 "이 자리에 이 값이 들어간다"고 선언만 해도, 실제 갱신은 Vue가 알아서 최소한으로 해 준다. 세 번째 장점인 추상화가 핵심이고 나머지는 그 덕분에 따라오는 것이다.

실제 DOM을 직접 만지지 않는다

Vue가 가상 DOM 기준으로 화면을 관리하는데 우리가 실제 DOM을 직접 고치면, Vue가 알고 있는 상태와 화면이 어긋난다. 다음 갱신 때 Vue가 자기 기준으로 덮어쓰면서 변경이 사라지거나, 반대로 Vue가 관리하던 요소가 사라져 에러가 난다.

📌 원본 노트의 "addEventListener 사용 금지"는 범위를 좁혀 읽어야 한다
금지 대상은 Vue가 관리하는 요소를 직접 찾아 조작하는 것이다. document.querySelector로 컴포넌트 안의 요소를 찾아 textContent를 바꾸거나 createElement로 노드를 끼워 넣는 행위가 여기 해당한다.
반면 window나 document에 리스너를 다는 것은 정상적인 패턴이다. 스크롤 위치 추적, 화면 리사이즈 감지, 전역 키보드 단축키는 Vue의 템플릿 문법으로 표현할 방법이 없다.

onMounted(() => window.addEventListener('scroll', onScroll))
onUnmounted(() => window.removeEventListener('scroll', onScroll))

Vue의 영역 밖에 붙이고, 반드시 정리한다. 이 두 조건을 지키면 문제가 없다.

 


9. 실제 DOM이 정말 필요할 때 — 템플릿 ref

그래도 DOM 요소에 직접 접근해야 하는 경우가 있다. 입력창에 자동으로 포커스를 주거나, 차트 라이브러리에 그릴 자리를 넘겨줘야 할 때다.

이때는 querySelector가 아니라 ref 속성을 쓴다.

<template>
  <input ref="input">
</template>

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

const input = ref(null)

onMounted(() => {
  console.log(input.value)   // <input> 요소
  input.value.focus()
})
</script>

조건이 둘이다.

  • 템플릿의 ref="input" 속성 이름과 const input = ref(null)의 변수 이름이 일치해야 연결된다
  • DOM이 생성된 후여야 하므로 onMounted 안에서 접근한다

setup 실행 시점에는 아직 DOM이 없어서 input.value가 null이다. 6번 항목의 라이프사이클 순서가 여기서 그대로 적용된다.

💡 Vue 3.5부터는 useTemplateRef()도 쓸 수 있다

import { useTemplateRef } from 'vue'
const inputEl = useTemplateRef('input')

변수 이름과 ref 속성 이름을 일치시키지 않아도 되고, 무엇을 가리키는지가 코드에 드러난다. 기존 방식도 그대로 동작한다.

 


10. 자주 걸려 넘어지는 지점들

① 템플릿에서 computed에 괄호를 붙인다

<p>{{ restOfTodos() }}</p>   <!-- computed는 값이지 함수가 아니다 -->
<p>{{ restOfTodos }}</p>     <!-- 올바르다 -->

method는 괄호가 필요하고 computed는 필요 없다. 둘을 오가다 보면 헷갈린다.

② computed 안에서 .value를 빠뜨린다

const restOfTodos = computed(() => todos.length > 0 ? '...' : '...')

<script> 안이므로 todos.value.length여야 한다. todos는 ref 객체이고 .length는 undefined다. [Vue 기초] 편에서 본 그 규칙이다.

③ ref 두 개를 watch로 동기화한다

watch(a, (n) => { b.value = n * 2 })   // computed의 일이다
const b = computed(() => a.value * 2)  // 이쪽이 맞다

5번 항목의 📌와 같은 이야기다. 한 값이 다른 값에서 자동으로 나온다면 computed다.

④ setup 본문에서 DOM에 접근한다

const input = ref(null)
console.log(input.value)   // null — 아직 렌더링 전이다

⑤ onUnmounted를 빠뜨린다

setInterval, addEventListener, 웹소켓 연결은 컴포넌트가 사라져도 자동으로 정리되지 않는다.

 


✅ 핵심 요약

주제 꼭 기억할 것
computed 계산된 값을 만든다. 의존 데이터를 자동 추적하고 캐싱한다
캐싱의 의미 같은 값을 여러 번 참조해도 한 번만 계산한다. method는 매 렌더링마다 실행
method를 쓸 때 동작을 수행할 때, 인자를 받아야 할 때
읽기 전용 computed 반환값은 스냅샷이다. 바꾸려면 원본 데이터를 바꾼다
정렬 주의 sort·reverse는 원본을 바꾼다. [...arr]로 복사한 뒤 쓴다
watch 값을 만들지 않고 일을 시킨다. API 호출, 저장 같은 부수 효과
선택 기준 값을 만들면 computed, 일을 시키면 watch
watch는 lazy 등록 시점엔 실행되지 않는다. 필요하면 { immediate: true }
라이프사이클 setup에는 DOM이 없다 → onMounted부터 있다
onUpdated 주의 안에서 반응형 데이터를 바꾸면 무한 루프가 될 수 있다
onUnmounted 타이머·리스너를 정리한다. onMounted와 한 쌍이다
Virtual DOM 새 트리를 만들어 이전과 비교하고 달라진 부분만 실제 DOM에 적용
진짜 가치 속도가 아니라 추상화 — 선언만 해도 갱신은 Vue가 맡는다
DOM 접근 Vue 영역 안은 만지지 않는다. 필요하면 템플릿 ref + onMounted

 

🔗 참고 자료

  • Vue.js — Computed 속성
  • Vue.js — Watchers
  • Vue.js — 라이프사이클 훅
  • Vue.js — 라이프사이클 훅 API
  • Vue.js — 렌더링 메커니즘
  • Vue.js — 템플릿 ref
반응형

'알면좋은것들' 카테고리의 다른 글

파이썬 기초(1) - 핵심문법  (0) 2026.08.16
Vue - pinia & router  (2) 2026.08.01
Vue - props & emit  (2) 2026.08.01
Vue Syntax(2)  (0) 2026.08.01
Vue Syntax(1)  (0) 2026.08.01
'알면좋은것들' 카테고리의 다른 글
  • 파이썬 기초(1) - 핵심문법
  • Vue - pinia & router
  • Vue - props & emit
  • Vue Syntax(2)
AglioOglio
AglioOglio
데이터분석, 딥러닝 공부하고 있습니다.
  • AglioOglio
    It것저것
    AglioOglio
  • 전체
    오늘
    어제
    • 분류 전체보기 (163) N
      • Stanford CS224N Lectures (10)
        • 강의정리 (8)
        • assignment (2)
      • 논문리뷰 (13)
        • NLP (11)
        • CV (1)
      • 구현 (3)
      • DL (5)
        • CV (4)
        • NLP (11)
        • 딥러닝 기초 (3)
        • 파이토치 기초 (5)
        • LLM & Agent (8)
      • 알고리즘 (49) N
        • BOJ (29)
        • SWEA (8)
        • Programmers (3)
        • 이론 (9) N
      • 시계열분석(Time Series Analysis) (1)
      • ML (2)
      • EffectivePython_125 (21)
      • 알면좋은것들 (28)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    effective python 125
    NLP
    자료구조
    백준
    effective python
    django
    JS
    자연어처리
    attention
    Transformer
    SWEA
    Vue
    파이썬
    논문리뷰
    Python
    Effective
    코딩테스트
    CS224N
    Baekjoon
    알고리즘
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.3
AglioOglio
Vue - computed lifecycle
상단으로

티스토리툴바