π¦ κ³Όλν μλνν° μ€ν¬λ¦½νΈ λ¨μ©: μ±λ₯ μ νμ 보μ μν μ¦κ°
Summary
μΉμ¬μ΄νΈμ λ무 λ§μ μλνν° μ€ν¬λ¦½νΈλ₯Ό μΆκ°νκ±°λ λΉν¨μ¨μ μΌλ‘ κ΄λ¦¬νλ©΄ μ±λ₯ μ ν, 보μ μ·¨μ½μ μ¦κ°, μ μ§λ³΄μ 볡μ‘μ± λ± μ¬κ°ν λ¬Έμ λ₯Ό μΌκΈ°ν©λλ€. νμν μ€ν¬λ¦½νΈλ§ μμ νκ³ , async
/defer
λ₯Ό ν΅ν λΉλκΈ°/μ§μ° λ‘λ©, preconnect
/dns-prefetch
, SRI, CSP λ±μ μ μ©νμ¬ ν¨μ¨μ μ΄κ³ μμ νκ² μλνν° μ€ν¬λ¦½νΈλ₯Ό κ΄λ¦¬ν΄μΌ ν©λλ€.
Why Wrong?
μΉμ¬μ΄νΈμ λ무 λ§μ μλνν°(Third-party) μ€ν¬λ¦½νΈλ₯Ό μΆκ°νκ±°λ λΉν¨μ¨μ μΌλ‘ λ‘λνλ κ²μ μ¬κ°ν μ±λ₯ λ° λ³΄μ λ¬Έμ λ₯Ό μΌκΈ°ν©λλ€.
π μ±λ₯ μ ν
κ° μλνν° μ€ν¬λ¦½νΈλ μλ‘μ΄ λ€νΈμν¬ μμ², λ€μ΄λ‘λ, νμ±, μ»΄νμΌ, μ€ν κ³Όμ μ κ±°μΉλ©° λ©μΈ μ€λ λλ₯Ό λΈλ‘νΉν μ μμ΅λλ€. μ΄λ First Contentful Paint (FCP), Largest Contentful Paint (LCP), Interaction to Next Paint (INP) λ± Core Web Vitals μ§νμ μ§μ μ μΈ μ μν₯μ μ£Όμ΄ μ¬μ©μ κ²½νμ μ νμν΅λλ€. νΉν, κ΄κ³ μ€ν¬λ¦½νΈ, λΆμ λꡬ, A/B ν μ€νΈ λꡬ λ±μ μμμΉ λͺ»ν 볡μ‘ν λ‘μ§μ ν¬ν¨νμ¬ λ λλ§μ μ§μ°μν€κ±°λ λ μ΄μμ λΆμμ (CLS)μ μ λ°ν μ μμ΅λλ€. μ€ν¬λ¦½νΈκ° λ€μ΄λ‘λλλ λμ λΈλΌμ°μ μ λ λλ§μ΄ λ©μΆ μ μμ΄ μ¬μ©μμκ² λΉ νλ©΄μ΄λ μ§μ°λ μνΈμμ©μ μ΄λν©λλ€.
π‘οΈ λ³΄μ μν
μλνν° μ€ν¬λ¦½νΈλ μ¬λ¬λΆμ μΉμ¬μ΄νΈμμ μ€νλλ μΈλΆ μ½λμ λλ€. μ μμ μΈ κ³΅κ²©μκ° μλνν° μ€ν¬λ¦½νΈ μ 곡μμ μμ€ν μ μΉ¨ν΄(Supply Chain Attack)νμ¬ μ μ± μ½λλ₯Ό μ£Όμ νκ±°λ, μ¬μ©μ λ°μ΄ν°λ₯Ό νμ·¨ν μ μλ ν΅λ‘κ° λ μ μμ΅λλ€. μ΄λ ν¬λ¦¬λ΄μ (μΈμ¦ μ 보) νμ·¨, μΈμ νμ΄μ¬νΉ, λ©μ¨μ΄ λ°°ν¬ λ± μΉλͺ μ μΈ κ²°κ³Όλ₯Ό μ΄λν μ μμ΅λλ€. λν, CDNμ ν΅ν΄ λ‘λλλ μ€ν¬λ¦½νΈμ κ²½μ° μλΈλ¦¬μμ€ λ¬΄κ²°μ±(Subresource Integrity, SRI)μ μ μ©νμ§ μμΌλ©΄ λ³μ‘°λ μ€ν¬λ¦½νΈκ° μ€νλ μνμ΄ μμ΅λλ€.
π μ μ§λ³΄μ λ° λ³΅μ‘μ± μ¦κ°
λΆνμν μλνν° μ€ν¬λ¦½νΈμ μ¦κ°λ μ λ°μ μΈ μ½λ λ² μ΄μ€μ 볡μ‘μ±μ λμ΄κ³ , νΉμ κΈ°λ₯μ λλ²κΉ μ μ΄λ ΅κ² λ§λ€λ©°, μλμΉ μμ μνΈμμ©μ΄λ μΆ©λμ μ λ°ν μ μμ΅λλ€. μ€ν¬λ¦½νΈ κ°μ μ’ μμ±μ΄λ μ μ λ³μ μ€μΌμ μμΈ‘ λΆκ°λ₯ν λ²κ·Έλ‘ μ΄μ΄μ§ μ μμ΅λλ€.
How to Fix?
μλνν° μ€ν¬λ¦½νΈμ λμ μ μ μ€νκ² κ²°μ νκ³ , λμ νμλ μ±λ₯κ³Ό 보μμ μ΅μ ννκΈ° μν λ Έλ ₯μ΄ νμν©λλ€.
β νμ μ¬λΆ λ° νμμ± μ¬νκ°
λͺ¨λ μλνν° μ€ν¬λ¦½νΈκ° μ λ§ νμνμ§ μ£ΌκΈ°μ μΌλ‘ κ°μ¬νκ³ , μ¬μ©νμ§ μλ μ€ν¬λ¦½νΈλ κ³Όκ°ν μ κ±°ν©λλ€. μ¬λ¬ μλνν° μ€ν¬λ¦½νΈκ° μ μ¬ν κΈ°λ₯μ μ 곡νλ κ²½μ°, μ€λ³΅μ νΌνκ³ κ°μ₯ ν¨μ¨μ μΈ νλλ₯Ό μ νν©λλ€.
β‘οΈ λΉλκΈ°/μ§μ° λ‘λ©
<script>
νκ·Έμ async
λλ defer
μμ±μ μ¬μ©νμ¬ μ€ν¬λ¦½νΈκ° HTML νμ±μ λΈλ‘νΉνμ§ μλλ‘ ν©λλ€.
async
: μ€ν¬λ¦½νΈλ₯Ό λΉλκΈ°μ μΌλ‘ λ€μ΄λ‘λνκ³ , λ€μ΄λ‘λ μλ£ μ¦μ μ€νν©λλ€. HTML νμ±μ μ€ν¬λ¦½νΈ λ€μ΄λ‘λ μ€μ κ³μλ©λλ€.defer
: μ€ν¬λ¦½νΈλ₯Ό λΉλκΈ°μ μΌλ‘ λ€μ΄λ‘λνμ§λ§, HTML νμ±μ΄ μλ£λ νμ λ¬Έμμ λνλλ μμλλ‘ μ€νν©λλ€.DOMContentLoaded
μ΄λ²€νΈ λ°μ μ μ μ€νλ©λλ€. μΌλ°μ μΌλ‘ νμ΄μ§ μ½ν μΈ λ λλ§μ νμμ μ΄μ§ μμ μ€ν¬λ¦½νΈμ μ ν©ν©λλ€. μ¬μ©μ μνΈμμ© νμ λ‘λλλ μ€ν¬λ¦½νΈ(μ:Intersection Observer
λ μ΄λ²€νΈ 리μ€λλ₯Ό νμ©)λ κ³ λ €νμ¬ μ΄κΈ° λ‘λ© μ±λ₯μ κ°μ ν©λλ€.
π preconnect
, dns-prefetch
νμ©
μ€ν¬λ¦½νΈκ° λ‘λλ λλ©μΈμ λν΄ λ―Έλ¦¬ μ°κ²°μ μ€μ νμ¬ μ§μ° μκ°μ μ€μ
λλ€. preconnect
λ DNS μ‘°ν, TCP νΈλμ
°μ΄ν¬, TLS νμκΉμ§ 미리 μννμ¬ λ€μ μμ²μ λκΈ° μκ°μ ν¬κ² μ€μ¬μ€λλ€. dns-prefetch
λ DNS μ‘°νλ§ λ―Έλ¦¬ μννμ¬ preconnect
λ³΄λ€ κ°λ³μ§λ§ ν¨κ³Όλ μ μ΅λλ€.
π μλΈλ¦¬μμ€ λ¬΄κ²°μ± (Subresource Integrity, SRI) μ μ©
μ€μν μλνν° μ€ν¬λ¦½νΈ(νΉν CDNμμ λ‘λλλ μ€ν¬λ¦½νΈ)μ λν΄ SRIλ₯Ό μ¬μ©νμ¬ μ€ν¬λ¦½νΈκ° λ³μ‘°λμ§ μμμμ νμΈν©λλ€. SRIλ μ€ν¬λ¦½νΈμ ν΄μ κ°μ μ§μ νμ¬, λ‘λλ μ€ν¬λ¦½νΈμ ν΄μ κ°κ³Ό μΌμΉνμ§ μμΌλ©΄ λΈλΌμ°μ κ° μ€ν¬λ¦½νΈ μ€νμ κ±°λΆνλλ‘ ν©λλ€. μ΄λ 곡κΈλ§ 곡격(Supply Chain Attack)μΌλ‘λΆν° 보νΈνλ μ€μν μλ¨μ λλ€.
π― νκ·Έ κ΄λ¦¬ μμ€ν (TMS) μ μ€ν νμ©
Google Tag Managerμ κ°μ TMSλ₯Ό μ¬μ©νλ©΄ μ€ν¬λ¦½νΈ κ΄λ¦¬κ° μ©μ΄νμ§λ§, λ무 λ§μ νκ·Έλ₯Ό μΆκ°νλ©΄ μ€νλ € μ±λ₯μ΄ μ νλ μ μμΌλ―λ‘, μ΅μνμ νμν νκ·Έλ§ μΆκ°νκ³ μ΅μ νν©λλ€. TMS λ΄μμλ λΉλκΈ° λ‘λ© μ΅μ μ νμ©ν΄μΌ ν©λλ€.
βοΈ Content Security Policy (CSP) κ°ν
μ λ’°ν μ μλ λλ©μΈμμλ§ μ€ν¬λ¦½νΈ λ‘λλ₯Ό νμ©νλλ‘ CSPλ₯Ό ꡬμ±νμ¬ XSS 곡격 λ° μ μ± μ€ν¬λ¦½νΈ μ£Όμ μ λ°©μ§ν©λλ€. CSPλ μΉνμ΄μ§κ° λ‘λν μ μλ 리μμ€(μ€ν¬λ¦½νΈ, μ€νμΌ, μ΄λ―Έμ§ λ±)μ μΆμ²λ₯Ό λͺ μμ μΌλ‘ μ§μ νμ¬ λ³΄μμ κ°ννλ HTTP μλ΅ ν€λμ λλ€.
π¦ λΆλΆμ μ체 νΈμ€ν
μΌλΆ μ€ν¬λ¦½νΈ(νΉν λΆμ μ€ν¬λ¦½νΈ)λ μΊμ± μ΄μ μ μν΄ μ체 νΈμ€ν μ κ³ λ €ν μ μμ§λ§, μ λ°μ΄νΈ κ΄λ¦¬μ μ΄λ €μμ κ³ λ €νμ¬ μ μ€νκ² κ²°μ ν΄μΌ ν©λλ€. μ체 νΈμ€ν μ CDNλ³΄λ€ λΉ λ₯Έ λ‘λ©μ μ 곡ν μ μμ§λ§, μ€ν¬λ¦½νΈ μ λ°μ΄νΈλ₯Ό μ§μ κ΄λ¦¬ν΄μΌ νλ λ¨μ μ΄ μμ΅λλ€.
Before Code (Bad)
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>λ¬Έμ μλ μλνν° μ€ν¬λ¦½νΈ μμ</title>
<!-- Google Analytics (headμ λκΈ° λ‘λ, νμ΄μ§ λ λλ§ λΈλ‘νΉ) -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
</script>
<!-- Hotjar (headμ λκΈ° λ‘λ, μ¬μ©μ λμ μμ΄ μ΄κΈ° λ‘λ) -->
<script>
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:XXXXX,hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'//static.hotjar.com/c/hotjar-','.js?sv=');
</script>
<!-- λΆνμνκ² λ§μ μΈλΆ μ€νμΌμνΈ (λ³λ ¬ μμ² μ μ ν) -->
<link rel="stylesheet" href="https://cdn.example.com/some-ui-kit.min.css">
<link rel="stylesheet" href="https://cdn.example.com/another-font-awesome.min.css">
</head>
<body>
<h1>Welcome to Our Site</h1>
<p>This is some content.</p>
<!-- μΈλΆ λΌμ΄λΈλ¬λ¦¬ (CDNμ ν΅ν΄ λκΈ° λ‘λ, HTML νμ± λΈλ‘νΉ) -->
<script src="https://unpkg.com/some-heavy-library@1.0.0/dist/library.min.js"></script>
<!-- Zendesk Chat Widget (body λ§μ§λ§μ μμ§λ§, λκΈ° λ‘λ© λ°©μ) -->
<script id="ze-snippet" src="https://static.zdassets.com/ekr/snippet.js?key=YOUR_ZENDESK_KEY"></script>
</body>
</html>
After Code (Good)
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>μ΅μ νλ μλνν° μ€ν¬λ¦½νΈ μμ</title>
<!-- μ€μν 리μμ€ λ‘λ μ 미리 μ°κ²° μ€μ (μ±λ₯ μ΅μ ν) -->
<link rel="preconnect" href="https://www.google-analytics.com">
<link rel="preconnect" href="https://static.hotjar.com">
<link rel="preconnect" href="https://unpkg.com">
<link rel="dns-prefetch" href="https://static.zdassets.com">
<!-- Content Security Policy (보μ κ°ν) -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://www.google-analytics.com https://static.hotjar.com https://unpkg.com https://static.zdassets.com; style-src 'self' 'unsafe-inline' https://cdn.example.com; font-src 'self' https://cdn.example.com;">
<!-- Google Analytics (async μμ± μ¬μ©, νμν κ²½μ° headμ μμΉ κ°λ₯) -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
</script>
<!-- Hotjar (defer μμ± μ¬μ©, νμν κ²½μ° μ¬μ©μ λμ ν λλ μ€ν¬λ‘€ μμ λ‘λ) -->
<script defer src="https://static.hotjar.com/c/hotjar-XXXXX.js?sv=6"></script>
<!-- μΈλΆ μ€νμΌμνΈλ μ€μλμ λ°λΌ μ΅μ ν (μ: ν΅μ¬ CSSλ§ λ²λ€μ ν¬ν¨, λλ¨Έμ§λ λμ€μ λ‘λ) -->
<link rel="stylesheet" href="/styles/main.css"> <!-- μ체 νΈμ€ν
CSS -->
</head>
<body>
<h1>Welcome to Our Site</h1>
<p>This is some content.</p>
<!-- μΈλΆ λΌμ΄λΈλ¬λ¦¬ (async/defer μμ±, SRI μ μ©) -->
<script async defer
src="https://unpkg.com/some-heavy-library@1.0.0/dist/library.min.js"
integrity="sha384-XXXXXX..." <!-- μ€μ λΌμ΄λΈλ¬λ¦¬ ν΄μκ°μΌλ‘ λ체. νμ! -->
crossorigin="anonymous">
</script>
<!-- Zendesk Chat Widget (μ¬μ©μ μνΈμμ© λλ νμν μμ μ λμ λ‘λ) -->
<button id="openChat">Open Chat</button>
<div id="chat-widget-container"></div>
<script>
document.getElementById('openChat').addEventListener('click', () => {
// μ€ν¬λ¦½νΈκ° λ‘λλμλμ§ νμΈνμ¬ μ€λ³΅ λ‘λ λ°©μ§
if (!window.ZendeskLoaded) {
const script = document.createElement('script');
script.id = 'ze-snippet';
script.src = 'https://static.zdassets.com/ekr/snippet.js?key=YOUR_ZENDESK_KEY';
script.async = true; // λΉλκΈ° λ‘λ©
script.onload = () => {
window.ZendeskLoaded = true;
// Zendesk μ΄κΈ°ν μ½λ (if any)
};
document.body.appendChild(script);
}
});
</script>
</body>
</html>