Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 145 additions & 15 deletions components/layout/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,161 @@
<template>
<aside class="app-header">
<div class="app-brand">
<NuxtLink to="/" class="app-brand-mark">HOME</NuxtLink>
<span class="app-brand-subtitle">문서와 노트를 읽고 정리하는 블로그</span>
<div class="app-header__brand">
<NuxtLink
to="/"
class="app-header__brand-mark"
:aria-current="isCurrentPath('/') ? 'page' : undefined"
>
</NuxtLink>
<span class="app-header__brand-subtitle">읽을 내용, 메모, 작업일지를 정리하는 블로그</span>
</div>

<nav class="app-nav" aria-label="Primary">
<details class="app-nav-accordion" :open="isNotesOpen">
<summary class="app-nav-link app-nav-summary">노트</summary>
<div class="app-nav-subnav" aria-label="노트 분류">
<NuxtLink to="/notes/css" class="app-nav-sublink">CSS 노트</NuxtLink>
<NuxtLink to="/notes/javascript" class="app-nav-sublink">JavaScript 노트</NuxtLink>
<NuxtLink to="/notes/vue" class="app-nav-sublink">Vue 노트</NuxtLink>
<nav class="app-header__nav" aria-label="주요 내비게이션">
<details class="app-header__accordion" :open="isMemoExpanded" @toggle="syncMemoExpanded">
<summary
:id="memoSummaryId"
class="app-header__link app-header__summary"
:aria-controls="memoSubnavId"
:aria-expanded="isMemoExpanded"
>
메모
</summary>
<div :id="memoSubnavId" class="app-header__subnav" :aria-labelledby="memoSummaryId">
<NuxtLink
to="/notes/css"
class="app-header__sublink"
:aria-current="isCurrentPath('/notes/css') ? 'page' : undefined"
>
CSS 메모
</NuxtLink>
<NuxtLink
to="/notes/javascript"
class="app-header__sublink"
:aria-current="isCurrentPath('/notes/javascript') ? 'page' : undefined"
>
JavaScript 메모
</NuxtLink>
<NuxtLink
to="/notes/v8-optimization"
class="app-header__sublink"
:aria-current="isCurrentPath('/notes/v8-optimization') ? 'page' : undefined"
>
V8 메모
</NuxtLink>
<NuxtLink
to="/notes/vue"
class="app-header__sublink"
:aria-current="isCurrentPath('/notes/vue') ? 'page' : undefined"
>
Vue 메모
</NuxtLink>
</div>
</details>
<NuxtLink to="/posts/reading" class="app-nav-link">읽을 글</NuxtLink>
<NuxtLink to="/posts/archive" class="app-nav-link">아카이브</NuxtLink>
<details class="app-header__accordion" :open="isWorklogExpanded" @toggle="syncWorklogExpanded">
<summary
:id="worklogSummaryId"
class="app-header__link app-header__summary"
:aria-controls="worklogSubnavId"
:aria-expanded="isWorklogExpanded"
>
작업일지
</summary>
<div :id="worklogSubnavId" class="app-header__subnav" :aria-labelledby="worklogSummaryId">
<div
v-for="(section, sectionIndex) in worklogSections"
:key="section.title"
class="app-header__subnav-section"
>
<p class="app-header__subnav-title">
{{ section.title }}
</p>

<NuxtLink
v-for="item in section.items"
:key="item.path"
:to="item.path"
class="app-header__sublink"
:aria-current="isCurrentPath(item.path) ? 'page' : undefined"
>
{{ item.label }}
</NuxtLink>

<div
v-if="sectionIndex < worklogSections.length - 1"
class="app-header__subnav-divider"
aria-hidden="true"
/>
</div>
</div>
</details>
<NuxtLink
to="/posts/reading"
class="app-header__link"
:aria-current="isCurrentPath('/posts/reading') ? 'page' : undefined"
>
읽을 내용
</NuxtLink>
<NuxtLink
to="/posts/archive"
class="app-header__link"
:aria-current="isCurrentPath('/posts/archive') ? 'page' : undefined"
>
보관함
</NuxtLink>
</nav>

<div class="app-sidebar-note">
<NuxtLink to="/about" class="app-sidebar-note__title">About me</NuxtLink>
<div class="app-header__sidebar-note">
<NuxtLink
to="/about"
class="app-header__sidebar-note-title"
:aria-current="isCurrentPath('/about') ? 'page' : undefined"
>
소개
</NuxtLink>
</div>
</aside>
</template>

<script setup lang="ts">
import { worklogSections } from '~/data/worklog-sections'

const route = useRoute();

const isNotesOpen = computed(() => route.path.startsWith("/notes"));
const memoSummaryId = "memo-summary";
const memoSubnavId = "memo-subnav";
const worklogSummaryId = "worklog-summary";
const worklogSubnavId = "worklog-subnav";

const isMemoExpanded = ref(false)
const isWorklogExpanded = ref(false)

watch(
() => route.path,
(path) => {
isMemoExpanded.value = path.startsWith('/notes')
isWorklogExpanded.value = path.startsWith('/posts')
},
{ immediate: true },
)

function isCurrentPath(path: string): boolean {
return route.path === path
}

function syncMemoExpanded(event: Event): void {
const target = event.currentTarget

if (target instanceof HTMLDetailsElement) {
isMemoExpanded.value = target.open
}
}

function syncWorklogExpanded(event: Event): void {
const target = event.currentTarget

if (target instanceof HTMLDetailsElement) {
isWorklogExpanded.value = target.open
}
}
</script>
73 changes: 73 additions & 0 deletions content/notes/v8-optimization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: V8 최적화는 코드 모양을 본다
description: V8이 좋아하는 객체 모양, 타입 안정성, hot path를 코드로 다루는 방법을 정리한 메모입니다.
date: 2026-04-27
category: JavaScript
---

이걸 “어떻게 써야 하는가”로 바꾸면, V8이 좋아하는 코드의 특징을 잡아두는 쪽이 더 중요합니다.
즉, **모든 코드를 똑같이 무겁게 최적화하는 게 아니라, 자주 실행되는 부분에만 최적화 비용을 쓰는 엔진**이라고 보면 됩니다.

## V8이 대충 이런 식으로 움직인다

- `Ignition`이 JavaScript를 먼저 `bytecode`로 실행합니다.
- 실행하면서 객체 모양(shape)과 타입에 대한 `type feedback`을 모읍니다.
- 자주 실행되는 함수, 즉 `hot function`은 더 강한 `JIT tier`로 올라갑니다.
- 이 최적화는 `speculative` 하므로, 가정이 틀리면 `deopt`로 다시 안전한 경로로 돌아갈 수 있습니다.

관련해서는 [Sparkplug](https://v8.dev/blog/sparkplug), [Maglev](https://v8.dev/blog/maglev), [Speculative optimizations](https://v8.dev/blog/wasm-speculative-optimizations)를 같이 보면 흐름이 잘 잡힙니다.

## 핵심 용어만 보면

- `Ignition`: 처음 코드를 빠르게 실행하는 단계
- `bytecode`: JS와 기계어 사이의 중간 명령어
- `객체 모양(shape)`: 같은 객체라도 속성 순서와 구조가 같으면 더 빨리 접근할 수 있는 내부 형태
- `type feedback`: 실행 중에 “여기엔 숫자가 오네”, “이 객체 구조는 이렇네” 같은 힌트를 모으는 것
- `hot function`: 자주 호출되는 함수
- `JIT tier`: 더 빠른 실행을 위해 단계적으로 올라가는 최적화 수준
- `speculative`: 지금까지의 패턴을 바탕으로 앞으로도 비슷할 거라고 가정하는 방식
- `deopt`: 그 가정이 깨졌을 때 최적화된 코드에서 안전한 코드로 되돌아가는 것

## 왜 hot code에만 더 투자하나

최적화는 공짜가 아닙니다. 관찰하고, 컴파일하고, 가정을 세우는 데 비용이 듭니다.
그래서 한 번만 실행되고 끝나는 함수는 빠르게 시작하는 게 더 중요하고, 수백 번 반복되는 함수는 더 강하게 최적화할 가치가 있습니다.

```js
function printWelcome() {
console.log("Welcome");
}

printWelcome();
```

이런 코드는 금방 끝나기 때문에, 무거운 최적화를 붙여도 이득이 적습니다.

```js
function sum(items) {
let total = 0;

for (let i = 0; i < items.length; i++) {
total += items[i];
}

return total;
}

for (let i = 0; i < 100000; i++) {
sum([1, 2, 3, 4, 5]);
}
```

이런 함수는 반복 실행되니까, V8이 더 적극적으로 최적화할 가능성이 큽니다.

## 그래서 실무에서는 이렇게 보면 된다

- 객체의 속성과 순서는 가능하면 일관되게 유지합니다.
- 타입이 자주 바뀌는 코드는 hot path에서 특히 조심합니다.
- `delete`나 실행 중 구조 변경은 가능한 피합니다.
- 진짜 성능 문제는 보통 hot path에 있으니, 먼저 그 구간부터 봅니다.

## 한 문장 요약

**V8은 처음엔 빠르게 실행하고, 실행하면서 배운 뒤, 자주 쓰이는 코드만 더 강하게 최적화한다. 그래서 성능은 hot path부터 보는 게 맞다.**
35 changes: 6 additions & 29 deletions pages/notes/[slug].vue
Original file line number Diff line number Diff line change
@@ -1,43 +1,20 @@
<template>
<section v-if="note" class="note-page">
<header class="note-page__header">
<p class="note-page__eyebrow">{{ note.category }}</p>
<h1 class="note-page__title">{{ note.title }}</h1>
<p class="note-page__meta">{{ note.date }}</p>
<p class="note-page__lead">{{ note.description }}</p>
</header>

<article class="note-page__article">
<ContentRenderer :value="note" />
</article>
</section>
<NoteContentPage :content-path="contentPath" />
</template>

<script setup lang="ts">
import { useBlogSeo } from '~/composables/useBlogSeo'
import NoteContentPage from '~/components/notes/NoteContentPage.vue'
import { resolveRouteSlug } from '~/utils/route'

const route = useRoute()
const slug = computed(() => {
const value = route.params.slug
const raw = Array.isArray(value) ? value[0] : value
return typeof raw === 'string' ? raw : ''
})

const note = await queryCollection('notes')
.path(`/notes/${slug.value}`)
.first()
const slug = resolveRouteSlug(route.params.slug)

if (!note) {
if (!slug) {
throw createError({
statusCode: 404,
statusMessage: '메모를 찾을 수 없습니다.',
})
}

useBlogSeo({
title: note.title,
description: note.description,
path: route.path,
type: 'article',
})
const contentPath = `/notes/${slug}`
</script>
Loading