logo

2025-11-17

🚨 Vue In-DOM ν…œν”Œλ¦Ώ, λ‹Ήμ‹ μ˜ script μ½”λ“œλ₯Ό 두 번 μ‹€ν–‰ν•˜κ³  μžˆλ‹€!

λ ˆκ±°μ‹œ μ½”λ“œμ—μ„œ λ§ˆμ£Όν•œ 의문

κ°œλ°œμ„ ν•˜λ‹€ 보면 μ˜ˆμƒμΉ˜ λͺ»ν•œ 버그λ₯Ό λ§ˆμ£Όν•  λ•Œκ°€ μžˆμ–΄μš”. 특히 λ ˆκ±°μ‹œ ν”„λ‘œμ νŠΈλ₯Ό μœ μ§€λ³΄μˆ˜ν•˜λ‹€ 보면 λ”μš± κ·Έλ ‡μ£ . μ € λ˜ν•œ νšŒμ‚¬μ˜ λ ˆκ±°μ‹œ μ½”λ“œλ₯Ό κ°œμ„ ν•˜λ˜ 쀑, μ΄μƒν•œ 이슈λ₯Ό λ°œκ²¬ν–ˆμ–΄μš”.

1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> 6 <title>Document</title> 7 </head> 8 <body> 9 <div id="vue-container"> 10 <div>컨텐츠</div> 11 <script type="application/javascript"> 12 console.log('script executed') 13 </script> 14 </div> 15 16 <script> 17 const vm = new Vue({ 18 el: '#vue-container', 19 mounted() { 20 console.log('vm mounted') 21 }, 22 }) 23 </script> 24 </body> 25</html>

μ½˜μ†” 좜λ ₯:

script executed
script executed  // μ™œ 두 번?
vm mounted

λΆ„λͺ… script νƒœκ·ΈλŠ” ν•˜λ‚˜μΈλ°, μ½˜μ†”μ—λŠ” script executedκ°€ 두 번 좜λ ₯되고 μžˆμ—ˆμ–΄μš”. μ΄λŸ¬ν•œ λ¬Έμ œλŠ” μ˜ˆμƒμΉ˜ λͺ»ν•œ λΆ€μž‘μš©μ„ μΌμœΌν‚¬ 수 있고, 특히 API ν˜ΈμΆœμ΄λ‚˜ DOM μ‘°μž‘μ΄ ν¬ν•¨λœ 경우 μ‹¬κ°ν•œ λ²„κ·Έλ‘œ μ΄μ–΄μ§ˆ 수 μžˆμ–΄μš”.

κ³Όμ—° VueλŠ” Scriptλ₯Ό μ–΄λ–»κ²Œ μ²˜λ¦¬ν•˜λŠ” 걸까?

SFC와 In-DOM Template의 차이

μ²˜μŒμ—λŠ” SFC(Single File Component)와 In-DOM template의 λ™μž‘ 방식 차이 λ•Œλ¬Έμ΄λΌκ³  μƒκ°ν–ˆμ–΄μš”.

  • SFC (.vue 파일): λΉŒλ“œ νƒ€μž„μ— μ»΄νŒŒμΌλ˜μ–΄ createElement ν•¨μˆ˜λ‘œ λ³€ν™˜λ˜κΈ° λ•Œλ¬Έμ— template μ•ˆμ˜ scriptλŠ” μ‹€ν–‰λ˜μ§€ μ•ŠμŒ
  • In-DOM template: HTML에 직접 μž‘μ„±λ˜μ–΄ λΈŒλΌμš°μ €κ°€ νŒŒμ‹±ν•˜λ©΄μ„œ 싀행됨

ν•˜μ§€λ§Œ In-DOM template라도 ν•œ 번만 싀행될 거라고 μ˜ˆμƒν–ˆλŠ”λ°, μ‹€μ œλ‘œλŠ” 두 번 μ‹€ν–‰λ˜κ³  μžˆμ—ˆμ–΄μš”.

λ””λ²„κ±°λ‘œ μΆ”μ ν•œ μ‹€ν–‰ 경둜

μ •ν™•ν•œ 원인을 μ°ΎκΈ° μœ„ν•΄ debugger 문을 μΆ”κ°€ν•˜κ³  Call Stack을 ν™•μΈν–ˆμ–΄μš”.

1<script type="application/javascript"> 2 debugger 3 console.log('script executed') 4 console.trace() 5</script>

1μ°¨ μ‹€ν–‰ (λΈŒλΌμš°μ € νŒŒμ‹±):

(anonymous) @ inline script

2μ°¨ μ‹€ν–‰ (Vue λ‚΄λΆ€):

(anonymous) @ inline script
insertBefore (native)
insert @ vue.js:xxxx
createElm @ vue.js:xxxx
patch @ vue.js:xxxx
Vue._update @ vue.js:xxxx

μ—¬κΈ°μ„œ 핡심은 Vue의 insertBeforeμ—μ„œ scriptκ°€ λ‹€μ‹œ μ‹€ν–‰λ˜κ³  μžˆλ‹€λŠ” μ μ΄μ—μš”.

Vue의 DOM Patching λ©”μ»€λ‹ˆμ¦˜

Vueκ°€ In-DOM template을 μ²˜λ¦¬ν•˜λŠ” 과정을 μ‚΄νŽ΄λ³΄λ©΄:

  1. #vue-container의 λ‚΄μš©μ„ template으둜 읽기
  2. Virtual DOM 생성
  3. μ‹€μ œ DOM으둜 patch (μ—¬κΈ°μ„œ insertBefore 호좜)
  4. script λ…Έλ“œκ°€ DOM에 μ‚½μž…λ˜λ©΄μ„œ μž¬μ‹€ν–‰

μ€‘μš”ν•œ 점은, insertBeforeλ‚˜ appendChild둜 script λ…Έλ“œλ₯Ό DOM에 μ‚½μž…ν•˜λ©΄ λΈŒλΌμš°μ €κ°€ μžλ™μœΌλ‘œ μ‹€ν–‰ν•œλ‹€λŠ” κ±°μ˜ˆμš”.

1// κ°„λ‹¨ν•œ ν…ŒμŠ€νŠΈ 2const script = document.createElement('script') 3script.textContent = 'console.log("test")' 4// μ—¬κΈ°κΉŒμ§€λŠ” μ‹€ν–‰ μ•ˆ 됨 5 6document.body.appendChild(script) 7// μ—¬κΈ°μ„œ 싀행됨!

그런데 λͺ¨λ“  Scriptκ°€ 두 번 μ‹€ν–‰λ˜λŠ” 건 μ•„λ‹ˆμ—ˆλ‹€

μ—¬κΈ°κΉŒμ§€ μ΄ν•΄ν•˜κ³  λ‚˜λ‹ˆ, 또 λ‹€λ₯Έ 의문이 μƒκ²Όμ–΄μš”. "그럼 λͺ¨λ“  In-DOM template의 scriptκ°€ 두 번 μ‹€ν–‰λ˜λŠ” 건가?" μ‹€μ œλ‘œ ν…ŒμŠ€νŠΈλ₯Ό ν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

1<div id="vue-container"> 2 <script type="text/javascript"> 3 console.log('text/javascript') 4 </script> 5 <script type="application/javascript"> 6 console.log('application/javascript') 7 </script> 8</div>

κ²°κ³Ό:

text/javascript (1회만 μ‹€ν–‰!)
application/javascript (2회 μ‹€ν–‰)

λ†€λžκ²Œλ„ text/javascriptλŠ” ν•œ 번만 μ‹€ν–‰λ˜κ³ , application/javascript만 두 번 μ‹€ν–‰λ˜μ—ˆμ–΄μš”.

Vue의 Script 필터링 둜직

VueλŠ” μ™œ νŠΉμ • Script만 μ œκ±°ν• κΉŒ?

Vue 2λŠ” In-DOM template을 μ²˜λ¦¬ν•  λ•Œ, νŠΉμ • script νƒœκ·Έλ₯Ό μ œκ±°ν•˜λŠ” 둜직이 μžˆμ–΄μš”. 이미 λΈŒλΌμš°μ €κ°€ μ‹€ν–‰ν–ˆλ‹€κ³  κ°„μ£Όν•˜κ³ , μž¬μ‹€ν–‰μ„ λ°©μ§€ν•˜κΈ° μœ„ν•΄μ„œμ£ .

1// Vue 2 λ‚΄λΆ€ 둜직 (μΆ”μ •) 2function shouldRemoveScript(type) { 3 // ν‘œμ€€ νƒ€μž…λ§Œ 제거 4 return !type || type === '' || type === 'text/javascript' 5} 6 7if (tagName === 'script' && shouldRemoveScript(scriptType)) { 8 // templateμ—μ„œ 제거 β†’ μž¬μ‹€ν–‰ μ•ˆ 됨 9} else { 10 // 일반 μš”μ†Œλ‘œ μœ μ§€ β†’ μž¬μ‚½μž… μ‹œ μž¬μ‹€ν–‰λ¨ 11}

MDNμ—μ„œ 찾은 λ‹΅

MDN λ¬Έμ„œλ₯Ό μ‚΄νŽ΄λ³΄λ‹ˆ λͺ…ν™•ν•œ 닡이 μžˆμ—ˆμ–΄μš”.

JavaScript content should always be served with the MIME type text/javascript.

Legacy JavaScript MIME types (λͺ¨λ‘ Deprecated):

  • application/javascript - Deprecated
  • application/ecmascript - Deprecated
  • text/ecmascript - Deprecated
  • κ·Έ μ™Έ λ‹€μ–‘ν•œ λΉ„ν‘œμ€€ νƒ€μž…λ“€...

application/javascriptλŠ” 역사적인 이유둜 λΈŒλΌμš°μ €κ°€ μ§€μ›ν•˜μ§€λ§Œ, deprecated된 λ ˆκ±°μ‹œ νƒ€μž…μ΄μ—ˆμ–΄μš”. VueλŠ” ν‘œμ€€μ„ λ”°λ₯΄λŠ” νƒ€μž…λ§Œ ν•„ν„°λ§ν•˜κ³ , deprecated νƒ€μž…μ€ κ°œλ°œμžκ°€ νŠΉμˆ˜ν•œ λͺ©μ μœΌλ‘œ μ‚¬μš©ν–ˆμ„ 수 μžˆλ‹€κ³  κ°„μ£Όν•˜μ—¬ κ·ΈλŒ€λ‘œ μœ μ§€ν•˜λŠ” κ²ƒμœΌλ‘œ λ³΄μ—¬μš”.

ν•΄κ²° 방법

1. ν‘œμ€€μ„ λ”°λ₯΄λŠ” 것이 κ°€μž₯ μ•ˆμ „ν•΄μš”

κ°€μž₯ κ°„λ‹¨ν•œ 해결책은 type 속성을 μƒλž΅ν•˜κ±°λ‚˜ text/javascriptλ₯Ό μ‚¬μš©ν•˜λŠ” κ±°μ˜ˆμš”.

1<!-- ꢌμž₯: type μƒλž΅ --> 2<div id="vue-container"> 3 <script> 4 console.log('1회만 싀행됨!') 5 </script> 6</div>
1<!-- λ˜λŠ” λͺ…μ‹œμ μœΌλ‘œ text/javascript μ‚¬μš© --> 2<div id="vue-container"> 3 <script type="text/javascript"> 4 console.log('1회만 싀행됨!') 5 </script> 6</div>

2. In-DOM Templateμ—μ„œ Script μ‚¬μš©μ„ ν”Όν•˜κΈ°

In-DOM template에 μ‹€ν–‰ κ°€λŠ₯ν•œ scriptλ₯Ό λ„£λŠ” 건 쒋은 νŒ¨ν„΄μ΄ μ•„λ‹ˆμ—μš”. Vue의 라이프사이클 훅을 ν™œμš©ν•˜λŠ” 것이 더 λͺ…ν™•ν•˜κ³  μ•ˆμ „ν•΄μš”.

1<div id="vue-container"> 2 <div>컨텐츠</div> 3</div> 4 5<script> 6 const vm = new Vue({ 7 el: '#vue-container', 8 mounted() { 9 // ν•„μš”ν•œ λ‘œμ§μ€ μ—¬κΈ°μ„œ μ‹€ν–‰ 10 console.log('μ•ˆμ „ν•˜κ²Œ 1회 μ‹€ν–‰!') 11 }, 12 }) 13</script>

3. 데이터 μ €μž₯이 λͺ©μ μ΄λΌλ©΄

λ§Œμ•½ 싀행이 μ•„λ‹Œ 데이터 μ €μž₯이 λͺ©μ μ΄λΌλ©΄, application/json νƒ€μž…μ„ μ‚¬μš©ν•˜λ©΄ λ©λ‹ˆλ‹€. 이 경우 λΈŒλΌμš°μ €κ°€ μ‹€ν–‰ν•˜μ§€ μ•Šμ•„μš”.

1<div id="vue-container"> 2 <script type="application/json" id="config"> 3 { 4 "apiUrl": "https://api.example.com", 5 "timeout": 5000 6 } 7 </script> 8</div> 9 10<script> 11 new Vue({ 12 el: '#vue-container', 13 mounted() { 14 const config = JSON.parse(document.getElementById('config').textContent) 15 console.log(config) 16 }, 17 }) 18</script>

μœ μ—°ν•œ 사고와 μ›Ή ν‘œμ€€μ— λŒ€ν•œ 이해

이 버그λ₯Ό ν•΄κ²°ν•˜λŠ” κ³Όμ •μ—μ„œ λͺ‡ κ°€μ§€ μ€‘μš”ν•œ κ΅ν›ˆμ„ μ–»μ—ˆμ–΄μš”.

  1. ν‘œμ€€μ„ λ”°λ₯΄λŠ” κ²ƒμ˜ μ€‘μš”μ„±: application/javascript처럼 deprecated된 νƒ€μž…μ€ μ˜ˆμƒμΉ˜ λͺ»ν•œ λ™μž‘μ„ μΌμœΌν‚¬ 수 μžˆμ–΄μš”.
  2. ν”„λ ˆμž„μ›Œν¬μ˜ λ‚΄λΆ€ λ™μž‘ 이해: Vueκ°€ μ™œ νŠΉμ • λ°©μ‹μœΌλ‘œ λ™μž‘ν•˜λŠ”μ§€ μ΄ν•΄ν•˜λ©΄, 더 λ‚˜μ€ μ½”λ“œλ₯Ό μž‘μ„±ν•  수 μžˆμ–΄μš”.
  3. 디버깅 λ„κ΅¬μ˜ ν™œμš©: Call Stack을 μΆ”μ ν•˜μ—¬ μ •ν™•ν•œ μ‹€ν–‰ 경둜λ₯Ό νŒŒμ•…ν•˜λŠ” 것이 문제 ν•΄κ²°μ˜ ν•΅μ‹¬μ΄μ—μš”.

더 λ‚˜μ•„κ°€, λ ˆκ±°μ‹œ μ½”λ“œλ₯Ό μœ μ§€λ³΄μˆ˜ν•  λ•ŒλŠ” "이게 μ™œ μ΄λ ‡κ²Œ μž‘μ„±λ˜μ—ˆμ„κΉŒ?"λΌλŠ” μ˜λ¬Έμ„ 항상 κ°€μ§€κ³  μ ‘κ·Όν•˜λŠ” 것이 μ€‘μš”ν•΄μš”. λ•Œλ‘œλŠ” 과거의 μ½”λ“œκ°€ λ‹Ήμ‹œμ˜ μ œμ•½μ‚¬ν•­μ΄λ‚˜ 버전 이슈 λ•Œλ¬Έμ— μž‘μ„±λœ 것일 수 있고, ν˜„μž¬λŠ” 더 λ‚˜μ€ 방법이 μžˆμ„ 수 μžˆμ–΄μš”.