<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>nana's commit-log</title>
        <link>https://velog.io/</link>
        <description>안녕하세요 프론트엔드&amp;퍼블리셔 nana의 블로그입니다.</description>
        <lastBuildDate>Fri, 07 Aug 2026 04:53:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>nana's commit-log</title>
            <url>https://velog.velcdn.com/images/kim-na-hyeong/profile/e5ff55ba-74b7-4eac-88f5-d4f19c71db24/image.jfif</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. nana's commit-log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kim-na-hyeong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[퍼블리셔 복기 시리즈] 기본 모달 vanilla JS vs jQuery]]></title>
            <link>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94%EB%A6%AC%EC%85%94-%EB%B3%B5%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EA%B8%B0%EB%B3%B8-%EB%AA%A8%EB%8B%AC-vanilla-JS-vs-jQuery</link>
            <guid>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94%EB%A6%AC%EC%85%94-%EB%B3%B5%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EA%B8%B0%EB%B3%B8-%EB%AA%A8%EB%8B%AC-vanilla-JS-vs-jQuery</guid>
            <pubDate>Fri, 07 Aug 2026 04:53:21 GMT</pubDate>
            <description><![CDATA[<h2 id="codepen⚡">codepen⚡</h2>
<p>!codepen[editor/na-hyeong9/embed/019fd9fb-50f4-73b8-b9ce-06e0a3ce98ee?default-tab=html%2Cresult]</p>
<h2 id="day-5-·-모달-팝업-바닐라--jquery">Day 5 · 모달 팝업 (바닐라 + jQuery)</h2>
<blockquote>
<p>버튼을 누르면 뜨고, 바깥/X/ESC로 닫히는 창.
핵심은 오버레이 클릭 판별(이벤트 버블링)과 ESC 키 처리, 그리고 접근성이다.
JS는 일반 함수 기준.</p>
</blockquote>
<hr>
<h2 id="핵심-3가지">핵심 3가지</h2>
<p><strong>① 오버레이 클릭 판별</strong>
어두운 배경(오버레이)을 누르면 닫히고, 모달 안쪽을 누르면 안 닫혀야 한다. <code>event.target</code>이 오버레이 자신인지 확인해서 구분한다.</p>
<p><strong>② ESC 키로 닫기</strong>
키보드 사용자를 위해 ESC로도 닫히게 한다.</p>
<p><strong>③ 접근성</strong>
열릴 때 포커스를 모달로 옮기고, <code>role=&quot;dialog&quot;</code>, <code>aria-modal=&quot;true&quot;</code>를 붙인다.</p>
<hr>
<h2 id="html-공통">HTML (공통)</h2>
<pre><code class="language-html">&lt;body&gt;
  &lt;button type=&quot;button&quot; class=&quot;open-btn&quot; data-target=&quot;modal1&quot;&gt;① 글래스모피즘&lt;/button&gt;
  &lt;button type=&quot;button&quot; class=&quot;open-btn&quot; data-target=&quot;modal2&quot;&gt;② 미니멀&lt;/button&gt;
  &lt;button type=&quot;button&quot; class=&quot;open-btn&quot; data-target=&quot;modal3&quot;&gt;③ 그라데이션&lt;/button&gt;
  &lt;button type=&quot;button&quot; class=&quot;open-btn&quot; data-target=&quot;modal4&quot;&gt;④ 파스텔&lt;/button&gt;

  &lt;!-- ① 글래스모피즘 --&gt;
  &lt;div class=&quot;overlay&quot; id=&quot;modal1&quot; hidden&gt;
    &lt;div class=&quot;modal glass&quot; role=&quot;dialog&quot; aria-modal=&quot;true&quot; aria-labelledby=&quot;t1&quot;&gt;
      &lt;button type=&quot;button&quot; class=&quot;close-btn js-close&quot; aria-label=&quot;닫기&quot;&gt;&amp;times;&lt;/button&gt;
      &lt;div class=&quot;modal-icon&quot;&gt;✨&lt;/div&gt;
      &lt;h2 id=&quot;t1&quot; class=&quot;modal-title&quot;&gt;글래스모피즘&lt;/h2&gt;
      &lt;p class=&quot;modal-body&quot;&gt;반투명 유리 질감에 배경이 흐릿하게 비쳐 보이는 요즘 유행하는 스타일이에요.&lt;/p&gt;
      &lt;div class=&quot;modal-actions&quot;&gt;
        &lt;button type=&quot;button&quot; class=&quot;btn btn-ghost js-close&quot;&gt;취소&lt;/button&gt;
        &lt;button type=&quot;button&quot; class=&quot;btn btn-primary&quot;&gt;확인&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- ② 미니멀 --&gt;
  &lt;div class=&quot;overlay&quot; id=&quot;modal2&quot; hidden&gt;
    &lt;div class=&quot;modal clean&quot; role=&quot;dialog&quot; aria-modal=&quot;true&quot; aria-labelledby=&quot;t2&quot;&gt;
      &lt;button type=&quot;button&quot; class=&quot;close-btn js-close&quot; aria-label=&quot;닫기&quot;&gt;&amp;times;&lt;/button&gt;
      &lt;div class=&quot;modal-icon&quot;&gt;📄&lt;/div&gt;
      &lt;h2 id=&quot;t2&quot; class=&quot;modal-title&quot;&gt;미니멀 클린&lt;/h2&gt;
      &lt;p class=&quot;modal-body&quot;&gt;군더더기 없이 깔끔한 흑백 기반 디자인. 어디에나 잘 어울려요.&lt;/p&gt;
      &lt;div class=&quot;modal-actions&quot;&gt;
        &lt;button type=&quot;button&quot; class=&quot;btn btn-ghost js-close&quot;&gt;취소&lt;/button&gt;
        &lt;button type=&quot;button&quot; class=&quot;btn btn-primary&quot;&gt;확인&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- ③ 그라데이션 --&gt;
  &lt;div class=&quot;overlay&quot; id=&quot;modal3&quot; hidden&gt;
    &lt;div class=&quot;modal gradient&quot; role=&quot;dialog&quot; aria-modal=&quot;true&quot; aria-labelledby=&quot;t3&quot;&gt;
      &lt;button type=&quot;button&quot; class=&quot;close-btn js-close&quot; aria-label=&quot;닫기&quot;&gt;&amp;times;&lt;/button&gt;
      &lt;div class=&quot;modal-icon&quot;&gt;🚀&lt;/div&gt;
      &lt;h2 id=&quot;t3&quot; class=&quot;modal-title&quot;&gt;그라데이션&lt;/h2&gt;
      &lt;p class=&quot;modal-body&quot;&gt;화사한 보라-파랑 그라데이션으로 시선을 끄는 화려한 스타일이에요.&lt;/p&gt;
      &lt;div class=&quot;modal-actions&quot;&gt;
        &lt;button type=&quot;button&quot; class=&quot;btn btn-ghost js-close&quot;&gt;취소&lt;/button&gt;
        &lt;button type=&quot;button&quot; class=&quot;btn btn-primary&quot;&gt;확인&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- ④ 파스텔 --&gt;
  &lt;div class=&quot;overlay&quot; id=&quot;modal4&quot; hidden&gt;
    &lt;div class=&quot;modal pastel&quot; role=&quot;dialog&quot; aria-modal=&quot;true&quot; aria-labelledby=&quot;t4&quot;&gt;
      &lt;button type=&quot;button&quot; class=&quot;close-btn js-close&quot; aria-label=&quot;닫기&quot;&gt;&amp;times;&lt;/button&gt;
      &lt;div class=&quot;modal-icon&quot;&gt;🌸&lt;/div&gt;
      &lt;h2 id=&quot;t4&quot; class=&quot;modal-title&quot;&gt;파스텔 귀여움&lt;/h2&gt;
      &lt;p class=&quot;modal-body&quot;&gt;부드러운 핑크 파스텔톤에 둥근 테두리로 귀엽고 사랑스러운 느낌이에요.&lt;/p&gt;
      &lt;div class=&quot;modal-actions&quot;&gt;
        &lt;button type=&quot;button&quot; class=&quot;btn btn-ghost js-close&quot;&gt;취소&lt;/button&gt;
        &lt;button type=&quot;button&quot; class=&quot;btn btn-primary&quot;&gt;확인&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;script src=&quot;./script.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
</code></pre>
<hr>
<h2 id="css-공통">CSS (공통)</h2>
<pre><code class="language-css">* { box-sizing: border-box; }

body {
  margin: 0; min-height: 100vh;
  display: flex; flex-wrap: wrap;
  align-items: center; justify-content: center;
  gap: 14px; padding: 40px 20px;
  background: linear-gradient(135deg, #fff3ed 0%, #ffe8f0 100%);
  font-family: system-ui, sans-serif;
}

.open-btn {
  padding: 14px 22px;
  border: none; border-radius: 14px;
  background: #fff; color: #6c5ce7;
  font-size: 14px; font-weight: 700;
  cursor: pointer;
  box-shadow: 0 4px 14px rgba(0,0,0,.08);
  transition: transform .2s, box-shadow .2s;
}
.open-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(0,0,0,.12); }

.overlay {
  position: fixed; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  animation: fadeIn .25s ease;
}
.overlay[hidden] { display: none; }

.modal {
  position: relative;
  max-width: 380px; width: 100%;
  padding: 36px 32px;
  animation: popIn .3s cubic-bezier(.68,-0.55,.27,1.55);
}

.close-btn {
  position: absolute; top: 18px; right: 18px;
  width: 32px; height: 32px;
  border: none; background: rgba(0,0,0,.05);
  border-radius: 50%;
  font-size: 20px; cursor: pointer; line-height: 1;
  transition: background .2s;
}
.close-btn:hover { background: rgba(0,0,0,.12); }

.modal-icon {
  width: 60px; height: 60px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 28px; margin-bottom: 18px;
}
.modal-title { margin: 0 0 10px; font-size: 21px; font-weight: 800; }
.modal-body { margin: 0; font-size: 14px; line-height: 1.65; }
.modal-actions { display: flex; gap: 10px; margin-top: 26px; }
.btn {
  flex: 1; padding: 13px;
  border: none; border-radius: 12px;
  font-size: 14px; font-weight: 700; cursor: pointer;
  transition: transform .15s, filter .2s;
}
.btn:hover { filter: brightness(1.05); }
.btn:active { transform: scale(.97); }

/* ═══ ① 글래스모피즘 ═══ */
#modal1 { background: rgba(30, 20, 60, .35); backdrop-filter: blur(8px); }
.glass {
  background: rgba(255,255,255,.15);
  backdrop-filter: blur(20px);
  border: 1px solid rgba(255,255,255,.3);
  border-radius: 24px;
  box-shadow: 0 20px 60px rgba(0,0,0,.3);
  color: #fff;
}
.glass .close-btn { background: rgba(255,255,255,.2); color: #fff; }
.glass .close-btn:hover { background: rgba(255,255,255,.35); }
.glass .modal-icon { background: rgba(255,255,255,.2); }
.glass .modal-body { color: rgba(255,255,255,.85); }
.glass .btn-primary { background: #fff; color: #6c5ce7; }
.glass .btn-ghost { background: rgba(255,255,255,.2); color: #fff; }

/* ═══ ② 미니멀·클린 ═══ */
#modal2 { background: rgba(0,0,0,.4); }
.clean {
  background: #fff;
  border-radius: 16px;
  box-shadow: 0 20px 60px rgba(0,0,0,.2);
  color: #1a1a1a;
}
.clean .modal-icon { background: #f0f0f5; }
.clean .modal-title { color: #1a1a1a; }
.clean .modal-body { color: #666; }
.clean .btn-primary { background: #1a1a1a; color: #fff; }
.clean .btn-ghost { background: #f0f0f0; color: #555; }

/* ═══ ③ 그라데이션 ═══ */
#modal3 { background: rgba(20,0,40,.5); }
.gradient {
  background: linear-gradient(145deg, #667eea 0%, #764ba2 100%);
  border-radius: 24px;
  box-shadow: 0 20px 60px rgba(118,75,162,.5);
  color: #fff;
}
.gradient .close-btn { background: rgba(255,255,255,.2); color: #fff; }
.gradient .close-btn:hover { background: rgba(255,255,255,.35); }
.gradient .modal-icon {
  background: rgba(255,255,255,.2);
  box-shadow: 0 8px 20px rgba(0,0,0,.15);
}
.gradient .modal-body { color: rgba(255,255,255,.9); }
.gradient .btn-primary { background: #fff; color: #764ba2; }
.gradient .btn-ghost { background: rgba(255,255,255,.2); color: #fff; }

/* ═══ ④ 파스텔·귀여움 ═══ */
#modal4 { background: rgba(255, 182, 193, .35); backdrop-filter: blur(4px); }
.pastel {
  background: #fff;
  border: 3px solid #ffd6e8;
  border-radius: 28px;
  box-shadow: 0 16px 48px rgba(255,150,190,.35);
  color: #6b4a5a;
}
.pastel .close-btn { background: #ffe8f0; color: #d67ba0; }
.pastel .close-btn:hover { background: #ffd0e4; }
.pastel .modal-icon { background: #ffe8f0; }
.pastel .modal-title { color: #d15b8a; }
.pastel .modal-body { color: #9a7684; }
.pastel .btn-primary { background: #ff9ec4; color: #fff; }
.pastel .btn-ghost { background: #fff0f6; color: #d67ba0; }

@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes popIn {
  from { opacity: 0; transform: scale(.9) translateY(10px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .overlay, .modal { animation: none !important; }
}</code></pre>
<hr>
<h2 id="버전-①-바닐라-js">버전 ① 바닐라 JS</h2>
<pre><code class="language-js">var openButtons = document.querySelectorAll(&quot;.open-btn&quot;);
var closeButtons = document.querySelectorAll(&quot;.js-close&quot;);
var overlays = document.querySelectorAll(&quot;.overlay&quot;);
var lastFocused = null;

openButtons.forEach(function (btn) {
  btn.addEventListener(&quot;click&quot;, function () {
    var overlay = document.getElementById(this.getAttribute(&quot;data-target&quot;));
    openModal(overlay);
  });
});

closeButtons.forEach(function (btn) {
  btn.addEventListener(&quot;click&quot;, function () {
    closeModal(this.closest(&quot;.overlay&quot;));
  });
});

overlays.forEach(function (overlay) {
  overlay.addEventListener(&quot;click&quot;, function (e) {
    if (e.target === overlay) closeModal(overlay);
  });
});

document.addEventListener(&quot;keydown&quot;, function (e) {
  if (e.key === &quot;Escape&quot;) {
    var open = document.querySelector(&quot;.overlay:not([hidden])&quot;);
    if (open) closeModal(open);
  }
});

function openModal(overlay) {
  lastFocused = document.activeElement;
  overlay.hidden = false;
  var target = overlay.querySelector(&quot;.close-btn&quot;);
  if (target) target.focus();
}

function closeModal(overlay) {
  overlay.hidden = true;
  if (lastFocused) lastFocused.focus();
}</code></pre>
<hr>
<h2 id="버전-②-jquery">버전 ② jQuery</h2>
<pre><code class="language-html">&lt;script src=&quot;https://code.jquery.com/jquery-3.7.1.min.js&quot;&gt;&lt;/script&gt;</code></pre>
<pre><code class="language-js">var $lastFocused = null;

// 열기
$(&quot;.open-btn&quot;).on(&quot;click&quot;, function () {
  var targetId = $(this).data(&quot;target&quot;);
  openModal($(&quot;#&quot; + targetId));
});

// 닫기 (X, 취소)
$(&quot;.js-close&quot;).on(&quot;click&quot;, function () {
  closeModal($(this).closest(&quot;.overlay&quot;));
});

// 오버레이 바깥 클릭
$(&quot;.overlay&quot;).on(&quot;click&quot;, function (e) {
  if (e.target === this) closeModal($(this));
});

// ESC로 닫기
$(document).on(&quot;keydown&quot;, function (e) {
  if (e.key === &quot;Escape&quot;) {
    var $open = $(&quot;.overlay&quot;).not(&quot;[hidden]&quot;);
    if ($open.length) closeModal($open);
  }
});

function openModal($overlay) {
  $lastFocused = document.activeElement;
  $overlay.prop(&quot;hidden&quot;, false);
  $overlay.find(&quot;.close-btn&quot;).first().focus();
}

function closeModal($overlay) {
  $overlay.prop(&quot;hidden&quot;, true);
  if ($lastFocused) $lastFocused.focus();
}</code></pre>
<hr>
<h2 id="이벤트-버블링이-핵심">이벤트 버블링이 핵심</h2>
<p>오버레이 바깥 클릭 판별이 이 챌린지의 핵심이다.</p>
<pre><code class="language-js">overlay.addEventListener(&quot;click&quot;, function (e) {
  if (e.target === overlay) {   // ← 이 조건
    closeModal(overlay);
  }
});</code></pre>
<p>오버레이 안에 모달이 들어있어서, 모달을 클릭해도 그 클릭이 <strong>부모인 오버레이까지 전파(버블링)</strong> 된다. 그냥 오버레이 클릭 = 닫기로 하면, 모달 안을 눌러도 닫혀버린다.</p>
<p><code>e.target</code>은 <strong>실제로 클릭된 요소</strong>다. 오버레이 자신을 눌렀을 때만 <code>e.target === overlay</code>가 참이 된다. 모달 안쪽을 누르면 <code>e.target</code>은 모달이라 조건이 거짓이 되어 안 닫힌다.</p>
<pre><code>오버레이 클릭 → e.target = 오버레이 → 닫힘 ✅
모달 안 클릭  → e.target = 모달   → 안 닫힘 ✅</code></pre><hr>
<h2 id="두-버전-비교">두 버전 비교</h2>
<table>
<thead>
<tr>
<th>작업</th>
<th>바닐라</th>
<th>jQuery</th>
</tr>
</thead>
<tbody><tr>
<td>data 속성 읽기</td>
<td><code>getAttribute(&quot;data-target&quot;)</code></td>
<td><code>.data(&quot;target&quot;)</code></td>
</tr>
<tr>
<td>가장 가까운 부모</td>
<td><code>.closest(&quot;.overlay&quot;)</code></td>
<td><code>.closest(&quot;.overlay&quot;)</code></td>
</tr>
<tr>
<td>hidden 설정</td>
<td><code>.hidden = true</code></td>
<td><code>.prop(&quot;hidden&quot;, true)</code></td>
</tr>
<tr>
<td>조건부 선택</td>
<td><code>querySelector(&quot;:not([hidden])&quot;)</code></td>
<td><code>.not(&quot;[hidden]&quot;)</code></td>
</tr>
</tbody></table>
<p><code>closest</code>는 바닐라에도 있어서 jQuery랑 거의 같다. data 속성 읽기는 jQuery의 <code>.data()</code>가 조금 더 짧다.</p>
<hr>
<h2 id="접근성-포인트">접근성 포인트</h2>
<p><strong><code>role=&quot;dialog&quot;</code> + <code>aria-modal=&quot;true&quot;</code></strong>
스크린리더에게 &quot;지금 대화상자가 열렸고, 이것만 조작 가능하다&quot;고 알린다.</p>
<p><strong><code>aria-labelledby</code></strong>
모달 제목과 연결해서, 열릴 때 제목을 읽어준다.</p>
<p><strong>포커스 이동 &amp; 복귀</strong>
열 때 모달 안으로 포커스를 옮기고(<code>focus()</code>), 닫을 때 원래 있던 버튼으로 되돌린다(<code>lastFocused</code>). 키보드 사용자가 길을 잃지 않게 하는 필수 처리다.</p>
<p><strong>닫기 버튼에 <code>aria-label</code></strong>
X 버튼은 <code>&amp;times;</code> 기호뿐이라 스크린리더가 못 읽는다. <code>aria-label=&quot;닫기&quot;</code>로 이름을 준다.</p>
<hr>
<h2 id="실험-포인트">실험 포인트</h2>
<ul>
<li><strong>popIn 이징 바꾸기</strong>: <code>cubic-bezier(.68,-0.55,.27,1.55)</code>를 <code>ease</code>로 → 튐 없이 부드럽게</li>
<li><strong>오버레이 배경 흐리기</strong>: <code>.overlay</code>에 <code>backdrop-filter: blur(4px)</code> 추가 → 뒤가 흐려짐</li>
<li><strong>body 스크롤 잠그기</strong> (심화): 모달 열릴 때 <code>document.body.style.overflow = &quot;hidden&quot;</code>으로 뒤 스크롤 막기</li>
<li><strong>포커스 트랩</strong> (심화): Tab이 모달 밖으로 못 나가게 가두기</li>
</ul>
<hr>
<h2 id="기록용-한-줄">기록용 한 줄</h2>
<ul>
<li><strong>뭘 만들었나</strong>: 바깥/X/ESC로 닫히는 모달 2종 (바닐라 + jQuery)</li>
<li><strong>어디서 막혔나</strong>: 모달 안을 눌러도 닫혀버림 → <code>e.target === overlay</code> 조건으로 오버레이 자신일 때만 닫도록 해결</li>
<li><strong>뭘 알게 됐나</strong>: 이벤트 버블링 때문에 자식 클릭도 부모로 전파된다. <code>e.target</code>으로 진짜 클릭 지점을 구분하는 게 핵심</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 모던 자바스크립트부터 다시 시작 - 7 - 프로토타입]]></title>
            <link>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-7-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-9f4ovj2u</link>
            <guid>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-7-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85-9f4ovj2u</guid>
            <pubDate>Mon, 27 Jul 2026 04:53:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지난 편에서 this를 다뤘다. 이번엔 자바스크립트의 상속 방식 <strong>프로토타입</strong>을 짧게 정리한다. class 문법 뒤에 숨은 구조다.</p>
</blockquote>
<hr>
<h2 id="자바스크립트엔-원래-class가-없었다">자바스크립트엔 원래 class가 없었다</h2>
<p>ES6 전까지 class 문법이 없었다. 대신 <strong>프로토타입(Prototype)</strong> 으로 상속을 구현했다. 지금 쓰는 <code>class</code>도 사실 프로토타입을 감싼 문법일 뿐이다.</p>
<hr>
<h2 id="객체는-부모를-참조한다">객체는 부모를 참조한다</h2>
<p>모든 객체는 자신의 부모 역할을 하는 객체(프로토타입)를 가리키는 숨은 링크가 있다.</p>
<pre><code class="language-js">const obj = { name: &#39;Alice&#39; };

console.log(obj.toString); // 함수 — 만든 적 없는데 있다</code></pre>
<p><code>toString</code>을 만든 적 없는데 쓸 수 있다. obj에 없으면 <strong>프로토타입을 타고 올라가서 찾기</strong> 때문이다.</p>
<hr>
<h2 id="프로토타입-체인">프로토타입 체인</h2>
<p>없으면 위로, 또 없으면 더 위로 찾아 올라가는 사슬이다.</p>
<pre><code>obj → Object.prototype → null</code></pre><p>스코프 체인이 변수를 위로 찾는 것과 닮았다. 이건 속성을 위로 찾는다.</p>
<hr>
<h2 id="prototype에-메서드를-두는-이유">prototype에 메서드를 두는 이유</h2>
<pre><code class="language-js">function User(name) {
  this.name = name;
}
User.prototype.greet = function () {
  console.log(this.name);
};

const a = new User(&#39;Alice&#39;);
const b = new User(&#39;Bob&#39;);
a.greet(); // Alice
b.greet(); // Bob</code></pre>
<p><code>greet</code>를 인스턴스마다 만들지 않고 <code>prototype</code>에 한 번만 만든다. <code>a</code>, <code>b</code>가 그걸 <strong>공유</strong>한다. 인스턴스 100개를 만들어도 함수는 하나다.</p>
<hr>
<h2 id="class는-프로토타입이다">class는 프로토타입이다</h2>
<pre><code class="language-js">class User {
  constructor(name) { this.name = name; }
  greet() { console.log(this.name); }
}</code></pre>
<p>깔끔하지만 동작은 위 생성자 함수와 똑같다. <code>greet</code>는 여전히 <code>User.prototype</code>에 들어간다.</p>
<pre><code class="language-js">console.log(typeof User); // &#39;function&#39; — class도 함수다</code></pre>
<p>class는 문법적 설탕(Syntactic Sugar)이다. async/await가 Promise를 감쌌듯, class는 프로토타입을 감쌌다.</p>
<hr>
<h2 id="오늘-얻은-것">오늘 얻은 것</h2>
<p>자바스크립트는 없는 속성을 프로토타입 체인을 타고 위로 찾는다. <code>prototype</code>에 메서드를 두면 인스턴스가 공유해서 효율적이고, class는 이걸 보기 좋게 감싼 문법이다. 결국 뿌리는 프로토타입이다.</p>
<p>다음 편은 <strong>모듈 시스템</strong> import/export와 CommonJS vs ESM을 정리할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[퍼블 복기 시리즈] 탭 탭 탭 탭 전환]]></title>
            <link>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94-%EB%B3%B5%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-%ED%83%AD-%ED%83%AD-%ED%83%AD-%ED%83%AD-%EC%A0%84%ED%99%98</link>
            <guid>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94-%EB%B3%B5%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-%ED%83%AD-%ED%83%AD-%ED%83%AD-%ED%83%AD-%EC%A0%84%ED%99%98</guid>
            <pubDate>Sat, 25 Jul 2026 14:49:31 GMT</pubDate>
            <description><![CDATA[<p>본인은 탭 전환을 제이쿼리로 만든 기억이 많다.
그래서! 이번엔 JS랑 jQuery 비교 버전을 만들어 볼 것이다.
(갑자기 든 생각인데 둘의 비교 버전 시리즈도 만들어봐야겠다. 나란 사람 성인 ADHD의 표본...ㅎㅎㅎ)</p>
<p>잡담은 이만하고 복기 시리즈 레쓰고</p>
<h2 id="day-4-·-탭-전환-바닐라--jquery">Day 4 · 탭 전환 (바닐라 + jQuery)</h2>
<blockquote>
<p>탭을 누르면 해당 콘텐츠만 보이는 UI.
여러 요소 중 &quot;지금 활성인 하나&quot;를 관리하는 감각을 익히는 게 목적이다.
JS는 일반 함수(function) 기준으로 작성했다. 이벤트 핸들러에서 <code>this</code>를 쓰기 좋기 때문이다.</p>
</blockquote>
<hr>
<h2 id="핵심-아이디어">핵심 아이디어</h2>
<p>탭은 &quot;여러 개 중 하나만 활성&quot;이라는 구조다.</p>
<pre><code>[탭1] [탭2] [탭3]   ← 하나만 활성 표시
─────────────────
   활성 탭의 내용만 보임</code></pre><p>아코디언은 여러 개가 동시에 열릴 수 있지만, 탭은 항상 정확히 하나만 보인다. 새 탭을 누르면 이전 탭을 꺼주는 처리가 필요하다.</p>
<hr>
<h3 id="codepen">codepen</h3>
<p>!codepen[editor/na-hyeong9/embed/019f99c2-a02a-7dd6-879a-d3e64ad5a928?default-tab=html%2Cresult]</p>
<h2 id="왜-일반-함수를-쓰나">왜 일반 함수를 쓰나???</h2>
<p>이벤트 핸들러 안에서 <code>this</code>는 <strong>이벤트가 걸린 요소</strong>를 가리킨다. 일반 함수라야 이게 성립한다.</p>
<pre><code class="language-js">// 일반 함수 — this = 클릭한 탭
tab.addEventListener(&#39;click&#39;, function () {
  this.classList.add(&#39;active&#39;); // 정상
});

// 화살표 함수 — this = 상위 스코프(window)
tab.addEventListener(&#39;click&#39;, () =&gt; {
  this.classList.add(&#39;active&#39;); // this가 탭이 아님, 원하는 대로 안 됨
});</code></pre>
<p>특히 jQuery는 <code>$(this)</code> 패턴이 핵심이라 일반 함수가 훨씬 자연스럽다.</p>
<hr>
<h2 id="html-두-버전-공통">HTML (두 버전 공통)</h2>
<pre><code class="language-html">&lt;main class=&quot;wrap&quot;&gt;
  &lt;h1 class=&quot;sr-only&quot;&gt;탭 전환 예제&lt;/h1&gt;

  &lt;div class=&quot;tabs&quot;&gt;
    &lt;div class=&quot;tab-list&quot; role=&quot;tablist&quot; aria-label=&quot;상품 정보&quot;&gt;
      &lt;button class=&quot;tab active&quot; role=&quot;tab&quot;
              aria-selected=&quot;true&quot; aria-controls=&quot;panel-info&quot; id=&quot;tab-info&quot;&gt;
        상품정보
      &lt;/button&gt;
      &lt;button class=&quot;tab&quot; role=&quot;tab&quot;
              aria-selected=&quot;false&quot; aria-controls=&quot;panel-review&quot; id=&quot;tab-review&quot;&gt;
        리뷰
      &lt;/button&gt;
      &lt;button class=&quot;tab&quot; role=&quot;tab&quot;
              aria-selected=&quot;false&quot; aria-controls=&quot;panel-qna&quot; id=&quot;tab-qna&quot;&gt;
        Q&amp;A
      &lt;/button&gt;
      &lt;span class=&quot;tab-indicator&quot; aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;
    &lt;/div&gt;

    &lt;div class=&quot;panel active&quot; role=&quot;tabpanel&quot;
         id=&quot;panel-info&quot; aria-labelledby=&quot;tab-info&quot;&gt;
      &lt;h3&gt;상품정보&lt;/h3&gt;
      &lt;p&gt;이 상품의 소재, 사이즈, 원산지 등 기본 정보를 담고 있습니다.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&quot;panel&quot; role=&quot;tabpanel&quot;
         id=&quot;panel-review&quot; aria-labelledby=&quot;tab-review&quot; hidden&gt;
      &lt;h3&gt;리뷰&lt;/h3&gt;
      &lt;p&gt;실제 구매자들이 남긴 후기입니다. 평점과 사진 리뷰를 확인할 수 있어요.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&quot;panel&quot; role=&quot;tabpanel&quot;
         id=&quot;panel-qna&quot; aria-labelledby=&quot;tab-qna&quot; hidden&gt;
      &lt;h3&gt;Q&amp;A&lt;/h3&gt;
      &lt;p&gt;상품에 대한 궁금한 점을 질문하고 답변을 받을 수 있는 공간입니다.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/main&gt;</code></pre>
<hr>
<h2 id="css-두-버전-공통">CSS (두 버전 공통)</h2>
<pre><code class="language-css">* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #fff3ed;
  font-family: system-ui, sans-serif;
  padding: 20px;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

.wrap { width: 100%; max-width: 480px; }

.tabs {
  background: #fff;
  border-radius: 16px;
  box-shadow: 0 4px 20px rgba(0,0,0,.06);
  overflow: hidden;
}

.tab-list {
  position: relative;
  display: flex;
  border-bottom: 1px solid #eee;
}

.tab {
  flex: 1;
  padding: 16px 8px;
  background: none;
  border: none;
  font-size: 15px;
  font-weight: 600;
  color: #999;
  cursor: pointer;
  transition: color .25s;
  font-family: inherit;
}
.tab:hover { color: #6c5ce7; }
.tab.active { color: #6c5ce7; }
.tab:focus-visible {
  outline: 3px solid #2d3436;
  outline-offset: -3px;
}

.tab-indicator {
  position: absolute;
  bottom: 0;
  left: 0;
  height: 3px;
  width: 33.333%;
  background: #6c5ce7;
  border-radius: 3px;
  transition: transform .3s cubic-bezier(.4,0,.2,1);
}

.panel {
  padding: 24px;
  animation: fade .3s ease;
}
.panel h3 { margin: 0 0 10px; font-size: 16px; color: #2d2540; }
.panel p { margin: 0; font-size: 14px; line-height: 1.7; color: #5c5470; }

@keyframes fade {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .tab-indicator, .tab { transition: none !important; }
  .panel { animation: none !important; }
}</code></pre>
<hr>
<h2 id="버전-①-바닐라-js-일반-함수">버전 ① 바닐라 JS (일반 함수)</h2>
<pre><code class="language-js">var tabs = document.querySelectorAll(&#39;.tab&#39;);
var panels = document.querySelectorAll(&#39;.panel&#39;);
var indicator = document.querySelector(&#39;.tab-indicator&#39;);

tabs.forEach(function (tab, index) {
  tab.addEventListener(&#39;click&#39;, function () {
    // 1. 모든 탭·패널 비활성화
    tabs.forEach(function (t) {
      t.classList.remove(&#39;active&#39;);
      t.setAttribute(&#39;aria-selected&#39;, &#39;false&#39;);
    });
    panels.forEach(function (p) {
      p.classList.remove(&#39;active&#39;);
      p.hidden = true;
    });

    // 2. 클릭한 탭만 활성화 (this = 클릭한 탭)
    this.classList.add(&#39;active&#39;);
    this.setAttribute(&#39;aria-selected&#39;, &#39;true&#39;);

    // 3. 짝이 되는 패널 켜기
    var panelId = this.getAttribute(&#39;aria-controls&#39;);
    var panel = document.getElementById(panelId);
    panel.classList.add(&#39;active&#39;);
    panel.hidden = false;

    // 4. 밑줄을 클릭한 탭 위치로 이동
    indicator.style.transform = &#39;translateX(&#39; + index * 100 + &#39;%)&#39;;
  });
});</code></pre>
<hr>
<h2 id="버전-②-jquery-일반-함수">버전 ② jQuery (일반 함수)</h2>
<pre><code class="language-html">&lt;!-- HTML 맨 아래에 jQuery CDN 추가 --&gt;
&lt;script src=&quot;https://code.jquery.com/jquery-3.7.1.min.js&quot;&gt;&lt;/script&gt;</code></pre>
<pre><code class="language-js">$(&#39;.tab&#39;).on(&#39;click&#39;, function () {
  // index() — 형제 중 몇 번째인지
  var index = $(this).index();

  // 1. 모든 탭·패널 비활성화
  $(&#39;.tab&#39;).removeClass(&#39;active&#39;).attr(&#39;aria-selected&#39;, &#39;false&#39;);
  $(&#39;.panel&#39;).removeClass(&#39;active&#39;).prop(&#39;hidden&#39;, true);

  // 2. 클릭한 탭만 활성화 ($(this) = 클릭한 탭)
  $(this).addClass(&#39;active&#39;).attr(&#39;aria-selected&#39;, &#39;true&#39;);

  // 3. 짝이 되는 패널 켜기
  var panelId = $(this).attr(&#39;aria-controls&#39;);
  $(&#39;#&#39; + panelId).addClass(&#39;active&#39;).prop(&#39;hidden&#39;, false);

  // 4. 밑줄 이동
  $(&#39;.tab-indicator&#39;).css(&#39;transform&#39;, &#39;translateX(&#39; + index * 100 + &#39;%)&#39;);
});</code></pre>
<hr>
<h2 id="두-버전-비교">두 버전 비교</h2>
<p>같은 동작을 나란히 놓으면 jQuery가 얼마나 짧아지는지 보인다.</p>
<table>
<thead>
<tr>
<th>작업</th>
<th>바닐라</th>
<th>jQuery</th>
</tr>
</thead>
<tbody><tr>
<td>요소 선택</td>
<td><code>document.querySelectorAll</code></td>
<td><code>$(&#39;.tab&#39;)</code></td>
</tr>
<tr>
<td>클래스 추가</td>
<td><code>el.classList.add(&#39;x&#39;)</code></td>
<td><code>.addClass(&#39;x&#39;)</code></td>
</tr>
<tr>
<td>클래스 제거</td>
<td><code>el.classList.remove(&#39;x&#39;)</code></td>
<td><code>.removeClass(&#39;x&#39;)</code></td>
</tr>
<tr>
<td>속성 설정</td>
<td><code>el.setAttribute(&#39;a&#39;,&#39;b&#39;)</code></td>
<td><code>.attr(&#39;a&#39;,&#39;b&#39;)</code></td>
</tr>
<tr>
<td>여러 요소 한 번에</td>
<td><code>forEach</code>로 반복</td>
<td>알아서 전체 적용</td>
</tr>
<tr>
<td>몇 번째인지</td>
<td>직접 index 전달</td>
<td><code>$(this).index()</code></td>
</tr>
</tbody></table>
<p>jQuery는 <strong>선택한 여러 요소에 메서드를 한 번에 적용</strong>해준다. 바닐라는 <code>forEach</code>로 하나씩 돌려야 하는 걸, jQuery는 <code>$(&#39;.tab&#39;).removeClass(&#39;active&#39;)</code> 한 줄로 끝낸다.</p>
<p>다만 요즘은 바닐라 JS만으로도 충분히 간결해졌고, 프레임워크(React 등)를 쓰면 jQuery를 거의 안 쓴다. SI/퍼블리싱 실무에서는 여전히 jQuery를 만나는 경우가 많으니 둘 다 익혀두면 좋다.</p>
<hr>
<h2 id="흐름-정리">흐름 정리</h2>
<p>두 버전 다 핵심은 &quot;전부 끄고 → 하나만 켜기&quot;다.</p>
<pre><code>탭 클릭
  → 모든 탭·패널 OFF   (한 번 싹 비운다)
  → 클릭한 탭만 ON
  → 짝 패널만 ON
  → 밑줄 이동</code></pre><p>한 번에 모두 끈 다음 하나만 켜면, 이전 상태를 추적할 필요가 없어서 코드가 단순해진다.</p>
<hr>
<h2 id="접근성-포인트">접근성 포인트</h2>
<p><strong><code>role=&quot;tablist&quot;</code> / <code>role=&quot;tab&quot;</code> / <code>role=&quot;tabpanel&quot;</code></strong>
이 세트가 스크린리더에게 &quot;탭 UI&quot;임을 알린다.</p>
<p><strong><code>aria-selected</code></strong>
지금 선택된 탭을 표시한다. 활성 탭만 <code>true</code>.</p>
<p><strong><code>aria-controls</code> / <code>aria-labelledby</code></strong>
탭과 패널을 양방향으로 연결한다.</p>
<p><strong><code>hidden</code> 속성</strong>
숨긴 패널을 스크린리더가 읽지 않도록 막는다.</p>
<hr>
<h2 id="실험-포인트">실험 포인트</h2>
<ul>
<li><strong>밑줄 이징</strong>: <code>cubic-bezier(.4,0,.2,1)</code> → <code>cubic-bezier(.68,-0.55,.27,1.55)</code>로 통통 튀게</li>
<li><strong>패널 전환</strong>: <code>@keyframes fade</code>의 <code>translateY(6px)</code> → <code>translateX(20px)</code>로 슬라이드 느낌</li>
<li><strong>탭 4개로 늘리기</strong>: <code>.tab-indicator</code>의 <code>width</code>를 <code>25%</code>로 바꿔야 한다</li>
<li><strong>키보드 좌우 화살표 지원</strong> (심화): 지금은 Tab 키로만 이동한다. 화살표로 넘기는 기능을 넣으면 완성도가 올라간다</li>
</ul>
<hr>
<h2 id="기록용-한-줄">기록용 한 줄</h2>
<ul>
<li><strong>뭘 만들었나</strong>: 밑줄이 따라 움직이는 탭 UI (바닐라 + jQuery)</li>
<li><strong>어디서 막혔나</strong>: 새 탭 켤 때 이전 탭 끄는 걸 빼먹어 여러 개가 동시 활성화됨 → &quot;전부 끄고 하나만 켜기&quot;로 해결</li>
<li><strong>뭘 알게 됐나</strong>: jQuery는 여러 요소에 메서드를 한 번에 적용해줘서 반복문이 사라진다. 바닐라의 <code>forEach</code>가 <code>$(&#39;.tab&#39;).removeClass()</code> 한 줄이 된다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[퍼블 복기 시리즈] 완전. 기본. 아코디언.]]></title>
            <link>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94-%EB%B3%B5%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EC%99%84%EC%A0%84.-%EA%B8%B0%EB%B3%B8.-%EC%95%84%EC%BD%94%EB%94%94%EC%96%B8</link>
            <guid>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94-%EB%B3%B5%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-%EC%99%84%EC%A0%84.-%EA%B8%B0%EB%B3%B8.-%EC%95%84%EC%BD%94%EB%94%94%EC%96%B8</guid>
            <pubDate>Mon, 20 Jul 2026 06:08:21 GMT</pubDate>
            <description><![CDATA[<p>먼저 말씀 드릴 건 이 글은 <strong>쌩기초, 쌩초보</strong> 를 위한 글인 점을 유의해주세요. ( _ _ )</p>
<blockquote>
<p>제목을 클릭하면 내용이 펼쳐지는 FAQ 형태.
이번엔 CSS만으로 안 되는 벽을 만난다. 높이 애니메이션의 함정을 직접 겪어보는 게 목적이다.</p>
</blockquote>
<hr>
<h3 id="codepen-⚡">codepen ⚡</h3>
<p>!codepen[editor/na-hyeong9/embed/019f7e07-8894-7b27-bce3-6f1262dddc44?default-tab=html%2Cresult]</p>
<h3 id="버튼이나-토글은-css만으로-끝났다-아코디언은-다르다">버튼이나 토글은 CSS만으로 끝났다. 아코디언은 다르다.</h3>
<pre><code class="language-css">.panel {
  height: 0;
  transition: height .3s;
}
.panel.open {
  height: auto;  /* ← 이게 안 먹는다 */
}</code></pre>
<p><code>height: auto</code>로는 애니메이션이 동작하지 않는다. 브라우저가 <code>auto</code>를 구체적인 픽셀 값으로 계산하지 못하기 때문에, 어디서 어디로 움직여야 할지 모르는 것이다.</p>
<p>내용의 높이는 글자 수에 따라 매번 달라진다. 그래서 이 문제를 우회하는 방법이 여러 개 생겼다. 셋 다 만들어보고 차이를 체감해보자.</p>
<hr>
<h2 id="방법-①-max-height-트릭-css만">방법 ① max-height 트릭 (CSS만)</h2>
<p>가장 널리 쓰이는 우회법이다. <code>height</code> 대신 <code>max-height</code>를 쓴다.</p>
<pre><code class="language-css">.panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height .3s ease;
}
.btn[aria-expanded=&quot;true&quot;] + .panel {
  max-height: 300px;  /* 내용보다 넉넉하게 */
}</code></pre>
<p><strong>장점</strong>: JS 없이 CSS만으로 된다.</p>
<p><strong>단점</strong>: <code>300px</code>이라는 숫자를 사람이 정해야 한다. 내용이 실제로 120px이면, 펼칠 때 나머지 180px만큼 빈 시간이 생겨서 애니메이션이 미묘하게 늦게 끝나는 느낌이 든다. 반대로 내용이 300px을 넘으면 잘려버린다.</p>
<hr>
<h2 id="방법-②-details--summary-네이티브">방법 ② details / summary (네이티브)</h2>
<p>HTML이 원래 제공하는 태그다.</p>
<pre><code class="language-html">&lt;details&gt;
  &lt;summary&gt;제목&lt;/summary&gt;
  &lt;p&gt;내용&lt;/p&gt;
&lt;/details&gt;</code></pre>
<p><strong>장점</strong>: 마크업 몇 줄이면 끝. 키보드·스크린리더 지원이 기본으로 딸려온다. 접근성 측면에서 가장 안전하다.</p>
<p><strong>단점</strong>: 열고 닫는 애니메이션이 없다. 그냥 툭 나타났다 툭 사라진다. 부드럽게 만들려면 결국 JS나 추가 트릭이 필요하다.</p>
<hr>
<h2 id="방법-③-scrollheight-측정-js">방법 ③ scrollHeight 측정 (JS)</h2>
<p>실제 내용의 높이를 JS로 재서 그 값을 넣어주는 방식이다.</p>
<pre><code class="language-js">const panel = document.querySelector(&#39;.panel&#39;);

// 열 때: 실제 높이를 재서 넣기
panel.style.height = panel.scrollHeight + &#39;px&#39;;

// 닫을 때
panel.style.height = &#39;0&#39;;</code></pre>
<p><code>scrollHeight</code>는 요소의 실제 콘텐츠 높이다. 이걸 픽셀 값으로 지정하면 애니메이션이 정확히 동작한다.</p>
<p><strong>장점</strong>: 내용 길이에 상관없이 항상 정확하다.</p>
<p><strong>단점</strong>: JS가 필요하고, 창 크기가 바뀌면 다시 재야 한다.</p>
<hr>
<h2 id="완성-코드">완성 코드</h2>
<p>세 방식을 한 페이지에 다 넣었다.</p>
<h3 id="html">HTML</h3>
<pre><code class="language-html">&lt;main class=&quot;wrap&quot;&gt;
  &lt;h1 class=&quot;sr-only&quot;&gt;아코디언 갤러리&lt;/h1&gt;

  &lt;!-- ① max-height 트릭 --&gt;
  &lt;section class=&quot;demo&quot;&gt;
    &lt;h2 class=&quot;demo-title&quot;&gt;① max-height 트릭 &lt;em&gt;CSS only&lt;/em&gt;&lt;/h2&gt;
    &lt;div class=&quot;ac ac-mh&quot;&gt;
      &lt;div class=&quot;ac-item&quot;&gt;
        &lt;button class=&quot;ac-btn&quot; aria-expanded=&quot;false&quot; aria-controls=&quot;p1&quot;&gt;
          배송은 얼마나 걸리나요?
          &lt;span class=&quot;ac-icon&quot; aria-hidden=&quot;true&quot;&gt;▾&lt;/span&gt;
        &lt;/button&gt;
        &lt;div class=&quot;ac-panel&quot; id=&quot;p1&quot;&gt;
          &lt;div class=&quot;ac-inner&quot;&gt;주문 후 영업일 기준 2~3일 소요됩니다. 도서산간 지역은 하루 더 걸릴 수 있어요.&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;ac-item&quot;&gt;
        &lt;button class=&quot;ac-btn&quot; aria-expanded=&quot;false&quot; aria-controls=&quot;p2&quot;&gt;
          교환·반품이 가능한가요?
          &lt;span class=&quot;ac-icon&quot; aria-hidden=&quot;true&quot;&gt;▾&lt;/span&gt;
        &lt;/button&gt;
        &lt;div class=&quot;ac-panel&quot; id=&quot;p2&quot;&gt;
          &lt;div class=&quot;ac-inner&quot;&gt;수령 후 7일 이내 가능합니다. 단순 변심의 경우 왕복 배송비가 부과됩니다.&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;!-- ② details/summary --&gt;
  &lt;section class=&quot;demo&quot;&gt;
    &lt;h2 class=&quot;demo-title&quot;&gt;② details / summary &lt;em&gt;native&lt;/em&gt;&lt;/h2&gt;
    &lt;div class=&quot;ac ac-det&quot;&gt;
      &lt;details&gt;
        &lt;summary&gt;회원가입 없이 주문할 수 있나요?&lt;/summary&gt;
        &lt;div class=&quot;det-body&quot;&gt;비회원 주문도 가능합니다. 다만 주문 조회 시 주문번호와 연락처가 필요해요.&lt;/div&gt;
      &lt;/details&gt;
      &lt;details&gt;
        &lt;summary&gt;영수증 발급은 어떻게 하나요?&lt;/summary&gt;
        &lt;div class=&quot;det-body&quot;&gt;마이페이지 &gt; 주문내역에서 현금영수증 및 세금계산서를 발급받을 수 있습니다.&lt;/div&gt;
      &lt;/details&gt;
    &lt;/div&gt;
  &lt;/section&gt;

  &lt;!-- ③ scrollHeight 측정 --&gt;
  &lt;section class=&quot;demo&quot;&gt;
    &lt;h2 class=&quot;demo-title&quot;&gt;③ scrollHeight 측정 &lt;em&gt;JS&lt;/em&gt;&lt;/h2&gt;
    &lt;div class=&quot;ac ac-js&quot;&gt;
      &lt;div class=&quot;ac-item&quot;&gt;
        &lt;button class=&quot;ac-btn js-btn&quot; aria-expanded=&quot;false&quot; aria-controls=&quot;p3&quot;&gt;
          포인트는 언제 적립되나요?
          &lt;span class=&quot;ac-icon&quot; aria-hidden=&quot;true&quot;&gt;▾&lt;/span&gt;
        &lt;/button&gt;
        &lt;div class=&quot;ac-panel js-panel&quot; id=&quot;p3&quot;&gt;
          &lt;div class=&quot;ac-inner&quot;&gt;구매 확정 후 자동 적립됩니다. 적립된 포인트는 다음 주문부터 사용할 수 있어요.&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;ac-item&quot;&gt;
        &lt;button class=&quot;ac-btn js-btn&quot; aria-expanded=&quot;false&quot; aria-controls=&quot;p4&quot;&gt;
          내용이 긴 경우는 어떻게 되나요?
          &lt;span class=&quot;ac-icon&quot; aria-hidden=&quot;true&quot;&gt;▾&lt;/span&gt;
        &lt;/button&gt;
        &lt;div class=&quot;ac-panel js-panel&quot; id=&quot;p4&quot;&gt;
          &lt;div class=&quot;ac-inner&quot;&gt;
            이 항목은 일부러 내용을 길게 넣었습니다. max-height 방식이었다면 미리 정해둔 값에 따라
            잘리거나 애니메이션이 어색해졌을 겁니다. scrollHeight로 실제 높이를 재면 내용이 아무리
            길어져도 정확한 지점까지만 펼쳐집니다. 길이에 신경 쓸 필요가 없다는 게 이 방식의 장점입니다.
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/section&gt;
&lt;/main&gt;</code></pre>
<h3 id="css">CSS</h3>
<pre><code class="language-css">* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  background: #fff3ed;
  font-family: system-ui, sans-serif;
  padding: 40px 20px;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

.wrap {
  width: 100%;
  max-width: 560px;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.demo-title {
  font-size: 14px;
  font-weight: 700;
  color: #4a3f6b;
  margin: 0 0 10px;
}
.demo-title em {
  font-style: normal;
  font-weight: 400;
  font-size: 12px;
  color: #8a7fb0;
  margin-left: 6px;
}

.ac {
  background: #fff;
  border: 1px solid #e5ddf0;
  border-radius: 12px;
  overflow: hidden;
}

.ac-item + .ac-item { border-top: 1px solid #e5ddf0; }

.ac-btn {
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 16px;
  font-size: 15px;
  font-weight: 600;
  color: #2d2540;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  font-family: inherit;
}
.ac-btn:hover { background: #faf7ff; }
.ac-btn:focus-visible {
  outline: 3px solid #2d3436;
  outline-offset: -3px;
}

.ac-icon {
  font-size: 12px;
  color: #8a7fb0;
  transition: transform .3s ease;
  flex-shrink: 0;
}
.ac-btn[aria-expanded=&quot;true&quot;] .ac-icon { transform: rotate(180deg); }

.ac-inner {
  padding: 0 16px 16px;
  font-size: 14px;
  line-height: 1.7;
  color: #5c5470;
}

/* ── ① max-height 방식 ── */
.ac-mh .ac-panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height .3s ease;
}
.ac-mh .ac-btn[aria-expanded=&quot;true&quot;] + .ac-panel {
  max-height: 300px;   /* 내용보다 넉넉하게 잡아야 함 */
}

/* ── ② details/summary ── */
.ac-det details { border-bottom: 1px solid #e5ddf0; }
.ac-det details:last-child { border-bottom: none; }
.ac-det summary {
  padding: 16px;
  font-size: 15px;
  font-weight: 600;
  color: #2d2540;
  cursor: pointer;
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.ac-det summary::-webkit-details-marker { display: none; }
.ac-det summary:hover { background: #faf7ff; }
.ac-det summary::after {
  content: &quot;▾&quot;;
  font-size: 12px;
  color: #8a7fb0;
  transition: transform .3s;
}
.ac-det details[open] summary::after { transform: rotate(180deg); }
.ac-det .det-body {
  padding: 0 16px 16px;
  font-size: 14px;
  line-height: 1.7;
  color: #5c5470;
}

/* ── ③ JS 방식 ── */
.ac-js .ac-panel {
  height: 0;
  overflow: hidden;
  transition: height .3s ease;
}

/* 모션 최소화 배려 */
@media (prefers-reduced-motion: reduce) {
  .ac-panel, .ac-icon, .ac-det summary::after { transition: none !important; }
}</code></pre>
<h3 id="js">JS</h3>
<pre><code class="language-js">/* ── ① max-height 방식: 클래스 토글만 ── */
document.querySelectorAll(&#39;.ac-mh .ac-btn&#39;).forEach((btn) =&gt; {
  btn.addEventListener(&#39;click&#39;, () =&gt; {
    const isOpen = btn.getAttribute(&#39;aria-expanded&#39;) === &#39;true&#39;;
    btn.setAttribute(&#39;aria-expanded&#39;, String(!isOpen));
  });
});

/* ── ③ scrollHeight 방식: 실제 높이를 재서 지정 ── */
document.querySelectorAll(&#39;.js-btn&#39;).forEach((btn) =&gt; {
  const panel = btn.nextElementSibling;

  btn.addEventListener(&#39;click&#39;, () =&gt; {
    const isOpen = btn.getAttribute(&#39;aria-expanded&#39;) === &#39;true&#39;;

    if (isOpen) {
      // 닫기
      panel.style.height = &#39;0&#39;;
      btn.setAttribute(&#39;aria-expanded&#39;, &#39;false&#39;);
    } else {
      // 열기 — 실제 콘텐츠 높이를 측정해서 지정
      panel.style.height = panel.scrollHeight + &#39;px&#39;;
      btn.setAttribute(&#39;aria-expanded&#39;, &#39;true&#39;);
    }
  });
});</code></pre>
<hr>
<h2 id="세-방식-비교">세 방식 비교</h2>
<table>
<thead>
<tr>
<th></th>
<th>max-height</th>
<th>details/summary</th>
<th>scrollHeight (JS)</th>
</tr>
</thead>
<tbody><tr>
<td>JS 필요</td>
<td>토글용 최소한</td>
<td>불필요</td>
<td>필요</td>
</tr>
<tr>
<td>애니메이션</td>
<td>있음 (부정확)</td>
<td>없음</td>
<td>있음 (정확)</td>
</tr>
<tr>
<td>접근성</td>
<td>직접 챙겨야 함</td>
<td>기본 제공</td>
<td>직접 챙겨야 함</td>
</tr>
<tr>
<td>내용 길이 대응</td>
<td>미리 값 지정 필요</td>
<td>자유</td>
<td>자유</td>
</tr>
<tr>
<td>추천 상황</td>
<td>내용 길이가 일정할 때</td>
<td>애니메이션이 필요 없을 때</td>
<td>정확한 애니메이션이 필요할 때</td>
</tr>
</tbody></table>
<hr>
<h2 id="접근성-포인트">접근성 포인트</h2>
<p>아코디언은 접근성 요구사항이 명확한 컴포넌트다.</p>
<p><strong><code>aria-expanded</code></strong>
버튼이 지금 펼쳐진 상태인지 접힌 상태인지 알려준다. 스크린리더가 &quot;확장됨&quot; / &quot;축소됨&quot;이라고 읽어준다. 이게 없으면 시각장애 사용자는 클릭했을 때 뭔가 열렸는지조차 알 수 없다.</p>
<p><strong><code>aria-controls</code></strong>
이 버튼이 어떤 영역을 제어하는지 연결한다. 패널의 <code>id</code>와 짝을 맞춘다.</p>
<p><strong><code>&lt;button&gt;</code> 사용</strong>
클릭 가능한 제목을 <code>div</code>로 만들면 안 된다. 버튼은 Tab으로 이동하고 Enter/Space로 눌리는 게 기본으로 딸려온다. <code>div</code>로 만들면 그걸 전부 직접 구현해야 한다.</p>
<p><strong>화살표 아이콘은 <code>aria-hidden</code></strong>
▾ 기호는 시각적 장식일 뿐 의미가 없다. 스크린리더가 &quot;아래쪽 삼각형&quot;이라고 읽으면 방해만 된다.</p>
<hr>
<h2 id="실험-포인트">실험 포인트</h2>
<ul>
<li><strong>max-height 값을 60px로 줄여보기</strong> → 내용이 잘리는 걸 확인. 이게 이 방식의 한계다</li>
<li><strong>max-height 값을 2000px으로 키워보기</strong> → 펼칠 때 뚝 끊기는 느낌. 값이 클수록 실제 높이까지 오는 속도가 부자연스러워진다</li>
<li><strong>JS 방식에서 창 크기 줄여보기</strong> → 열린 상태에서 창을 좁히면 내용이 넘칠 수 있다. <code>resize</code> 이벤트로 다시 재는 처리가 필요하다</li>
<li><strong><code>details</code>에 CSS 애니메이션 붙여보기</strong> → 왜 안 되는지 직접 확인해보면 재밌다</li>
</ul>
<hr>
<h2 id="기록용-한-줄">기록용 한 줄</h2>
<ul>
<li><strong>뭘 만들었나</strong>: 아코디언 3종 (max-height / details / scrollHeight)</li>
<li><strong>어디서 막혔나</strong>: <code>height: auto</code>로는 transition이 안 먹음 → 픽셀 값이 있어야 브라우저가 보간할 수 있다는 걸 알게 됨</li>
<li><strong>뭘 알게 됐나</strong>: 같은 UI도 구현 방식마다 트레이드오프가 있다. CSS만으로 되는 게 항상 좋은 건 아니고, 정확도가 필요하면 JS가 낫다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[퍼블 복기 시리즈] 토글 사세요... 토글 팔아요.]]></title>
            <link>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94-%EB%B3%B5%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-%ED%86%A0%EA%B8%80-%EC%82%AC%EC%84%B8%EC%9A%94...-%ED%86%A0%EA%B8%80-%ED%8C%94%EC%95%84%EC%9A%94</link>
            <guid>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94-%EB%B3%B5%EA%B8%B0-%EC%8B%9C%EB%A6%AC%EC%A6%88-%ED%86%A0%EA%B8%80-%EC%82%AC%EC%84%B8%EC%9A%94...-%ED%86%A0%EA%B8%80-%ED%8C%94%EC%95%84%EC%9A%94</guid>
            <pubDate>Thu, 16 Jul 2026 15:25:29 GMT</pubDate>
            <description><![CDATA[<h2 id="다양한-토글이-필요한-당신">다양한 토글이 필요한 당신</h2>
<h3 id="코드펜-작업물">코드펜 작업물</h3>
<p>토글스위치 냠냠 하세요,,, 한 입 하세요,,</p>
<p>버튼 다음으로 어디서든 쓰이는 토글 모음집을 만들어보았다. 하하.</p>
<p>!codepen[editor/na-hyeong9/embed/019f6b85-9b25-7e63-afcc-fa7b8f8fa61f?default-tab=html%2Cresult]</p>
<h2 id="day-2-·-토글-스위치-갤러리-6종">Day 2 · 토글 스위치 갤러리 (6종)</h2>
<blockquote>
<p>체크박스를 CSS로 변신시켜 만드는 토글 스위치. JS 없이 <code>:checked</code>만으로 동작한다.
접근성: 실제 <code>&lt;input type=&quot;checkbox&quot;&gt;</code>를 숨겨서 쓰기 때문에 키보드·스크린리더 기본 지원.</p>
</blockquote>
<hr>
<h2 id="핵심-원리">핵심 원리</h2>
<p>토글 스위치의 비밀은 <strong>진짜 체크박스를 숨기고, 그 뒤 요소를 꾸미는 것</strong>이다.</p>
<pre><code>[숨긴 체크박스] + [보이는 스위치 모양]
        ↓ 체크되면
input:checked + .switch { 스타일 변경 }</code></pre><p><code>input</code>을 화면에서 안 보이게 하되 삭제하진 않는다. 그래야 클릭·키보드·스크린리더가 정상 작동한다. 옆의 <code>+ .switch</code>(인접 형제 선택자)로 체크 상태에 따라 모양을 바꾼다.</p>
<hr>
<h2 id="html">HTML</h2>
<pre><code class="language-html">&lt;main class=&quot;wrap&quot;&gt;
  &lt;h1 class=&quot;sr-only&quot;&gt;토글 스위치 갤러리&lt;/h1&gt;

  &lt;ul class=&quot;tg-list&quot;&gt;
    &lt;li&gt;
      &lt;label&gt;
        &lt;input type=&quot;checkbox&quot;&gt;
        &lt;span class=&quot;sw sw1&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;sr-only&quot;&gt;기본 스위치&lt;/span&gt;
      &lt;/label&gt;
      &lt;span class=&quot;label&quot;&gt;기본&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;label&gt;
        &lt;input type=&quot;checkbox&quot;&gt;
        &lt;span class=&quot;sw sw2&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;sr-only&quot;&gt;iOS 스타일 스위치&lt;/span&gt;
      &lt;/label&gt;
      &lt;span class=&quot;label&quot;&gt;iOS 스타일&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;label&gt;
        &lt;input type=&quot;checkbox&quot;&gt;
        &lt;span class=&quot;sw sw3&quot;&gt;
          &lt;span class=&quot;ico-moon&quot; aria-hidden=&quot;true&quot;&gt;🌙&lt;/span&gt;
          &lt;span class=&quot;ico-sun&quot; aria-hidden=&quot;true&quot;&gt;☀️&lt;/span&gt;
        &lt;/span&gt;
        &lt;span class=&quot;sr-only&quot;&gt;다크모드 스위치&lt;/span&gt;
      &lt;/label&gt;
      &lt;span class=&quot;label&quot;&gt;아이콘&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;label&gt;
        &lt;input type=&quot;checkbox&quot;&gt;
        &lt;span class=&quot;sw sw4&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;sr-only&quot;&gt;네모 스위치&lt;/span&gt;
      &lt;/label&gt;
      &lt;span class=&quot;label&quot;&gt;네모&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;label&gt;
        &lt;input type=&quot;checkbox&quot;&gt;
        &lt;span class=&quot;sw sw5&quot;&gt;
          &lt;span class=&quot;txt on&quot; aria-hidden=&quot;true&quot;&gt;ON&lt;/span&gt;
          &lt;span class=&quot;txt off&quot; aria-hidden=&quot;true&quot;&gt;OFF&lt;/span&gt;
        &lt;/span&gt;
        &lt;span class=&quot;sr-only&quot;&gt;ON/OFF 스위치&lt;/span&gt;
      &lt;/label&gt;
      &lt;span class=&quot;label&quot;&gt;텍스트&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;label&gt;
        &lt;input type=&quot;checkbox&quot;&gt;
        &lt;span class=&quot;sw sw6&quot;&gt;&lt;/span&gt;
        &lt;span class=&quot;sr-only&quot;&gt;통통 튀는 스위치&lt;/span&gt;
      &lt;/label&gt;
      &lt;span class=&quot;label&quot;&gt;통통 튐&lt;/span&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/main&gt;</code></pre>
<hr>
<h2 id="css">CSS</h2>
<pre><code class="language-css">* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #fff3ed;
  font-family: system-ui, sans-serif;
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

.wrap { max-width: 560px; padding: 32px; }

.tg-list {
  list-style: none;
  margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px 18px;
}

@media (max-width: 480px) {
  .tg-list { grid-template-columns: repeat(2, 1fr); }
}

.tg-list li {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.tg-list label {
  cursor: pointer;
  display: inline-flex;
}

/* 진짜 체크박스는 숨김 (삭제 아님!) */
.tg-list input {
  position: absolute;
  opacity: 0;
  width: 0; height: 0;
}

.label {
  font-size: 13px;
  font-weight: 600;
  color: #4a3f6b;
}

/* 키보드 포커스 시 스위치에 표시 */
.tg-list input:focus-visible + .sw {
  outline: 3px solid #2d3436;
  outline-offset: 3px;
}

/* ── ① 기본 ── */
.sw1 { display:inline-block; width:52px; height:28px; background:#ccc; border-radius:20px; position:relative; transition:background .3s; }
.sw1::after { content:&quot;&quot;; position:absolute; top:3px; left:3px; width:22px; height:22px; background:#fff; border-radius:50%; transition:transform .3s; }
input:checked + .sw1 { background:#6c5ce7; }
input:checked + .sw1::after { transform:translateX(24px); }

/* ── ② iOS 스타일 (그림자 손잡이) ── */
.sw2 { display:inline-block; width:52px; height:30px; background:#e0e0e0; border-radius:20px; position:relative; transition:background .3s; }
.sw2::after { content:&quot;&quot;; position:absolute; top:2px; left:2px; width:26px; height:26px; background:#fff; border-radius:50%; box-shadow:0 2px 4px rgba(0,0,0,.2); transition:transform .3s; }
input:checked + .sw2 { background:#00b894; }
input:checked + .sw2::after { transform:translateX(22px); }

/* ── ③ 아이콘 (해/달) ── */
.sw3 { display:inline-flex; align-items:center; justify-content:space-between; width:60px; height:28px; background:#374151; border-radius:20px; position:relative; padding:0 6px; font-size:12px; transition:background .3s; }
.sw3 .ico-sun, .sw3 .ico-moon { z-index:1; }
.sw3::after { content:&quot;&quot;; position:absolute; top:3px; left:3px; width:22px; height:22px; background:#fff; border-radius:50%; transition:transform .3s; }
input:checked + .sw3 { background:#f0b429; }
input:checked + .sw3::after { transform:translateX(32px); }

/* ── ④ 네모 ── */
.sw4 { display:inline-block; width:52px; height:28px; background:#ccc; border-radius:6px; position:relative; transition:background .3s; }
.sw4::after { content:&quot;&quot;; position:absolute; top:3px; left:3px; width:22px; height:22px; background:#fff; border-radius:4px; transition:transform .3s; }
input:checked + .sw4 { background:#d63384; }
input:checked + .sw4::after { transform:translateX(24px); }

/* ── ⑤ 텍스트 (ON/OFF) ── */
.sw5 { display:inline-flex; align-items:center; width:64px; height:28px; background:#e17055; border-radius:20px; position:relative; transition:background .3s; }
.sw5 .txt { position:absolute; font-size:10px; font-weight:700; color:#fff; transition:opacity .3s; }
.sw5 .on { left:9px; opacity:0; }
.sw5 .off { right:9px; opacity:1; }
.sw5::after { content:&quot;&quot;; position:absolute; top:3px; left:3px; width:22px; height:22px; background:#fff; border-radius:50%; transition:transform .3s; z-index:1; }
input:checked + .sw5 { background:#00b894; }
input:checked + .sw5::after { transform:translateX(36px); }
input:checked + .sw5 .on { opacity:1; }
input:checked + .sw5 .off { opacity:0; }

/* ── ⑥ 통통 튐 (bouncy) ── */
.sw6 { display:inline-block; width:64px; height:34px; background:#d1d5db; border-radius:20px; position:relative; transition:background .3s; }
.sw6::after { content:&quot;&quot;; position:absolute; top:3px; left:3px; width:28px; height:28px; background:#fff; border-radius:50%; box-shadow:0 2px 5px rgba(0,0,0,.25); transition:transform .35s cubic-bezier(.68,-0.55,.27,1.55); }
input:checked + .sw6 { background:#0984e3; }
input:checked + .sw6::after { transform:translateX(30px); }

/* 모션 최소화 배려 */
@media (prefers-reduced-motion: reduce) {
  .sw, .sw::after { transition: none !important; }
}</code></pre>
<hr>
<h2 id="js">JS</h2>
<p>없다. <code>:checked</code> 상태만으로 전부 CSS로 처리된다. 이게 이 챌린지의 핵심이다.</p>
<hr>
<h2 id="각-스위치-핵심-정리">각 스위치 핵심 정리</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th>
<th>핵심 기술</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>기본</td>
<td><code>::after</code> 손잡이 + <code>translateX</code></td>
</tr>
<tr>
<td>2</td>
<td>iOS 스타일</td>
<td>손잡이에 <code>box-shadow</code></td>
</tr>
<tr>
<td>3</td>
<td>아이콘</td>
<td>해/달 이모지 배치 + 손잡이 이동</td>
</tr>
<tr>
<td>4</td>
<td>네모</td>
<td><code>border-radius</code> 작게</td>
</tr>
<tr>
<td>5</td>
<td>텍스트</td>
<td>ON/OFF 텍스트 <code>opacity</code> 전환</td>
</tr>
<tr>
<td>6</td>
<td>통통 튐</td>
<td><code>cubic-bezier</code>로 튀는 이징</td>
</tr>
</tbody></table>
<hr>
<h2 id="실험-포인트">실험 포인트</h2>
<ul>
<li><strong>손잡이 이동 거리</strong>: <code>translateX</code> 값은 (스위치 너비 − 손잡이 너비 − 여백×2)로 계산. 너비를 바꾸면 이 값도 맞춰야 한다</li>
<li><strong>이징</strong>: ⑥번의 <code>cubic-bezier(.68,-0.55,.27,1.55)</code>가 통통 튀는 비결. ①번에도 넣어보면 느낌이 확 다르다</li>
<li><strong>색 전환 타이밍</strong>: 배경색과 손잡이 이동의 <code>transition</code> 시간을 다르게 주면 미묘하게 달라진다</li>
</ul>
<hr>
<h2 id="왜-체크박스를-안-지우고-숨기나">왜 체크박스를 안 지우고 숨기나</h2>
<p><code>display: none</code>으로 체크박스를 완전히 없애면 안 된다. 그러면 <strong>키보드로 포커스가 안 가고, 스크린리더도 인식 못 한다.</strong> 그래서 <code>opacity: 0</code> + <code>width/height: 0</code>으로 시각적으로만 숨긴다. 기능은 그대로 살아있는 상태다.</p>
<p><code>:focus-visible</code> 스타일을 손잡이(<code>.sw</code>)에 준 것도 같은 이유다. 키보드 사용자가 지금 어느 스위치에 있는지 보여줘야 한다.</p>
<hr>
<h2 id="기록용-한-줄">기록용 한 줄</h2>
<ul>
<li><strong>뭘 만들었나</strong>: CSS만으로 동작하는 토글 스위치 6종</li>
<li><strong>어디서 막혔나</strong>: 손잡이가 스위치 밖으로 삐져나감 → <code>translateX</code> 거리를 너비에 맞게 다시 계산</li>
<li><strong>뭘 알게 됐나</strong>: 체크박스는 숨기되 삭제하면 안 된다 — <code>opacity:0</code>으로 기능을 살려야 접근성이 유지된다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[퍼블 복기 시리즈]다양한 버튼을 보러오세요.]]></title>
            <link>https://velog.io/@kim-na-hyeong/%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B2%84%ED%8A%BC%EC%9D%84-%EB%B3%B4%EB%9F%AC%EC%98%A4%EC%84%B8%EC%9A%94</link>
            <guid>https://velog.io/@kim-na-hyeong/%EB%8B%A4%EC%96%91%ED%95%9C-%EB%B2%84%ED%8A%BC%EC%9D%84-%EB%B3%B4%EB%9F%AC%EC%98%A4%EC%84%B8%EC%9A%94</guid>
            <pubDate>Wed, 15 Jul 2026 13:51:01 GMT</pubDate>
            <description><![CDATA[<h2 id="다양한-버튼이-필요한-당신">다양한 버튼이 필요한 당신</h2>
<p>나는 이제 퍼블리셔가 사용할 수 있는 모든 컴포넌트를 제작하기로 마음 먹었다. 진짜다. 진짜... 할거다~ 이것은 버튼 시리즈다.</p>
<p>일단 처음 작업은 가장 친숙한 HTML과 CSS, JS로 시작하겠다.</p>
<h3 id="코드펜-작업물">코드펜 작업물</h3>
<p>!codepen[editor/na-hyeong9/embed/019f65a8-af96-7981-b9c9-1bf7b94c1457?default-tab=html%2Cresult]</p>
<h1 id="day-1-·-호버-버튼-갤러리-12종">Day 1 · 호버 버튼 갤러리 (12종)</h1>
<blockquote>
<p>CodePen용 코드. 버튼 목록이라 <code>&lt;ul&gt;</code> + <code>&lt;li&gt;</code>로 구성하고, 4열로 정렬했다.
접근성 요소(포커스 표시, 색 비의존 라벨, 모션 최소화) 포함.</p>
</blockquote>
<hr>
<h2 id="html">HTML</h2>
<pre><code class="language-html">&lt;main class=&quot;wrap&quot;&gt;
  &lt;h1 class=&quot;sr-only&quot;&gt;호버 버튼 효과 갤러리&lt;/h1&gt;

  &lt;ul class=&quot;btn-list&quot;&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b1&quot;&gt;Hover&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;색 + 확대 &lt;em&gt;fill scale&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b2&quot;&gt;Hover&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;색 반전 &lt;em&gt;outline&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b3&quot;&gt;Hover&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;떠오름 &lt;em&gt;lift&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b4&quot;&gt;&lt;span class=&quot;txt&quot;&gt;Hover&lt;/span&gt;&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;슬라이드 채움 &lt;em&gt;slide&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b5&quot;&gt;&lt;span class=&quot;txt&quot;&gt;Hover&lt;/span&gt;&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;물결 &lt;em&gt;ripple&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b6&quot;&gt;Hover&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;글로우 &lt;em&gt;glow&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b7&quot;&gt;Hover&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;그라데이션 &lt;em&gt;gradient&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b8&quot;&gt;Hover&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;자간 확장 &lt;em&gt;spacing&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b9&quot;&gt;Hover&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;눌림 &lt;em&gt;press&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b10&quot;&gt;Hover&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;모서리 변형 &lt;em&gt;radius&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b11&quot;&gt;Hover &lt;span class=&quot;arrow&quot; aria-hidden=&quot;true&quot;&gt;→&lt;/span&gt;&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;아이콘 이동 &lt;em&gt;icon shift&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
    &lt;li&gt;
      &lt;button class=&quot;btn b12&quot; id=&quot;magnetic&quot;&gt;Hover&lt;/button&gt;
      &lt;span class=&quot;label&quot;&gt;자석 &lt;em&gt;magnetic&lt;/em&gt;&lt;/span&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/main&gt;</code></pre>
<hr>
<h2 id="css">CSS</h2>
<pre><code class="language-css">* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #fff3ed;
  font-family: system-ui, sans-serif;
}

/* 스크린리더 전용 텍스트 */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

.wrap { max-width: 720px; padding: 32px; }

.btn-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);  /* 4열 고정 */
  gap: 28px 18px;
}

/* 모바일: 2열 */
@media (max-width: 600px) {
  .btn-list { grid-template-columns: repeat(2, 1fr); }
}

.btn-list li {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}

.label {
  font-size: 13px;
  font-weight: 600;
  color: #4a3f6b;
  text-align: center;
  line-height: 1.4;
}

.label em {
  display: block;
  font-size: 11px;
  font-weight: 400;
  font-style: normal;
  color: #8a7fb0;
}

.btn {
  padding: 14px 26px;
  font-size: 15px;
  font-weight: 600;
  border: none;
  border-radius: 12px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  font-family: inherit;
}

.btn:focus-visible {
  outline: 3px solid #2d3436;
  outline-offset: 3px;
}

/* ① 색 + 확대 */
.b1 { background: #6c5ce7; color: #fff; transition: all .25s ease; }
.b1:hover { background: #4834b4; transform: scale(1.06); }

/* ② 색 반전 */
.b2 { background: #fff; color: #4834b4; border: 2px solid #4834b4; transition: all .25s ease; }
.b2:hover { background: #4834b4; color: #fff; }

/* ③ 떠오름 */
.b3 { background: #fff; color: #333; box-shadow: 0 2px 6px rgba(0,0,0,.12); transition: all .25s ease; }
.b3:hover { transform: translateY(-4px); box-shadow: 0 10px 22px rgba(0,0,0,.18); }

/* ④ 슬라이드 채움 */
.b4 { background: #008068; color: #fff; transition: all .3s ease; }
.b4::before { content: &quot;&quot;; position: absolute; inset: 0; background: #005c4a; transform: translateX(-100%); transition: transform .3s ease; z-index: 0; }
.b4:hover::before { transform: translateX(0); }
.b4 .txt { position: relative; z-index: 1; }

/* ⑤ 물결 */
.b5 { background: #c0492a; color: #fff; transition: color .3s; }
.b5::before { content: &quot;&quot;; position: absolute; left: 50%; top: 50%; width: 0; height: 0; background: #8a2f19; border-radius: 50%; transform: translate(-50%,-50%); transition: width .4s ease, height .4s ease; z-index: 0; }
.b5:hover::before { width: 250%; height: 250%; }
.b5 .txt { position: relative; z-index: 1; }

/* ⑥ 글로우 */
.b6 { background: #0069b3; color: #fff; transition: box-shadow .3s ease; }
.b6:hover { box-shadow: 0 0 18px 2px #4da3e0; }

/* ⑦ 그라데이션 흐름 */
.b7 { color: #fff; background: linear-gradient(90deg, #6c5ce7, #d6467f, #6c5ce7); background-size: 200% 100%; background-position: 0 0; transition: background-position .5s ease; }
.b7:hover { background-position: 100% 0; }

/* ⑧ 자간 확장 */
.b8 { background: #2d3436; color: #fff; transition: letter-spacing .25s ease, background .25s; }
.b8:hover { letter-spacing: 3px; background: #6c5ce7; }

/* ⑨ 눌림 */
.b9 { background: #008068; color: #fff; transition: transform .2s; }
.b9:hover { transform: translateY(-2px); }
.b9:active { transform: scale(.92); }

/* ⑩ 모서리 변형 */
.b10 { background: #fff; color: #b02a6b; border: 2px solid #b02a6b; border-radius: 30px; transition: all .3s ease; }
.b10:hover { border-radius: 10px; background: #b02a6b; color: #fff; }

/* ⑪ 아이콘 이동 */
.b11 { background: #9a6a08; color: #fff; }
.b11 .arrow { display: inline-block; transition: transform .3s ease; }
.b11:hover .arrow { transform: translateX(5px); }

/* ⑫ 자석 */
.b12 { background: #555e62; color: #fff; transition: transform .15s ease; }

/* 모션 최소화 배려 */
@media (prefers-reduced-motion: reduce) {
  .btn, .btn::before, .b11 .arrow { transition: none !important; }
}</code></pre>
<hr>
<h2 id="js-자석-버튼">JS (자석 버튼)</h2>
<pre><code class="language-js">const magnetic = document.getElementById(&quot;magnetic&quot;);

magnetic.addEventListener(&quot;mousemove&quot;, (e) =&gt; {
  const rect = magnetic.getBoundingClientRect();
  const x = e.clientX - rect.left - rect.width / 2;
  const y = e.clientY - rect.top - rect.height / 2;
  magnetic.style.transform = `translate(${x * 0.4}px, ${y * 0.4}px)`;
});

magnetic.addEventListener(&quot;mouseleave&quot;, () =&gt; {
  magnetic.style.transform = &quot;translate(0, 0)&quot;;
});</code></pre>
<hr>
<h2 id="각-버튼-핵심-정리">각 버튼 핵심 정리</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th>
<th>핵심 기술</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>색 + 확대</td>
<td><code>transition</code> + <code>transform: scale()</code></td>
</tr>
<tr>
<td>2</td>
<td>색 반전</td>
<td><code>:hover</code>로 배경/글자색 교체</td>
</tr>
<tr>
<td>3</td>
<td>떠오름</td>
<td><code>translateY</code> + <code>box-shadow</code></td>
</tr>
<tr>
<td>4</td>
<td>슬라이드 채움</td>
<td><code>::before</code> + <code>translateX</code></td>
</tr>
<tr>
<td>5</td>
<td>물결</td>
<td><code>::before</code> 원형 확대</td>
</tr>
<tr>
<td>6</td>
<td>글로우</td>
<td><code>box-shadow</code> 번짐</td>
</tr>
<tr>
<td>7</td>
<td>그라데이션 흐름</td>
<td><code>background-position</code> 이동</td>
</tr>
<tr>
<td>8</td>
<td>자간 확장</td>
<td><code>letter-spacing</code> 전환</td>
</tr>
<tr>
<td>9</td>
<td>눌림</td>
<td><code>:active</code> + <code>scale</code></td>
</tr>
<tr>
<td>10</td>
<td>모서리 변형</td>
<td><code>border-radius</code> 전환</td>
</tr>
<tr>
<td>11</td>
<td>아이콘 이동</td>
<td>자식만 <code>translateX</code></td>
</tr>
<tr>
<td>12</td>
<td>자석</td>
<td>JS로 마우스 위치 추적</td>
</tr>
</tbody></table>
<hr>
<h2 id="실험-포인트">실험 포인트</h2>
<ul>
<li><strong>transition 시간</strong>: <code>.25s</code> → <code>.6s</code>로 느린 느낌 체감</li>
<li><strong>easing</strong>: <code>ease</code> → <code>cubic-bezier(.68,-0.55,.27,1.55)</code> 넣으면 통통 튀는 느낌</li>
<li><strong>자석 강도</strong>: JS의 <code>0.4</code>를 <code>0.8</code>로 올리면 더 강하게 끌려옴</li>
<li><strong>::before 트릭</strong>: ④⑤번은 가짜 요소를 깔고 <code>overflow: hidden</code>으로 잘라내는 패턴 — 응용 범위가 넓다</li>
</ul>
<hr>
<h2 id="기록용-한-줄">기록용 한 줄</h2>
<ul>
<li><strong>뭘 만들었나</strong>: 호버 버튼 12종 갤러리 (ul/li, 4열)</li>
<li><strong>어디서 막혔나</strong>: <code>::before</code>가 글자를 덮어서 안 보임 → <code>.txt</code>에 <code>position: relative; z-index: 1</code>로 해결</li>
<li><strong>뭘 알게 됐나</strong>: <code>overflow: hidden</code> + <code>::before</code> 조합이 슬라이드·물결 효과의 핵심</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[퍼블리셔가 프론트엔드가 될 수 있을까]]></title>
            <link>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94%EB%A6%AC%EC%85%94%EA%B0%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EA%B0%80-%EB%90%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</link>
            <guid>https://velog.io/@kim-na-hyeong/%ED%8D%BC%EB%B8%94%EB%A6%AC%EC%85%94%EA%B0%80-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EA%B0%80-%EB%90%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C</guid>
            <pubDate>Wed, 15 Jul 2026 13:36:14 GMT</pubDate>
            <description><![CDATA[<h3 id="나는-퍼블리셔다">나는 퍼블리셔다.</h3>
<p>맞다. 나는 2년 7개월치 퍼블리셔다. 회사가 망해버려 만 7개월째 백수가 되어버린 개백수다.</p>
<p>6년 전 회사를 퇴사하고 나서 내가 무얼 좋아하는지 고민 끝에 개발에 뛰어들었다.
바로 학원을 등록해서 풀스택 과정을 들었다. 파이썬과 mongo DB를 이용한 과정이었다. 내가 처음부터 끝까지 하나의 웹사이트를 만드는 것이 신나는 일이었다.</p>
<p>그런데 왠 퍼블리셔로 직무를 전환했을까.</p>
<p>이건.... 그냥 꼬여버렸다. 인생은 선택의 연속이다. 그리고 난 내 선택에 책임을 져야했다.
처음엔 파이썬으로 백엔드 지원을 했다. 정부에서 자바를 많이 쓴다는 걸 그제서야 알았다. 공고의 수가 현저히 적었다. 공고의 수가 적다고 해서 물러날 순 없었다. 이력서 난사가 시작되었다. 그 당시 신입의 개기로 본 면접은 1시간의 회사 설명을 듣다가 끝나기도 했고, 바로 할 수 있냐는 몇몇 질문을 받았다.</p>
<p>난 쓸 데 없이 솔직하게 바로 하기엔 실력이 부족하다고 대답했다. 그리고 회사에서 사용한다면 개인적으로 학습을 해서라도 실무에 차질 없도록 노력하겠다고.
노오력. 노력<del>~</del>!!! 하겠다고 했다.</p>
<p>뭔 면접만 가면 노력하겠다는 소리가 나온다.</p>
<p>면접 제 1 법칙(2 법칙은 모른다.) 노력이라는 열정보단 내가 실무에서 실질적으로 어떻게 할 수 있을지 알아야되는 걸 자리만 가면 제가 잘 해보겠습니다 대감님. 이런 자세가 나온다. </p>
<p>그런데 내가 실제로 일하는 방식이 그렇다 노력하고 결과까지 이어진다. 그렇기에 한 대답이 면접에선 씨알도 먹히지 않았다.
왜 이제 노력하고 느려도 결과내는 사람은 먹히지 않는 걸까. 난 7-80년대 사람인가보다.. 그래서 빠른 속도를 내기 위해 부단히 노력중이다. 태생이 빠른 사람을 이길 수 없겠지만서도 이것마저 노력해야했다.</p>
<p>나는 파이썬의 광탈에 못이겨 퍼블리셔 공고를 하나 넣어버린다.
그냥 넣어 버린 회사에서 연락이 왔다. 나는 연습이라도 하자는 생각으로 면접을 잡았다.</p>
<p>면접은 이렇다할 분별력 없이 진행됐다. 그 당시만 해도 중소기업 중에 그런 곳이 많았다. 심지어 나는 포트폴리오도 개발자 포트폴리오를 제출한 상태였다.</p>
<p>질문은 이러했다. (자세히 기억이 나지 않지만 대략적으로 적어보겠다.)</p>
<p>Q1. 퍼블리셔 지원 맞아요? &gt; 맞긴했다. 보고하긴 했다.
Q2. 지원한 이유가 뭔가요? &gt; 저는 보이는 화면을 그리는 작업이 재밌습니다. html과 css 작업에 손이 빠릅니다. (비교할 대상도 없으면서 뻔뻔했음.)
Q3. 파견 괜찮으신가요? &gt; 네. (파견만 나가는 회산지 몰랐음.)</p>
<p>그 외 인성 질문 등등</p>
<p>회사를 나오자마자 합격 문자가 왔다. 솔직히 기뻤다. 오랜만에 보는 면접 합격 문자였다. 나는 면접에서 많이 떠는 편이어서 실력을 발휘하기 어려웠다.</p>
<p>이틀 뒤? 인가 입사 가능하냐고 물었을 때 (다른 면접을 보기 위해) 2주 정도 미룰 수 있냐고 물었다.</p>
<p>애매한 답변에 서둘러 오케이를 해버렸다. 돈이 필요하니까 다니면서 생각하자는 마음이 들었다. 연봉을 맞춰준 게 컸다.</p>
<h3 id="첫-프로젝트와-나">첫 프로젝트와 나</h3>
<p>내가 면접중 한 질문은 하나였다. 사수유무에 대한 여부였다.
전 회사에서 사수없이 일을 치른 적이 있었다. 정말 악몽이었다. 물어볼 사람도 없이 해결을 해야한다는 게 여간 어려운 게 아니었다.
그래서 다음 회사에서 원하는 게 있다면 사수, 그리고 직원들의 근속년수였다.</p>
<p>사수가 있긴 했다. 근데 없다. 왜냐하면 파견 특성상 혼자 나가는 경우가 있었고, 나는 첫 프로젝트를 제외하고 한 번도 사수와 같이 했던 적이 없다. 나는 나에 대한 평가를 듣는 걸 좋아했다. 내가 어느 정도 수준인지 객관적으로 알고 싶은데 내 스스로는 판단이 어려웠다.</p>
<p>첫 프로젝트에서 왜 이렇게 해요? 라는 소리에 뭐라 말하기가 어려웠다. 그냥 짜다보니... 구조를 이렇게 하는 게 좋지 않을까요?.. 라는 대답을 자신 없게 했다. div가 너무 많다는 거 였다. 그래서 나는 그 작업을 할 때 최대한 div를 쓰지 않으려고 노력했다. 거기서 오는 오류는 그거 신경쓰느라 작업이 늦었다.</p>
<p>대체 어느정도가 많이 쓰는 거고 어느 정도가 적당한 건지 분간이 되지 않았다.</p>
<p>그 이후로는 크게 container/wrap로 layout을 잡고 자잘한 컴포넌트 위주로 짰다. 칭찬을 듣지는 못했지만 적어도 그 전보다 나아진 것 같았다. (너무 예전이라 상세한 기억이 나지 않는다.)</p>
<p>실무에 투입되자 여차저차 굴러갔다. 퍼블리싱이라는 게 쉽다고는 생각 하지 않았지만 금세 적응할 수 있었다. 나는 그 중에 자바스크립트에 관심이 많았다.</p>
<h3 id="자바스크립트-너-쉽지-않다">자바스크립트 너 쉽지 않다.</h3>
<p>나는 기본기가 부족했다. 프로젝트 들어가지 여실히 느꼈다. 그리고 SI 프로젝트는 특성상 외부 서버가 열리지 않는다. 이클립스 IDE를 사용하고 기존 소스에 추가하는 작업이 시작되자 미칠듯이 스트레스 받았다.</p>
<p>같이 개발하는 백엔드 한 분과 퍼블리셔 나만 투입된 숏츠 기능 추가 프로젝트였다. 개발기간은 짧았다 2달이내로 진행됐다.</p>
<p>기획/디자인/퍼블리싱을 동시에 들어갔다.
난 회사가 시키면 일단 한다는 주의다. 불만이 일어도 해보고 문제점을 찾는다. 근거가 있어야 설득이 된다고 생각하기 때문이다. 그게 주니어의 자세라고 생각하기 때문이다.</p>
<p>기획과 디자인은 유튜브를 벤치마킹했다. 단 시간에 진행해야하는 만큼 고객사에서 편의를 많이 봐주셨다. 그래서 예상보다 수월했는데...</p>
<p>자바스크립트 쌰갈! 진짜 쌰갈이었다.</p>
<p>모르겠어서 눈물이 앞을 가렸다.
video.js를 처음 다뤄봤다. 자바스크립트니까 이번을 기회를 발판으로 자바스크립트 공부를 열심히 해보자는 마음에 악귀가 꼈다.</p>
<p>기능 1. 영상이 60퍼 이상 차지하면 자동재생/ 영상이 40퍼 이하면 일시정지와 음소거
기능 2. 볼륨 제어기</p>
<p>그 외 작업</p>
<ul>
<li>댓글창 열리는 동시에 영상 밀리는 인터랙션</li>
<li>버튼 인터랙션</li>
<li>로고, 아이콘 작업 (다수)
등등...</li>
</ul>
<p>저 영상이랑 볼륨 제어기가 반응형으로 들어가서 문제를 계속 일으켰다.</p>
<p>내일 이어서 쓰겠다....</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 모던 자바스크립트부터 다시 시작 - 8 - 모듈 시스템]]></title>
            <link>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-8-%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-8-%EB%AA%A8%EB%93%88-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Thu, 09 Jul 2026 14:22:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지난 편에서 프로토타입을 다뤘다. 이번엔 코드를 여러 파일로 나눠 쓰는 방법 — 모듈 시스템이다. import/export를 매일 쓰면서도 정확히 뭘 하는 건지 넘어갔던 개념을 정리해본다.</p>
</blockquote>
<hr>
<h2 id="모듈이-왜-필요한가">모듈이 왜 필요한가</h2>
<p>처음 자바스크립트를 배울 때는 파일 하나에 코드를 다 넣는다. 근데 프로젝트가 커지면 문제가 생긴다.</p>
<pre><code class="language-html">&lt;script src=&quot;utils.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;user.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;main.js&quot;&gt;&lt;/script&gt;</code></pre>
<p>예전에는 이렇게 <code>&lt;script&gt;</code> 태그를 여러 개 넣어서 파일을 나눴다. 문제가 두 가지 있었다.</p>
<p><strong>① 순서를 지켜야 한다</strong>
<code>main.js</code>가 <code>utils.js</code>의 함수를 쓴다면, <code>utils.js</code>가 먼저 로드돼야 한다. 순서가 틀리면 에러가 난다.</p>
<p><strong>② 전역 오염</strong>
모든 변수가 전역으로 공유된다. <code>utils.js</code>에 <code>const user = ...</code>가 있고 <code>user.js</code>에도 <code>const user = ...</code>가 있으면 충돌한다.</p>
<p>이 문제를 해결하려고 나온 게 <strong>모듈 시스템</strong>이다. 파일마다 독립된 공간을 갖고, 필요한 것만 주고받는 방식이다.</p>
<hr>
<h2 id="export--내보내기">export — 내보내기</h2>
<p>다른 파일에서 쓸 수 있게 &quot;이거 공개할게&quot;라고 표시하는 것이다.</p>
<h3 id="named-export-이름-붙여-내보내기">named export (이름 붙여 내보내기)</h3>
<pre><code class="language-js">// utils.js
export const PI = 3.14;

export function add(a, b) {
  return a + b;
}

export function multiply(a, b) {
  return a * b;
}</code></pre>
<p><code>export</code>를 붙인 것만 밖에서 쓸 수 있다. 붙이지 않은 건 그 파일 안에서만 쓰인다.</p>
<h3 id="default-export-기본-내보내기">default export (기본 내보내기)</h3>
<pre><code class="language-js">// User.js
export default function User(name) {
  return { name };
}</code></pre>
<p>파일당 하나만 가능하다. &quot;이 파일의 대표 값&quot;이라는 의미다.</p>
<hr>
<h2 id="import--가져오기">import — 가져오기</h2>
<p>내보낸 것을 다른 파일에서 가져다 쓴다.</p>
<h3 id="named-import">named import</h3>
<pre><code class="language-js">// main.js
import { PI, add } from &#39;./utils.js&#39;;

console.log(PI);        // 3.14
console.log(add(1, 2)); // 3</code></pre>
<p>중괄호 <code>{}</code> 안에 가져올 이름을 정확히 적는다. 내보낸 이름과 같아야 한다.</p>
<h3 id="default-import">default import</h3>
<pre><code class="language-js">// main.js
import User from &#39;./User.js&#39;;

const u = User(&#39;Alice&#39;);</code></pre>
<p>중괄호 없이 가져온다. 이름은 마음대로 붙일 수 있다. default는 &quot;대표 값&quot;이라 이름이 고정되지 않는다.</p>
<pre><code class="language-js">import MyUser from &#39;./User.js&#39;; // 이렇게 다른 이름도 가능</code></pre>
<h3 id="같이-가져오기">같이 가져오기</h3>
<pre><code class="language-js">import User, { PI, add } from &#39;./someFile.js&#39;;
// default(User) + named(PI, add) 동시에</code></pre>
<hr>
<h2 id="자주-쓰는-패턴">자주 쓰는 패턴</h2>
<h3 id="별칭as으로-이름-바꾸기">별칭(as)으로 이름 바꾸기</h3>
<p>이름이 겹치거나 더 명확하게 쓰고 싶을 때.</p>
<pre><code class="language-js">import { add as sum } from &#39;./utils.js&#39;;

console.log(sum(1, 2)); // 3</code></pre>
<h3 id="전부-가져오기">전부 가져오기(*)</h3>
<pre><code class="language-js">import * as utils from &#39;./utils.js&#39;;

console.log(utils.PI);     // 3.14
console.log(utils.add(1, 2)); // 3</code></pre>
<p>객체처럼 묶어서 가져온다. <code>utils.함수명</code> 형태로 접근한다.</p>
<hr>
<h2 id="commonjs-vs-esm--왜-두-개인가">CommonJS vs ESM — 왜 두 개인가</h2>
<p>여기서부터 살짝 헷갈리는 부분이다. 자바스크립트 모듈 방식이 <strong>두 가지</strong>가 있다.</p>
<h3 id="commonjs-cjs">CommonJS (CJS)</h3>
<p>Node.js가 초기에 쓰던 방식이다. 브라우저에는 모듈 개념이 없던 시절, 서버(Node.js)에서 먼저 모듈이 필요해서 만든 것이다.</p>
<pre><code class="language-js">// 내보내기
const add = (a, b) =&gt; a + b;
module.exports = { add };

// 가져오기
const { add } = require(&#39;./utils.js&#39;);</code></pre>
<p><code>require</code>와 <code>module.exports</code>를 쓴다.</p>
<h3 id="esm-es-modules">ESM (ES Modules)</h3>
<p>ES6에서 자바스크립트 표준으로 정해진 방식이다. 지금까지 위에서 본 <code>import</code>/<code>export</code>가 바로 ESM이다.</p>
<pre><code class="language-js">// 내보내기
export const add = (a, b) =&gt; a + b;

// 가져오기
import { add } from &#39;./utils.js&#39;;</code></pre>
<h3 id="비교">비교</h3>
<table>
<thead>
<tr>
<th></th>
<th>CommonJS</th>
<th>ESM</th>
</tr>
</thead>
<tbody><tr>
<td>내보내기</td>
<td><code>module.exports</code></td>
<td><code>export</code></td>
</tr>
<tr>
<td>가져오기</td>
<td><code>require()</code></td>
<td><code>import</code></td>
</tr>
<tr>
<td>등장</td>
<td>Node.js 초기</td>
<td>ES6 표준</td>
</tr>
<tr>
<td>로딩 방식</td>
<td>동기 (실행 중 로드)</td>
<td>정적 (미리 분석)</td>
</tr>
<tr>
<td>주 사용처</td>
<td>구형 Node.js</td>
<td>브라우저, 최신 환경</td>
</tr>
</tbody></table>
<hr>
<h2 id="왜-esm이-표준이-됐나">왜 ESM이 표준이 됐나</h2>
<p>CommonJS의 <code>require</code>는 <strong>실행 중에</strong> 모듈을 불러온다. 코드가 돌아가다가 <code>require</code>를 만나면 그때 파일을 읽는다.</p>
<pre><code class="language-js">if (조건) {
  const something = require(&#39;./something.js&#39;); // 조건에 따라 로드
}</code></pre>
<p>유연하지만, 어떤 모듈을 쓰는지 실행해봐야 알 수 있다.</p>
<p>ESM의 <code>import</code>는 <strong>정적</strong>이다. 코드를 실행하기 전에 어떤 모듈을 가져오는지 먼저 분석한다.</p>
<pre><code class="language-js">import something from &#39;./something.js&#39;; // 항상 파일 맨 위, 정적</code></pre>
<p>이 덕분에 사용하지 않는 코드를 빌드 단계에서 제거하는 <strong>트리 셰이킹(Tree Shaking)</strong> 이 가능하다. 빌드 도구가 &quot;이건 안 쓰네&quot; 하고 걸러낼 수 있는 것이다. 번들 크기가 줄어든다.</p>
<p>그래서 요즘 프론트엔드는 대부분 ESM을 쓴다. Next.js, Vite 같은 최신 도구도 ESM 기반이다.</p>
<hr>
<h2 id="정리">정리</h2>
<ul>
<li>모듈은 파일을 독립된 공간으로 나눠서 필요한 것만 주고받는 방식이다</li>
<li><code>export</code>로 내보내고 <code>import</code>로 가져온다</li>
<li>named export는 <code>{}</code>로, default export는 <code>{}</code> 없이 가져온다</li>
<li>모듈 방식은 CommonJS(<code>require</code>)와 ESM(<code>import</code>) 두 가지가 있다</li>
<li>ESM은 정적 분석이 가능해 트리 셰이킹 등 최적화에 유리하고, 지금의 표준이다</li>
</ul>
<hr>
<h2 id="오늘-얻은-것">오늘 얻은 것</h2>
<p>매일 <code>import</code>를 쓰면서도 CommonJS랑 뭐가 다른지, 왜 <code>import</code>는 항상 파일 맨 위에 써야 하는지 넘어갔었다. ESM이 정적이라 미리 분석된다는 걸 알고 나니, 파일 맨 위에 고정되는 이유도 트리 셰이킹이 가능한 이유도 이해가 됐다.</p>
<p>다음 편은 <strong>배열 고차 함수</strong> — map, filter, reduce를 실무에서 어떻게 조합해서 쓰는지 정리해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 모던 자바스크립트부터 다시 시작 - 7 - 프로토타입]]></title>
            <link>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-7-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-7-%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85</guid>
            <pubDate>Wed, 01 Jul 2026 05:21:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지난 편에서 this를 다뤘다. 이번엔 자바스크립트의 상속 구조 — 프로토타입을 파본다. class 문법 뒤에 실제로 뭐가 숨어있는지 알게 되면 JS가 좀 다르게 보인다.</p>
</blockquote>
<hr>
<h2 id="자바스크립트엔-원래-클래스가-없었다">자바스크립트엔 원래 클래스가 없었다</h2>
<p>다른 언어는 클래스로 객체를 찍어낸다. 자바스크립트는 ES6 전까지 class 문법이 아예 없었다.</p>
<p>대신 <strong>프로토타입(Prototype)</strong> 이라는 방식으로 상속을 구현했다. 지금 쓰는 <code>class</code>도 사실 프로토타입을 보기 좋게 감싼 문법일 뿐이다. 안을 들여다보면 결국 프로토타입이다.</p>
<hr>
<h2 id="객체는-다른-객체를-참조한다">객체는 다른 객체를 참조한다</h2>
<p>모든 객체는 자신의 <strong>부모 역할을 하는 객체</strong>를 가리키는 숨은 링크를 가진다. 이걸 프로토타입이라고 한다.</p>
<pre><code class="language-js">const obj = { name: &#39;Alice&#39; };

console.log(obj.name);     // &#39;Alice&#39; — 자기가 가진 것
console.log(obj.toString); // 함수 — 내가 만든 적 없는데?</code></pre>
<p><code>toString</code>을 만든 적이 없는데 쓸 수 있다. obj가 가지고 있지 않으면, 프로토타입을 타고 올라가서 찾기 때문이다.</p>
<hr>
<h2 id="프로토타입-체인">프로토타입 체인</h2>
<p>객체에서 어떤 속성을 찾을 때, 자기 자신에게 없으면 프로토타입으로 올라간다. 거기도 없으면 또 그 위로. 이렇게 연결된 사슬을 <strong>프로토타입 체인</strong>이라고 한다.</p>
<pre><code class="language-js">const obj = { name: &#39;Alice&#39; };

// obj → Object.prototype → null</code></pre>
<pre><code>obj
 ↓ (없으면 위로)
Object.prototype  ← toString, hasOwnProperty 등이 여기 있음
 ↓
null  ← 체인의 끝</code></pre><p>스코프 체인이랑 닮았다. 스코프 체인은 변수를 위로 찾고, 프로토타입 체인은 속성을 위로 찾는다.</p>
<hr>
<h2 id="생성자-함수와-prototype">생성자 함수와 prototype</h2>
<p>ES6 전에는 이렇게 객체를 찍어냈다.</p>
<pre><code class="language-js">function User(name) {
  this.name = name;
}

User.prototype.greet = function() {
  console.log(`안녕, ${this.name}`);
};

const a = new User(&#39;Alice&#39;);
const b = new User(&#39;Bob&#39;);

a.greet(); // &#39;안녕, Alice&#39;
b.greet(); // &#39;안녕, Bob&#39;</code></pre>
<p><code>greet</code>를 각 인스턴스마다 만드는 게 아니라 <code>User.prototype</code>에 한 번만 만든다. <code>a</code>와 <code>b</code>는 그걸 공유한다.</p>
<p>만약 prototype을 안 쓰고 이렇게 하면?</p>
<pre><code class="language-js">function User(name) {
  this.name = name;
  this.greet = function() { // 인스턴스마다 함수가 새로 생성됨
    console.log(`안녕, ${this.name}`);
  };
}</code></pre>
<p>인스턴스를 100개 만들면 똑같은 함수가 100개 생긴다. 메모리 낭비다. prototype에 두면 하나만 만들어서 공유하니까 효율적이다.</p>
<hr>
<h2 id="class는-결국-프로토타입이다">class는 결국 프로토타입이다</h2>
<p>ES6 class 문법으로 똑같이 써보면 이렇다.</p>
<pre><code class="language-js">class User {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log(`안녕, ${this.name}`);
  }
}

const a = new User(&#39;Alice&#39;);
a.greet(); // &#39;안녕, Alice&#39;</code></pre>
<p>훨씬 깔끔하다. 근데 동작은 위의 생성자 함수랑 완전히 같다. <code>greet</code>는 여전히 <code>User.prototype</code>에 들어간다.</p>
<pre><code class="language-js">console.log(typeof User);              // &#39;function&#39; — class도 사실 함수다
console.log(User.prototype.greet);     // 함수 — prototype에 들어있음</code></pre>
<p>class는 <strong>문법적 설탕(Syntactic Sugar)</strong> 이다. async/await가 Promise를 감싼 것처럼, class도 프로토타입을 감싼 것이다.</p>
<hr>
<h2 id="상속도-프로토타입-체인이다">상속도 프로토타입 체인이다</h2>
<pre><code class="language-js">class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(`${this.name}가 소리를 낸다`);
  }
}

class Dog extends Animal {
  speak() {
    console.log(`${this.name}가 멍멍 짖는다`);
  }
}

const dog = new Dog(&#39;초코&#39;);
dog.speak();      // &#39;초코가 멍멍 짖는다&#39; — Dog에서 찾음
console.log(dog.name); // &#39;초코&#39; — Animal에서 상속</code></pre>
<p><code>extends</code>로 상속하면 프로토타입 체인이 연결된다.</p>
<pre><code>dog
 ↓
Dog.prototype     ← speak (재정의된 것)
 ↓
Animal.prototype  ← speak (원본)
 ↓
Object.prototype
 ↓
null</code></pre><p><code>dog.speak()</code>를 호출하면 <code>Dog.prototype</code>에서 먼저 찾고, 거기 있으니 그걸 쓴다. 없었으면 <code>Animal.prototype</code>까지 올라갔을 것이다.</p>
<hr>
<h2 id="proto-와-prototype-헷갈리지-말기"><strong>proto</strong> 와 prototype 헷갈리지 말기</h2>
<p>이름이 비슷해서 자주 헷갈린다.</p>
<table>
<thead>
<tr>
<th></th>
<th>누구한테 있나</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>prototype</code></td>
<td>함수(생성자)에만</td>
<td>인스턴스가 참조할 원본</td>
</tr>
<tr>
<td><code>__proto__</code></td>
<td>모든 객체에</td>
<td>자신의 프로토타입을 가리키는 링크</td>
</tr>
</tbody></table>
<pre><code class="language-js">function User() {}
const a = new User();

console.log(a.__proto__ === User.prototype); // true</code></pre>
<p><code>User.prototype</code>은 원본이고, <code>a.__proto__</code>는 그 원본을 가리키는 링크다. 둘이 같은 객체를 가리킨다.</p>
<hr>
<h2 id="정리">정리</h2>
<ul>
<li>자바스크립트는 프로토타입으로 상속을 구현한다</li>
<li>속성을 찾을 때 없으면 프로토타입 체인을 타고 위로 올라간다</li>
<li>생성자 함수의 <code>prototype</code>에 메서드를 두면 인스턴스가 공유한다</li>
<li><code>class</code>는 프로토타입을 감싼 문법적 설탕이다</li>
<li><code>extends</code> 상속도 결국 프로토타입 체인 연결이다</li>
</ul>
<hr>
<h2 id="오늘-얻은-것">오늘 얻은 것</h2>
<p>class만 쓰다가 그 아래 프로토타입을 보니까, 왜 메서드가 인스턴스마다 안 생기는지, 상속이 어떻게 동작하는지가 납득이 됐다. class는 편하지만 그게 전부라고 생각하면 동작을 오해하기 쉽다. 결국 자바스크립트의 뿌리는 프로토타입이다.</p>
<p>다음 편은 <strong>모듈 시스템</strong> — import/export가 어떻게 동작하는지, CommonJS와 ESM이 왜 갈라졌는지 정리해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 모던 자바스크립트부터 다시 시작 - 6 - this 바인딩]]></title>
            <link>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-6-this-%EB%B0%94%EC%9D%B8%EB%94%A9-7q7jvye8</link>
            <guid>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-6-this-%EB%B0%94%EC%9D%B8%EB%94%A9-7q7jvye8</guid>
            <pubDate>Thu, 21 May 2026 14:35:38 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지난 편에서 실행 컨텍스트를 다뤘다. this는 그것과 떼어놓을 수 없는 개념이다. 실행 컨텍스트가 만들어질 때 this가 결정되기 때문이다.</p>
</blockquote>
<hr>
<h2 id="this가-헷갈리는-이유">this가 헷갈리는 이유</h2>
<p>대부분의 언어에서 this는 &quot;나 자신&quot;이다. 클래스 안에서 쓰면 항상 그 인스턴스를 가리킨다.</p>
<p>자바스크립트는 다르다. <strong>this는 함수가 어떻게 호출됐느냐에 따라 달라진다.</strong> 선언한 위치가 아니라 호출한 방식이 기준이다. 이게 혼란의 시작이다.</p>
<hr>
<h2 id="1-전역에서의-this">1. 전역에서의 this</h2>
<pre><code class="language-js">console.log(this); // 브라우저: window / Node.js: {}</code></pre>
<p>전역에서 this는 전역 객체다. 브라우저면 <code>window</code>, Node.js면 <code>global</code>이다.</p>
<hr>
<h2 id="2-일반-함수-호출">2. 일반 함수 호출</h2>
<pre><code class="language-js">function show() {
  console.log(this);
}

show(); // 브라우저: window</code></pre>
<p>그냥 함수로 호출하면 this는 전역 객체다. strict mode에서는 <code>undefined</code>가 된다.</p>
<pre><code class="language-js">&#39;use strict&#39;;

function show() {
  console.log(this); // undefined
}

show();</code></pre>
<hr>
<h2 id="3-메서드-호출">3. 메서드 호출</h2>
<pre><code class="language-js">const user = {
  name: &#39;홍길동&#39;,
  greet() {
    console.log(this.name);
  }
};

user.greet(); // &#39;홍길동&#39;</code></pre>
<p>점(<code>.</code>) 앞에 있는 객체가 this다. <code>user.greet()</code>에서 this는 <code>user</code>가 된다.</p>
<p>근데 여기서 실수가 자주 나온다.</p>
<pre><code class="language-js">const greet = user.greet;
greet(); // undefined — this가 window가 됨</code></pre>
<p>같은 함수인데 결과가 다르다. <strong>호출하는 방식이 바뀌면 this도 바뀐다.</strong></p>
<hr>
<h2 id="4-화살표-함수">4. 화살표 함수</h2>
<p>화살표 함수는 this를 가지지 않는다. 대신 <strong>선언된 위치의 상위 스코프 this를 그대로 쓴다.</strong></p>
<pre><code class="language-js">const user = {
  name: &#39;홍길동&#39;,
  greet: function() {
    const inner = () =&gt; {
      console.log(this.name); // &#39;홍길동&#39;
    };
    inner();
  }
};

user.greet();</code></pre>
<p><code>inner</code>가 화살표 함수라서 <code>greet</code>의 this, 즉 <code>user</code>를 그대로 가져온다.</p>
<p>일반 함수였으면 어떻게 됐을까.</p>
<pre><code class="language-js">const user = {
  name: &#39;홍길동&#39;,
  greet: function() {
    function inner() {
      console.log(this.name); // undefined — this가 window
    }
    inner();
  }
};

user.greet();</code></pre>
<p><code>inner()</code>는 그냥 함수 호출이라 this가 window로 튀어버린다.</p>
<p>이게 콜백 함수에서 특히 자주 나오는 문제다.</p>
<pre><code class="language-js">const timer = {
  count: 0,
  start: function() {
    setInterval(function() {
      this.count++; // this가 window — 의도한 대로 안 됨
      console.log(this.count);
    }, 1000);
  }
};

timer.start();</code></pre>
<pre><code class="language-js">const timer = {
  count: 0,
  start: function() {
    setInterval(() =&gt; {
      this.count++; // this가 timer — 정상 동작
      console.log(this.count);
    }, 1000);
  }
};

timer.start();</code></pre>
<p>콜백을 화살표 함수로 바꿨더니 해결된다. React에서 이벤트 핸들러를 화살표 함수로 쓰는 것도 같은 이유다.</p>
<hr>
<h2 id="5-명시적-바인딩--call-apply-bind">5. 명시적 바인딩 — call, apply, bind</h2>
<p>this를 직접 지정하고 싶을 때 쓴다.</p>
<h3 id="call">call</h3>
<pre><code class="language-js">function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

const user = { name: &#39;홍길동&#39; };

greet.call(user, &#39;안녕&#39;); // &#39;안녕, 홍길동&#39;</code></pre>
<p>첫 번째 인자로 this를 지정하고, 나머지는 인자로 넘긴다.</p>
<h3 id="apply">apply</h3>
<pre><code class="language-js">greet.apply(user, [&#39;안녕&#39;]); // &#39;안녕, 홍길동&#39;</code></pre>
<p>call이랑 똑같은데 인자를 배열로 넘긴다.</p>
<h3 id="bind">bind</h3>
<pre><code class="language-js">const boundGreet = greet.bind(user);
boundGreet(&#39;안녕&#39;); // &#39;안녕, 홍길동&#39;</code></pre>
<p>바로 실행하지 않고 this가 고정된 새 함수를 반환한다. 나중에 호출할 때 쓴다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>호출 방식</th>
<th>this</th>
</tr>
</thead>
<tbody><tr>
<td>전역</td>
<td>window (strict: undefined)</td>
</tr>
<tr>
<td>일반 함수 호출</td>
<td>window (strict: undefined)</td>
</tr>
<tr>
<td>메서드 호출</td>
<td>점 앞의 객체</td>
</tr>
<tr>
<td>화살표 함수</td>
<td>상위 스코프의 this</td>
</tr>
<tr>
<td>call / apply / bind</td>
<td>직접 지정한 객체</td>
</tr>
</tbody></table>
<hr>
<h2 id="오늘-얻은-것">오늘 얻은 것</h2>
<p>this는 &quot;누가 호출했냐&quot;가 전부다. 선언 위치가 아니라 호출 방식을 보면 된다. 화살표 함수가 this를 따로 갖지 않는다는 것만 기억해도 콜백에서 this가 튀는 문제는 대부분 해결된다.</p>
<p>다음 편은 <strong>프로토타입</strong> — 클래스 문법 뒤에 실제로 어떤 구조가 숨어있는지 파보려 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 모던 자바스크립트부터 다시 시작 - 5 - 이벤트 루프]]></title>
            <link>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-5-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84</link>
            <guid>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-5-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84</guid>
            <pubDate>Fri, 15 May 2026 07:39:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지난 편에서 콜백, Promise, async/await의 흐름을 짚었다. 이번엔 그게 실제로 어떻게 돌아가는지 — 브라우저 안을 들여다본다.</p>
</blockquote>
<hr>
<h2 id="지난-편-복습">지난 편 복습</h2>
<p>async/await로 비동기 코드를 동기처럼 쓸 수 있게 됐다. 근데 한 가지 의문이 생긴다.</p>
<pre><code class="language-js">console.log(&#39;1&#39;);

setTimeout(function() {
  console.log(&#39;2&#39;);
}, 0);

console.log(&#39;3&#39;);</code></pre>
<p>결과가 뭘까? <code>1 → 2 → 3</code>이라고 생각하기 쉽다. 근데 실제로는 <code>1 → 3 → 2</code>다.</p>
<p><code>setTimeout</code>의 딜레이가 <strong>0ms인데도</strong> 왜 <code>3</code>이 먼저 나올까. 이걸 이해하려면 이벤트 루프를 알아야 한다.</p>
<hr>
<h2 id="js는-싱글-스레드다">JS는 싱글 스레드다</h2>
<p>자바스크립트는 한 번에 하나의 작업만 처리한다. 멀티 스레드가 아니다.</p>
<p>그런데 어떻게 비동기가 가능한 걸까. 코드가 실행되는 동안 타이머도 돌아가고, 네트워크 요청도 기다리고, 클릭 이벤트도 받는다.</p>
<p>비밀은 JS 엔진 혼자 하는 게 아니라는 것이다. <strong>브라우저(또는 Node.js)가 함께 돌아간다.</strong></p>
<hr>
<h2 id="구성-요소">구성 요소</h2>
<h3 id="콜-스택-call-stack">콜 스택 (Call Stack)</h3>
<p>현재 실행 중인 코드가 쌓이는 곳이다. 함수가 호출되면 스택에 쌓이고, 리턴하면 빠진다.</p>
<pre><code class="language-js">function a() {
  b();
}
function b() {
  console.log(&#39;b&#39;);
}
a();</code></pre>
<pre><code>[ b ]        ← 실행 중
[ a ]
[ 전역 ]</code></pre><p>JS 엔진은 콜 스택이 비어야 다음 작업을 가져올 수 있다.</p>
<hr>
<h3 id="web-api-브라우저-영역">Web API (브라우저 영역)</h3>
<p><code>setTimeout</code>, <code>fetch</code>, <code>addEventListener</code> 같은 건 JS 엔진이 처리하지 않는다. <strong>브라우저가 대신 처리해준다.</strong></p>
<pre><code class="language-js">setTimeout(function() {
  console.log(&#39;타이머 완료&#39;);
}, 1000);</code></pre>
<p><code>setTimeout</code>이 호출되는 순간 브라우저에 타이머를 맡기고, JS는 다음 코드로 넘어간다. 1초 후 브라우저가 콜백을 큐에 넣어준다.</p>
<hr>
<h3 id="태스크-큐-task-queue">태스크 큐 (Task Queue)</h3>
<p>브라우저가 완료된 비동기 작업의 콜백을 여기에 넣는다. <code>setTimeout</code>, <code>setInterval</code>, 이벤트 핸들러가 여기에 쌓인다.</p>
<hr>
<h3 id="마이크로태스크-큐-microtask-queue">마이크로태스크 큐 (Microtask Queue)</h3>
<p>Promise의 <code>.then()</code>, <code>async/await</code>의 콜백이 여기에 쌓인다. 태스크 큐보다 <strong>우선순위가 높다.</strong></p>
<hr>
<h3 id="이벤트-루프-event-loop">이벤트 루프 (Event Loop)</h3>
<p>이벤트 루프는 딱 한 가지 일을 한다.</p>
<blockquote>
<p><strong>콜 스택이 비었을 때, 큐에서 작업을 꺼내 콜 스택에 올린다.</strong></p>
</blockquote>
<p>순서는 이렇다.</p>
<pre><code>1. 콜 스택 비었는지 확인
2. 마이크로태스크 큐에 뭔가 있으면 전부 꺼내서 실행
3. 태스크 큐에서 작업 하나 꺼내서 실행
4. 다시 1번으로</code></pre><p>마이크로태스크 큐를 먼저, <strong>전부</strong> 처리한 다음 태스크 큐로 넘어간다는 게 핵심이다.</p>
<hr>
<h2 id="처음-예제-다시-보기">처음 예제 다시 보기</h2>
<pre><code class="language-js">console.log(&#39;1&#39;);        // (A)

setTimeout(function() {
  console.log(&#39;2&#39;);      // (B)
}, 0);

console.log(&#39;3&#39;);        // (C)</code></pre>
<p>실행 순서를 따라가 보면:</p>
<pre><code>(A) console.log(&#39;1&#39;) → 콜 스택에서 실행 → &#39;1&#39; 출력
setTimeout → 브라우저에 타이머 맡기고 바로 리턴
(C) console.log(&#39;3&#39;) → 콜 스택에서 실행 → &#39;3&#39; 출력
콜 스택 비워짐
브라우저가 (B) 콜백을 태스크 큐에 넣음
이벤트 루프가 태스크 큐에서 꺼내서 실행 → &#39;2&#39; 출력</code></pre><p>결과: <code>1 → 3 → 2</code></p>
<p>딜레이가 0ms여도 태스크 큐를 거치기 때문에 항상 나중에 실행된다.</p>
<hr>
<h2 id="마이크로태스크가-먼저다">마이크로태스크가 먼저다</h2>
<pre><code class="language-js">console.log(&#39;1&#39;);

setTimeout(function() {
  console.log(&#39;setTimeout&#39;); // 태스크 큐
}, 0);

Promise.resolve().then(function() {
  console.log(&#39;Promise&#39;);    // 마이크로태스크 큐
});

console.log(&#39;2&#39;);</code></pre>
<p>결과: <code>1 → 2 → Promise → setTimeout</code></p>
<p>콜 스택이 비워지면 마이크로태스크 큐를 먼저 <strong>전부</strong> 처리하고, 그 다음 태스크 큐로 넘어간다.</p>
<pre><code>콜 스택 실행: &#39;1&#39; 출력
setTimeout → 태스크 큐 예약
Promise.then → 마이크로태스크 큐 예약
콜 스택 실행: &#39;2&#39; 출력
콜 스택 비워짐
→ 마이크로태스크 큐 처리: &#39;Promise&#39; 출력
→ 태스크 큐 처리: &#39;setTimeout&#39; 출력</code></pre><hr>
<h2 id="asyncawait는-결국-promise다">async/await는 결국 Promise다</h2>
<pre><code class="language-js">async function run() {
  console.log(&#39;A&#39;);
  await Promise.resolve();
  console.log(&#39;B&#39;); // await 이후는 마이크로태스크 큐에 들어감
}

console.log(&#39;시작&#39;);
run();
console.log(&#39;끝&#39;);</code></pre>
<p>결과: <code>시작 → A → 끝 → B</code></p>
<p><code>await</code> 뒤의 코드는 Promise의 <code>.then()</code> 콜백처럼 마이크로태스크 큐에 들어간다. 그래서 <code>끝</code>이 먼저 출력된 다음 <code>B</code>가 나온다.</p>
<p>async/await가 동기처럼 <strong>읽히는</strong> 것이지, 실제로 동기인 건 아니다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th></th>
<th>콜 스택</th>
<th>태스크 큐</th>
<th>마이크로태스크 큐</th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>현재 실행 중인 코드</td>
<td>비동기 콜백 대기</td>
<td>Promise 콜백 대기</td>
</tr>
<tr>
<td>예시</td>
<td>일반 함수 실행</td>
<td>setTimeout, 이벤트 핸들러</td>
<td>Promise.then, async/await</td>
</tr>
<tr>
<td>우선순위</td>
<td>최우선</td>
<td>낮음</td>
<td>태스크 큐보다 높음</td>
</tr>
</tbody></table>
<hr>
<h2 id="오늘-얻은-것">오늘 얻은 것</h2>
<p><code>setTimeout 0ms</code>가 왜 나중에 실행되는지, Promise가 왜 setTimeout보다 먼저 실행되는지가 이제 납득이 된다. 코드를 외우는 게 아니라 콜 스택 → 마이크로태스크 큐 → 태스크 큐 순서로 흐름을 그려보면 결과를 예측할 수 있다.</p>
<p>다음 편은 <strong>클로저(Closure)</strong> — 함수가 선언된 환경을 기억한다는 게 무슨 뜻인지, 실제로 어디서 쓰이는지 정리해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TS]스크롤 기반 상태와 복원 상태의 race condition]]></title>
            <link>https://velog.io/@kim-na-hyeong/TS%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B8%B0%EB%B0%98-%EC%83%81%ED%83%9C%EC%99%80-%EB%B3%B5%EC%9B%90-%EC%83%81%ED%83%9C%EC%9D%98-race-condition</link>
            <guid>https://velog.io/@kim-na-hyeong/TS%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B8%B0%EB%B0%98-%EC%83%81%ED%83%9C%EC%99%80-%EB%B3%B5%EC%9B%90-%EC%83%81%ED%83%9C%EC%9D%98-race-condition</guid>
            <pubDate>Wed, 06 May 2026 13:32:30 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/ff24d40b-8b6a-4902-b382-3f69a4cc7629/image.png" alt="">
또다. 또. 에러가 나왔다! next.js를 제대로 학습하지 못한 탓일까.
개인적으론 직접 개발해가면서 공부하는 게 도움이 된다고 생각했는데...</p>
<p>주니어는 역시 기본기가 탄탄해야한다.</p>
<p>문제를 풀어가는 과정이 321654987시간 소요되기 때문이다.</p>
<p>그래도..... AI와 선생님과 함께 해쳐나가자....!!!!!!</p>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/b3afce3e-77fd-4eeb-a1de-f3f57b23f3a5/image.png" alt=""></p>
<h2 id="문제-상황">문제 상황</h2>
<p>프로젝트 슬라이더에서 카드를 선택하고 디테일 페이지로 이동했다가 뒤로가기로 돌아오면, 내가 보던 카드가 아니라 <strong>첫 번째 카드로 초기화</strong>되는 현상이 있었다.</p>
<p>원인을 추적해보니 두 가지 로직이 동시에 실행되면서 충돌하고 있었다.</p>
<hr>
<h2 id="구조-이해">구조 이해</h2>
<p>이 슬라이더는 스크롤 위치에 따라 활성 카드가 바뀌는 구조다.</p>
<pre><code class="language-ts">const handleScroll = () =&gt; {
  const progress = currentScroll / maxScroll;
  const nextIndex = Math.round(progress * (items.length - 1));
  setActiveIndex(nextIndex); // 스크롤 위치 → 활성 카드
};

window.addEventListener(&quot;scroll&quot;, handleScroll, { passive: true });</code></pre>
<p>뒤로가기로 돌아올 때는 <code>sessionStorage</code>에 저장해둔 카드 ID를 읽어서 해당 카드로 스크롤을 복원하는 로직도 있다.</p>
<pre><code class="language-ts">// sessionStorage에서 이전 카드 ID 복원
const savedId = window.sessionStorage.getItem(ACTIVE_PROJECT_KEY);
const savedIndex = items.findIndex((item) =&gt; item.id === savedId);

window.scrollTo({
  top: section.offsetTop + maxScroll * progress,
  behavior: &quot;auto&quot;,
});</code></pre>
<hr>
<h2 id="원인-race-condition">원인? <code>race condition</code>!!</h2>
<p>문제는 이 두 로직이 <strong>동시에 실행</strong>된다는 것이다.</p>
<ol>
<li>복원 로직이 <code>scrollTo()</code>로 원하는 위치로 이동시킨다</li>
<li><code>scrollTo()</code>가 실행되는 순간 <strong>scroll 이벤트가 발생</strong>한다</li>
<li>스크롤 핸들러가 즉시 실행되어 <code>setActiveIndex</code>를 덮어쓴다</li>
<li>복원하려던 카드가 아닌 다른 카드가 활성화된다</li>
</ol>
<pre><code>복원 로직: scrollTo(저장된 위치) ──────────────────────────────→ ✅ 위치 이동
scroll 이벤트 발생:  handleScroll() → setActiveIndex(엉뚱한 값) → ❌ 상태 덮어씀</code></pre><p>이것이 <strong>Scroll-driven state와 navigation restoration이 동시에 실행될 때 생기는 race condition</strong>이다.</p>
<hr>
<h2 id="해결-isrestoringref로-동기화-일시-차단">해결.... isRestoringRef로 동기화 일시 차단</h2>
<p>핵심 아이디어는 <strong>&quot;복원 중일 때는 스크롤 핸들러가 상태를 덮어쓰지 못하게 막는 것&quot;</strong> 이다.</p>
<pre><code class="language-ts">const isRestoringRef = React.useRef(false);</code></pre>
<p><code>useState</code>가 아닌 <code>useRef</code>를 쓰는 이유가 중요하다. 리렌더 없이 즉시 값을 읽고 써야 하기 때문이다.
스크롤 이벤트는 매우 빠르게 발생하는데, <code>useState</code>로 플래그를 관리하면 리렌더 사이클이 끼어들어 타이밍이 맞지 않는다.</p>
<h3 id="복원-시작-시-플래그-on">복원 시작 시 플래그 ON</h3>
<pre><code class="language-ts">React.useEffect(() =&gt; {
  const savedIndex = items.findIndex((item) =&gt; item.id === savedId);

  isRestoringRef.current = true; // 🔒 스크롤 핸들러 차단 시작

  scrollToSavedCard(); // 저장된 위치로 이동

  const timeoutId = window.setTimeout(() =&gt; {
    scrollToSavedCard();
    isRestoringRef.current = false; // 🔓 차단 해제
    window.sessionStorage.removeItem(RESTORE_ACTIVE_KEY);
  }, 180);

  return () =&gt; {
    window.clearTimeout(timeoutId);
    isRestoringRef.current = false; // 언마운트 시 항상 해제
  };
}, [items, sectionRef]);</code></pre>
<h3 id="스크롤-핸들러에서-플래그-확인">스크롤 핸들러에서 플래그 확인</h3>
<pre><code class="language-ts">const handleScroll = () =&gt; {
  if (draggedRef.current || isRestoringRef.current) {
    return; // 🚫 복원 중이면 상태 덮어쓰기 차단
  }

  const nextIndex = Math.round(progress * (items.length - 1));
  setActiveIndex(nextIndex);
};</code></pre>
<p>복원이 완료될 때까지 (180ms) 스크롤 핸들러가 <code>setActiveIndex</code>를 호출하지 못하도록 막는다. 복원이 끝나면 플래그를 해제하고 이후 스크롤은 정상적으로 동작한다.</p>
<hr>
<h2 id="전체-흐름-정리">전체 흐름 정리</h2>
<pre><code>뒤로가기 감지
  → isRestoringRef = true         // 🔒 차단 시작
  → scrollTo(저장된 위치)
  → scroll 이벤트 발생
      → handleScroll 실행
      → isRestoringRef 확인 → true → return (차단)  ✅
  → 180ms 후
  → isRestoringRef = false        // 🔓 차단 해제
  → 이후 스크롤은 정상 동작      ✅</code></pre><hr>
<h2 id="배운-것">배운 것</h2>
<blockquote>
<p><strong>항상 켜져 있는 이벤트 핸들러는, 복원 중인 상태를 모른다.</strong></p>
</blockquote>
<p>스크롤 핸들러는 스크롤이 발생하면 무조건 실행된다. <code>scrollTo()</code>로 인한 스크롤인지, 사용자가 직접 스크롤한 것인지 구분하지 않는다. 복원 로직이 만들어낸 스크롤 이벤트가 복원 상태를 덮어쓰는 것이 이 race condition의 본질이다.</p>
<p>해결의 핵심 패턴은 두 가지다.</p>
<ul>
<li><strong><code>useRef</code> 플래그</strong>로 리렌더 없이 즉시 상태를 동기화</li>
<li><strong>시간 기반 보호 구간</strong>(<code>setTimeout</code>)으로 복원이 완료될 때까지 핸들러를 일시 차단</li>
</ul>
<p>이벤트 기반 상태 관리에서 &quot;지금 내가 원하는 상태를 잠깐 보호해야 한다&quot;는 상황이 생기면, <code>isRestoringRef</code> 같은 flag 패턴을 먼저 떠올도록..하자....ㅠㅠㅠ....... 실은 쓰면서도 모르겠다. 내일 다시 복기해야지.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[포트폴리오에 이메일 기능을 추가하자 Resend + Vercel 환경변수 설정]]></title>
            <link>https://velog.io/@kim-na-hyeong/%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4%EC%97%90-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EC%B6%94%EA%B0%80%ED%95%98%EC%9E%90-Resend-Vercel-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@kim-na-hyeong/%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4%EC%97%90-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EC%B6%94%EA%B0%80%ED%95%98%EC%9E%90-Resend-Vercel-%ED%99%98%EA%B2%BD%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Thu, 30 Apr 2026 09:12:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Next.js 포트폴리오에 문의 폼을 달고 싶었다. 이메일 발송 서비스를 찾아보다가 <strong>Resend</strong>를 선택했는데, 개발자 친화적인 API와 넉넉한 무료 플랜이 마음에 들었다. 이 글은 Resend API 키 발급부터 Vercel 배포까지의 전 과정을 기록한 것이다.</p>
</blockquote>
<hr>
<h2 id="전체-흐름-한눈에-보기">전체 흐름 한눈에 보기</h2>
<pre><code>Resend 가입 → API 키 발급 → .env.local 설정 → 로컬 테스트 → Vercel 환경변수 등록 → Redeploy</code></pre><hr>
<h2 id="1단계--resend-가입-및-api-키-발급">1단계 — Resend 가입 및 API 키 발급</h2>
<p><a href="https://resend.com/">resend.com</a> 에 접속해서 회원가입한다. 이메일 가입과 GitHub 연동 모두 가능하다.</p>
<p>가입 아이디와 메일을 받을 주소를 동일하게 썼다. (나중에 헷갈릴 까봐)</p>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/80dd27aa-2889-4b57-bdfd-86fb75f13b38/image.png" alt=""></p>
<p>로그인 후 왼쪽 메뉴에서 <strong>API Keys</strong> 클릭 → 우측 상단 <strong>Create API Key</strong> 버튼을 누른다.</p>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/f7539a60-563c-45cd-a585-b8241208127d/image.png" alt=""></p>
<p>이름은 프로젝트명으로 지어두면 나중에 알아보기 편하다. (예: <code>portfolio</code>), 본인은 프로젝트명과 로컬, 
<strong>Add</strong> 를 누르면 아래처럼 생긴 키가 생성된다.</p>
<pre><code>re_abc123xxxxxxxxxxxxxxxxxxxxxxxx</code></pre><blockquote>
<p>⚠️ <strong>이 키는 생성 직후에만 볼 수 있다.</strong> 창을 닫으면 다시 볼 수 없으니 반드시 복사해서 어딘가에 저장해두자.</p>
</blockquote>
<hr>
<h2 id="2단계--로컬-테스트용-envlocal-설정">2단계 — 로컬 테스트용 <code>.env.local</code> 설정</h2>
<p>프로젝트 루트 폴더에 <code>.env.local</code> 파일을 생성하고 아래 세 가지 환경변수를 입력한다.</p>
<pre><code class="language-bash">RESEND_API_KEY=&quot;re_복사한키_여기에_붙여넣기&quot;
CONTACT_TO_EMAIL=&quot;your@email.com&quot;
CONTACT_FROM_EMAIL=&quot;Portfolio Contact &lt;onboarding@resend.dev&gt;&quot;</code></pre>
<p><strong>각 변수의 역할:</strong></p>
<table>
<thead>
<tr>
<th>변수명</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>RESEND_API_KEY</code></td>
<td>Resend API 인증 키</td>
</tr>
<tr>
<td><code>CONTACT_TO_EMAIL</code></td>
<td>문의 메일을 받을 내 이메일 주소</td>
</tr>
<tr>
<td><code>CONTACT_FROM_EMAIL</code></td>
<td>발신자로 표시될 이름과 주소</td>
</tr>
</tbody></table>
<blockquote>
<p><code>CONTACT_FROM_EMAIL</code>의 <code>onboarding@resend.dev</code>는 Resend가 기본으로 제공하는 발신 주소다. 도메인을 별도로 인증하지 않은 경우 이 주소를 쓰면 된다.</p>
</blockquote>
<p>환경변수를 저장한 뒤 서버를 재시작한다.</p>
<pre><code class="language-bash">npm run dev</code></pre>
<p>이제 로컬에서 문의 폼 테스트가 가능하다.</p>
<hr>
<h2 id="3단계--vercel에-환경변수-등록">3단계 — Vercel에 환경변수 등록</h2>
<p>로컬에서 잘 동작했다면, 이제 프로덕션 환경에도 동일한 환경변수를 등록해야 한다.</p>
<h3 id="3-1-vercel-프로젝트-설정-진입">3-1. Vercel 프로젝트 설정 진입</h3>
<ol>
<li><a href="https://vercel.com/">vercel.com</a> 로그인</li>
<li>대시보드에서 배포된 프로젝트 클릭</li>
<li>상단 탭 <strong>Settings</strong> 클릭</li>
<li>왼쪽 사이드바 <strong>Environment Variables</strong> 클릭</li>
</ol>
<h3 id="3-2-환경변수-3개-입력">3-2. 환경변수 3개 입력</h3>
<p>아래 세 가지를 각각 입력하고 <strong>Save</strong> 한다.<br>Environment는 <strong>Production, Preview, Development</strong> 모두 체크한다.</p>
<p><strong>첫 번째</strong></p>
<pre><code>Key:   RESEND_API_KEY
Value: re_복사한키_여기에_붙여넣기</code></pre><p><strong>두 번째</strong></p>
<pre><code>Key:   CONTACT_TO_EMAIL
Value: your@email.com</code></pre><p><strong>세 번째</strong></p>
<pre><code>Key:   CONTACT_FROM_EMAIL
Value: Portfolio Contact &lt;onboarding@resend.dev&gt;</code></pre><h3 id="3-3-redeploy">3-3. Redeploy</h3>
<p>환경변수를 저장해도 기존 배포에는 자동으로 적용되지 않는다. 반드시 재배포가 필요하다.</p>
<ol>
<li>상단 탭 <strong>Deployments</strong> 클릭</li>
<li>가장 최근 배포의 오른쪽 <code>···</code> 메뉴 클릭</li>
<li><strong>Redeploy</strong> 선택 → 확인</li>
</ol>
<p>재배포가 완료되면 프로덕션 환경에서도 이메일 발송이 정상 동작한다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<pre><code>✅ Resend API 키 발급
✅ .env.local 로컬 환경변수 설정
✅ Vercel 환경변수 3개 등록
✅ Redeploy 완료</code></pre><p>생각보다 단순한 과정이었다. 다음 글에서는 실제 Next.js API Route에서 Resend를 호출하는 코드를 다룰 예정이다.</p>
<hr>
<p><em>Next.js App Router + TypeScript + Resend로 만드는 포트폴리오 문의 폼 시리즈</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] Next.js에서 뒤로가기 시 스크롤이 맨 위로 간다.]]></title>
            <link>https://velog.io/@kim-na-hyeong/Next.js%EC%97%90%EC%84%9C-%EB%92%A4%EB%A1%9C%EA%B0%80%EA%B8%B0-%EC%8B%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EC%9D%B4-%EB%A7%A8-%EC%9C%84%EB%A1%9C-%EA%B0%80%EB%8A%94-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0-usePathname%EC%9D%B4%EB%9E%80</link>
            <guid>https://velog.io/@kim-na-hyeong/Next.js%EC%97%90%EC%84%9C-%EB%92%A4%EB%A1%9C%EA%B0%80%EA%B8%B0-%EC%8B%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4%EC%9D%B4-%EB%A7%A8-%EC%9C%84%EB%A1%9C-%EA%B0%80%EB%8A%94-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0-usePathname%EC%9D%B4%EB%9E%80</guid>
            <pubDate>Mon, 27 Apr 2026 14:00:52 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>프로젝트 목록에서 카드를 클릭해 디테일 페이지로 이동한 뒤, 브라우저 뒤로가기를 누르면 이전에 보던 위치가 아니라 <strong>맨 위로 튀어 올라가는 현상</strong>이 있었다.</p>
<p>사용자 입장에서는 스크롤을 내려서 프로젝트 카드를 클릭했는데, 돌아오면 처음부터 다시 스크롤해야 하는 불편함이 생긴다.</p>
<hr>
<h2 id="원인-분석">원인 분석</h2>
<h3 id="usepathname이란">usePathname이란?</h3>
<p><code>usePathname</code>은 Next.js App Router에서 제공하는 훅으로, <strong>현재 URL의 경로(pathname)를 반환</strong>한다.</p>
<p>예를 들어 브라우저 주소창에 <code>https://mysite.com/project/exp1</code>이 표시되고 있다면, <code>usePathname()</code>은 <code>/project/exp1</code>을 반환한다.</p>
<pre><code class="language-tsx">const pathname = usePathname();
// 메인 페이지: &quot;/&quot;
// 프로젝트 디테일: &quot;/project/exp1&quot;</code></pre>
<p>핵심은 <strong>pathname이 바뀔 때마다 <code>useEffect</code>가 다시 실행</strong>된다는 점이다. 즉, 페이지가 이동할 때마다 감지할 수 있는 트리거 역할을 한다.</p>
<pre><code class="language-tsx">React.useEffect(() =&gt; {
  // pathname이 바뀔 때마다 이 코드가 실행됨
  console.log(&quot;페이지가 이동했다:&quot;, pathname);
}, [pathname]); // ← pathname을 의존성 배열에 넣으면 변경될 때마다 실행</code></pre>
<p>이 프로젝트에서는 <code>usePathname</code>으로 페이지 이동을 감지하고, 이동할 때마다 스크롤을 맨 위로 올리는 동작을 구현했다.</p>
<h3 id="문제의-원인">문제의 원인</h3>
<p>Next.js App Router는 페이지 이동 시 기본적으로 <code>scrollTo(0, 0)</code>을 실행한다. 문제는 이게 <strong>뒤로가기(popstate)일 때도 동일하게 적용</strong>된다는 것이다.</p>
<p>일반 페이지 이동이라면 맨 위로 가는 게 맞다. 하지만 뒤로가기는 이전 스크롤 위치를 복원해야 자연스럽다. Next.js가 이 둘을 구분하지 않아서 생기는 이슈다.</p>
<hr>
<h2 id="해결">해결</h2>
<p><code>popstate</code> 이벤트로 뒤로가기 여부를 감지하고, 뒤로가기일 때는 <code>scrollTo</code>를 건너뛰는 방식으로 해결했다.</p>
<pre><code class="language-tsx">export function MainLayout({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  const isPopStateNavigation = React.useRef(false);

  // 뒤로가기 감지
  React.useEffect(() =&gt; {
    const handlePopState = () =&gt; {
      isPopStateNavigation.current = true;
    };

    window.addEventListener(&quot;popstate&quot;, handlePopState);
    return () =&gt; window.removeEventListener(&quot;popstate&quot;, handlePopState);
  }, []);

  // 페이지 이동 시 스크롤 처리
  React.useEffect(() =&gt; {
    // 뒤로가기면 스크롤 초기화 건너뜀
    if (isPopStateNavigation.current) {
      isPopStateNavigation.current = false;
      return;
    }

    // 일반 이동이면 맨 위로
    if (pathname !== &quot;/&quot;) {
      requestAnimationFrame(() =&gt; {
        window.scrollTo({ top: 0, left: 0, behavior: &quot;auto&quot; });
      });
    }
  }, [pathname]);
}</code></pre>
<h3 id="포인트">포인트</h3>
<p><strong><code>useRef</code>로 플래그 관리</strong>
<code>useState</code>가 아닌 <code>useRef</code>를 쓴 이유는, 값이 바뀌어도 리렌더를 일으키지 않아야 하기 때문이다. 스크롤 위치 복원은 렌더링과 무관한 사이드 이펙트라서 ref가 적합하다.</p>
<p><strong><code>requestAnimationFrame</code> 타이밍</strong>
DOM이 업데이트된 직후 스크롤을 실행해야 정확하게 맨 위로 이동한다. 동기로 바로 호출하면 렌더 전에 실행되어 의도한 대로 동작하지 않을 수 있다.</p>
<p><strong><code>popstate</code> → <code>pathname</code> 변경 순서</strong>
브라우저가 뒤로가기를 실행하면 <code>popstate</code>가 먼저 발생하고, 이후 <code>pathname</code>이 바뀐다. 그래서 ref 플래그를 먼저 세워두고 pathname effect에서 확인하는 순서가 성립한다.</p>
<hr>
<h2 id="배운-것">배운 것</h2>
<blockquote>
<p><strong>Next.js는 뒤로가기와 일반 이동을 구분하지 않는다.</strong></p>
</blockquote>
<p><code>popstate</code> 이벤트를 직접 감지해서 네비게이션 타입을 구분하고, 그에 맞는 스크롤 동작을 수동으로 제어해야 한다. 브라우저 기본 동작과 프레임워크 동작이 충돌할 때는 이벤트 레벨에서 직접 제어하는 것이 확실한 해결책이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[트러블슈팅] GSAP ScrollTrigger가 탭 전환 후 섹션을 못 찾는 이유
]]></title>
            <link>https://velog.io/@kim-na-hyeong/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-GSAP-ScrollTrigger%EA%B0%80-%ED%83%AD-%EC%A0%84%ED%99%98-%ED%9B%84-%EC%84%B9%EC%85%98%EC%9D%84-%EB%AA%BB-%EC%B0%BE%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@kim-na-hyeong/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-GSAP-ScrollTrigger%EA%B0%80-%ED%83%AD-%EC%A0%84%ED%99%98-%ED%9B%84-%EC%84%B9%EC%85%98%EC%9D%84-%EB%AA%BB-%EC%B0%BE%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Fri, 24 Apr 2026 10:54:36 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>포트폴리오 프로젝트를 개발하던 중 이상한 현상을 발견했다.</p>
<p>Projects 섹션에서 탭을 <code>All → Work → Project</code> 순으로 전환하면, 아이템 수가 줄어들면서 페이지 전체 높이가 짧아진다. 그런데 Project 탭이 활성화 된 채로 스크롤을 내리면 <strong>Contact 섹션이 아예 나타나지 않는 것이다.</strong> 
<img src="https://velog.velcdn.com/images/kim-na-hyeong/post/57a28250-0ffb-4ab8-b594-0132f1161783/image.png" width="300" /></p>
<p>아무 반응 없을 때 너무 짧은 data 길이 때문에 Height 값을 인식 못했던 경우가 있어서 있어서 겸사겸사 복기 했다.</p>
<hr>
<h2 id="원인-분석">원인 분석</h2>
<p>문제는 두 가지였다.</p>
<h3 id="①-scrolltrigger가-dom-변화를-자동으로-감지하지-않는다">① ScrollTrigger가 DOM 변화를 자동으로 감지하지 않는다</h3>
<p>GSAP의 <code>ScrollTrigger</code>는 초기화 시점에 각 섹션의 위치값을 계산해서 캐싱해둔다. 
이후 DOM이 변경되어 페이지 높이가 달라져도, ScrollTrigger는 그 사실을 모른다. 캐싱된 위치값을 그대로 사용하기 때문에 ContactSection의 trigger 시작점이 실제 위치와 달라져 버린다.</p>
<p>ProjectGrid에서 필터를 바꾸면 렌더링되는 카드 수가 줄어들고, 그만큼 페이지 높이도 줄어든다. 하지만 ContactSection의 ScrollTrigger는 이전 높이를 기준으로 <code>start: &quot;top 90%&quot;</code>를 잡고 있으니, 스크롤이 그 지점에 닿기 전에 페이지가 끝나버리는 상황이 된 것이다.</p>
<h3 id="②-y--20이-목표값으로-설정된-버그">② <code>y: -20</code>이 목표값으로 설정된 버그</h3>
<p>ContactSection의 애니메이션을 보면:</p>
<pre><code class="language-ts">gsap.set(revealTargets, { opacity: 0, y: 50 });

gsap.to(revealTargets, {
  opacity: 1,
  y: -20, // ← 이게 문제
  ...
});</code></pre>
<p><code>gsap.set</code>으로 <code>y: 50</code> (아래로 50px 밀린 상태)에서 시작해서 <code>y: -20</code> (위로 20px 올라간 상태)으로 이동하는 구조였다. 자연스러운 제자리(<code>y: 0</code>)가 아니라 <strong>20px 위에 떠서</strong> 애니메이션이 끝난 것이다.</p>
<hr>
<h2 id="✅해결">✅해결!</h2>
<h3 id="projectgridtsx--필터-변경-후-scrolltriggerrefresh-호출">ProjectGrid.tsx — 필터 변경 후 <code>ScrollTrigger.refresh()</code> 호출</h3>
<pre><code class="language-ts">// filter가 바뀌면 DOM이 새 높이로 렌더된 직후 재계산
const rafId = requestAnimationFrame(() =&gt; ScrollTrigger.refresh());

return () =&gt; {
  cancelAnimationFrame(rafId);
  st.kill();
};</code></pre>
<p><code>requestAnimationFrame</code> 안에서 호출하는 게 핵심이다. 필터 변경 → React 리렌더 → DOM 업데이트 → 다음 프레임에서 <code>refresh()</code> 순서로 실행되어야 새 높이를 정확히 읽을 수 있다. 동기로 바로 호출하면 DOM이 아직 안 바뀐 상태에서 계산해버린다.</p>
<h3 id="contactsectiontsx--y--20-→-y-0-수정">ContactSection.tsx — <code>y: -20</code> → <code>y: 0</code> 수정</h3>
<pre><code class="language-ts">gsap.to(revealTargets, {
  opacity: 1,
  y: 0, // 자연스러운 제자리
  duration: 0.9,
  stagger: 0.14,
  ease: &quot;power3.out&quot;,
});</code></pre>
<hr>
<h2 id="til">TIL</h2>
<blockquote>
<p><strong>GSAP ScrollTrigger는 DOM 변화를 자동으로 감지하지 않는다.</strong></p>
</blockquote>
<p>React에서 조건부 렌더링, 탭 전환, 필터링 등으로 DOM 높이가 바뀌는 경우, 반드시 <code>ScrollTrigger.refresh()</code>를 수동으로 호출해줘야 한다. 그리고 타이밍은 <code>requestAnimationFrame</code>으로 DOM 업데이트 직후를 잡아야 정확하다.</p>
<p>조용한 버그일수록 원인을 찾기 어렵다. 에러가 없다는 건 코드가 잘못된 게 아니라, 타이밍이나 순서가 틀렸다는 신호일 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 모던 자바스크립트부터 다시 시작 - 4 - 콜백 지옥과 Promise, async/await]]></title>
            <link>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-4-%EC%BD%9C%EB%B0%B1-%EC%A7%80%EC%98%A5%EA%B3%BC-Promise-asyncawait-4w0zsih0</link>
            <guid>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-4-%EC%BD%9C%EB%B0%B1-%EC%A7%80%EC%98%A5%EA%B3%BC-Promise-asyncawait-4w0zsih0</guid>
            <pubDate>Thu, 16 Apr 2026 11:14:32 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>지난 편에서 비동기가 뭔지 개념을 잡았다. 이번엔 비동기를 실제 코드로 다루는 방법을 알아본다.</p>
</blockquote>
<hr>
<h2 id="지난-편-복습">지난 편 복습</h2>
<p>비동기는 &quot;기다리지 않고 다음 작업을 먼저 처리하는 방식&quot;이었다.</p>
<p>근데 문제가 있었다.</p>
<pre><code class="language-js">let userData;

fetch(&#39;/api/user&#39;)
  .then(res =&gt; res.json())
  .then(data =&gt; { userData = data; });

console.log(userData); // undefined — 아직 안 왔음</code></pre>
<p>비동기라서 순서 보장이 안 된다. 그래서 &quot;데이터 받은 다음에 이걸 실행해줘&quot;라고 명시해야 한다고 했다.</p>
<p>오늘은 그 방법 세 가지를 순서대로 살펴본다. <strong>콜백 → Promise → async/await</strong>, 왜 이렇게 발전해왔는지를 흐름으로 이해하면 된다.</p>
<hr>
<h2 id="1-콜백callback">1. 콜백(Callback)</h2>
<p>콜백은 가장 원시적인 방법이다. &quot;나중에 이거 실행해줘&quot;하고 함수를 넘기는 것.</p>
<pre><code class="language-js">function getUserData(callback) {
  setTimeout(function() {
    const data = { name: &#39;김나형&#39;, age: 25 };
    callback(data); // 데이터 준비되면 콜백 실행
  }, 1000);
}

getUserData(function(data) {
  console.log(data.name); // &#39;김나형&#39;
});</code></pre>
<p>진동벨에 비유하면, 벨이 울렸을 때 할 행동을 미리 적어두는 것이다. &quot;벨 울리면 카운터로 와서 커피 받아가세요&quot; 같은 느낌.</p>
<p>직관적이고 간단하다. <strong>근데 문제가 생긴다.</strong></p>
<hr>
<h2 id="콜백-지옥callback-hell">콜백 지옥(Callback Hell)</h2>
<p>현실에서는 비동기 작업이 하나로 끝나지 않는다. 예를 들어 이런 시나리오라면?</p>
<pre><code>1. 로그인해서 유저 ID 받기
2. 유저 ID로 게시글 목록 받기
3. 첫 번째 게시글로 댓글 받기
4. 댓글 작성자 정보 받기</code></pre><p>콜백으로 구현하면 이렇게 된다.</p>
<pre><code class="language-js">login(function(userId) {
  getPosts(userId, function(posts) {
    getComments(posts[0].id, function(comments) {
      getUser(comments[0].authorId, function(user) {
        console.log(user);
        // 여기서 또 뭔가 해야 한다면...?
      });
    });
  });
});</code></pre>
<p>오른쪽으로 계속 파고드는 모양새다. 이걸 <strong>콜백 지옥(Callback Hell)</strong> 또는 <strong>Pyramid of Doom</strong>이라고 부른다.</p>
<pre><code>login(
  getPosts(
    getComments(
      getUser(
        또 뭔가(
          또또 뭔가(
            ← 여기까지 도달한 나
          )
        )
      )
    )
  )
)</code></pre><p>읽기도 힘들고, 에러 처리를 각 단계마다 해줘야 해서 유지보수가 악몽이 된다.</p>
<hr>
<h2 id="2-promise">2. Promise</h2>
<p>Promise는 ES6에서 등장한 해결책이다.</p>
<p>이름 그대로 <strong>&quot;약속&quot;</strong> 이다. &quot;나중에 결과를 줄게. 성공하면 이거, 실패하면 저거 해줄게.&quot;</p>
<pre><code class="language-js">const promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    const success = true;

    if (success) {
      resolve({ name: &#39;김나형&#39; }); // 성공
    } else {
      reject(&#39;에러 발생&#39;); // 실패
    }
  }, 1000);
});

promise
  .then(function(data) {
    console.log(data.name); // 성공했을 때
  })
  .catch(function(error) {
    console.log(error); // 실패했을 때
  });</code></pre>
<p>Promise는 세 가지 상태를 가진다.</p>
<table>
<thead>
<tr>
<th>상태</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>pending</code></td>
<td>아직 결과가 없음 (대기 중)</td>
</tr>
<tr>
<td><code>fulfilled</code></td>
<td>성공적으로 완료됨</td>
</tr>
<tr>
<td><code>rejected</code></td>
<td>실패함</td>
</tr>
</tbody></table>
<p>한 번 fulfilled나 rejected가 되면 상태가 바뀌지 않는다. 결과가 확정된 것.</p>
<hr>
<h3 id="콜백-지옥을-promise로-해결하면">콜백 지옥을 Promise로 해결하면</h3>
<p><code>.then()</code>을 체이닝(chaining)할 수 있어서 깊이가 늘어나지 않는다.</p>
<pre><code class="language-js">login()
  .then(userId =&gt; getPosts(userId))
  .then(posts =&gt; getComments(posts[0].id))
  .then(comments =&gt; getUser(comments[0].authorId))
  .then(user =&gt; console.log(user))
  .catch(error =&gt; console.log(error)); // 에러는 여기서 한 번에</code></pre>
<p>훨씬 읽기 편해졌다. 에러 처리도 <code>.catch()</code> 하나로 끝난다.</p>
<p>근데 아직도 <code>.then().then().then()...</code>이 이어지면 눈에 잘 안 들어오는 느낌이 있다.</p>
<hr>
<h2 id="3-asyncawait">3. async/await</h2>
<p>ES2017에서 등장한 방법이다. Promise를 더 읽기 쉽게 써주는 문법이라고 보면 된다. Promise를 대체하는 게 아니라 <strong>Promise 위에 얹힌 문법적 설탕(Syntactic Sugar)</strong> 이다.</p>
<pre><code class="language-js">async function getUser() {
  const userId = await login();                    // 기다려
  const posts = await getPosts(userId);            // 기다려
  const comments = await getComments(posts[0].id); // 기다려
  const user = await getUser(comments[0].authorId);
  console.log(user);
}

getUser();</code></pre>
<p><code>await</code>는 &quot;이거 끝날 때까지 기다려&quot;라는 뜻이다. 동기 코드처럼 위에서 아래로 읽힌다.</p>
<p>단, <code>await</code>는 반드시 <code>async</code> 함수 안에서만 쓸 수 있다.</p>
<hr>
<h3 id="에러-처리는-trycatch로">에러 처리는 try/catch로</h3>
<pre><code class="language-js">async function getUser() {
  try {
    const userId = await login();
    const posts = await getPosts(userId);
    console.log(posts);
  } catch (error) {
    console.log(&#39;에러 발생:&#39;, error); // 어디서 에러가 나든 여기서 잡힘
  }
}</code></pre>
<p>일반 동기 코드에서 에러 잡는 것과 동일한 방식이라 익숙하게 쓸 수 있다.</p>
<hr>
<h2 id="세-가지-비교-정리">세 가지 비교 정리</h2>
<p>같은 작업을 세 가지 방식으로 쓰면 이렇다.</p>
<pre><code class="language-js">// 콜백
getData(function(result) {
  process(result, function(final) {
    console.log(final);
  });
});

// Promise
getData()
  .then(result =&gt; process(result))
  .then(final =&gt; console.log(final))
  .catch(err =&gt; console.log(err));

// async/await
async function run() {
  try {
    const result = await getData();
    const final = await process(result);
    console.log(final);
  } catch (err) {
    console.log(err);
  }
}</code></pre>
<table>
<thead>
<tr>
<th></th>
<th>콜백</th>
<th>Promise</th>
<th>async/await</th>
</tr>
</thead>
<tbody><tr>
<td>등장 시기</td>
<td>초창기</td>
<td>ES6 (2015)</td>
<td>ES2017 (2017)</td>
</tr>
<tr>
<td>가독성</td>
<td>중첩되면 최악</td>
<td>체이닝으로 개선</td>
<td>동기 코드처럼 읽힘</td>
</tr>
<tr>
<td>에러 처리</td>
<td>각 단계마다</td>
<td><code>.catch()</code> 하나</td>
<td><code>try/catch</code></td>
</tr>
<tr>
<td>현재 사용</td>
<td>레거시 코드</td>
<td>병렬 처리 등</td>
<td>주로 이걸 씀</td>
</tr>
</tbody></table>
<hr>
<h2 id="오늘-얻은-것">오늘 얻은 것</h2>
<p>콜백 → Promise → async/await 순서로 <strong>&quot;어떤 불편함을 해결하려다 나왔는지&quot;</strong> 를 따라가니까 왜 이렇게 생겼는지가 이해됐다. 외울 필요 없이 흐름이 자연스럽게 납득이 가는 느낌.</p>
<p>현업에서는 async/await을 가장 많이 쓰지만, Promise를 모르면 async/await도 제대로 쓸 수 없다. 결국 셋 다 알아야 한다.</p>
<p>다음 편은 <strong>이벤트 루프</strong> — 콜 스택, 태스크 큐, 마이크로태스크 큐가 실제로 어떻게 돌아가는지 정리해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[mcp 이용하여 노션 작성하기]]></title>
            <link>https://velog.io/@kim-na-hyeong/mcp-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EB%85%B8%EC%85%98-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kim-na-hyeong/mcp-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EB%85%B8%EC%85%98-%EC%9D%B4%EB%A0%A5%EC%84%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 13 Apr 2026 10:19:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>노션을 이용하여 &#39;이력서&#39; 폼과 &#39;경력기술서&#39; 폼을 만들다가 내가 만든 것과 클로드 AI를 이용하여 만드는 것이 어느 정도의 차이를 보이는지 궁금해서 겸사겸사 MCP를 사용해 보았다.</p>
</blockquote>
<h2 id="✔️mcp란-뭘까">✔️mcp란 뭘까?</h2>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/353b325a-e8be-4a58-9ad5-135443b9b438/image.png" alt=""></p>
<p>MCP는 _Model Context Protocol_의 약어로, AI 모델이 외부 데이터 소스나 도구와 표준화된 방식으로 연결되어 기능을 호출하고 결과를 받아오는 프로토콜을 뜻한다.</p>
<p>간단하게 이해하자면 위 사진과 같이 USB-C포트처럼 케이블 하나로 내가 사용하고자 하는 다양한 <strong><em>AI를 연결해주는 매개체</em></strong>라고 할 수 있다. </p>
<p>이를 통해 나만의 자동화 도구를 간단히 만들 수 있다는 게 가장 큰 장점이다. 예전이라면 간단한 프로젝트도 기획에 1시간, 레이아웃 잡는 데 1시간은 족히 걸렸겠지만, MCP를 활용하면 그 과정을 훨씬 빠르게 줄일 수 있다.</p>
<h3 id="api의-어려움">API의 어려움..</h3>
<p>MCP를 이해하기 위해서 API를 알아야 한다.</p>
<blockquote>
<p><strong>API</strong> (Application Programming Interface)
소프트웨어 간 통신을 위한 전통적인 방법으로 컴퓨터나 컴퓨터 프로그램 사이를 연결하는 것이다.
일종의 소프트웨어 인터페이스이며 다른 종류의 소프트웨어에 서비스를 제공한다.</p>
</blockquote>
<ol>
<li>다중 통합의 복잡성: 여러 서비스를 이용하려고 개별 API를 통합해야 한다.</li>
<li>일관성 부족: 각 API마다 다른 인증 방식, 문서화, 오류 처리 방식을 가지고 있다.</li>
<li>유지보수 부담: API가 변경될 때마다 개발자는 코드를 업데이트해야 한다.</li>
</ol>
<h3 id="api-방식과-다른-점">API 방식과 다른 점</h3>
<ol>
<li>단일 프로토콜: 하나의 MCP 통합으로 여러 도구와 서비스에 접근할 수 있다.</li>
<li>동적 발견: AI 모델이 필요한 도구를 스스로 찾아 상호작용이 간으하다.</li>
<li>양방향 통신: 실시간으로 데이터를 가져오거나 동작을 실행할 수 있다.</li>
</ol>
<h2 id="notion-mcp-연동하기">Notion MCP 연동하기</h2>
<h3 id="1-notion-api-발급-받기">1. notion api 발급 받기</h3>
<blockquote>
<p>노션 API 링크</p>
<p><a href="https://developers.notion.com/">https://developers.notion.com/</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/e50fa3e7-d4b3-4486-89c6-35bca005d808/image.png" alt=""></p>
<p>Notion Developers 사이트에서 View my integrations로 이동한다.</p>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/f8538ffe-a49d-4069-b0fb-21a604825ae1/image.png" alt=""></p>
<p>새 API 통합 만들기를 선택하고 정보를 입력하면 API가 생성된다.</p>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/c38dc063-b8df-4efb-9b62-ff4910dc33bc/image.png" alt=""></p>
<p>표시하기를 눌러 API를 복사한다. (<em>❗타인에게 절대 노출 금지❗</em>)</p>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/38bd7bd0-3f8e-4f0a-922a-32e6ef5ccfd9/image.png" alt=""></p>
<p>사용자가 API 통합을 승인할 때 해당 기능을 요청하기 때문에 필요한 기능을 체크한다.</p>
<h3 id="2-claude-mcp-서버-연결하기">2. Claude MCP 서버 연결하기</h3>
<blockquote>
<p>데스크탑 앱 설치 링크</p>
<p><a href="https://support.claude.com/ko/articles/10065433-&gt;claude-desktop-%EC%84%A4%EC%B9%98">https://support.claude.com/ko/articles/10065433-&gt;claude-desktop-%EC%84%A4%EC%B9%98</a></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/ba715a26-db6a-4161-892c-cf48246b9bff/image.png" alt=""></p>
<p>개인별 운영체제에 맞는 클로드 데스크탑 설치 후 로그인하고 설정&gt;개발자&gt;구성편집을 클릭한다.</p>
<p>claude_desktop_config.json 파일에서 아래 코드를 입력한 후 <strong>[프라이빗 API 통합 토큰]</strong>에 <strong>Notion Developers에서 받은 API</strong>를 넣어준다.</p>
<pre><code class="language-json">{
  &quot;mcpServers&quot;: {
      &quot;notionApi&quot;: {
          &quot;command&quot;: &quot;npx&quot;,
          &quot;args&quot;: [&quot;-y&quot;, &quot;@notionhq/notion-mcp-server&quot;],
          &quot;env&quot;: {
              &quot;OPENAPI_MCP_HEADERS&quot;: &quot;{\&quot;Authorization\&quot;: \&quot;Bearer [프라이빗 API 통합 토큰]\&quot;, \&quot;Notion-Version\&quot;: \&quot;2022-06-28\&quot; }&quot;
          }
      }
  }
}</code></pre>
<h3 id="3-claude-재시작-하기">3. Claude 재시작 하기</h3>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/043579d3-5470-4018-8ee5-f226befdfa58/image.png" alt=""></p>
<p>이렇게 notionApi가 활성화 되어있는 걸 볼 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/77ebbabb-d755-4f94-b5fc-674f28bb09cb/image.png" alt=""></p>
<p>확인까지 완료 됐으니 이력서 샘플을 만들어 봅시다.</p>
<h3 id="4-클로드로-생성하기">4. 클로드로 생성하기</h3>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/215c07ee-0699-4a08-b329-d7448d3ebfd4/image.png" alt=""></p>
<p>위와 같이 프롬프트를 입력했다.</p>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/871a0ab4-3365-456d-b6a4-b9059dd1e611/image.png" alt=""></p>
<p>간단한 한 줄짜리 프롬프트를 입력했는데, 결과물이 생각보다 괜찮았다.</p>
<p>앞으로 문서 작업에 클로드를 자주 활용하게 될 것 같다.
다만, 너무 의존하다 보면 스스로 생각하는 능력이 무뎌질 것 같아서, 스토리나 줄글은 직접 쓰고 클로드는 보조 역할로만 활용할 계획이다.
아무튼 새로운 기술을 써보는 과정이 꽤 재미있었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 모던 자바스크립트부터 다시 시작 - 3 - 비동기(Async)란 뭔가]]></title>
            <link>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-3-%EB%B9%84%EB%8F%99%EA%B8%B0Async%EB%9E%80-%EB%AD%94%EA%B0%80</link>
            <guid>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-3-%EB%B9%84%EB%8F%99%EA%B8%B0Async%EB%9E%80-%EB%AD%94%EA%B0%80</guid>
            <pubDate>Sat, 11 Apr 2026 13:58:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이번엔 코드보다 개념을 먼저 잡아본다.</p>
</blockquote>
<hr>
<h2 id="동기sync부터-이해하자">동기(Sync)부터 이해하자</h2>
<p>비동기를 알려면 동기를 먼저 알아야 한다.</p>
<p><strong>동기(Synchronous)</strong> 는 말 그대로 순서대로, 하나가 끝나야 다음이 시작되는 방식이다.</p>
<p>카페에 비유하면 이렇다.</p>
<pre><code>손님 A 주문 → 커피 제조 → 완성 → 손님 B 주문 → 커피 제조 → 완성</code></pre><p>A 커피가 다 나올 때까지 B는 주문도 못 한다. 줄이 하나씩 처리되는 것.</p>
<p>코드로 보면 이렇다.</p>
<pre><code class="language-js">console.log(&#39;A 주문&#39;);
console.log(&#39;A 커피 제조 중...&#39;); // A가 끝나야
console.log(&#39;A 완성&#39;);
console.log(&#39;B 주문&#39;); // B가 시작됨</code></pre>
<p>순서대로 위에서 아래로 실행된다. 직관적이고 예측하기 쉽다. 근데 문제가 있다.</p>
<hr>
<h2 id="동기-방식의-문제">동기 방식의 문제</h2>
<p>만약 커피 제조에 5분이 걸린다면?</p>
<pre><code class="language-js">const data = 서버에서_데이터_가져오기(); // 이게 3초 걸린다면...
console.log(data);
console.log(&#39;화면 보여주기&#39;); // 3초 동안 이 줄은 실행도 못 함</code></pre>
<p><strong>3초 동안 브라우저 전체가 멈춘다.</strong> 버튼도 안 눌리고, 스크롤도 안 되고, 아무것도 못 한다. 이걸 <strong>블로킹(Blocking)</strong> 이라고 한다.</p>
<p>서버에서 데이터 받아오는 건 네트워크 상황에 따라 1초가 될 수도, 10초가 될 수도 있다. 그동안 사용자가 멍하니 기다려야 한다면? 그냥 창 닫아버릴 것 같다.</p>
<hr>
<h2 id="비동기async란">비동기(Async)란</h2>
<p><strong>비동기(Asynchronous)</strong> 는 기다리지 않고 다음 작업을 먼저 처리하는 방식이다.</p>
<p>카페로 다시 돌아가면.</p>
<pre><code>손님 A 주문 → (제조 시작, 기다리는 동안)
손님 B 주문 → (제조 시작)
손님 C 주문 → (제조 시작)
A 커피 완성 → A에게 전달
B 커피 완성 → B에게 전달</code></pre><p>진동벨을 주고 &quot;다 되면 알려드릴게요&quot; 하는 방식이다. 기다리는 동안 다른 일을 처리할 수 있다!</p>
<hr>
<h2 id="js가-비동기를-처리하는-방식">JS가 비동기를 처리하는 방식</h2>
<p>JS는 원래 <strong>싱글 스레드</strong>, 즉 한 번에 하나의 일밖에 못 한다. 그럼 어떻게 비동기 처리를 하는 걸까?</p>
<p>브라우저가 도와주는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/kim-na-hyeong/post/ebd40315-2f28-488d-b45d-d8b1776eda2f/image.png" alt=""></p>
<p>JS가 &quot;이거 시간 걸리는 작업이다&quot; 싶으면 브라우저 Web API한테 넘긴다. 그동안 JS는 다음 코드를 계속 실행한다. 작업이 끝나면 브라우저가 JS한테 &quot;다 됐어!&quot; 하고 알려주는 구조다.</p>
<pre><code class="language-js">console.log(&#39;시작&#39;);

setTimeout(function() {
  console.log(&#39;3초 후 실행&#39;); // 브라우저한테 맡겨둠
}, 3000);

console.log(&#39;끝&#39;); // setTimeout 기다리지 않고 바로 실행됨!

// 출력:
// 시작
// 끝
// (3초 후) 3초 후 실행</code></pre>
<p><code>setTimeout</code>을 브라우저한테 넘기고, JS는 바로 다음 줄로 넘어간다. 3초가 지나면 브라우저가 콜백 함수를 다시 JS한테 돌려준다.</p>
<hr>
<h2 id="비동기가-필요한-상황들">비동기가 필요한 상황들</h2>
<p>실제로 비동기 처리가 필요한 상황은 이렇다.</p>
<ul>
<li><strong>API 요청</strong> — 서버에서 데이터 받아올 때 (얼마나 걸릴지 모름)</li>
<li><strong>타이머</strong> — <code>setTimeout</code>, <code>setInterval</code></li>
<li><strong>파일 읽기/쓰기</strong> — Node.js 환경에서</li>
<li><strong>이미지 로딩</strong> — 이미지가 다 불러와지면 실행</li>
<li><strong>사용자 이벤트</strong> — 버튼 클릭, 키보드 입력 등</li>
</ul>
<p>전부 &quot;언제 끝날지 모르는&quot; 작업들이다. 이걸 동기로 처리하면 그때마다 화면이 멈춰버리니까 비동기로 처리하는 것.</p>
<hr>
<h2 id="근데-순서가-보장이-안-되면-문제-아닌가">근데 순서가 보장이 안 되면 문제 아닌가?</h2>
<p>맞다. 비동기의 가장 큰 난관이 여기에 있다.</p>
<pre><code class="language-js">let userData;

fetch(&#39;/api/user&#39;) // 서버에 요청 (비동기)
  .then(res =&gt; res.json())
  .then(data =&gt; { userData = data; });

console.log(userData); // undefined! 아직 데이터가 안 왔음</code></pre>
<p>비동기라서 데이터를 받기도 전에 <code>console.log</code>가 먼저 실행돼버린다.</p>
<p>그래서 &quot;데이터 받은 다음에 이걸 실행해줘&quot;라고 명시해야 하는데, 그게 바로 <strong>콜백, Promise, async/await</strong> 이다.</p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th></th>
<th>동기</th>
<th>비동기</th>
</tr>
</thead>
<tbody><tr>
<td>실행 방식</td>
<td>순서대로, 하나씩</td>
<td>기다리지 않고 다음 실행</td>
</tr>
<tr>
<td>블로킹</td>
<td>있음</td>
<td>없음</td>
</tr>
<tr>
<td>순서 보장</td>
<td>됨</td>
<td>별도 처리 필요</td>
</tr>
<tr>
<td>사용 상황</td>
<td>일반 코드 흐름</td>
<td>API, 타이머, 이벤트</td>
</tr>
</tbody></table>
<hr>
<h2 id="오늘-얻은-것">오늘 얻은 것</h2>
<p>비동기 자체가 어려운 게 아니라 <strong>&quot;기다리지 않는다&quot;</strong> 는 개념이 처음엔 낯선 것 같음. 진동벨 받고 자리에 앉는 것처럼, JS도 작업을 맡겨두고 다른 일을 먼저 한다고 생각하면 됐다.</p>
<p>다음 편은 <strong>콜백 지옥이 왜 생기고, Promise가 어떻게 해결했고, async/await이 뭔지</strong> 까지 이어서 정리해볼 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 모던 자바스크립트부터 다시 시작 - 2 - 스코프, 실행 컨텍스트, 클로저]]></title>
            <link>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-2-%EC%8A%A4%EC%BD%94%ED%94%84-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%ED%81%B4%EB%A1%9C%EC%A0%80</link>
            <guid>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-2-%EC%8A%A4%EC%BD%94%ED%94%84-%EC%8B%A4%ED%96%89-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8-%ED%81%B4%EB%A1%9C%EC%A0%80</guid>
            <pubDate>Thu, 09 Apr 2026 18:53:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>1편에서 변수 선언이랑 함수를 정리했는데, 사실 그것만으로는 반쪽짜리다.<br><code>var</code>가 왜 블록을 무시하는지, 클로저에 대해 알아보자.</p>
</blockquote>
<hr>
<h2 id="📌-실행-컨텍스트-execution-context">📌 실행 컨텍스트 (Execution Context)</h2>
<p>JS 엔진이 코드를 실행할 때, <strong>코드가 동작하는 환경 정보를 담은 객체</strong>를 만든다. 이게 실행 컨텍스트다.</p>
<p>코드가 실행되면 <strong>콜 스택(Call Stack)</strong> 위에 실행 컨텍스트가 쌓이고, 함수 실행이 끝나면 제거된다.</p>
<pre><code>전역 실행 컨텍스트 (기본으로 깔림)
└── greet() 호출 → greet 실행 컨텍스트 생성 &amp; 스택에 추가
    └── greet 종료 → 스택에서 제거</code></pre><p>실행 컨텍스트 안에는 이런 정보들이 들어있다.</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Variable Environment</td>
<td>변수, 함수 선언 저장</td>
</tr>
<tr>
<td>Lexical Environment</td>
<td>스코프 체인 정보</td>
</tr>
<tr>
<td>this 바인딩</td>
<td>현재 컨텍스트의 this가 뭔지</td>
</tr>
</tbody></table>
<p>이걸 모르면 호이스팅이 왜 그렇게 동작하는지, <code>this</code>가 왜 맨날 꼬이는지 설명이 안 된다.</p>
<hr>
<h2 id="📌-스코프-scope">📌 스코프 (Scope)</h2>
<p><strong>변수에 접근할 수 있는 유효 범위</strong>다. JS에는 크게 세 가지가 있음.</p>
<h3 id="전역-스코프">전역 스코프</h3>
<p>코드 어디서든 접근 가능하다. 근데 전역 변수를 남발하면 어디서 건드렸는지 추적이 힘들어져서 최대한 안 쓰는 게 좋다.</p>
<h3 id="함수-스코프">함수 스코프</h3>
<p><code>var</code>가 여기에 해당된다. 블록(<code>{}</code>)이 아닌 <strong>함수 단위</strong>로 스코프가 만들어진다.</p>
<pre><code class="language-js">function test() {
  var x = 10;
  if (true) {
    var x = 20; // 같은 함수 스코프라 덮어씌워짐!
  }
  console.log(x); // 20 — 의도한 게 아닌데도 바뀐다.
}</code></pre>
<h3 id="블록-스코프">블록 스코프</h3>
<p><code>let</code>, <code>const</code>가 여기에 해당된다. <code>{}</code>로 감싸인 블록 단위로 스코프가 만들어진다.</p>
<pre><code class="language-js">function test() {
  let x = 10;
  if (true) {
    let x = 20; // 이 블록 안에서만 유효함
  }
  console.log(x); // 10 — 예측 가능하다!
}</code></pre>
<p>1편에서 <code>var</code> 쓰지 말라고 했던 이유가 바로 이거다. 블록을 무시하고 함수 전체를 휘젓기 때문임.</p>
<hr>
<h2 id="📌-스코프-체인-scope-chain">📌 스코프 체인 (Scope Chain)</h2>
<p>변수를 찾을 때 JS 엔진은 현재 스코프에서 먼저 찾고, 없으면 <strong>바깥 스코프</strong>로 올라가면서 찾는다. 이 탐색 과정이 체인처럼 연결되어 있다는 뜻!</p>
<pre><code class="language-js">const name = &#39;전역&#39;;

function outer() {
  const city = &#39;서울&#39;;

  function inner() {
    const age = 26;
    console.log(name); // &#39;전역&#39; — 최상위 스코프에서 찾아옴
    console.log(city); // &#39;서울&#39; — 부모 스코프에서 찾아옴
    console.log(age);  // 26 — 현재 스코프에서 찾음
  }

  inner();
}</code></pre>
<p>전역까지 올라갔는데도 없으면 그때 <code>ReferenceError</code>가 발생한다.</p>
<h3 id="렉시컬-스코프-lexical-scope">렉시컬 스코프 (Lexical Scope)</h3>
<p>JS는 <strong>어디서 호출됐는지가 아니라, 어디서 정의됐는지</strong>를 기준으로 스코프가 결정된다.</p>
<pre><code class="language-js">const x = &#39;전역&#39;;

function printX() {
  console.log(x); // 정의된 위치 기준으로 스코프 결정
}

function test() {
  const x = &#39;지역&#39;;
  printX(); // &#39;전역&#39; — test 안에서 호출됐지만, printX는 전역에서 정의됨
}

test();</code></pre>
<p><code>printX</code>가 <code>test</code> 안에서 호출됐어도, 정의된 건 전역이라 전역의 <code>x</code>를 가져온다. 이 개념이 바로 다음에 나올 클로저의 핵심 원리다.</p>
<hr>
<h2 id="📌-클로저-closure">📌 클로저 (Closure)</h2>
<blockquote>
<p>면접에서 가장 많이 나오는 개념 중 하나다. 꼭 기억하자!!!!</p>
</blockquote>
<p><strong>함수가 자신이 선언될 때의 스코프를 기억하고, 그 스코프 밖에서 호출되어도 해당 스코프에 접근할 수 있는 현상</strong>이다.</p>
<p>말이 어렵게 느껴지는데, 코드로 보면 이해가 된다.</p>
<pre><code class="language-js">function makeCounter() {
  let count = 0; // makeCounter의 지역 변수

  return function() {
    count++; // 바깥 함수의 변수에 접근!
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3</code></pre>
<p><code>makeCounter()</code>는 이미 실행이 끝났는데, 반환된 내부 함수는 여전히 <code>count</code>에 접근하고 있다. 실행 컨텍스트는 사라졌는데 변수가 살아있는 것처럼 보임!</p>
<p>이게 클로저다. 내부 함수가 외부 함수의 스코프를 <strong>기억</strong>하고 있기 때문이다.</p>
<h3 id="클로저는-어디에-쓰일까✨">클로저는 어디에 쓰일까✨</h3>
<p><strong>1. 데이터 은닉</strong></p>
<p>외부에서 직접 변수를 건드리지 못하게 막을 수 있다.</p>
<pre><code class="language-js">function makeWallet(initialAmount) {
  let balance = initialAmount; // 외부에서 직접 접근 불가

  return {
    deposit(amount) { balance += amount; },
    withdraw(amount) { balance -= amount; },
    getBalance() { return balance; }
  };
}

const wallet = makeWallet(1000);
wallet.deposit(500);
console.log(wallet.getBalance()); // 1500
console.log(wallet.balance); // undefined 직접 접근 불가!</code></pre>
<p><strong>2. 함수 팩토리</strong></p>
<p>설정값을 기억해서 같은 로직을 다르게 활용할 수 있다.</p>
<pre><code class="language-js">function makeMultiplier(multiplier) {
  return (num) =&gt; num * multiplier; // multiplier를 기억함
}

const double = makeMultiplier(2);
const triple = makeMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15</code></pre>
<p><strong>3. React의 useState도 클로저다!</strong></p>
<pre><code class="language-js">// 개념적으로 useState는 이런 구조다
function useState(initialValue) {
  let state = initialValue;

  function setState(newValue) {
    state = newValue;
    // 리렌더링 트리거...
  }

  return [state, setState];
}</code></pre>
<p><code>setState</code>가 <code>state</code>를 기억하고 있는 것 — 이것도 클로저의 활용이다.</p>
<hr>
<h2 id="✨-면접-qa">✨ 면접 Q&amp;A</h2>
<h3 id="q1-스코프scope란-무엇인가요">Q1. 스코프(Scope)란 무엇인가요?</h3>
<blockquote>
<p>변수에 접근할 수 있는 유효 범위입니다. JS에는 전역 스코프, 함수 스코프, 블록 스코프가 있습니다. <code>var</code>는 함수 스코프를 가져 블록을 무시하고, <code>let</code>과 <code>const</code>는 블록 스코프를 가져 <code>{}</code> 안에서만 유효합니다. 또한 JS는 렉시컬 스코프 방식을 따르기 때문에, 함수가 어디서 호출됐는지가 아닌 <strong>어디서 정의됐는지</strong>를 기준으로 스코프가 결정됩니다.</p>
</blockquote>
<h3 id="q2-클로저closure란-무엇이며-어떤-상황에서-활용하나요">Q2. 클로저(Closure)란 무엇이며, 어떤 상황에서 활용하나요?</h3>
<blockquote>
<p>클로저는 함수가 자신이 선언된 외부 스코프의 변수를 기억하고, 해당 스코프 밖에서 호출되어도 그 변수에 접근할 수 있는 현상입니다.</p>
<p>대표적인 활용 사례로는 <strong>데이터 은닉</strong>이 있습니다. 외부에서 직접 변수를 수정하지 못하도록 막고, 함수를 통해서만 제어할 수 있게 설계할 수 있습니다. 또한 React의 <code>useState</code>도 클로저 원리를 활용한 예시입니다. <code>setState</code>가 컴포넌트의 상태 변수를 기억하고 접근하는 것이 클로저의 동작 방식과 동일합니다.</p>
</blockquote>
<hr>
<h2 id="개인의견">개인의견</h2>
<p>스코프 → 스코프 체인 → 렉시컬 스코프 → 클로저, 이 흐름이 사실 전부 연결되어 있다. 하나를 알면 다음이 자연스럽게 이해되는 구조로 공부하니까 머리에 잘 들어왔다. 코드의 중복과 에러를 항시 주의하자. 제발!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 모던 자바스크립트부터 다시 시작 - 1 - js란, 변수, 함수]]></title>
            <link>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-1-js%EB%9E%80-%EB%B3%80%EC%88%98-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@kim-na-hyeong/JS-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%B6%80%ED%84%B0-%EB%8B%A4%EC%8B%9C-%EC%8B%9C%EC%9E%91-1-js%EB%9E%80-%EB%B3%80%EC%88%98-%ED%95%A8%EC%88%98</guid>
            <pubDate>Tue, 07 Apr 2026 10:10:00 GMT</pubDate>
            <description><![CDATA[<p>오늘의 정리</p>
<p>기초를 다 까먹었다. 이러다 면접에서 <del>죽 쑬까 봐</del> 다시 복기한다..</p>
<p><del>_추후 계속 수정될 수 있다...
_</del></p>
<h2 id="📌-javascript-란">📌 &quot;JavaScript&quot; 란?</h2>
<p>_자바스크립트_는 ‘웹페이지에 생동감을 불어넣기 위해’ 만들어진 프로그래밍 언어이다.
_자바스크립트_는 브라우저의 &#39;자바스크립트 가상 머신&#39;이라고 불리는 _자바스크립트 엔진_을 통하여 코드를 위에서 아래로 읽고<strong>(파싱)</strong> 기계어<strong>(컴파일)</strong>로 해석한다.
이를 _인터프리터 언어_라고 한다.</p>
<p>그러나 어플리케이션의 규모가 커짐에 따라 성능의 한계가 있었고, 극복하기 위하여 크롬의 V8같은 모던 자바스크립트 엔진은 인터프리터와 컴파일의 장점을 결합한 JIT(Just in time) 컴파일 방식을 사용하고 있다.</p>
<h2 id="📌-변수-선언-var-let-const">📌 변수 선언: var, let, const</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th align="left">스코프</th>
<th align="center">재선언</th>
<th align="center">재할당</th>
<th>호이스팅 시</th>
</tr>
</thead>
<tbody><tr>
<td>var</td>
<td align="left">함수</td>
<td align="center">가능</td>
<td align="center">가능</td>
<td>undefined로 초기화됨</td>
</tr>
<tr>
<td>let</td>
<td align="left">블록</td>
<td align="center">불가능</td>
<td align="center">가능</td>
<td>초기화되지 않음 (TDZ 발생)</td>
</tr>
<tr>
<td>const</td>
<td align="left">블록</td>
<td align="center">불가능</td>
<td align="center">불가능</td>
<td>초기화되지 않음 (TDZ 발생)</td>
</tr>
</tbody></table>
<p>_var_는 블록({})을 무시하고 함수 단위로만 스코프가 형성되어, 의도치 않게 변수 값이 바뀌는 버그가 생기기 쉽다. 또한 선언하기도 전에 변수를 사용할 수 있는 현상 때문에 코드의 흐름을 방해한다.</p>
<p> <strong>따라서 현대 개발에서는 const를 기본으로 쓰고, 재할당이 필요한 경우에만 let을 사용하는 것이 정석!</strong></p>
<h2 id="📌-일반함수와-화살표-함수">📌 일반함수와 화살표 함수</h2>
<h3 id="1-일반-함수-regular-function">1) 일반 함수 (Regular Function)</h3>
<blockquote>
<p>function 키워드를 사용하며, 함수가 호출되는 방식에 따라 this가 동적으로 변한다.</p>
</blockquote>
<pre><code class="language-JavaScript">function add(a, b) {
  return a + b;
}</code></pre>
<h3 id="2-화살표-함수-arrow-function">2) 화살표 함수 (Arrow Function)</h3>
<blockquote>
<p>문법을 사용하며, this를 스스로 가지지 않고 <strong>자신이 선언된 주변 환경(Lexical Scope)</strong>의 this를 그대로 사용한다.</p>
</blockquote>
<pre><code class="language-JavaScript">const add = (a, b) =&gt; a + b;</code></pre>
<p>💡 this 바인딩의 차이
가장 큰 차이는 역시 this! 일반 함수는 호출한 대상에 따라 this가 바뀌지만, 화살표 함수는 언제나 상위 스코프의 this를 가르킨다. 이 특성 덕분에 React 컴포넌트나 콜백 함수 내에서 this가 꼬이는 문제를 아주 쉽게 해결할 수 있다.</p>
<h2 id="✨-모던-자바스크립트---qa">✨ 모던 자바스크립트 - Q&amp;A</h2>
<h3 id="q1-var-let-const의-차이점과-호이스팅hoisting에-대해-설명해주세요">Q1. var, let, const의 차이점과 호이스팅(Hoisting)에 대해 설명해주세요.</h3>
<p>🗣️ <strong>답변</strong>: &gt; 가장 큰 차이점은 <strong>스코프(Scope)</strong>와 호이스팅 동작 방식입니다.</p>
<p><strong>var (ES5)</strong>: 함수 레벨 스코프를 가집니다. 변수를 재선언하고 재할당하는 것이 모두 가능하여 예기치 못한 에러를 발생시킬 위험이 있습니다.</p>
<p><strong>let &amp; const (ES6)</strong>: 블록 레벨 스코프({})를 가집니다. let은 재할당이 가능하지만, _const_는 상수이므로 재선언과 재할당이 모두 불가능하다.</p>
<p><strong>호이스팅(Hoisting)</strong> 관점에서의 차이:
자바스크립트 엔진은 코드를 실행하기 전, 실행 컨텍스트를 생성할 때 모든 선언문을 메모리에 먼저 등록(호이스팅)한다.</p>
<p>이때 _var_는 선언과 동시에 _undefined_로 초기화되어 에러 없이 접근 가능하지만, _let_과 _const_는 메모리에 등록은 되지만 초기화되지 않아 </p>
<p>선언문 이전에 접근하면 <strong>TDZ(Temporal Dead Zone, 일시적 사각지대)</strong>에 빠져 ReferenceError를 발생시킵니다. 이를 통해 코드의 안정성을 높일 수 있습니다.</p>
<pre><code class="language-js">// 1. var의 호이스팅 (에러가 나지 않아 버그의 원인이 됨)
console.log(name); // 결과: undefined
var name = &quot;a&quot;;

// 2. let의 호이스팅 (TDZ에 빠져 에러 발생 -&gt; 안전함!)
console.log(age); // 결과: ReferenceError: Cannot access &#39;age&#39; before initialization
let age = 28;</code></pre>
<h3 id="q2-화살표-함수arrow-function와-일반-함수의-가장-큰-차이는-무엇인가요">Q2. 화살표 함수(Arrow Function)와 일반 함수의 가장 큰 차이는 무엇인가요?</h3>
<p>🗣️ <strong>답변</strong>: &gt; 가장 핵심적인 차이는 this가 바인딩되는 방식입니다.</p>
<ul>
<li><p><strong>일반 함수</strong>: 함수가 &#39;어떻게 호출되었는지&#39;에 따라 this가 동적으로 결정됩니다. (예: 객체의 메서드로 호출되면 그 객체를 가리킴)</p>
</li>
<li><p><strong>화살표 함수</strong>: 호출 방식과 무관하게, <strong>자신이 선언된 외부 환경(Lexical Scope)의 this</strong>를 그대로 물려받아 사용합니다.</p>
</li>
</ul>
<p>이 외에도 화살표 함수는 arguments 객체를 바인딩하지 않으며, new 키워드를 사용해 생성자 함수로 사용할 수 없다는 특징이 있습니다. 따라서 React 등에서 콜백 함수를 작성할 때 this 꼬임 문제를 해결하기 위해 주로 사용됩니다.</p>
<pre><code class="language-js">const user = {
  name: &quot;gildong&quot;,

  // 1. 일반 함수에서의 this
  normalFunc: function() {
    setTimeout(function() {
      console.log(this.name); // 결과: undefined (this가 전역 객체인 window를 가리킴)
    }, 1000);
  },

  // 2. 화살표 함수에서의 this
  arrowFunc: function() {
    setTimeout(() =&gt; {
      console.log(this.name); // 결과: &quot;const user = {
  name: &quot;길동&quot;,

  // 1. 일반 함수에서의 this
  normalFunc: function() {
    setTimeout(function() {
      console.log(this.name); // 결과: undefined (this가 전역 객체인 window를 가리킴)
    }, 1000);
  },

  // 2. 화살표 함수에서의 this
  arrowFunc: function() {
    setTimeout(() =&gt; {
      console.log(this.name); // 결과: &quot;길동&quot; (자신을 감싼 user 객체의 this를 그대로 물려받음!)
    }, 1000);
  }
};

user.normalFunc();
user.arrowFunc();&quot; (자신을 감싼 user 객체의 this를 그대로 물려받음!)
    }, 1000);
  }
};

user.normalFunc();
user.arrowFunc();</code></pre>
]]></description>
        </item>
    </channel>
</rss>