<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dangkke.log</title>
        <link>https://velog.io/</link>
        <description>프론트는 순항중 ¿¿</description>
        <lastBuildDate>Wed, 06 May 2026 05:45:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dangkke.log</title>
            <url>https://velog.velcdn.com/images/wow_da65/profile/3cf5682c-be37-4353-98b5-53d7d7624667/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dangkke.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/wow_da65" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[label과 input 연결 방식 비교 (암묵적 vs 명시적)]]></title>
            <link>https://velog.io/@wow_da65/label%EA%B3%BC-input-%EC%97%B0%EA%B2%B0-%EB%B0%A9%EC%8B%9D-%EB%B9%84%EA%B5%90-%EC%95%94%EB%AC%B5%EC%A0%81-vs-%EB%AA%85%EC%8B%9C%EC%A0%81</link>
            <guid>https://velog.io/@wow_da65/label%EA%B3%BC-input-%EC%97%B0%EA%B2%B0-%EB%B0%A9%EC%8B%9D-%EB%B9%84%EA%B5%90-%EC%95%94%EB%AC%B5%EC%A0%81-vs-%EB%AA%85%EC%8B%9C%EC%A0%81</guid>
            <pubDate>Wed, 06 May 2026 05:45:14 GMT</pubDate>
            <description><![CDATA[<p>label과 input을 연결하는 방식엔 두 가지가 있다.
둘 다 알고 있었지만 정확히 어떤 차이가 있는지 몰라서 이번에 제대로 정리해보았다. 
나는 주로 명시적 연결로만 사용해왔었는데, 상황에 따라 골라쓰면 될거 같다.</p>
<h2 id="두-가지-방식">두 가지 방식</h2>
<h3 id="암묵적-연결-implicit-association">암묵적 연결 (Implicit Association)</h3>
<p>label 태그가 input 을 감싸는 방식. id/for 없이 자동 연결.</p>
<pre><code class="language-html">&lt;label&gt;
    &lt;input type=&quot;checkbox&quot;&gt; 가방
&lt;/label&gt;</code></pre>
<h3 id="명시적-연결-explicit-association">명시적 연결 (Explicit Association)</h3>
<p>input 에 id 부여, label 의 for 속성으로 연결.</p>
<pre><code class="language-html">&lt;input type=&quot;checkbox&quot; id=&quot;cate_bag&quot;&gt;
&lt;label for=&quot;cate_bag&quot;&gt;가방&lt;/label&gt;</code></pre>
<hr>
<h2 id="비교">비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>암묵적 (감싸기)</th>
<th>명시적 (id/for)</th>
</tr>
</thead>
<tbody><tr>
<td>HTML5 표준</td>
<td>정식</td>
<td>정식</td>
</tr>
<tr>
<td>마크업 길이</td>
<td>짧음</td>
<td>길음 (id + for 필요)</td>
</tr>
<tr>
<td>id 관리</td>
<td>불필요</td>
<td>유일성 보장 필요</td>
</tr>
<tr>
<td>동적 생성</td>
<td>유리 (PHP/JS 루프에서 id 충돌 없음)</td>
<td>불편 (id 를 매번 유니크하게 생성)</td>
</tr>
<tr>
<td>레이아웃 자유도</td>
<td>낮음 (input 과 텍스트가 붙어있어야 함)</td>
<td>높음 (떨어뜨려 배치 가능)</td>
</tr>
<tr>
<td>스크린리더</td>
<td>대부분 정상 인식</td>
<td>구형 보조기기에서 더 확실</td>
</tr>
<tr>
<td>커스텀 체크박스</td>
<td>가능 (input + span 구조)</td>
<td>가능 (input + label 구조)</td>
</tr>
</tbody></table>
<hr>
<h2 id="각각-언제-쓰나">각각 언제 쓰나</h2>
<h3 id="암묵적-방식이-적합한-경우">암묵적 방식이 적합한 경우</h3>
<ul>
<li>체크박스/라디오 + 텍스트가 나란히 붙어있는 기본 폼</li>
<li>PHP/JS 로 동적으로 여러 개 생성하는 경우 (id 관리 부담 제거)</li>
<li>별도 커스텀 디자인 없이 기본 브라우저 체크박스 사용</li>
</ul>
<pre><code class="language-html">&lt;!-- PHP 동적 생성 예시 --&gt;
&lt;?php while($row = fetch()) { ?&gt;
    &lt;label&gt;
        &lt;input type=&quot;checkbox&quot; name=&quot;item_&lt;?=$row[&#39;id&#39;]?&gt;&quot;&gt; 
        &lt;?=$row[&#39;name&#39;]?&gt;
    &lt;/label&gt;
&lt;?php } ?&gt;</code></pre>
<h3 id="명시적-방식이-적합한-경우">명시적 방식이 적합한 경우</h3>
<ul>
<li>input 과 label 이 DOM 상 떨어져 있어야 하는 레이아웃</li>
<li>접근성 심사 대상 공공기관/정부 사이트 (WCAG 엄격 준수)</li>
</ul>
<pre><code class="language-html">&lt;input type=&quot;checkbox&quot; id=&quot;agree&quot;&gt;
&lt;label for=&quot;agree&quot;&gt;동의합니다&lt;/label&gt;</code></pre>
<hr>
<h2 id="커스텀-체크박스">커스텀 체크박스</h2>
<p>두 방식 모두 커스텀 체크박스 구현 가능. 원리는 동일 (CSS <code>+</code> 인접 형제 선택자).</p>
<h3 id="명시적-방식">명시적 방식</h3>
<pre><code class="language-html">&lt;div class=&quot;custom-check&quot;&gt;
    &lt;input type=&quot;checkbox&quot; id=&quot;agree&quot;&gt;
    &lt;label for=&quot;agree&quot;&gt;동의합니다&lt;/label&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.custom-check input { display: none; }
.custom-check label::before {
    content: &#39;&#39;;
    display: inline-block;
    width: 18px; height: 18px;
    border: 2px solid #ccc;
    border-radius: 3px;
    vertical-align: middle;
    margin-right: 6px;
}
.custom-check input:checked + label::before {
    background: #169dab;
    border-color: #169dab;
}</code></pre>
<h3 id="암묵적-방식">암묵적 방식</h3>
<pre><code class="language-html">&lt;label class=&quot;custom-check&quot;&gt;
    &lt;input type=&quot;checkbox&quot;&gt;
    &lt;span&gt;동의합니다&lt;/span&gt;
&lt;/label&gt;</code></pre>
<pre><code class="language-css">.custom-check input { display: none; }
.custom-check span::before {
    content: &#39;&#39;;
    display: inline-block;
    width: 18px; height: 18px;
    border: 2px solid #ccc;
    border-radius: 3px;
    vertical-align: middle;
    margin-right: 6px;
}
.custom-check input:checked + span::before {
    background: #169dab;
    border-color: #169dab;
}</code></pre>
<h3 id="차이점">차이점</h3>
<table>
<thead>
<tr>
<th></th>
<th>명시적</th>
<th>암묵적</th>
</tr>
</thead>
<tbody><tr>
<td>CSS 셀렉터</td>
<td><code>input:checked + label::before</code></td>
<td><code>input:checked + span::before</code></td>
</tr>
<tr>
<td>구조</td>
<td>input 과 label 이 형제</td>
<td>label 안에서 input 과 span 이 형제</td>
</tr>
<tr>
<td>원리</td>
<td>동일 (<code>+</code> 인접 형제 선택자)</td>
<td>동일</td>
</tr>
</tbody></table>
<h3 id="핵심-제약">핵심 제약</h3>
<p>CSS <code>+</code> 선택자는 <strong>뒤쪽 형제만</strong> 선택 가능. input 이 반드시 텍스트(span/label) 보다 <strong>앞에</strong> 와야 한다.</p>
<pre><code class="language-html">&lt;!-- 올바름: input 먼저 --&gt;
&lt;label&gt;&lt;input type=&quot;checkbox&quot;&gt;&lt;span&gt;텍스트&lt;/span&gt;&lt;/label&gt;

&lt;!-- 안 됨: span 먼저 → CSS 제어 불가 --&gt;
&lt;label&gt;&lt;span&gt;텍스트&lt;/span&gt;&lt;input type=&quot;checkbox&quot;&gt;&lt;/label&gt;</code></pre>
<hr>
<h2 id="실무-트렌드">실무 트렌드</h2>
<ul>
<li>대부분 <strong>암묵적 방식</strong> 사용 (기본 폼)</li>
<li>커스텀 UI 컴포넌트 라이브러리 (MUI, Ant Design 등) 는 내부적으로 <strong>명시적 방식</strong> 사용</li>
<li>React/Vue 컴포넌트에서는 자동 id 생성 유틸(useId 등) 로 명시적 방식 편하게 처리</li>
</ul>
<hr>
<h2 id="주의사항">주의사항</h2>
<h3 id="암묵적-방식에서-흔한-실수">암묵적 방식에서 흔한 실수</h3>
<pre><code class="language-html">&lt;!-- 잘못: label 안에 여러 input --&gt;
&lt;label&gt;
    &lt;input type=&quot;checkbox&quot;&gt; 옵션A
    &lt;input type=&quot;checkbox&quot;&gt; 옵션B
&lt;/label&gt;

&lt;!-- 올바름: input 하나당 label 하나 --&gt;
&lt;label&gt;&lt;input type=&quot;checkbox&quot;&gt; 옵션A&lt;/label&gt;
&lt;label&gt;&lt;input type=&quot;checkbox&quot;&gt; 옵션B&lt;/label&gt;</code></pre>
<h3 id="명시적-방식에서-흔한-실수">명시적 방식에서 흔한 실수</h3>
<pre><code class="language-html">&lt;!-- 잘못: id 중복 --&gt;
&lt;input type=&quot;checkbox&quot; id=&quot;item&quot;&gt; &lt;label for=&quot;item&quot;&gt;A&lt;/label&gt;
&lt;input type=&quot;checkbox&quot; id=&quot;item&quot;&gt; &lt;label for=&quot;item&quot;&gt;B&lt;/label&gt;

&lt;!-- 올바름: id 유일 --&gt;
&lt;input type=&quot;checkbox&quot; id=&quot;item_1&quot;&gt; &lt;label for=&quot;item_1&quot;&gt;A&lt;/label&gt;
&lt;input type=&quot;checkbox&quot; id=&quot;item_2&quot;&gt; &lt;label for=&quot;item_2&quot;&gt;B&lt;/label&gt;</code></pre>
<hr>
<h2 id="결론">결론</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천</th>
</tr>
</thead>
<tbody><tr>
<td>기본 폼, 동적 생성, 빠른 작업</td>
<td>암묵적 (감싸기)</td>
</tr>
<tr>
<td>커스텀 디자인, 접근성 엄격, 복잡 레이아웃</td>
<td>명시적 (id/for)</td>
</tr>
</tbody></table>
<p>둘 다 표준이고 둘 다 접근성 충족. 커스텀 체크박스도 둘 다 가능. 프로젝트 상황에 맞게 선택하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Stretched Link 패턴 — 카드/영역 전체 클릭 가능하게 만들기]]></title>
            <link>https://velog.io/@wow_da65/Stretched-Link-%ED%8C%A8%ED%84%B4-%EC%B9%B4%EB%93%9C%EC%98%81%EC%97%AD-%EC%A0%84%EC%B2%B4-%ED%81%B4%EB%A6%AD-%EA%B0%80%EB%8A%A5%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@wow_da65/Stretched-Link-%ED%8C%A8%ED%84%B4-%EC%B9%B4%EB%93%9C%EC%98%81%EC%97%AD-%EC%A0%84%EC%B2%B4-%ED%81%B4%EB%A6%AD-%EA%B0%80%EB%8A%A5%ED%95%98%EA%B2%8C-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 28 Apr 2026 23:47:42 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>옛날엔 a 안에 button 넣고 JS 로 stopPropagation 했는데, HTML 표준 위반이라는 걸 알게 됐고 모던 표준 (Stretched Link) 으로 정리해 둠.</p>
</blockquote>
<p>카드나 영역 전체를 클릭 가능하게 만들 때, a/button 으로 컨텐츠를 감싸지 않고 <strong>별도 요소를 만들어 absolute 로 영역 전체 덮는</strong> 패턴.</p>
<h2 id="1-왜-필요한가">1. 왜 필요한가</h2>
<p>리스트 카드, 메뉴 항목, 알림 같은 UI 에서 <strong>영역 전체가 클릭 가능해야</strong> 사용자 경험이 좋음.</p>
<ul>
<li>모바일에서 좁은 텍스트만 탭 영역이면 누르기 어려움</li>
<li>카드 어디 눌러도 동일 동작이 직관적</li>
</ul>
<h2 id="2-옛날-방식---카드-전체를-a-로-감싸기">2. 옛날 방식 - 카드 전체를 a 로 감싸기</h2>
<pre><code class="language-html">&lt;a href=&quot;/product/1&quot;&gt;
    &lt;article class=&quot;card&quot;&gt;
        &lt;img&gt;
        &lt;h3&gt;상품명&lt;/h3&gt;
        &lt;p&gt;가격&lt;/p&gt;
        &lt;button&gt;찜&lt;/button&gt;
    &lt;/article&gt;
&lt;/a&gt;</code></pre>
<h3 id="문제---html-표준-위반">문제 - HTML 표준 위반</h3>
<p><code>&lt;a&gt;</code> 의 content model 은 <strong>&quot;Transparent, but there must be no interactive content descendant&quot;</strong>. 즉 a 안에 a, button, input, select 같은 인터랙티브 요소를 두면 표준 위반.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>W3C HTML Validator</td>
<td>실패</td>
</tr>
<tr>
<td>접근성</td>
<td>스크린리더 혼란 (link 안 link/button)</td>
</tr>
<tr>
<td>클릭 동작</td>
<td>모호 (a 트리거 vs button 트리거)</td>
</tr>
<tr>
<td>브라우저 렌더링</td>
<td>동작은 함</td>
</tr>
</tbody></table>
<h2 id="3-옛-회피-방식---js-stoppropagation--z-index">3. 옛 회피 방식 - JS stopPropagation + z-index</h2>
<pre><code class="language-html">&lt;a href=&quot;/product/1&quot;&gt;
    &lt;article class=&quot;card&quot;&gt;
        &lt;img&gt;
        &lt;h3&gt;상품명&lt;/h3&gt;
        &lt;button onclick=&quot;event.stopPropagation()&quot;&gt;찜&lt;/button&gt;
    &lt;/article&gt;
&lt;/a&gt;</code></pre>
<p>button 클릭이 부모 a 까지 이벤트 전파되지 않게 JS 로 막음.</p>
<h3 id="문제">문제</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>평가</th>
</tr>
</thead>
<tbody><tr>
<td>동작</td>
<td>OK</td>
</tr>
<tr>
<td>HTML 표준</td>
<td>여전히 위반 (a 안 button)</td>
</tr>
<tr>
<td>JS 의존</td>
<td>stopPropagation 필요</td>
</tr>
<tr>
<td>마크업 직관성</td>
<td>떨어짐</td>
</tr>
</tbody></table>
<h2 id="4-모던-표준---stretched-link">4. 모던 표준 - Stretched Link</h2>
<p>크게 두 가지 방식. <strong>둘 다 표준 패턴</strong>, 케이스 따라 선택.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>A. 가상요소 (<code>::after</code>)</strong></td>
<td>a 자체는 의미 텍스트 그대로, 가상요소가 영역 덮음. Bootstrap 공식 헬퍼 방식</td>
</tr>
<tr>
<td><strong>B. 빈 a (또는 button)</strong></td>
<td>마크업이 단순. 빈 태그가 영역 덮음. 의미 보충 필요 (sr-only/aria-label)</td>
</tr>
</tbody></table>
<h3 id="방식-a---가상요소-패턴">방식 A - 가상요소 패턴</h3>
<p>a 자체는 의미 있는 텍스트 (상품명 등) 를 그대로 표시하고, <code>::after</code> 가상요소가 부모 영역 전체 덮음. Bootstrap 4.3 (2020) 부터 공식 stretched-link 헬퍼.</p>
<h3 id="마크업">마크업</h3>
<pre><code class="language-html">&lt;article class=&quot;card&quot;&gt;
    &lt;img&gt;
    &lt;h3&gt;
        &lt;a href=&quot;/product/1&quot; class=&quot;stretched-link&quot;&gt;스튜디오 메신저&lt;/a&gt;
    &lt;/h3&gt;
    &lt;p&gt;2,377,000&lt;/p&gt;
    &lt;button class=&quot;like&quot;&gt;찜&lt;/button&gt;
&lt;/article&gt;</code></pre>
<h3 id="css">CSS</h3>
<pre><code class="language-css">.card {
    position: relative;        /* stretched-link::after 의 기준 */
}

.stretched-link::after {
    content: &quot;&quot;;
    position: absolute;
    inset: 0;                  /* 부모 카드 전체 덮음 */
    z-index: 1;
}

.like {
    position: relative;        /* z-index 적용 위해 필요 */
    z-index: 2;                /* stretched-link 위로 (별도 클릭 가능) */
}</code></pre>
<h3 id="핵심">핵심</h3>
<ul>
<li><code>&lt;a&gt;</code> 자체는 <strong>자기 자리에서 시각 텍스트 (상품명 등) 그대로 표시</strong></li>
<li><code>::after</code> 가상요소가 <strong>부모 카드 전체 덮어 클릭 영역 확장</strong></li>
<li>시각: a 텍스트 평소처럼 보임 + 카드 전체 클릭 가능</li>
<li>자식 button/a 는 z-index 더 높게 두면 별도 클릭 가능</li>
</ul>
<h3 id="bootstrap-5-의-stretched-link-실제-코드">Bootstrap 5 의 stretched-link 실제 코드</h3>
<pre><code class="language-css">.stretched-link::after {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 1;
    content: &quot;&quot;;
}</code></pre>
<p>→ Bootstrap 이 정확히 이 방식. <code>inset: 0</code> 대신 top/right/bottom/left 분리 (구 브라우저 호환).</p>
<h3 id="장점">장점</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>효과</th>
</tr>
</thead>
<tbody><tr>
<td>HTML 표준</td>
<td>a 안에 인터랙티브 요소 없음, 위반 X</td>
</tr>
<tr>
<td>시멘틱</td>
<td>a 에 의미 텍스트 그대로 (검색엔진/스크린리더 인식 명확)</td>
</tr>
<tr>
<td>마크업 직관성</td>
<td>시각 컨텐츠와 클릭 영역 분리</td>
</tr>
<tr>
<td>자식 인터랙티브 요소</td>
<td>자유롭게 추가 (z-index 조정)</td>
</tr>
<tr>
<td>JS 의존</td>
<td>없음 (CSS only)</td>
</tr>
<tr>
<td><strong>sr-only 불필요</strong></td>
<td>a 자체에 의미 텍스트 있음</td>
</tr>
<tr>
<td><strong>SEO</strong></td>
<td>a 텍스트 그대로 인덱싱</td>
</tr>
</tbody></table>
<h3 id="방식-b---빈-a-패턴">방식 B - 빈 a 패턴</h3>
<p>가상요소 대신 <strong>빈 a 자체를 absolute</strong> 로 만드는 방식. 동일한 효과지만 a 가 빈 태그가 됨. feelavel 등 실제 회사 코드에서도 흔히 사용.</p>
<pre><code class="language-html">&lt;article class=&quot;card&quot;&gt;
    &lt;img&gt;
    &lt;h3&gt;스튜디오 메신저&lt;/h3&gt;
    &lt;p&gt;2,377,000&lt;/p&gt;
    &lt;a href=&quot;/product/1&quot; class=&quot;stretched-link&quot;&gt;&lt;/a&gt;     &lt;!-- 빈 a --&gt;
&lt;/article&gt;</code></pre>
<pre><code class="language-css">.stretched-link {
    position: absolute;
    inset: 0;
    z-index: 1;
}</code></pre>
<h3 id="두-방식-비교-둘-다-표준">두 방식 비교 (둘 다 표준)</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>A. 가상요소</th>
<th>B. 빈 a</th>
</tr>
</thead>
<tbody><tr>
<td>a 컨텐츠</td>
<td>의미 텍스트 (상품명 등)</td>
<td>비어있음</td>
</tr>
<tr>
<td>sr-only/aria-label 필요</td>
<td>X</td>
<td>필요 (의미 보충)</td>
</tr>
<tr>
<td>마크업 위치</td>
<td>a 자기 자리 (h3 안 등)</td>
<td>빈 a 카드 안 어디든</td>
</tr>
<tr>
<td>SEO</td>
<td>a 텍스트 그대로 인덱싱</td>
<td>sr-only 텍스트 보충 시</td>
</tr>
<tr>
<td>Bootstrap 헬퍼 채택</td>
<td>이 방식</td>
<td>-</td>
</tr>
<tr>
<td>실제 회사 코드 (feelavel 등)</td>
<td>-</td>
<td>이 방식</td>
</tr>
</tbody></table>
<h3 id="어느-걸-선택">어느 걸 선택?</h3>
<table>
<thead>
<tr>
<th>케이스</th>
<th>추천</th>
</tr>
</thead>
<tbody><tr>
<td>카드 안에 상품명/제목 (a 안에 의미 텍스트 둘 자리 있음)</td>
<td>A. 가상요소</td>
</tr>
<tr>
<td>카드가 이미지/디자인 위주 (a 텍스트 둘 자리 없음)</td>
<td>B. 빈 a + sr-only</td>
</tr>
<tr>
<td>모달/액션 트리거 (button)</td>
<td>빈 button + aria-label</td>
</tr>
<tr>
<td>단순함 우선</td>
<td>B 빈 a (마크업 적음)</td>
</tr>
<tr>
<td>SEO/시멘틱 우선</td>
<td>A 가상요소</td>
</tr>
</tbody></table>
<p>→ <strong>둘 다 표준이고 광범위 사용. 케이스 따라 선택.</strong></p>
<h2 id="5-빈-abutton-의-접근성-보완---sr-only--aria-label">5. 빈 a/button 의 접근성 보완 - sr-only / aria-label</h2>
<blockquote>
<p>컨텍스트: <strong>4-2 변형 (빈 a)</strong> 또는 <strong>6번 Stretched Button (빈 button)</strong> 사용 시 보완책. 정통 방식 (4번 가상요소 + 의미 텍스트) 사용 시 불필요.</p>
</blockquote>
<p>빈 <code>&lt;a&gt;&lt;/a&gt;</code> 또는 <code>&lt;button&gt;&lt;/button&gt;</code> 은 <strong>시각 사용자에겐 잘 동작</strong>하지만 <strong>스크린리더 사용자에겐 의미가 약함</strong>.</p>
<h3 id="빈-a-의-문제">빈 a 의 문제</h3>
<pre><code class="language-html">&lt;article class=&quot;card&quot;&gt;
    &lt;img&gt;
    &lt;h3&gt;스튜디오 메신저&lt;/h3&gt;
    &lt;p&gt;2,377,000&lt;/p&gt;
    &lt;a href=&quot;/product/1&quot; class=&quot;stretched-link&quot;&gt;&lt;/a&gt;
&lt;/article&gt;</code></pre>
<p>스크린리더가 이 a 를 읽을 때:</p>
<ul>
<li>&quot;Link&quot; (정확한 의미 모름)</li>
<li>또는 href URL 자체를 읽음 (&quot;product 1&quot; 등)</li>
</ul>
<p>시각 사용자는 카드 컨텐츠 보고 &quot;이건 상품 카드 링크&quot; 이해 가능. 하지만 스크린리더 사용자는 그 연결이 약함.</p>
<h3 id="보완-방법-1---sr-only-텍스트">보완 방법 1 - sr-only 텍스트</h3>
<pre><code class="language-html">&lt;a href=&quot;/product/1&quot; class=&quot;stretched-link&quot;&gt;
    &lt;span class=&quot;sr-only&quot;&gt;스튜디오 메신저 상품 상세보기&lt;/span&gt;
&lt;/a&gt;</code></pre>
<pre><code class="language-css">.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;
}</code></pre>
<p><code>sr-only</code> (screen reader only) = <strong>시각 hide + 스크린리더 인식</strong>. 거의 0 사이즈로 줄여서 시각상 안 보이지만 DOM 에 존재.</p>
<h3 id="보완-방법-2---aria-label">보완 방법 2 - aria-label</h3>
<pre><code class="language-html">&lt;a href=&quot;/product/1&quot; class=&quot;stretched-link&quot; aria-label=&quot;스튜디오 메신저 상품 상세보기&quot;&gt;&lt;/a&gt;</code></pre>
<p>aria-label 속성으로 스크린리더용 라벨 제공.</p>
<h3 id="두-방법-비교">두 방법 비교</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>sr-only</th>
<th>aria-label</th>
</tr>
</thead>
<tbody><tr>
<td>마크업</td>
<td><code>&lt;span&gt;</code> 자식 추가</td>
<td>속성 한 줄</td>
</tr>
<tr>
<td>단순함</td>
<td>약간 복잡</td>
<td>단순</td>
</tr>
<tr>
<td>다국어 처리</td>
<td>텍스트라 자연스러움</td>
<td>i18n 처리 별도 필요</td>
</tr>
<tr>
<td>검색엔진</td>
<td>텍스트로 인식</td>
<td>속성이라 일부만 인식</td>
</tr>
<tr>
<td>추천 케이스</td>
<td>SEO 까지 챙길 때</td>
<td>단순 접근성만 챙길 때</td>
</tr>
</tbody></table>
<p>→ <strong>상황 따라 골라쓰기.</strong> 빈 a/button stretched link 쓸 땐 둘 중 하나 반드시 사용.</p>
<h2 id="6-button-버전---stretched-button">6. button 버전 - Stretched Button</h2>
<p>페이지 이동이 아니라 모달/액션이 필요하면 a 대신 button.</p>
<pre><code class="language-html">&lt;div class=&quot;dropdown&quot;&gt;
    &lt;h2&gt;현재 옵션 이름&lt;/h2&gt;
    &lt;p&gt;설명 텍스트&lt;/p&gt;
    &lt;button class=&quot;stretched-btn&quot; aria-label=&quot;옵션 변경&quot;&gt;&lt;/button&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.dropdown { position: relative; }

.stretched-btn {
    position: absolute;
    inset: 0;
    z-index: 1;
    background: transparent;
    border: 0;
    cursor: pointer;
}</code></pre>
<p>컨셉 동일. a 와 button 만 다름.</p>
<h2 id="7-자식-안에-또-다른-클릭-요소-두기">7. 자식 안에 또 다른 클릭 요소 두기</h2>
<p>stretched-link 위에 다른 button/a 두려면:</p>
<pre><code class="language-css">.stretched-link { z-index: 1; }

.child-btn {
    position: relative;        /* z-index 적용 위해 필요 (정적 박스에 z-index 무효) */
    z-index: 2;                /* stretched-link 위로 */
}</code></pre>
<h3 id="핵심-1">핵심</h3>
<ul>
<li>자식에 <code>position: relative</code> 안 주면 <strong>z-index 적용 안 됨</strong> (static 박스는 z-index 무시)</li>
<li>z-index 더 높게 두면 클릭이 위에서 잡힘</li>
</ul>
<h2 id="8-한계--주의점">8. 한계 / 주의점</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>텍스트 드래그 선택 불가</td>
<td>stretched-link 가 컨텐츠 위에 있어서 드래그 선택 안 됨. 일반 카드/메뉴엔 큰 문제 X</td>
</tr>
<tr>
<td>드래그 가능 컨텐츠 (긴 텍스트, 코드 블록 등)</td>
<td>이 패턴 적합하지 않음</td>
</tr>
<tr>
<td>클릭 영역 시각 단서 필요</td>
<td>사용자가 어디 클릭 가능한지 hover/focus 스타일로 명시</td>
</tr>
<tr>
<td>접근성</td>
<td>aria-label 또는 적절한 텍스트로 link/button 의도 명시</td>
</tr>
</tbody></table>
<h2 id="9-역사">9. 역사</h2>
<table>
<thead>
<tr>
<th>시기</th>
<th>트렌드</th>
</tr>
</thead>
<tbody><tr>
<td>2017 이전</td>
<td>a 카드 감싸기 + JS stopPropagation (관행)</td>
</tr>
<tr>
<td>2017 ~ 2020</td>
<td>&quot;표준 위반&quot; 인식 확산, Bootstrap 등에서 패턴 정리 시작</td>
</tr>
<tr>
<td>2020 (Bootstrap 4.3)</td>
<td><code>stretched-link</code> 헬퍼 클래스 공식 도입</td>
</tr>
<tr>
<td>현재</td>
<td>Bootstrap, Tailwind, Material, Chakra 등 모던 라이브러리 다 채택</td>
</tr>
</tbody></table>
<h2 id="10-참고">10. 참고</h2>
<ul>
<li>Bootstrap stretched-link: <a href="https://getbootstrap.com/docs/5.0/helpers/stretched-link/">https://getbootstrap.com/docs/5.0/helpers/stretched-link/</a></li>
<li>HTML Spec a content model: <a href="https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element">https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element</a></li>
<li>MDN position absolute: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/position">https://developer.mozilla.org/en-US/docs/Web/CSS/position</a></li>
<li>MDN inset: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/inset">https://developer.mozilla.org/en-US/docs/Web/CSS/inset</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[PHP 에서 URL 의 값 받기]]></title>
            <link>https://velog.io/@wow_da65/PHP-%EC%97%90%EC%84%9C-URL-%EC%9D%98-%EA%B0%92-%EB%B0%9B%EA%B8%B0</link>
            <guid>https://velog.io/@wow_da65/PHP-%EC%97%90%EC%84%9C-URL-%EC%9D%98-%EA%B0%92-%EB%B0%9B%EA%B8%B0</guid>
            <pubDate>Tue, 28 Apr 2026 08:00:41 GMT</pubDate>
            <description><![CDATA[<p>퍼블리셔로 일하다가 처음으로 백엔드 코드를 들여다볼 일이 생겼는데, URL 에서 값 받는 패턴이 계속 나와서 이번에 제대로 정리해봤다.</p>
<p>PHP 가 URL 의 정보를 받아서 변수에 담는 패턴 정리</p>
<hr>
<h2 id="1-url-이란">1. URL 이란?</h2>
<pre><code>https://example.com/page.php?cate_no=101&amp;keyword=메신저
                              └────────┘ └──────────┘
                              파라미터 1   파라미터 2</code></pre><ul>
<li><code>?</code> 뒤부터 = <strong>쿼리스트링</strong> (query string)</li>
<li><code>&amp;</code> 로 여러 파라미터 구분</li>
<li>형식: <code>이름=값</code></li>
</ul>
<hr>
<h2 id="2-php-가-url-의-값-받는-법-_get">2. PHP 가 URL 의 값 받는 법: <code>$_GET</code></h2>
<p>PHP 는 URL 의 파라미터를 <strong>자동으로</strong> 읽어서 <code>$_GET</code> 이라는 배열에 담아둠.</p>
<pre><code class="language-php">// URL: ?cate_no=101&amp;keyword=메신저
$_GET[&#39;cate_no&#39;]    // → &quot;101&quot;
$_GET[&#39;keyword&#39;]    // → &quot;메신저&quot;</code></pre>
<p><strong>비유</strong>: URL = 편지봉투, <code>$_GET</code> = 편지 내용을 PHP 가 자동으로 꺼내 담아둔 메모지</p>
<hr>
<h2 id="3-값이-있는지-체크-isset">3. 값이 있는지 체크: <code>isset()</code></h2>
<p>URL 에 그 파라미터가 없으면 <code>$_GET[&#39;cate_no&#39;]</code> 자체가 존재하지 않음 → 그냥 쓰면 에러.</p>
<pre><code class="language-php">isset($_GET[&#39;cate_no&#39;])  // 있으면 true, 없으면 false</code></pre>
<p><strong>비유</strong>: &quot;이 메모지에 cate_no 라는 항목이 있나요?&quot; 확인</p>
<hr>
<h2 id="4-짧은-if-else-삼항-연산자--">4. 짧은 if-else: <code>삼항 연산자</code> (<code>? :</code>)</h2>
<pre><code class="language-php">$a = (조건) ? (참일 때 값) : (거짓일 때 값);</code></pre>
<p>긴 if-else 를 한 줄로 줄임:</p>
<pre><code class="language-php">// 긴 버전
if (isset($_GET[&#39;cate_no&#39;])) {
    $cate_no = $_GET[&#39;cate_no&#39;];
} else {
    $cate_no = 0;
}

// 짧은 버전 (같은 의미)
$cate_no = isset($_GET[&#39;cate_no&#39;]) ? $_GET[&#39;cate_no&#39;] : 0;</code></pre>
<p><strong>비유</strong>: &quot;있으면 A, 없으면 B&quot;</p>
<hr>
<h2 id="5-타입-강제-변환-int-string-등">5. 타입 강제 변환: <code>(int)</code>, <code>(string)</code> 등</h2>
<p>URL 에서 받은 값은 <strong>항상 문자열</strong>. 숫자처럼 쓰려면 강제로 변환.</p>
<pre><code class="language-php">$_GET[&#39;cate_no&#39;]           // → &quot;101&quot;   ← 문자열 (따옴표 있음)
(int)$_GET[&#39;cate_no&#39;]      // → 101     ← 진짜 숫자</code></pre>
<p><strong>왜 필요?</strong> SQL 쿼리에 숫자만 들어가야 안전 (보안 + 정확성).</p>
<pre><code class="language-php">$cate_no = (int)$_GET[&#39;cate_no&#39;];  // 숫자 강제 변환
// &quot;101abc&quot; → 101 (뒤 abc 무시)
// &quot;메신저&quot; → 0   (숫자 아니면 0)</code></pre>
<p><strong>비유</strong>: &quot;이거 숫자로 받아라&quot; 강제 명령</p>
<hr>
<h2 id="6-양쪽-공백-제거-trim">6. 양쪽 공백 제거: <code>trim()</code></h2>
<p>사용자가 실수로 검색어 앞뒤에 공백 넣을 수 있음. <code>trim()</code> 으로 제거.</p>
<pre><code class="language-php">trim(&quot;  메신저  &quot;)  // → &quot;메신저&quot;</code></pre>
<hr>
<h2 id="7-조립-실전-패턴">7. 조립: 실전 패턴</h2>
<p>지금까지 배운 거 다 합치면:</p>
<pre><code class="language-php">// URL 의 cate_no 받아서 안전하게 처리
$cate_no = isset($_GET[&#39;cate_no&#39;]) ? (int)$_GET[&#39;cate_no&#39;] : 0;
//          └─────────────────┘   └──────────────────┘   └┘
//          (1) 있는지 체크         (2) 숫자로 변환        (3) 없으면 default

// keyword 도 비슷 (단 문자열은 trim 까지)
$keyword = isset($_GET[&#39;keyword&#39;]) ? trim($_GET[&#39;keyword&#39;]) : &#39;&#39;;</code></pre>
<hr>
<h2 id="8-한-줄-요약">8. 한 줄 요약</h2>
<pre><code class="language-php">$변수 = isset($_GET[&#39;이름&#39;]) ? (타입)$_GET[&#39;이름&#39;] : 기본값;</code></pre>
<table>
<thead>
<tr>
<th>부분</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>isset(...)</code></td>
<td>URL 에 값 있는지 체크</td>
</tr>
<tr>
<td><code>(int)</code> 또는 <code>trim()</code></td>
<td>안전하게 변환</td>
</tr>
<tr>
<td><code>: 기본값</code></td>
<td>없을 때 default</td>
</tr>
</tbody></table>
<hr>
<h2 id="9-모던-php-에선">9. 모던 PHP 에선?</h2>
<p>PHP 7+ 에선 더 짧은 문법 (<code>??</code> null coalescing 연산자) 가능:</p>
<pre><code class="language-php">// PHP 7+
$cate_no = (int)($_GET[&#39;cate_no&#39;] ?? 0);

// PHP 4.4 (회사 환경) — 위 문법 안 됨, 풀어 써야 함
$cate_no = isset($_GET[&#39;cate_no&#39;]) ? (int)$_GET[&#39;cate_no&#39;] : 0;</code></pre>
<p>→ Laravel 같은 framework 쓰면 더 짧음:</p>
<pre><code class="language-php">$cate_no = $request-&gt;integer(&#39;cate_no&#39;, 0);</code></pre>
<p>framework 가 isset/타입변환/default 다 알아서 해줌.</p>
<hr>
<h2 id="10-다양한-예시-실전-시나리오">10. 다양한 예시 (실전 시나리오)</h2>
<p>같은 패턴이 여러 사이트/페이지에서 어떻게 쓰이는지.</p>
<h3 id="예시-1-게시판-페이지네이션">예시 1. 게시판 페이지네이션</h3>
<pre><code>URL: /board.php?page=3</code></pre><pre><code class="language-php">$page = isset($_GET[&#39;page&#39;]) ? (int)$_GET[&#39;page&#39;] : 1;
// → URL 에 page=3 → $page = 3 (3번째 페이지 보여주기)
// → URL 에 page 없으면 → $page = 1 (첫 페이지 default)</code></pre>
<h3 id="예시-2-쇼핑몰-카테고리--정렬">예시 2. 쇼핑몰 카테고리 + 정렬</h3>
<pre><code>URL: /shop.php?category=fashion&amp;sort=price</code></pre><pre><code class="language-php">$category = isset($_GET[&#39;category&#39;]) ? trim($_GET[&#39;category&#39;]) : &#39;all&#39;;
$sort     = isset($_GET[&#39;sort&#39;])     ? trim($_GET[&#39;sort&#39;])     : &#39;reg&#39;;
// → category=fashion → 패션 카테고리만
// → sort=price → 가격순 정렬</code></pre>
<h3 id="예시-3-검색-결과">예시 3. 검색 결과</h3>
<pre><code>URL: /search.php?q=메신저&amp;type=goods</code></pre><pre><code class="language-php">$keyword = isset($_GET[&#39;q&#39;])    ? trim($_GET[&#39;q&#39;])    : &#39;&#39;;
$type    = isset($_GET[&#39;type&#39;]) ? trim($_GET[&#39;type&#39;]) : &#39;all&#39;;
// → q=메신저 → &quot;메신저&quot; 검색
// → type=goods → 상품에서만 검색</code></pre>
<h3 id="예시-4-사용자-프로필">예시 4. 사용자 프로필</h3>
<pre><code>URL: /user.php?id=123</code></pre><pre><code class="language-php">$user_id = isset($_GET[&#39;id&#39;]) ? (int)$_GET[&#39;id&#39;] : 0;

if ($user_id === 0) {
    // 잘못된 접근 → 메인페이지 redirect 같은 처리
    header(&#39;Location: /&#39;);
    exit;
}
// → id=123 → 123번 사용자 프로필 보여주기</code></pre>
<h3 id="예시-5-다중-옵션-체크박스-등">예시 5. 다중 옵션 (체크박스 등)</h3>
<pre><code>URL: /filter.php?colors[]=red&amp;colors[]=blue&amp;colors[]=green</code></pre><pre><code class="language-php">$colors = isset($_GET[&#39;colors&#39;]) ? $_GET[&#39;colors&#39;] : array();
// → $colors = array(&#39;red&#39;, &#39;blue&#39;, &#39;green&#39;)
// → URL 에 [] 붙으면 PHP 가 자동으로 배열로 받음</code></pre>
<h3 id="예시-6-가격대-필터">예시 6. 가격대 필터</h3>
<pre><code>URL: /goods.php?min=10000&amp;max=50000</code></pre><pre><code class="language-php">$min_price = isset($_GET[&#39;min&#39;]) ? (int)$_GET[&#39;min&#39;] : 0;
$max_price = isset($_GET[&#39;max&#39;]) ? (int)$_GET[&#39;max&#39;] : 999999999;
// → min=10000, max=50000 → 1만원~5만원 사이
// → 둘 다 없으면 전체 (0 ~ 매우 큰 수)</code></pre>
<hr>
<h2 id="11-다음에-코드-볼-때">11. 다음에 코드 볼 때</h2>
<p>회사 PHP 페이지 어디서든 위 패턴 보면:</p>
<blockquote>
<p>&quot;아, URL 의 값 안전하게 받아서 변수에 담는 거구나&quot;</p>
</blockquote>
<p>라고 바로 이해 가능.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSS Checkbox/Radio Hack — JS 없이 토글·탭·모달 만들기]]></title>
            <link>https://velog.io/@wow_da65/CSS-CheckboxRadio-Hack-JS-%EC%97%86%EC%9D%B4-%ED%86%A0%EA%B8%80%ED%83%AD%EB%AA%A8%EB%8B%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@wow_da65/CSS-CheckboxRadio-Hack-JS-%EC%97%86%EC%9D%B4-%ED%86%A0%EA%B8%80%ED%83%AD%EB%AA%A8%EB%8B%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Thu, 23 Apr 2026 01:46:26 GMT</pubDate>
            <description><![CDATA[<p>토글 스위치 구현 방법 찾다가 :checked 해킹을 알게 됐는데, 토글뿐 아니라 탭·모달·아코디언까지 같은 원리로 만들 수 있길래 정리해보았다.</p>
<p>input의 :checked 상태랑 CSS 형제 선택자(+, ~)만으로 JS 없이 토글, 탭, 모달, 아코디언을 만들 수 있다. input을 숨기고, label을 클릭 영역으로 쓰고, :checked에 따라 형제 요소 스타일을 바꾸는 게 핵심. 체크박스는 ON/OFF, 라디오는 택일 상황에 쓰고, 패턴은 총 7가지 정리했다.</p>
<h2 id="1-핵심-원리">1. 핵심 원리</h2>
<pre><code>[1] input (시각 hide)
[2] label (실제 클릭 영역, for=input id 매칭)
[3] :checked + sibling selector → 형제 요소의 스타일 변경</code></pre><h3 id="핵심-selector">핵심 selector</h3>
<table>
<thead>
<tr>
<th>selector</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>input:checked + label</code></td>
<td>checked 된 input <strong>바로 다음</strong> label</td>
</tr>
<tr>
<td><code>input:checked ~ .target</code></td>
<td>checked 된 input 의 <strong>이후 어떤</strong> sibling .target</td>
</tr>
<tr>
<td><code>input:checked + label::before</code></td>
<td>checked 된 label 의 가상 요소</td>
</tr>
</tbody></table>
<h3 id="input-시각-hide-방법-비교">input 시각 hide 방법 비교</h3>
<table>
<thead>
<tr>
<th>방법</th>
<th>접근성</th>
<th>폼 동작</th>
<th>추천</th>
</tr>
</thead>
<tbody><tr>
<td><code>display: none</code></td>
<td>❌ 스크린리더 못 읽음</td>
<td>✓ submit 됨</td>
<td>△ 간단하지만 권장 X</td>
</tr>
<tr>
<td><code>opacity: 0 + position: absolute + pointer-events: none</code></td>
<td>✓ 인식 가능</td>
<td>✓ submit 됨</td>
<td>★ 권장</td>
</tr>
<tr>
<td><code>visibility: hidden</code></td>
<td>❌ 키보드 탐색 안 됨</td>
<td>✓ submit 됨</td>
<td>✗ 비권장</td>
</tr>
</tbody></table>
<h2 id="2-checkbox-vs-radio--어떤-걸-쓸까">2. Checkbox vs Radio — 어떤 걸 쓸까</h2>
<p>둘 다 위 핵심 원리(<code>:checked + sibling</code>)로 동일하게 동작하지만, <strong>의미(시멘틱)가 다름</strong>.</p>
<h3 id="차이">차이</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Checkbox</th>
<th>Radio</th>
</tr>
</thead>
<tbody><tr>
<td>input 개수</td>
<td>1개 (단일 토글)</td>
<td><strong>같은 name 여러 개</strong> (그룹)</td>
</tr>
<tr>
<td>본질</td>
<td><strong>ON/OFF</strong> (boolean)</td>
<td><strong>여러 옵션 중 하나 선택</strong> (택일)</td>
</tr>
<tr>
<td>미선택 form 전송</td>
<td>안 됨 (체크 시만 name=value 전송)</td>
<td>그룹 전체 미전송 (또는 default)</td>
</tr>
<tr>
<td>시각 형태</td>
<td>보통 토글 스위치 한 개</td>
<td>보통 세그먼트 컨트롤 / 라디오 버튼</td>
</tr>
</tbody></table>
<h3 id="선택-기준-한-줄-가이드">선택 기준 (한 줄 가이드)</h3>
<blockquote>
<p><strong>본질이 boolean(켜다/끄다)이면 checkbox, 둘 이상의 명시적 옵션 중 택일이면 radio.</strong></p>
</blockquote>
<h3 id="예시">예시</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>시멘틱</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>알림 받음 / 받지않음</td>
<td><strong>checkbox</strong></td>
<td>&quot;받음=true&quot; boolean. 미체크는 &quot;받지않음&quot;으로 처리</td>
</tr>
<tr>
<td>다크모드 ON / OFF</td>
<td><strong>checkbox</strong></td>
<td>단순 토글</td>
</tr>
<tr>
<td>약관 동의</td>
<td><strong>checkbox</strong></td>
<td>동의함 = true</td>
</tr>
<tr>
<td>결제수단 (카드 / 계좌이체 / 간편결제)</td>
<td><strong>radio</strong></td>
<td>셋 중 하나 명시적 선택</td>
</tr>
<tr>
<td>성별 (남 / 여)</td>
<td><strong>radio</strong></td>
<td>두 명시적 값</td>
</tr>
<tr>
<td>필터 (전체 / 중고)</td>
<td><strong>radio</strong></td>
<td>&quot;전체&quot;도 &quot;중고&quot;도 명시적 선택값</td>
</tr>
<tr>
<td>사이즈 (S / M / L)</td>
<td><strong>radio</strong></td>
<td>택일</td>
</tr>
</tbody></table>
<h3 id="둘-다-가능한-경우">둘 다 가능한 경우</h3>
<p>&quot;전체/중고&quot; 같이 두 옵션이라면 둘 다 구현 가능:</p>
<pre><code class="language-html">&lt;!-- radio (택일이 명확) --&gt;
&lt;input type=&quot;radio&quot; name=&quot;filter&quot; value=&quot;all&quot;&gt;전체
&lt;input type=&quot;radio&quot; name=&quot;filter&quot; value=&quot;used&quot;&gt;중고

&lt;!-- checkbox (단순화 — &quot;중고만 보기&quot; 의미) --&gt;
&lt;input type=&quot;checkbox&quot; name=&quot;used_only&quot;&gt;중고만 보기</code></pre>
<p>→ form 데이터 처리 방식이 다름.</p>
<ul>
<li>radio: <code>?filter=all</code> 또는 <code>?filter=used</code> (둘 다 명시 값)</li>
<li>checkbox: 체크 시 <code>?used_only=on</code>, 미체크 시 파라미터 없음</li>
</ul>
<p><strong>의도에 따라 결정.</strong> 둘 다 명시 값으로 보내야 하면 radio, 미체크가 기본 의미면 checkbox.</p>
<h2 id="3-장단점">3. 장단점</h2>
<h3 id="장점">장점</h3>
<ul>
<li><strong>JS 없이 동작</strong> (가벼움, 의존성 없음)</li>
<li>form submit 자동 처리 (input 값이 form data에 포함)</li>
<li>접근성 (키보드 탐색, 스크린리더 — opacity 방식 hide 시)</li>
<li>브라우저 호환성 좋음 (모든 모던 브라우저)</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li><strong>마크업 순서 제약</strong>: sibling selector라서 input → label → target 순서 필요</li>
<li><strong>target은 input의 형제(sibling)</strong> 만 가능 (자식 X, 부모 X) — <code>:has()</code> 로 일부 우회 가능</li>
<li>복잡한 인터랙션은 결국 JS 필요</li>
</ul>
<h2 id="4-패턴-7가지">4. 패턴 7가지</h2>
<h3 id="패턴-1-토글-스위치-onoff">패턴 1. 토글 스위치 (ON/OFF)</h3>
<p>iOS 스타일 토글.</p>
<p><strong>언제 쓰나</strong>: 알림 ON/OFF, 다크모드, 설정값 토글</p>
<pre><code class="language-html">&lt;div class=&quot;toggle&quot;&gt;
    &lt;input type=&quot;checkbox&quot; id=&quot;alarm&quot; name=&quot;alarm&quot; class=&quot;toggle__input&quot;&gt;
    &lt;label for=&quot;alarm&quot; class=&quot;toggle__label&quot;&gt;
        &lt;span class=&quot;toggle__knob&quot;&gt;&lt;/span&gt;
    &lt;/label&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.toggle__input { position: absolute; opacity: 0; pointer-events: none; }

.toggle__label {
    position: relative; display: inline-block;
    width: 40px; height: 24px;
    background: #ccc; border-radius: 999px;
    cursor: pointer; transition: background 0.25s;
}

.toggle__knob {
    position: absolute; top: 2px; left: 2px;
    width: 20px; height: 20px;
    background: #fff; border-radius: 50%;
    transition: transform 0.25s;
}

/* :checked 시 */
.toggle__input:checked + .toggle__label { background: #169DAB; }
.toggle__input:checked + .toggle__label .toggle__knob { transform: translateX(16px); }</code></pre>
<h3 id="패턴-2-세그먼티드-컨트롤-radio-택일">패턴 2. 세그먼티드 컨트롤 (radio 택일)</h3>
<p>탭처럼 보이지만 본질은 radio.</p>
<p><strong>언제 쓰나</strong>: 필터 (전체/중고), 정렬 옵션, 탭 메뉴</p>
<pre><code class="language-html">&lt;div class=&quot;segment&quot;&gt;
    &lt;input type=&quot;radio&quot; name=&quot;filter&quot; id=&quot;all&quot; value=&quot;all&quot; checked&gt;
    &lt;label for=&quot;all&quot;&gt;전체&lt;/label&gt;
    &lt;input type=&quot;radio&quot; name=&quot;filter&quot; id=&quot;used&quot; value=&quot;used&quot;&gt;
    &lt;label for=&quot;used&quot;&gt;중고&lt;/label&gt;
    &lt;span class=&quot;segment__indicator&quot;&gt;&lt;/span&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.segment { position: relative; display: inline-flex; padding: 2px;
    background: #fff; border: 1px solid #ddd; border-radius: 999px; }

.segment input { position: absolute; opacity: 0; pointer-events: none; }

.segment label { position: relative; z-index: 1; padding: 6px 16px;
    color: #999; cursor: pointer; transition: color 0.2s; white-space: nowrap; }

.segment input:checked + label { color: #fff; }

.segment__indicator { position: absolute; top: 2px; bottom: 2px; left: 2px;
    width: calc(50% - 2px);
    background: #222; border-radius: 999px;
    transition: transform 0.25s; pointer-events: none; }

/* &quot;중고&quot; 체크 시 indicator 우측으로 */
.segment input[value=&quot;used&quot;]:checked ~ .segment__indicator {
    transform: translateX(100%);
}</code></pre>
<h4 id="핵심-함정--indicator-가-label-너비와-안-맞을-때">핵심 함정 — indicator 가 label 너비와 안 맞을 때</h4>
<p>두 label 자연 너비가 다르거나 (글자 길이 차이), <code>flex: 1</code> 안 주면 indicator 가 label 너비랑 어긋남.</p>
<p><strong>해결 두 가지:</strong></p>
<p><strong>1. container width 명시 + label <code>flex: 1</code></strong> (옵션 고정일 때 권장)</p>
<pre><code class="language-css">.segment {
    width: 84px;
    height: 32px;
    box-sizing: border-box;
}
.segment label { flex: 1; }                    /* 균등 50% */
.segment__indicator { width: calc(50% - 2px); } /* container padding 보정 */</code></pre>
<p>장점: 디자인 시안과 정확 매칭, indicator 완벽 정렬
단점: 컴포넌트마다 width/height 매번 명시</p>
<p><strong>2. 가변 (label padding으로 사이즈)</strong> (옵션 동적일 때)</p>
<pre><code class="language-css">.segment label { flex: 1; padding: 7px 14px; }</code></pre>
<p>장점: 글자 길이/옵션 추가 시 자동 대응, padding만 조정
단점: 디자인 시안 정확 매칭 미묘 차이 가능</p>
<h4 id="옵션이-n개일-때-3개-이상">옵션이 N개일 때 (3개 이상)</h4>
<p><code>flex: 1</code> 은 자식 수에 따라 균등 분배 (반반 한정 X). 옵션 N개면 각 100/N %.</p>
<pre><code class="language-css">.segment label { flex: 1; }                              /* 자동으로 100/N % 균등 */
.segment__indicator { width: calc(100% / 3 - 2px); }     /* 3개면 33.33% */

/* transform은 인덱스 × 100% */
.segment input[value=&quot;opt2&quot;]:checked ~ .segment__indicator {
    transform: translateX(100%);
}
.segment input[value=&quot;opt3&quot;]:checked ~ .segment__indicator {
    transform: translateX(200%);
}</code></pre>
<blockquote>
<p>비균등 비율 원하면 flex 값 다르게: <code>flex: 1</code> vs <code>flex: 2</code> → 1:2 비율.</p>
</blockquote>
<h3 id="패턴-3-아코디언--펼치기">패턴 3. 아코디언 / 펼치기</h3>
<p>체크박스 toggle 로 컨텐츠 show/hide. (또는 HTML5 <code>&lt;details&gt;</code> 사용 가능)</p>
<p><strong>언제 쓰나</strong>: FAQ, 약관 펼치기, 설정 그룹</p>
<pre><code class="language-html">&lt;div class=&quot;accordion&quot;&gt;
    &lt;input type=&quot;checkbox&quot; id=&quot;faq1&quot; class=&quot;accordion__input&quot;&gt;
    &lt;label for=&quot;faq1&quot; class=&quot;accordion__title&quot;&gt;자주 묻는 질문 1&lt;/label&gt;
    &lt;div class=&quot;accordion__content&quot;&gt;
        답변 내용입니다.
    &lt;/div&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.accordion__input { display: none; }   /* 아코디언은 display:none 무방 (form 데이터 의미 없음) */

.accordion__title { display: block; padding: 16px;
    background: #f5f5f5; cursor: pointer; }

.accordion__content {
    max-height: 0;                      /* 닫힌 상태 */
    overflow: hidden;
    transition: max-height 0.3s ease;
}

/* checked 시 펼침 */
.accordion__input:checked ~ .accordion__content {
    max-height: 500px;                  /* 충분히 큰 값 */
}</code></pre>
<blockquote>
<p>더 단순한 버전: HTML5 <code>&lt;details&gt;</code> + <code>&lt;summary&gt;</code> — 아예 input 없이도 됨.</p>
</blockquote>
<h3 id="패턴-4-모달--팝업-열기·닫기">패턴 4. 모달 / 팝업 열기·닫기</h3>
<p>체크박스로 overlay show/hide. JS 없이 모달.</p>
<p><strong>언제 쓰나</strong>: 햄버거 메뉴, 간단한 팝업, lightbox</p>
<pre><code class="language-html">&lt;input type=&quot;checkbox&quot; id=&quot;modal&quot; class=&quot;modal__input&quot;&gt;
&lt;label for=&quot;modal&quot; class=&quot;modal__open&quot;&gt;팝업 열기&lt;/label&gt;

&lt;div class=&quot;modal__overlay&quot;&gt;
    &lt;div class=&quot;modal__box&quot;&gt;
        팝업 내용
        &lt;label for=&quot;modal&quot; class=&quot;modal__close&quot;&gt;X&lt;/label&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.modal__input { display: none; }
.modal__overlay {
    display: none;
    position: fixed; top: 0; left: 0; right: 0; bottom: 0;
    background: rgba(0,0,0,0.5); z-index: 1000;
}
.modal__input:checked ~ .modal__overlay { display: flex; }</code></pre>
<h3 id="패턴-5-별점-평가">패턴 5. 별점 평가</h3>
<p>radio 5개 + label로 별. CSS만으로 별점 UI.</p>
<p><strong>언제 쓰나</strong>: 리뷰 별점, 만족도 평가</p>
<pre><code class="language-html">&lt;div class=&quot;rating&quot;&gt;
    &lt;input type=&quot;radio&quot; name=&quot;rate&quot; id=&quot;r5&quot; value=&quot;5&quot;&gt;&lt;label for=&quot;r5&quot;&gt;★&lt;/label&gt;
    &lt;input type=&quot;radio&quot; name=&quot;rate&quot; id=&quot;r4&quot; value=&quot;4&quot;&gt;&lt;label for=&quot;r4&quot;&gt;★&lt;/label&gt;
    &lt;input type=&quot;radio&quot; name=&quot;rate&quot; id=&quot;r3&quot; value=&quot;3&quot;&gt;&lt;label for=&quot;r3&quot;&gt;★&lt;/label&gt;
    &lt;input type=&quot;radio&quot; name=&quot;rate&quot; id=&quot;r2&quot; value=&quot;2&quot;&gt;&lt;label for=&quot;r2&quot;&gt;★&lt;/label&gt;
    &lt;input type=&quot;radio&quot; name=&quot;rate&quot; id=&quot;r1&quot; value=&quot;1&quot;&gt;&lt;label for=&quot;r1&quot;&gt;★&lt;/label&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.rating {
    display: inline-flex;
    flex-direction: row-reverse;        /* 우→좌 정렬 (호버/체크 ~로 좌측 별 채우기 위해) */
}

.rating input { display: none; }

.rating label {
    color: #ccc; font-size: 24px; cursor: pointer;
}

/* 체크된 별 + 그 좌측(=row-reverse 기준 ~) 모두 노란색 */
.rating input:checked ~ label,
.rating label:hover,
.rating label:hover ~ label {
    color: #ffc107;
}</code></pre>
<h3 id="패턴-6-탭-컨텐츠-전환">패턴 6. 탭 컨텐츠 전환</h3>
<p>radio + 탭별 컨텐츠 swap. JS 없이 탭.</p>
<p><strong>언제 쓰나</strong>: 상품 상세 탭(상세/리뷰/Q&amp;A), 카테고리 탭</p>
<pre><code class="language-html">&lt;div class=&quot;tabs&quot;&gt;
    &lt;input type=&quot;radio&quot; name=&quot;tab&quot; id=&quot;tab1&quot; checked&gt;&lt;label for=&quot;tab1&quot;&gt;탭1&lt;/label&gt;
    &lt;input type=&quot;radio&quot; name=&quot;tab&quot; id=&quot;tab2&quot;&gt;&lt;label for=&quot;tab2&quot;&gt;탭2&lt;/label&gt;

    &lt;div class=&quot;tabs__content tab1&quot;&gt;탭1 내용&lt;/div&gt;
    &lt;div class=&quot;tabs__content tab2&quot;&gt;탭2 내용&lt;/div&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.tabs__content { display: none; }

#tab1:checked ~ .tab1,
#tab2:checked ~ .tab2 { display: block; }</code></pre>
<blockquote>
<p><strong>단점</strong>: id 가 글로벌이라 페이지에 같은 패턴 여러 개 두기 까다로움. 컴포넌트 단위로 쓸 때는 JS가 더 깔끔.</p>
</blockquote>
<h3 id="패턴-7-검색창-확장">패턴 7. 검색창 확장</h3>
<p>체크박스 toggle로 input 펼치기.</p>
<p><strong>언제 쓰나</strong>: 헤더 검색 아이콘 → 입력창 확장</p>
<pre><code class="language-html">&lt;input type=&quot;checkbox&quot; id=&quot;search-toggle&quot; class=&quot;search__toggle&quot;&gt;
&lt;label for=&quot;search-toggle&quot; class=&quot;search__icon&quot;&gt;🔍&lt;/label&gt;
&lt;div class=&quot;search__form&quot;&gt;
    &lt;input type=&quot;text&quot; placeholder=&quot;검색어 입력&quot;&gt;
&lt;/div&gt;</code></pre>
<pre><code class="language-css">.search__toggle { display: none; }
.search__form {
    width: 0; overflow: hidden;
    transition: width 0.3s ease;
}
.search__toggle:checked ~ .search__form { width: 200px; }</code></pre>
<h2 id="5-주의-사항">5. 주의 사항</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ID 충돌</strong></td>
<td>label <code>for</code> ↔ input <code>id</code> 매칭. 같은 페이지에 여러 개 두면 id 유니크해야 함</td>
</tr>
<tr>
<td><strong>마크업 순서</strong></td>
<td>sibling selector라서 <code>input → label → target</code> 순서 강제</td>
</tr>
<tr>
<td><strong>접근성 hide</strong></td>
<td><code>display: none</code> 보다 <code>opacity: 0 + position: absolute</code> 가 권장 (스크린리더 인식)</td>
</tr>
<tr>
<td><strong>form 안에 둘 때</strong></td>
<td>submit 시 자동 전송 (name 속성 필수)</td>
</tr>
<tr>
<td><strong>상태 관리 복잡 시</strong></td>
<td>JS로 가는 게 깔끔. 이 패턴은 단순 toggle 에 최적</td>
</tr>
<tr>
<td><strong><code>:has()</code> 활용</strong></td>
<td>부모 selector. 최신 브라우저 지원 (2022+). 마크업 제약 일부 우회 가능</td>
</tr>
</tbody></table>
<h2 id="6-참고">6. 참고</h2>
<ul>
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox"><code>&lt;input type=&quot;checkbox&quot;&gt;</code></a></li>
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:checked"><code>:checked</code> pseudo-class</a></li>
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_combinator">Sibling combinators (<code>+</code>, <code>~</code>)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git에서 파일명 대소문자만 바꿀 때 주의할 점]]></title>
            <link>https://velog.io/@wow_da65/Git%EC%97%90%EC%84%9C-%ED%8C%8C%EC%9D%BC%EB%AA%85-%EB%8C%80%EC%86%8C%EB%AC%B8%EC%9E%90%EB%A7%8C-%EB%B0%94%EA%BF%80-%EB%95%8C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90</link>
            <guid>https://velog.io/@wow_da65/Git%EC%97%90%EC%84%9C-%ED%8C%8C%EC%9D%BC%EB%AA%85-%EB%8C%80%EC%86%8C%EB%AC%B8%EC%9E%90%EB%A7%8C-%EB%B0%94%EA%BF%80-%EB%95%8C-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90</guid>
            <pubDate>Thu, 16 Apr 2026 05:52:51 GMT</pubDate>
            <description><![CDATA[<p>파일명의 대소문자만 변경했을 때 Git이 변경사항을 감지하지 못하는 경우가 있다. 원인과 해결 방법을 정리한다.</p>
<hr>
<h2 id="문제-상황">문제 상황</h2>
<p>CSS 파일명이 <code>Main.css</code>로 되어 있어 소문자 <code>main.css</code>로 바꾸려고 했다.
에디터나 탐색기에서 이름만 바꾼 뒤 Git 상태를 확인했는데, <strong>변경사항 목록에 아무것도 잡히지 않았다.</strong></p>
<hr>
<h2 id="왜-이런-일이-생기는가">왜 이런 일이 생기는가</h2>
<p>Git은 기본적으로 <strong>파일명 대소문자 차이를 &quot;변경&quot;으로 보지 않는 설정</strong>이 적용되어 있다.
즉, <code>Main.css</code>와 <code>main.css</code>를 같은 파일로 취급해서 변경사항이 없다고 판단한다.</p>
<hr>
<h2 id="왜-위험한가">왜 위험한가</h2>
<p>운영 서버에서는 대소문자가 다르면 완전히 다른 파일로 취급한다. 예를 들어 실제 파일이 <code>Main.css</code>인데 HTML에서 <code>&lt;link href=&quot;main.css&quot;&gt;</code>로 참조하면 로컬에서는 정상 로드되지만 서버에서는 파일을 찾지 못한다.</p>
<table>
<thead>
<tr>
<th>참조 경로</th>
<th>로컬</th>
<th>서버</th>
</tr>
</thead>
<tbody><tr>
<td><code>main.css</code></td>
<td>정상</td>
<td>404</td>
</tr>
<tr>
<td><code>Main.css</code></td>
<td>정상</td>
<td>정상</td>
</tr>
</tbody></table>
<blockquote>
<p>실제 파일명이 <code>Main.css</code>인 경우 기준</p>
</blockquote>
<p>로컬에서는 잘 작동하던 페이지가 배포 후 CSS/JS가 깨지는 현상이 생긴다. 로컬에서 재현이 안 되기 때문에 원인을 찾기 어렵다.</p>
<hr>
<h2 id="해결-방법-git-mv-명령어-사용">해결 방법: <code>git mv</code> 명령어 사용</h2>
<p>파일명 대소문자만 바꿀 때는 에디터나 탐색기에서 바꾸지 말고, <strong>터미널에서 <code>git mv</code> 명령어로 변경</strong>한다.</p>
<pre><code class="language-bash">git mv Main.css main.css
git commit -m &quot;Main.css 파일명 소문자로 변경&quot;
git push</code></pre>
<p>이렇게 하면 Git이 파일명 변경을 정상적으로 인식한다.</p>
<hr>
<h2 id="git-mv-실행-시-에러가-나는-경우"><code>git mv</code> 실행 시 에러가 나는 경우</h2>
<p>다음과 같은 에러가 발생할 수 있다.</p>
<pre><code>fatal: renaming &#39;Main.css&#39; failed: Invalid argument</code></pre><p>이는 일부 환경에서 대소문자만 다른 두 파일명을 같은 파일로 인식해, 자기 자신으로의 이름 변경을 거부하기 때문이다. 이때는 <strong>임시 파일명을 거쳐 두 단계로 변경</strong>한다.</p>
<pre><code class="language-bash"># 1단계: 임시 파일명으로 변경
git mv Main.css temp-main.css
git commit -m &quot;임시 파일명 변경&quot;

# 2단계: 최종 파일명으로 변경
git mv temp-main.css main.css
git commit -m &quot;파일명 소문자로 변경&quot;

git push</code></pre>
<hr>
<h2 id="이미-에디터나-탐색기에서-바꿔버린-경우">이미 에디터나 탐색기에서 바꿔버린 경우</h2>
<p>이 상태에서는 <code>git status</code>에 아무 변경사항도 잡히지 않는다. Git이 파일명 변경을 감지하지 못했기 때문이다.</p>
<p>되돌리거나 별도 작업을 할 필요 없이, 그 상태 그대로 아래 명령어를 실행해본다.</p>
<pre><code class="language-bash">git mv Main.css main.css</code></pre>
<p>Git 내부 기록은 여전히 <code>Main.css</code>이므로 위 명령어가 그대로 처리되는 경우가 많다.</p>
<p>다만 환경에 따라서는 디스크상으로 이미 <code>main.css</code>로 바뀌어 있어 <code>Main.css</code>를 찾지 못한다는 에러가 발생할 수 있다. 이때는 고민하지 말고 곧바로 <strong>임시 파일명 2단계 방식</strong>으로 진행한다. 이 경우 디스크에 이미 존재하는 이름(<code>main.css</code>)부터 시작한다.</p>
<pre><code class="language-bash">git mv main.css temp-main.css
git commit -m &quot;임시 파일명 변경&quot;

git mv temp-main.css main.css
git commit -m &quot;파일명 소문자로 변경&quot;</code></pre>
<hr>
<h2 id="coreignorecase-설정에-대해"><code>core.ignorecase</code> 설정에 대해</h2>
<p>Git에는 파일명 대소문자 구분 여부를 제어하는 <code>core.ignorecase</code> 옵션이 있다.</p>
<pre><code class="language-bash">git config core.ignorecase false</code></pre>
<p>이 값을 <code>false</code>로 바꾸면 에디터나 탐색기에서 이름만 바꿔도 Git이 변경사항으로 인식한다.</p>
<p>다만 <strong>기본값이 <code>true</code>로 되어 있는 데는 이유가 있다.</strong> 파일시스템 자체가 대소문자를 구분하지 않는 환경에서 이 설정을 강제로 켜면, 추적되지 않는 파일명 충돌이 발생하거나 동일 파일이 두 번 등록되는 등 예상치 못한 문제가 생길 수 있다.</p>
<p>따라서 팀 표준으로 권장하기보다는, <strong>동작 원리를 이해한 사람이 필요할 때 선택적으로 쓰는 옵션</strong> 정도로 인지하는 편이 안전하다. 적용한다면 팀 전체가 사전 합의 후 동일하게 설정해야 한다.</p>
<hr>
<h2 id="예방-체크리스트">예방 체크리스트</h2>
<ul>
<li>새 파일은 처음부터 <strong>소문자 + 하이픈</strong> 규칙을 따른다 (예: <code>main-banner.css</code>)</li>
<li>대문자가 섞인 파일명을 발견하면 에디터/탐색기 대신 <code>git mv</code>를 사용한다</li>
<li>HTML/CSS/JS에서 파일 경로를 작성할 때 실제 파일명과 대소문자를 정확히 일치시킨다</li>
<li>커밋 전 <code>git status</code>로 파일명 변경이 정상적으로 잡혔는지 반드시 확인한다</li>
</ul>
<hr>
<h2 id="요약">요약</h2>
<ol>
<li>파일명의 대소문자만 변경하면 Git이 변경을 감지하지 못할 수 있다.</li>
<li>이때는 반드시 <code>git mv</code> 명령어를 사용한다.</li>
<li><code>git mv</code>가 실패하면 임시 파일명을 거쳐 두 단계로 나누어 변경한다.</li>
<li><code>core.ignorecase</code> 설정은 동작 원리를 이해한 뒤 팀 합의를 거쳐 신중히 적용한다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[div로 다 되는데 왜 form을 써야 할까?]]></title>
            <link>https://velog.io/@wow_da65/div%EB%A1%9C-%EB%8B%A4-%EB%90%98%EB%8A%94%EB%8D%B0-%EC%99%9C-form%EC%9D%84-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@wow_da65/div%EB%A1%9C-%EB%8B%A4-%EB%90%98%EB%8A%94%EB%8D%B0-%EC%99%9C-form%EC%9D%84-%EC%8D%A8%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Tue, 14 Apr 2026 23:50:12 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>사실 평소엔 <code>&lt;form&gt;</code> 태그를 잘 안 썼다. 로그인이든 검색이든 그냥 <code>&lt;div&gt;</code>로 감싸고 버튼에 이벤트 걸어서 JS로 값 긁어 보내는 식이었고, 동작은 잘 되니까 굳이 form을 써야 할 이유를 못 느꼈다.</p>
<p>그러다 최근에 레거시 페이지 리뉴얼 작업을 하면서 <code>&lt;form&gt;</code> 태그를 엄청 많이 보게 됐다. 처음엔 &quot;요즘도 이렇게 form을 많이 쓰나?&quot; 싶었는데, 찬찬히 뜯어보니 form이 <strong>엔터키 제출, 한글 IME 처리, 브라우저 자동완성, 스크린리더 인식 같은 걸 전부 공짜로 해결</strong>하고 있었다. 그동안 내가 div로 만들고 JS로 일일이 막고 있던 것들이다.</p>
<p>이 문서는 그때 정리해둔 내용.</p>
<hr>
<h2 id="1-form을-쓰는-이유">1. form을 쓰는 이유</h2>
<ul>
<li><strong>엔터키 자동 제출</strong> — input 포커스 상태에서 엔터 치면 submit 발생</li>
<li><strong>스크린리더 접근성</strong> — &quot;폼&quot; 랜드마크로 인식됨</li>
<li><strong>브라우저 기본 검증</strong> — <code>required</code>, <code>type</code>, <code>pattern</code> 속성</li>
<li><strong>한글 IME 처리</strong> — 조합 중 엔터와 제출용 엔터를 브라우저가 구분</li>
<li><strong><code>FormData</code>로 전체 값 일괄 수집 가능</strong></li>
</ul>
<hr>
<h2 id="2-기본-구조">2. 기본 구조</h2>
<pre><code class="language-html">&lt;form action=&quot;/order/submit&quot; method=&quot;post&quot;&gt;
    &lt;input type=&quot;text&quot; name=&quot;buyer_name&quot; required&gt;
    &lt;button type=&quot;submit&quot;&gt;주문하기&lt;/button&gt;
&lt;/form&gt;</code></pre>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>action</code></td>
<td>제출될 URL</td>
</tr>
<tr>
<td><code>method</code></td>
<td><code>get</code> (검색/필터) / <code>post</code> (결제/가입)</td>
</tr>
<tr>
<td><code>enctype</code></td>
<td>파일 업로드 시 <code>multipart/form-data</code></td>
</tr>
<tr>
<td><code>autocomplete</code></td>
<td><code>on</code>/<code>off</code> — 카드번호 등은 <code>off</code> 권장</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-실전-예제">3. 실전 예제</h2>
<h3 id="3-1-상품-검색-get">3-1. 상품 검색 (GET)</h3>
<pre><code class="language-html">&lt;form action=&quot;/goods/search&quot; method=&quot;get&quot; role=&quot;search&quot;&gt;
    &lt;label for=&quot;keyword&quot; class=&quot;sr-only&quot;&gt;상품 검색&lt;/label&gt;
    &lt;input type=&quot;search&quot; id=&quot;keyword&quot; name=&quot;keyword&quot;
           placeholder=&quot;브랜드, 상품명 입력&quot; required&gt;
    &lt;button type=&quot;submit&quot;&gt;검색&lt;/button&gt;
&lt;/form&gt;</code></pre>
<ul>
<li>GET 쓰면 URL에 쿼리스트링 남아서 <strong>즐겨찾기/공유</strong> 가능</li>
<li><code>type=&quot;search&quot;</code>는 모바일에서 X 버튼 자동 제공</li>
</ul>
<hr>
<h3 id="3-2-로그인">3-2. 로그인</h3>
<pre><code class="language-html">&lt;form action=&quot;/member/login&quot; method=&quot;post&quot;&gt;
    &lt;label for=&quot;user_id&quot;&gt;아이디&lt;/label&gt;
    &lt;input type=&quot;text&quot; id=&quot;user_id&quot; name=&quot;user_id&quot;
           autocomplete=&quot;username&quot; required&gt;

    &lt;label for=&quot;user_pw&quot;&gt;비밀번호&lt;/label&gt;
    &lt;input type=&quot;password&quot; id=&quot;user_pw&quot; name=&quot;user_pw&quot;
           autocomplete=&quot;current-password&quot; required&gt;

    &lt;label&gt;
        &lt;input type=&quot;checkbox&quot; name=&quot;remember&quot; value=&quot;Y&quot;&gt;
        로그인 상태 유지
    &lt;/label&gt;

    &lt;button type=&quot;submit&quot;&gt;로그인&lt;/button&gt;
&lt;/form&gt;</code></pre>
<ul>
<li><code>autocomplete=&quot;current-password&quot;</code> 있어야 브라우저/비밀번호 매니저가 인식</li>
<li>회원가입 시엔 <code>new-password</code> 사용</li>
</ul>
<hr>
<h3 id="3-3-회원가입-검증-속성-활용">3-3. 회원가입 (검증 속성 활용)</h3>
<pre><code class="language-html">&lt;form action=&quot;/member/join&quot; method=&quot;post&quot;&gt;
    &lt;input type=&quot;email&quot; name=&quot;email&quot; required
           placeholder=&quot;이메일&quot;&gt;

    &lt;input type=&quot;password&quot; name=&quot;password&quot;
           minlength=&quot;8&quot; maxlength=&quot;20&quot;
           pattern=&quot;(?=.*[A-Za-z])(?=.*\d).{8,}&quot;
           title=&quot;영문+숫자 포함 8자 이상&quot; required&gt;

    &lt;input type=&quot;tel&quot; name=&quot;phone&quot;
           pattern=&quot;010-?\d{4}-?\d{4}&quot;
           placeholder=&quot;010-0000-0000&quot; required&gt;

    &lt;input type=&quot;date&quot; name=&quot;birth&quot; max=&quot;2010-12-31&quot;&gt;

    &lt;button type=&quot;submit&quot;&gt;가입하기&lt;/button&gt;
&lt;/form&gt;</code></pre>
<ul>
<li><code>type=&quot;email&quot;</code> — 브라우저가 <code>@</code> 포함 여부 자동 검증</li>
<li><code>pattern</code> — 정규식 검증</li>
<li><code>title</code> — 검증 실패 시 툴팁 메시지</li>
</ul>
<hr>
<h3 id="3-4-장바구니-수량-변경">3-4. 장바구니 수량 변경</h3>
<pre><code class="language-html">&lt;form action=&quot;/cart/update&quot; method=&quot;post&quot;&gt;
    &lt;input type=&quot;hidden&quot; name=&quot;cart_id&quot; value=&quot;12345&quot;&gt;

    &lt;label for=&quot;qty&quot;&gt;수량&lt;/label&gt;
    &lt;input type=&quot;number&quot; id=&quot;qty&quot; name=&quot;qty&quot;
           min=&quot;1&quot; max=&quot;99&quot; value=&quot;1&quot; required&gt;

    &lt;button type=&quot;submit&quot;&gt;변경&lt;/button&gt;
&lt;/form&gt;</code></pre>
<ul>
<li><code>type=&quot;number&quot;</code> + <code>min</code>/<code>max</code> — 음수/초과 입력 차단</li>
<li>상품 ID 같은 값은 <code>type=&quot;hidden&quot;</code></li>
</ul>
<hr>
<h3 id="3-5-주문결제-배송지">3-5. 주문/결제 (배송지)</h3>
<pre><code class="language-html">&lt;form action=&quot;/order/pay&quot; method=&quot;post&quot; id=&quot;orderForm&quot;&gt;
    &lt;fieldset&gt;
        &lt;legend&gt;배송지 정보&lt;/legend&gt;

        &lt;input type=&quot;text&quot; name=&quot;receiver&quot; placeholder=&quot;받는 분&quot;
               required autocomplete=&quot;name&quot;&gt;

        &lt;input type=&quot;tel&quot; name=&quot;receiver_phone&quot;
               placeholder=&quot;연락처&quot; required autocomplete=&quot;tel&quot;&gt;

        &lt;input type=&quot;text&quot; name=&quot;zipcode&quot; readonly required
               autocomplete=&quot;postal-code&quot;&gt;
        &lt;button type=&quot;button&quot; onclick=&quot;openZipcodePopup()&quot;&gt;우편번호 찾기&lt;/button&gt;

        &lt;input type=&quot;text&quot; name=&quot;addr1&quot; readonly required
               autocomplete=&quot;address-line1&quot;&gt;
        &lt;input type=&quot;text&quot; name=&quot;addr2&quot; placeholder=&quot;상세주소&quot;
               autocomplete=&quot;address-line2&quot;&gt;

        &lt;textarea name=&quot;delivery_memo&quot; maxlength=&quot;50&quot;
                  placeholder=&quot;배송 요청사항&quot;&gt;&lt;/textarea&gt;
    &lt;/fieldset&gt;

    &lt;fieldset&gt;
        &lt;legend&gt;결제수단&lt;/legend&gt;
        &lt;label&gt;&lt;input type=&quot;radio&quot; name=&quot;pay_method&quot; value=&quot;card&quot; checked&gt; 신용카드&lt;/label&gt;
        &lt;label&gt;&lt;input type=&quot;radio&quot; name=&quot;pay_method&quot; value=&quot;vbank&quot;&gt; 무통장입금&lt;/label&gt;
        &lt;label&gt;&lt;input type=&quot;radio&quot; name=&quot;pay_method&quot; value=&quot;kakao&quot;&gt; 카카오페이&lt;/label&gt;
    &lt;/fieldset&gt;

    &lt;label&gt;
        &lt;input type=&quot;checkbox&quot; name=&quot;agree&quot; required&gt;
        주문 내용 확인 및 결제 동의 (필수)
    &lt;/label&gt;

    &lt;button type=&quot;submit&quot;&gt;결제하기&lt;/button&gt;
&lt;/form&gt;</code></pre>
<ul>
<li><code>&lt;fieldset&gt;</code> + <code>&lt;legend&gt;</code> — 접근성에서 섹션 그룹핑</li>
<li><code>autocomplete</code> 속성 — 브라우저 자동완성 지원 (결제 컨버전 향상)</li>
</ul>
<hr>
<h3 id="3-6-상품-이미지-업로드-리뷰-작성">3-6. 상품 이미지 업로드 (리뷰 작성)</h3>
<pre><code class="language-html">&lt;form action=&quot;/review/write&quot; method=&quot;post&quot; enctype=&quot;multipart/form-data&quot;&gt;
    &lt;input type=&quot;hidden&quot; name=&quot;goods_no&quot; value=&quot;98765&quot;&gt;

    &lt;select name=&quot;rating&quot; required&gt;
        &lt;option value=&quot;&quot;&gt;별점 선택&lt;/option&gt;
        &lt;option value=&quot;5&quot;&gt;★★★★★&lt;/option&gt;
        &lt;option value=&quot;4&quot;&gt;★★★★&lt;/option&gt;
        &lt;option value=&quot;3&quot;&gt;★★★&lt;/option&gt;
    &lt;/select&gt;

    &lt;textarea name=&quot;content&quot; minlength=&quot;10&quot; maxlength=&quot;1000&quot;
              required placeholder=&quot;10자 이상 작성&quot;&gt;&lt;/textarea&gt;

    &lt;input type=&quot;file&quot; name=&quot;photos[]&quot;
           accept=&quot;image/*&quot; multiple&gt;

    &lt;button type=&quot;submit&quot;&gt;리뷰 등록&lt;/button&gt;
&lt;/form&gt;</code></pre>
<ul>
<li><strong>파일 업로드 시 <code>enctype=&quot;multipart/form-data&quot;</code> 필수</strong></li>
<li><code>accept=&quot;image/*&quot;</code> — 모바일에서 카메라/갤러리 바로 열림</li>
<li><code>name=&quot;photos[]&quot;</code> — 다중 파일을 배열로 서버 수신</li>
</ul>
<hr>
<h2 id="4-js와-함께-쓰는-패턴">4. JS와 함께 쓰는 패턴</h2>
<p>요즘은 form을 써도 실제 제출은 Ajax로 처리하는 경우가 많다. 페이지 새로고침이 일어나면 SPA 흐름이 깨지기 때문이다. 그래서 <strong>&quot;form의 기본 동작은 쓰되, 실제 전송만 JS가 가로채는&quot;</strong> 방식이 자주 나온다. 엔터키 제출이나 접근성 같은 form의 이점은 그대로 가져가면서 제출 로직만 커스텀하는 패턴이다.</p>
<h3 id="4-1-제출-가로채서-ajax로-보내기">4-1. 제출 가로채서 Ajax로 보내기</h3>
<pre><code class="language-html">&lt;form id=&quot;searchForm&quot; action=&quot;/goods/search&quot; method=&quot;get&quot;&gt;
    &lt;input type=&quot;search&quot; name=&quot;keyword&quot; required&gt;
    &lt;button type=&quot;submit&quot;&gt;검색&lt;/button&gt;
&lt;/form&gt;

&lt;script&gt;
document.getElementById(&#39;searchForm&#39;).addEventListener(&#39;submit&#39;, function(e) {
    e.preventDefault(); // 기본 제출 막기

    const formData = new FormData(this);
    const params = new URLSearchParams(formData);

    fetch(&#39;/goods/search?&#39; + params)
        .then(res =&gt; res.json())
        .then(data =&gt; renderResults(data));
});
&lt;/script&gt;</code></pre>
<ul>
<li><code>FormData(this)</code> — 폼 안의 모든 input 값을 한 번에 수집</li>
<li>JS가 안 되는 환경에서도 <code>action</code>이 있어서 fallback 작동</li>
</ul>
<hr>
<h3 id="4-2-제출-전-커스텀-검증">4-2. 제출 전 커스텀 검증</h3>
<pre><code class="language-javascript">form.addEventListener(&#39;submit&#39;, function(e) {
    const price = this.querySelector(&#39;[name=min_price]&#39;).value;

    if (Number(price) &lt; 1000) {
        e.preventDefault();
        alert(&#39;최소 금액은 1,000원입니다.&#39;);
        return;
    }
    // 통과하면 그대로 제출됨
});</code></pre>
<hr>
<h2 id="5-주의사항">5. 주의사항</h2>
<h3 id="버튼-type-명시">버튼 type 명시</h3>
<p>form 안의 <code>&lt;button&gt;</code>은 <strong>기본값이 <code>type=&quot;submit&quot;</code></strong>. 제출하면 안 되는 버튼(우편번호 찾기, 중복확인 등)은 반드시 <code>type=&quot;button&quot;</code> 명시.</p>
<pre><code class="language-html">&lt;button type=&quot;button&quot; onclick=&quot;checkDuplicate()&quot;&gt;중복확인&lt;/button&gt;
&lt;button type=&quot;submit&quot;&gt;가입하기&lt;/button&gt;</code></pre>
<h3 id="form-중첩-금지">form 중첩 금지</h3>
<p><code>&lt;form&gt;</code> 안에 <code>&lt;form&gt;</code> 넣으면 브라우저가 바깥 form 무시함.</p>
<h3 id="엔터키-의도치-않은-제출-방지">엔터키 의도치 않은 제출 방지</h3>
<p>단일 input만 있는 form에서 엔터치면 자동 제출됨. 원치 않으면:</p>
<pre><code class="language-html">&lt;form onsubmit=&quot;return false;&quot;&gt;</code></pre>
<h3 id="한글-ime-이슈">한글 IME 이슈</h3>
<p>textarea에서 한글 조합 중 엔터 → 줄바꿈인지 제출인지 헷갈림. <code>compositionstart</code>/<code>compositionend</code> 이벤트로 구분 필요.</p>
<pre><code class="language-javascript">let isComposing = false;
textarea.addEventListener(&#39;compositionstart&#39;, () =&gt; isComposing = true);
textarea.addEventListener(&#39;compositionend&#39;, () =&gt; isComposing = false);
textarea.addEventListener(&#39;keydown&#39;, (e) =&gt; {
    if (e.key === &#39;Enter&#39; &amp;&amp; !isComposing &amp;&amp; !e.shiftKey) {
        e.preventDefault();
        form.requestSubmit();
    }
});</code></pre>
<hr>
<h2 id="6-input-type-치트시트-자주-쓰는-것">6. input type 치트시트 (자주 쓰는 것)</h2>
<table>
<thead>
<tr>
<th>type</th>
<th>용도</th>
<th>모바일 키패드</th>
</tr>
</thead>
<tbody><tr>
<td><code>text</code></td>
<td>일반 텍스트</td>
<td>기본</td>
</tr>
<tr>
<td><code>search</code></td>
<td>검색어</td>
<td>기본 + X버튼</td>
</tr>
<tr>
<td><code>email</code></td>
<td>이메일</td>
<td><code>@</code> 포함</td>
</tr>
<tr>
<td><code>tel</code></td>
<td>전화번호</td>
<td>숫자 패드</td>
</tr>
<tr>
<td><code>number</code></td>
<td>수량, 금액</td>
<td>숫자 패드</td>
</tr>
<tr>
<td><code>password</code></td>
<td>비밀번호</td>
<td>기본 (마스킹)</td>
</tr>
<tr>
<td><code>date</code></td>
<td>생년월일</td>
<td>날짜 피커</td>
</tr>
<tr>
<td><code>url</code></td>
<td>홈페이지 주소</td>
<td><code>.com</code> 포함</td>
</tr>
<tr>
<td><code>file</code></td>
<td>파일 업로드</td>
<td>카메라/갤러리</td>
</tr>
<tr>
<td><code>hidden</code></td>
<td>숨김값 (상품번호 등)</td>
<td>-</td>
</tr>
</tbody></table>
<hr>
<h2 id="마무리">마무리</h2>
<p>레거시 페이지를 리뉴얼하면서 느낀 건, 예전 개발자들이 <code>&lt;form&gt;</code>을 쓴 게 구식이라서가 아니라 <strong>그게 가장 안정적인 방식</strong>이라서였다는 점이다. 엔터키, 자동완성, 접근성, IME… 내가 div로 만들고 JS로 하나씩 막아가며 해결했던 문제들을 form은 태그 하나로 해결하고 있었다.</p>
<p>익숙하지 않다고 피하지 말고, 상황에 맞게 form을 쓸 수 있어야겠다는 생각이 들었다. 이 문서가 과거의 나 같은 사람한테 도움이 되면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[상황별로 `word-break`과 `overflow-wrap` 제대로 쓰기]]></title>
            <link>https://velog.io/@wow_da65/%EC%83%81%ED%99%A9%EB%B3%84%EB%A1%9C-word-break%EA%B3%BC-overflow-wrap-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@wow_da65/%EC%83%81%ED%99%A9%EB%B3%84%EB%A1%9C-word-break%EA%B3%BC-overflow-wrap-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Tue, 14 Apr 2026 07:13:03 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>댓글, 채팅, 리뷰처럼 사용자가 입력한 텍스트를 고정폭 박스 안에 보여줘야 할 때, 띄어쓰기 없이 길게 붙여 쓴 문자열(예: URL, 연속된 영문/숫자)이 레이아웃을 밀어내는 문제가 생긴다.</p>
<pre><code>| 댓글 영역 |
-----------
| https://verylongurlexampledotcomsomethingsomething... |  ← 박스 밖으로 삐져나감</code></pre><p>이걸 해결하려고 검색하면 <code>word-break</code>, <code>overflow-wrap</code>, <code>word-wrap</code> 같은 속성들이 우르르 나오는데, 비슷해 보여도 <strong>동작이 제각각</strong>이다. 상황에 맞춰 골라 써야 한다.</p>
<h2 id="네-가지-속성-한눈에-보기">네 가지 속성 한눈에 보기</h2>
<h3 id="1-word-break-break-all">1. <code>word-break: break-all</code></h3>
<p>글자 단위로 무조건 끊는다. 공백이 있어도 무시하고 줄 끝에 도달하면 바로 끊어버림.</p>
<pre><code class="language-css">.text-a { word-break: break-all; }</code></pre>
<h3 id="2-overflow-wrap-break-word">2. <code>overflow-wrap: break-word</code></h3>
<p><strong>평소엔 단어 단위로 끊고, 한 단어가 너무 길어 넘칠 때만 중간에서 끊는다.</strong> 가장 자연스러운 줄바꿈 동작.</p>
<pre><code class="language-css">.text-b { overflow-wrap: break-word; }</code></pre>
<h3 id="3-overflow-wrap-anywhere">3. <code>overflow-wrap: anywhere</code></h3>
<p><code>break-word</code>와 동작은 비슷하지만 <code>min-content</code> 계산에도 영향을 준다. 테이블 셀처럼 컨테이너가 콘텐츠 크기에 맞춰 늘어나는 환경에서 더 안정적.</p>
<pre><code class="language-css">.text-c { overflow-wrap: anywhere; }</code></pre>
<h3 id="4-word-wrap-break-word-구-이름">4. <code>word-wrap: break-word</code> (구 이름)</h3>
<p><code>overflow-wrap</code>이 표준으로 정리되기 전 이름. 동작은 <code>overflow-wrap: break-word</code>와 완전히 동일하고, IE5.5부터 지원하므로 <strong>레거시 호환용으로만</strong> 병기한다.</p>
<pre><code class="language-css">.text-d {
    overflow-wrap: break-word;
    word-wrap: break-word;   /* IE 대응 */
}</code></pre>
<h2 id="실제-차이-비교">실제 차이 비교</h2>
<p>같은 문자열을 <strong>폭 200px 박스</strong>에 넣고 속성만 바꿔보면 차이가 확실히 보인다. 실제 사용자 입력과 비슷하게, 공백이 섞인 문장을 예시로 들어보자.</p>
<pre><code>입력: &quot;안녕하세요 https://verylongurlexampledotcomsomething 입니다&quot;</code></pre><h3 id="word-break-break-all"><code>word-break: break-all</code></h3>
<pre><code>| 안녕하세요 https:/ |
| /verylongurlexamp |
| ledotcomsomething |
| 입니다            |</code></pre><p>→ &quot;안녕하세요&quot;와 &quot;https://&quot; 사이 공백을 무시하고 <strong>글자 단위로 뚝뚝 끊음</strong>. 한글 문장도 중간에 끊길 수 있음.</p>
<h3 id="overflow-wrap-break-word"><code>overflow-wrap: break-word</code></h3>
<pre><code>| 안녕하세요          |
| https://verylongur |
| lexampledotcomsome |
| thingsomething     |
| 입니다             |</code></pre><p>→ 일반 단어 사이는 공백 기준으로 자연스럽게 끊고, <strong>긴 URL만 중간에서 끊음</strong>. 한글 문장은 깨지지 않음. 가독성 훨씬 좋음.</p>
<p>이래서 사용자 입력 텍스트에는 <code>overflow-wrap: break-word</code>가 기본값처럼 쓰인다.</p>
<h3 id="참고-공백-없는-문자열만-있으면-결과가-같아-보인다">참고: 공백 없는 문자열만 있으면 결과가 같아 보인다</h3>
<pre><code>입력: &quot;https://verylongurlexampledotcomsomethingsomething&quot;</code></pre><p>이 경우엔 <code>break-all</code>과 <code>break-word</code> 둘 다 동일하게 글자 단위로 끊은 결과가 나온다.</p>
<pre><code>| https://verylongur |
| lexampledotcomsome |
| thingsomething     |</code></pre><p>하지만 동작 원리는 전혀 다르다. <code>break-all</code>은 <strong>항상</strong> 글자 단위로 끊고, <code>break-word</code>는 <strong>공백이 없어서 어쩔 수 없이</strong> 글자 단위로 끊은 것뿐. 공백이 포함된 순간 위 비교처럼 결과가 갈린다.</p>
<h2 id="상황별-선택-가이드">상황별 선택 가이드</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천 속성</th>
</tr>
</thead>
<tbody><tr>
<td>일반 <code>div</code>, <code>p</code> — 댓글/채팅/리뷰 같은 사용자 입력</td>
<td><code>overflow-wrap: break-word</code> (+ <code>word-wrap</code> 병기)</td>
</tr>
<tr>
<td>테이블 셀(<code>td</code>) 안 긴 문자열</td>
<td><code>overflow-wrap: anywhere</code> 또는 <code>table-layout: fixed</code></td>
</tr>
<tr>
<td>코드 블록, 해시값, 토큰 등 &quot;꽉 채워도 되는&quot; 연속 문자열</td>
<td><code>word-break: break-all</code></td>
</tr>
<tr>
<td>IE 포함 레거시 브라우저 지원 필요</td>
<td><code>word-wrap: break-word</code> 병기 필수</td>
</tr>
</tbody></table>
<h2 id="⚠️-함정-테이블-셀-안에서는-overflow-wrap-break-word가-안-먹힌다">⚠️ 함정: 테이블 셀 안에서는 <code>overflow-wrap: break-word</code>가 안 먹힌다</h2>
<p>여기까지 읽고 &quot;그럼 <code>overflow-wrap: break-word</code>로 통일해야지!&quot; 하고 적용했는데 전혀 동작하지 않는 경우가 있다. <strong>바로 테이블 셀(<code>td</code>) 안에 텍스트가 있을 때다.</strong></p>
<h3 id="왜-안-될까">왜 안 될까</h3>
<p>테이블은 기본적으로 <strong>콘텐츠 크기에 맞춰 폭이 자동으로 늘어나는</strong> 레이아웃이다. 긴 단어가 들어오면 브라우저는 줄바꿈 대신 &quot;셀을 넓히는&quot; 쪽을 선택한다.</p>
<p>그런데 <code>overflow-wrap: break-word</code>는 &quot;넘칠 때만 끊는다&quot;는 조건부 속성이다. 테이블이 애초에 넘치지 않게 셀을 늘려버리니까 <strong>발동 조건 자체가 성립하지 않는다.</strong></p>
<h3 id="같은-css-다른-결과">같은 CSS, 다른 결과</h3>
<pre><code class="language-html">&lt;!-- div 안: 정상 동작 --&gt;
&lt;div style=&quot;width:200px; overflow-wrap:break-word;&quot;&gt;
  verylongurlexampledotcom...
&lt;/div&gt;
→ 200px에서 칼같이 끊김 ✅

&lt;!-- td 안: 무시됨 --&gt;
&lt;table style=&quot;width:200px;&quot;&gt;
  &lt;tr&gt;&lt;td style=&quot;overflow-wrap:break-word;&quot;&gt;
    verylongurlexampledotcom...
  &lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
→ 셀이 단어 길이만큼 늘어나서 테이블이 밖으로 삐져나감 ❌</code></pre>
<h3 id="테이블-셀-해결책-세-가지">테이블 셀 해결책 세 가지</h3>
<h4 id="1-table-layout-fixed-추가">1. <code>table-layout: fixed</code> 추가</h4>
<p>테이블 폭을 콘텐츠와 무관하게 고정시키면 <code>break-word</code>가 정상 동작한다. 단, 다른 셀 폭 계산 방식까지 바뀌므로 기존 레이아웃이 틀어질 수 있다.</p>
<pre><code class="language-css">table { table-layout: fixed; width: 100%; }</code></pre>
<h4 id="2-overflow-wrap-anywhere-사용">2. <code>overflow-wrap: anywhere</code> 사용</h4>
<p><code>break-word</code>와 비슷하지만 <strong>min-content 계산에도 영향</strong>을 준다. 즉 테이블이 셀을 늘려서 피하는 것 자체를 막는다. 별도 설정 없이 테이블 셀에서도 동작.</p>
<pre><code class="language-css">td { overflow-wrap: anywhere; }</code></pre>
<p>단, IE 미지원이고 Safari는 15 이상부터 지원. 레거시 필요 없으면 가장 깔끔한 선택.</p>
<h4 id="3-그냥-word-break-break-all">3. 그냥 <code>word-break: break-all</code></h4>
<p>레이아웃 계산과 무관하게 무조건 끊어버리므로 테이블 안이든 밖이든 상관없이 동작한다. 단어 중간이 끊기는 건 감수.</p>
<pre><code class="language-css">td { word-break: break-all; }</code></pre>
<h3 id="테이블-셀-선택-가이드">테이블 셀 선택 가이드</h3>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천</th>
</tr>
</thead>
<tbody><tr>
<td>테이블 셀 + 모던 브라우저만</td>
<td><code>overflow-wrap: anywhere</code></td>
</tr>
<tr>
<td>테이블 셀 + 레이아웃 고정 가능</td>
<td><code>table-layout: fixed</code> + <code>overflow-wrap: break-word</code></td>
</tr>
<tr>
<td>테이블 셀 + IE 포함 레거시</td>
<td><code>word-break: break-all</code></td>
</tr>
</tbody></table>
<h2 id="실제-사용-사례">실제 사용 사례</h2>
<ul>
<li><strong>Tailwind CSS</strong> — <code>break-words</code> 유틸리티 = <code>overflow-wrap: break-word</code></li>
<li><strong>Bootstrap</strong> — <code>text-break</code> 유틸리티 = <code>word-wrap</code> + <code>overflow-wrap: break-word</code></li>
<li><strong>슬랙, 디스코드 웹, 카카오톡 웹</strong> — 채팅 메시지 영역에 동일 패턴 사용</li>
</ul>
<p>사용자 입력 텍스트를 다루는 거의 모든 서비스가 <code>overflow-wrap: break-word</code> 방식을 쓴다.</p>
<h2 id="정리">정리</h2>
<ul>
<li><code>word-break: break-all</code>은 공백도 무시하고 끊기 때문에 한국어 문장을 중간에서 쪼갤 수 있다. 일반 텍스트엔 부적합.</li>
<li>사용자 입력 같은 자연스러운 문장엔 <code>overflow-wrap: break-word</code>가 기본 선택.</li>
<li><strong>단, 테이블 셀 안에서는 <code>break-word</code>가 안 먹히므로</strong> <code>overflow-wrap: anywhere</code>나 <code>table-layout: fixed</code>를 써야 한다.</li>
<li>레거시까지 챙겨야 하거나 테이블 구조를 건드리기 애매하면, <code>word-break: break-all</code>이 가장 확실하고 안전한 선택지다. (실무에선 이쪽으로 타협하는 경우도 많다)</li>
<li>IE 지원이 필요하면 <code>overflow-wrap</code>과 함께 구 이름인 <code>word-wrap: break-word</code>를 병기한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Blade 템플릿 기초 정리 ]]></title>
            <link>https://velog.io/@wow_da65/Blade-%ED%85%9C%ED%94%8C%EB%A6%BF-%EA%B8%B0%EC%B4%88-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@wow_da65/Blade-%ED%85%9C%ED%94%8C%EB%A6%BF-%EA%B8%B0%EC%B4%88-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 03 Apr 2026 07:09:40 GMT</pubDate>
            <description><![CDATA[<p>Blade 처음 써보니까 생각보다 헷갈리는 부분이 많았다.<br>특히 for문 돌리려고 보니까 문법이 익숙하지 않아서 정리해봤다.<br>퍼블리셔 기준으로 &quot;이거 왜 쓰는지&quot; 위주로 이해하려고 한 글이다.</p>
<hr>
<h2 id="blade란">Blade란?</h2>
<p>Laravel에서 HTML을 만들어주는 템플릿 엔진.</p>
<p>파일 확장자는 <code>.blade.php</code><br>HTML 안에서 <code>@문법</code>, <code>{{ }}</code>를 사용해 서버 데이터를 화면에 출력한다.</p>
<p>🐸 React로 치면 JSX 안에서 map이나 조건 렌더링 하는 느낌이랑 비슷하다.</p>
<p>🐸 퍼블리셔 입장에서는<br>&quot;하드코딩 HTML → 데이터 기반 UI로 바꾸는 과정&quot;이라고 보면 이해가 쉽다.</p>
<hr>
<h2 id="1-변수-출력--">1. 변수 출력 <code>{{ }}</code></h2>
<pre><code class="language-blade">&lt;span&gt;{{ $user[&#39;name&#39;] }}&lt;/span&gt;</code></pre>
<ul>
<li>변수 값을 HTML에 출력</li>
<li><code>&lt;</code>, <code>&gt;</code> 같은 HTML 태그는 자동으로 escape 처리됨 (안전)</li>
</ul>
<p>🐸 사용자 이름, 텍스트 같은 기본 출력에 사용</p>
<hr>
<h2 id="2-반복문-foreach">2. 반복문 <code>@foreach</code></h2>
<pre><code class="language-blade">@foreach ($products as $product)
    &lt;div class=&quot;product-item&quot;&gt;
        &lt;p&gt;{{ $product[&#39;name&#39;] }}&lt;/p&gt;
        &lt;p&gt;{{ $product[&#39;price&#39;] }}원&lt;/p&gt;
    &lt;/div&gt;
@endforeach</code></pre>
<ul>
<li>배열 데이터를 반복해서 UI 생성</li>
<li>상품 리스트, 게시글 목록 등 반복되는 구조에 사용</li>
</ul>
<p>🐸 같은 구조 HTML을 여러 개 찍어내는 느낌<br>🐸 처음엔 그냥 PHP for문 아닌가..? 했는데 써보니까 또 다름</p>
<hr>
<h2 id="3-조건문-if">3. 조건문 <code>@if</code></h2>
<pre><code class="language-blade">@if (!empty($products))
    &lt;div&gt;상품이 있습니다&lt;/div&gt;
@endif</code></pre>
<ul>
<li>조건이 참일 때만 출력</li>
</ul>
<p>🐸 데이터 있을 때만 영역 보여주기 같은 데 많이 씀</p>
<hr>
<h2 id="4-조건-분기-if--elseif--else">4. 조건 분기 <code>@if / @elseif / @else</code></h2>
<pre><code class="language-blade">@if ($product[&#39;status&#39;] === &#39;sale&#39;)
    &lt;span&gt;판매중&lt;/span&gt;
@elseif ($product[&#39;status&#39;] === &#39;soldout&#39;)
    &lt;span&gt;품절&lt;/span&gt;
@else
    &lt;span&gt;준비중&lt;/span&gt;
@endif</code></pre>
<p>🐸 상태값에 따라 UI 바꿀 때 사용<br>🐸 이런 건 거의 무조건 들어가는 듯</p>
<hr>
<h2 id="5-변수-존재-여부-isset">5. 변수 존재 여부 <code>@isset</code></h2>
<pre><code class="language-blade">@isset($user)
    &lt;div&gt;{{ $user[&#39;name&#39;] }}&lt;/div&gt;
@endisset</code></pre>
<ul>
<li>변수가 존재하고 null이 아닐 때만 출력</li>
</ul>
<p>🐸 로그인 정보나 선택된 값 있을 때 체크용</p>
<hr>
<h2 id="6-데이터-접근-방식">6. 데이터 접근 방식</h2>
<p>서버 데이터 예시:</p>
<pre><code class="language-php">$product = [
    &#39;id&#39; =&gt; 1,
    &#39;name&#39; =&gt; &#39;스니커즈&#39;,
    &#39;price&#39; =&gt; 120000,
];</code></pre>
<p>사용:</p>
<pre><code class="language-blade">{{ $product[&#39;name&#39;] }}
{{ $product[&#39;price&#39;] }}</code></pre>
<p>🐸 배열에서 값 꺼내는 건 그냥 JS랑 비슷해서 금방 익숙해짐</p>
<hr>
<h2 id="7-forelse-이거-은근-중요">7. @forelse (이거 은근 중요)</h2>
<pre><code class="language-blade">@forelse ($products as $product)
    &lt;p&gt;{{ $product[&#39;name&#39;] }}&lt;/p&gt;
@empty
    &lt;p&gt;등록된 상품이 없습니다&lt;/p&gt;
@endforelse</code></pre>
<ul>
<li>데이터 있으면 반복</li>
<li>없으면 empty UI 출력</li>
</ul>
<p>🐸 이거 몰라서 if + foreach 따로 썼었는데<br>🐸 그냥 이거 쓰는 게 훨씬 깔끔함</p>
<hr>
<h2 id="8-html-그대로-출력---주의">8. HTML 그대로 출력 <code>{!! !!}</code> (주의)</h2>
<pre><code class="language-blade">{!! $description !!}</code></pre>
<ul>
<li>HTML 태그 그대로 출력</li>
<li>escape 안됨</li>
</ul>
<p>⚠️ 사용자 입력 데이터면 위험함 (XSS)</p>
<p>🐸 진짜 필요한 경우 아니면 잘 안 쓰는 게 맞는 듯</p>
<hr>
<h2 id="9-컴포넌트-분리-include">9. 컴포넌트 분리 <code>@include</code></h2>
<pre><code class="language-blade">@include(&#39;components.product-card&#39;, [&#39;product&#39; =&gt; $product])</code></pre>
<ul>
<li>반복 UI를 분리해서 재사용</li>
<li>유지보수에 중요</li>
</ul>
<p>🐸 같은 UI 여러 번 쓰면 이거로 빼는 게 편함<br>🐸 안 빼면 나중에 수정 지옥됨</p>
<hr>
<h2 id="10-blade-주석">10. Blade 주석</h2>
<pre><code class="language-blade">{{-- 이 주석은 브라우저에 보이지 않음 --}}</code></pre>
<p>🐸 HTML 주석이랑 다르게 아예 안 내려감</p>
<hr>
<h2 id="11-실전-예시-상품-리스트">11. 실전 예시 (상품 리스트)</h2>
<pre><code class="language-blade">@if (!empty($products))
&lt;div class=&quot;product-list&quot;&gt;

    @foreach ($products as $product)
        &lt;div class=&quot;product-card&quot;&gt;
            &lt;h3&gt;{{ $product[&#39;name&#39;] }}&lt;/h3&gt;
            &lt;p&gt;{{ $product[&#39;price&#39;] }}원&lt;/p&gt;

            @if ($product[&#39;status&#39;] === &#39;sale&#39;)
                &lt;span class=&quot;badge&quot;&gt;판매중&lt;/span&gt;
            @else
                &lt;span class=&quot;badge&quot;&gt;품절&lt;/span&gt;
            @endif
        &lt;/div&gt;
    @endforeach

&lt;/div&gt;
@endif</code></pre>
<p>🐸 실제로는 이런 구조 계속 반복해서 쓰게 됨<br>🐸 데이터 체크 + 반복 + 조건 이 세 개가 거의 기본 패턴</p>
<hr>
<h2 id="12-자주-쓰는-문법-요약">12. 자주 쓰는 문법 요약</h2>
<table>
<thead>
<tr>
<th>문법</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>{{ $변수 }}</code></td>
<td>값 출력</td>
</tr>
<tr>
<td><code>{!! $변수 !!}</code></td>
<td>HTML 그대로 출력</td>
</tr>
<tr>
<td><code>@if</code></td>
<td>조건문</td>
</tr>
<tr>
<td><code>@foreach</code></td>
<td>반복</td>
</tr>
<tr>
<td><code>@forelse</code></td>
<td>반복 + empty 처리</td>
</tr>
<tr>
<td><code>@isset</code></td>
<td>변수 존재 확인</td>
</tr>
<tr>
<td><code>@include</code></td>
<td>컴포넌트 분리</td>
</tr>
<tr>
<td><code>$arr[&#39;key&#39;]</code></td>
<td>배열 값 접근</td>
</tr>
</tbody></table>
<hr>
<h2 id="한줄-정리">한줄 정리</h2>
<p>🐸 결국 Blade는<br>&quot;데이터 받아서 HTML 찍어내는 도구&quot;라고 생각하면 편하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[iframe 쓸 때 꼭 알아야 하는 것들 (높이, postMessage, 실무 팁)]]></title>
            <link>https://velog.io/@wow_da65/iframe-%EC%93%B8-%EB%95%8C-%EA%BC%AD-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94-%EA%B2%83%EB%93%A4-%EB%86%92%EC%9D%B4-postMessage-%EC%8B%A4%EB%AC%B4-%ED%8C%81</link>
            <guid>https://velog.io/@wow_da65/iframe-%EC%93%B8-%EB%95%8C-%EA%BC%AD-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94-%EA%B2%83%EB%93%A4-%EB%86%92%EC%9D%B4-postMessage-%EC%8B%A4%EB%AC%B4-%ED%8C%81</guid>
            <pubDate>Fri, 03 Apr 2026 03:24:28 GMT</pubDate>
            <description><![CDATA[<h1 id="iframe-가이드">iframe 가이드</h1>
<p>iframe은 생각보다 자주 보이지만,<br>막상 직접 다루려고 하면 높이 문제나 도메인 제약 때문에 꽤 까다로운 태그다.</p>
<p>특히 그냥 붙여 넣기만 하면 되는 줄 알았다가<br>높이가 안 맞거나, 내부 페이지에 접근이 안 되거나, 반응형 대응이 꼬이는 경우가 많다.</p>
<p>이번 글은 실무에서 iframe을 쓸 때 헷갈리기 쉬운 포인트들을 기준으로 정리해봤다.</p>
<hr>
<h2 id="iframe이란">iframe이란?</h2>
<p>HTML 페이지 안에 <strong>또 다른 HTML 페이지</strong>를 삽입하는 태그.<br>말 그대로 &quot;내부 프레임(inline frame)&quot;.</p>
<pre><code class="language-html">&lt;iframe src=&quot;/other_page.php&quot; width=&quot;400&quot; height=&quot;300&quot;&gt;&lt;/iframe&gt;</code></pre>
<hr>
<h2 id="왜-쓰는지">왜 쓰는지</h2>
<ul>
<li><strong>외부 콘텐츠 삽입</strong> — 유튜브, 지도, 결제창 등 다른 도메인의 페이지를 그대로 가져올 때</li>
<li><strong>독립된 환경 격리</strong> — iframe 안의 CSS/JS가 부모 페이지에 영향을 주지 않음 (반대도 마찬가지)</li>
<li><strong>레거시 페이지 재사용</strong> — 기존 페이지를 수정 없이 다른 페이지 안에 넣고 싶을 때</li>
</ul>
<hr>
<h2 id="언제-쓰는지">언제 쓰는지</h2>
<ul>
<li>결제 모듈 (KCP, 이니시스 등) — 보안상 iframe 필수</li>
<li>외부 서비스 임베드 (YouTube, Google Maps, 광고)</li>
<li>사이드 패널에 별도 페이지를 띄울 때</li>
</ul>
<hr>
<h2 id="장점">장점</h2>
<ul>
<li>부모/자식 페이지의 CSS, JS가 서로 <strong>완전히 격리</strong>됨</li>
<li>외부 도메인 콘텐츠를 간단히 삽입 가능</li>
<li>페이지 전체 새로고침 없이 iframe만 교체 가능</li>
</ul>
<hr>
<h2 id="주의할점-단점">주의할점 (단점)</h2>
<h3 id="1-높이-자동조절-불가">1. 높이 자동조절 불가</h3>
<p>iframe은 일반 <code>&lt;div&gt;</code>와 다르게 <code>height: auto</code>가 <strong>안 먹힌다</strong>.  
height를 지정하지 않으면 브라우저 기본값 <strong>150px</strong>이 적용된다.</p>
<p>그래서 아무 생각 없이 붙이면<br>내용은 많은데 iframe은 <strong>150px짜리 박스만 보이는 상황</strong>이 생긴다.</p>
<p>콘텐츠에 맞추려면 JS로 내부 <code>scrollHeight</code>를 읽어서 직접 세팅해야 한다.</p>
<pre><code class="language-js">// iframe 내부 페이지에서 부모 iframe 높이를 세팅
parent.document.getElementById(&quot;my_iframe&quot;).height = document.body.scrollHeight;</code></pre>
<p>상한/하한이 필요하면 <code>Math.min</code>, <code>Math.max</code>를 사용한다.</p>
<pre><code class="language-js">// 최소 205px, 최대 495px
const nextHeight = Math.max(Math.min(document.body.scrollHeight, 495), 205);
parent.document.getElementById(&quot;my_iframe&quot;).height = nextHeight;</code></pre>
<h3 id="2-html-속성-height-vs-css-styleheight-충돌">2. HTML 속성 height vs CSS style.height 충돌</h3>
<p>iframe의 높이를 세팅하는 방법이 두 가지 있다.</p>
<pre><code class="language-js">// 방법 1: HTML 속성
iframe.height = &quot;300&quot;;

// 방법 2: 인라인 스타일
iframe.style.height = &quot;300px&quot;;</code></pre>
<p><strong>이 두 가지를 섞어 쓰면 충돌이 발생한다.</strong><br>인라인 style이 HTML 속성보다 우선하므로, 이전에 <code>style.height</code>를 세팅한 적이 있으면 이후 <code>iframe.height</code> 변경이 무시될 수 있다.</p>
<p>한 가지 방식으로 통일하거나, 전환 시 이전 값을 초기화해야 한다.</p>
<pre><code class="language-js">// 인라인 스타일 초기화
iframe.style.height = &quot;&quot;;</code></pre>
<p>실무에서는 이런 식으로 섞어 쓰다가<br>“분명 높이를 바꿨는데 왜 안 바뀌지?” 하고 한참 헤매는 경우가 많다.</p>
<h3 id="3-css-important와의-관계">3. CSS !important와의 관계</h3>
<pre><code class="language-css">#my_iframe { height: 495px !important; }
#my_iframe { max-height: 495px !important; }</code></pre>
<p><code>height: !important</code>는 JS의 인라인 스타일 세팅도 덮어쓸 수 있어서,<br>동적 높이 제어가 필요하면 <code>max-height</code>/<code>min-height</code>를 사용하는 것이 좋다.</p>
<h3 id="4-성능">4. 성능</h3>
<p>별도 문서를 로드하므로 CSS, JS 등 리소스가 이중으로 로딩된다.<br>페이지에 iframe이 많을수록 성능 부담이 커진다.</p>
<h3 id="5-seo">5. SEO</h3>
<p>iframe 안의 콘텐츠는 검색엔진에서 <strong>별도의 페이지로 인식된다</strong>.</p>
<p>즉,</p>
<ul>
<li>iframe src의 페이지는 따로 크롤링될 수 있지만  </li>
<li>부모 페이지의 SEO에는 거의 기여하지 않는다</li>
</ul>
<p>👉 iframe은 SEO에 불리하다기보다<br><strong>SEO 관점에서 “기여하지 않는 구조”에 가깝다.</strong></p>
<h3 id="6-반응형-어려움">6. 반응형 어려움</h3>
<p>내부 콘텐츠 크기가 변해도 부모가 자동으로 감지하지 못한다.<br>MutationObserver 등으로 변화를 감시하고 수동으로 높이를 업데이트해야 한다.</p>
<pre><code class="language-js">new MutationObserver(function() {
    parent.document.getElementById(&quot;my_iframe&quot;).height = document.body.scrollHeight;
}).observe(document.body, { childList: true, subtree: true });</code></pre>
<h3 id="7-크로스-도메인-제약">7. 크로스 도메인 제약</h3>
<p>다른 도메인의 iframe이면 JS로 내부 콘텐츠에 접근할 수 없다 (동일 출처 정책).<br>이 경우 postMessage를 사용해야 한다.</p>
<h3 id="8-postmessage">8. postMessage</h3>
<p>postMessage는 부모 페이지와 iframe 사이에서<br>👉 <strong>메시지 형태로 데이터를 주고받는 방식</strong>이다.</p>
<pre><code class="language-js">// iframe 내부에서 부모에게 높이 전달
parent.postMessage({ type: &#39;resize&#39;, height: document.body.scrollHeight }, &#39;*&#39;);

// 부모 페이지에서 수신
window.addEventListener(&#39;message&#39;, function(e) {
    if (e.data.type === &#39;resize&#39;) {
        document.getElementById(&quot;my_iframe&quot;).style.height = e.data.height + &#39;px&#39;;
    }
});</code></pre>
<h4 id="⚠️-보안-주의">⚠️ 보안 주의</h4>
<pre><code class="language-js">parent.postMessage(data, &#39;*&#39;);</code></pre>
<p><code>*</code>는 모든 도메인을 허용하는 방식이라 위험하다.</p>
<pre><code class="language-js">parent.postMessage(data, &#39;https://example.com&#39;);

window.addEventListener(&#39;message&#39;, function(e) {
    if (e.origin !== &#39;https://example.com&#39;) return;
});</code></pre>
<hr>
<h2 id="iframe을-쓰면-안-되는-경우">iframe을 쓰면 안 되는 경우</h2>
<ul>
<li>같은 도메인인데 단순 UI 삽입 → AJAX + div가 훨씬 편함</li>
<li>SEO가 중요한 콘텐츠 → 본문, 상품 설명</li>
<li>반응형이 중요한 경우 → 유지보수 난이도 상승</li>
</ul>
<hr>
<h2 id="대안">대안</h2>
<table>
<thead>
<tr>
<th>방식</th>
<th>설명</th>
<th>적합한 경우</th>
</tr>
</thead>
<tbody><tr>
<td>AJAX + div</td>
<td>fetch()로 HTML 삽입</td>
<td>같은 도메인</td>
</tr>
<tr>
<td>Web Component</td>
<td>Shadow DOM으로 격리</td>
<td>iframe 대체</td>
</tr>
<tr>
<td>모달/팝업</td>
<td>JS로 토글</td>
<td>단순 UI</td>
</tr>
</tbody></table>
<hr>
<h2 id="한-줄-요약">한 줄 요약</h2>
<blockquote>
<p>iframe은 <strong>격리와 외부 삽입에는 강하지만</strong>,  
<strong>높이 제어와 SEO, 반응형 대응에서 까다롭다.</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[모바일 페이지에서 입력창이 아닌 영역을 터치해도 키보드가 노출되는 현상

]]></title>
            <link>https://velog.io/@wow_da65/%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B2%80%EC%83%89-%ED%8E%98%EC%9D%B4%EC%A7%80%EC%97%90%EC%84%9C-%EC%95%84%EB%AC%B4-%EB%8D%B0%EB%82%98-%ED%84%B0%EC%B9%98%ED%95%B4%EB%8F%84-%ED%82%A4%EB%B3%B4%EB%93%9C%EA%B0%80-%EC%98%AC%EB%9D%BC%EC%98%A4%EB%8A%94-%EB%B2%84%EA%B7%B8</link>
            <guid>https://velog.io/@wow_da65/%EB%AA%A8%EB%B0%94%EC%9D%BC-%EA%B2%80%EC%83%89-%ED%8E%98%EC%9D%B4%EC%A7%80%EC%97%90%EC%84%9C-%EC%95%84%EB%AC%B4-%EB%8D%B0%EB%82%98-%ED%84%B0%EC%B9%98%ED%95%B4%EB%8F%84-%ED%82%A4%EB%B3%B4%EB%93%9C%EA%B0%80-%EC%98%AC%EB%9D%BC%EC%98%A4%EB%8A%94-%EB%B2%84%EA%B7%B8</guid>
            <pubDate>Thu, 02 Apr 2026 06:32:35 GMT</pubDate>
            <description><![CDATA[<h2 id="상황">상황</h2>
<p>모바일 검색 페이지에서 검색창이 아닌 다른 영역(스와이퍼, 빈 공간 등)을 터치해도 키보드가 올라왔다.</p>
<h2 id="원인">원인</h2>
<p><code>document</code> 전체에 <code>touchstart</code> 이벤트를 걸어서 검색창에 <code>.focus()</code>를 주고 있었다.</p>
<pre><code class="language-javascript">// 문제의 코드
document.addEventListener(&#39;touchstart&#39;, focusWithGesture, { passive: true });</code></pre>
<p>의도는 iOS 대응이었다. iOS에서는 사용자 제스처 없이 호출한 <code>.focus()</code>로는 키보드가 뜨지 않기 때문에, 첫 터치를 캐치해서 포커스를 주려고 한 것이다. 근데 대상이 <code>document</code>라 <strong>어디를 터치하든</strong> 키보드가 올라왔다.</p>
<h2 id="해결">해결</h2>
<p>이벤트 대상을 <code>document</code> → 검색 입력창으로 변경하고, 한 번 실행 후 이벤트를 제거했다.</p>
<pre><code class="language-javascript">var searchBox = document.getElementById(&#39;mainSearchBox&#39;);

function focusOnce() {
    searchBox.focus();
    searchBox.removeEventListener(&#39;touchstart&#39;, focusOnce);
}

searchBox.addEventListener(&#39;touchstart&#39;, focusOnce, { passive: true });</code></pre>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>기존</th>
<th>수정 후</th>
</tr>
</thead>
<tbody><tr>
<td>이벤트 대상</td>
<td><code>document</code> (페이지 전체)</td>
<td><code>searchBox</code> (검색창만)</td>
</tr>
<tr>
<td>키보드 팝업</td>
<td>아무 데나 터치하면 올라옴</td>
<td>검색창 터치할 때만 올라옴</td>
</tr>
</tbody></table>
<h2 id="배운-점">배운 점</h2>
<ul>
<li>iOS 모바일 브라우저는 사용자 제스처(터치) 컨텍스트 안에서만 <code>input.focus()</code>로 키보드를 띄울 수 있다</li>
<li>그래서 <code>window.onload</code>에서 <code>.focus()</code>를 호출해도 키보드가 안 뜬다</li>
<li>이벤트 위임할 때 대상 범위를 꼭 확인하자. <code>document</code>에 거는 건 진짜 필요한 경우만</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스켈레톤 UI란? 실무에서 쓰는 구현 방법 2가지 정리]]></title>
            <link>https://velog.io/@wow_da65/%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-UI%EB%9E%80-%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EC%93%B0%EB%8A%94-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95-2%EA%B0%80%EC%A7%80-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@wow_da65/%EC%8A%A4%EC%BC%88%EB%A0%88%ED%86%A4-UI%EB%9E%80-%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EC%93%B0%EB%8A%94-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95-2%EA%B0%80%EC%A7%80-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 01 Apr 2026 07:46:41 GMT</pubDate>
            <description><![CDATA[<h2 id="1-스켈레톤-ui란">1. 스켈레톤 UI란?</h2>
<p>목록 UI를 구현하다 보면 데이터 로딩 동안 화면이 비거나 레이아웃이 밀리는 상황을 자주 마주친다.<br>스켈레톤 UI(Skeleton UI)는 그 순간을 채우기 위한 로딩 패턴으로, 실제 콘텐츠와 비슷한 형태의 회색 블록을 먼저 보여준다.<br>뼈대(skeleton)만 먼저 그려두고, 데이터가 준비되면 실제 콘텐츠로 교체하는 방식이다.</p>
<hr>
<h2 id="2-왜-써야-하는가">2. 왜 써야 하는가?</h2>
<h3 id="기존-방식의-문제">기존 방식의 문제</h3>
<pre><code>[데이터 로딩 중] → 빈 화면 or 스피너 → [콘텐츠 등장]</code></pre><p>빈 화면이나 스피너는 사용자 입장에서 &quot;뭔가 고장난 건가?&quot; 하는 불안감을 준다.<br>또한 콘텐츠가 갑자기 나타나면서 레이아웃이 툭툭 밀리는 현상(Layout Shift)이 발생한다.</p>
<h3 id="스켈레톤의-장점">스켈레톤의 장점</h3>
<pre><code>[데이터 로딩 중] → 스켈레톤(뼈대) → [콘텐츠 등장]</code></pre><table>
<thead>
<tr>
<th>항목</th>
<th>효과</th>
</tr>
</thead>
<tbody><tr>
<td>체감 속도</td>
<td>실제보다 빠르게 느껴짐</td>
</tr>
<tr>
<td>레이아웃 안정성</td>
<td>자리를 미리 잡아두어 Layout Shift 없음</td>
</tr>
<tr>
<td>사용자 불안감</td>
<td>&quot;로딩 중&quot;임을 시각적으로 인지시킴</td>
</tr>
<tr>
<td>UX 완성도</td>
<td>서비스 전체의 완성도가 올라 보임</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-언제-쓰는가">3. 언제 쓰는가?</h2>
<p>스켈레톤은 <strong>모든 로딩에 쓰는 게 아니다.</strong> 상황에 맞게 선택해야 한다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>권장 방식</th>
</tr>
</thead>
<tbody><tr>
<td>로딩이 300ms 이상 걸릴 때</td>
<td>스켈레톤</td>
</tr>
<tr>
<td>목록, 카드, 랭킹 등 반복 구조</td>
<td>스켈레톤</td>
</tr>
<tr>
<td>버튼 클릭 후 즉시 응답</td>
<td>스피너 or 비활성화 처리</td>
</tr>
<tr>
<td>페이지 전체 전환</td>
<td>스피너 or 프로그레스바</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-구현-방법-2가지">4. 구현 방법 2가지</h2>
<h3 id="방법-a--별도-스켈레톤-마크업-교체">방법 A — 별도 스켈레톤 마크업 교체</h3>
<p>스켈레톤 전용 HTML을 따로 만들어두고, 로딩 시작 시 삽입 → 완료 시 실제 콘텐츠로 교체하는 방식.</p>
<p><strong>흐름</strong></p>
<pre><code>API 호출 시작 → 스켈레톤 HTML 삽입 → 데이터 수신 → 실제 콘텐츠 HTML로 교체</code></pre><p><strong>HTML (스켈레톤 전용)</strong></p>
<pre><code class="language-html">&lt;div class=&quot;card-skeleton&quot;&gt;
  &lt;div class=&quot;skeleton skeleton--thumbnail&quot;&gt;&lt;/div&gt;
  &lt;div class=&quot;skeleton skeleton--title&quot;&gt;&lt;/div&gt;
  &lt;div class=&quot;skeleton skeleton--desc&quot;&gt;&lt;/div&gt;
&lt;/div&gt;</code></pre>
<p><strong>CSS</strong></p>
<pre><code class="language-css">.skeleton {
  position: relative;
  display: block;
  overflow: hidden;
  background: #ececec;
  border-radius: 4px;
}

/* shimmer 애니메이션 */
.skeleton::after {
  content: &quot;&quot;;
  position: absolute;
  top: 0;
  left: -120px;
  width: 80px;
  height: 100%;
  background: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.7) 50%,
    rgba(255, 255, 255, 0) 100%
  );
  animation: shimmer 1.4s infinite;
}

@keyframes shimmer {
  0%   { left: -120px; }
  100% { left: 100%; }
}

.skeleton--thumbnail { width: 100%; height: 180px; }
.skeleton--title     { width: 70%; height: 16px; margin-top: 12px; }
.skeleton--desc      { width: 50%; height: 12px; margin-top: 8px; }</code></pre>
<p><strong>JS</strong></p>
<pre><code class="language-js">async function loadCard() {
  // 스켈레톤 삽입
  container.innerHTML = skeletonHTML;

  const data = await fetchData();

  // 실제 콘텐츠로 교체
  container.innerHTML = renderCard(data);
}</code></pre>
<p><strong>장점</strong></p>
<ul>
<li>CSS가 짧고 단순하다</li>
<li>스켈레톤 모양을 자유롭게 만들 수 있다</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>마크업이 바뀌면 스켈레톤 HTML도 같이 수정해야 한다</li>
<li>컴포넌트마다 별도 파일이 필요하다</li>
</ul>
<hr>
<h3 id="방법-b--css-클래스-토글">방법 B — CSS 클래스 토글</h3>
<p>기존 실제 마크업에 <code>.skeleton</code> 클래스만 추가/제거하는 방식.<br>별도 스켈레톤 HTML이 필요 없다.</p>
<p><strong>흐름</strong></p>
<pre><code>페이지 렌더링 → section에 .skeleton 클래스 추가 → 데이터 수신 → .skeleton 클래스 제거</code></pre><p><strong>HTML (실제 마크업 그대로 사용)</strong></p>
<pre><code class="language-html">&lt;!-- 로딩 중: skeleton 클래스 있음 --&gt;
&lt;section class=&quot;card-section skeleton&quot;&gt;
  &lt;h2 class=&quot;card-title&quot;&gt;인기 상품&lt;/h2&gt;
  &lt;ul class=&quot;card-list&quot;&gt;
    &lt;li class=&quot;card-item&quot;&gt;
      &lt;span class=&quot;card-rank&quot;&gt;1&lt;/span&gt;
      &lt;span class=&quot;card-name&quot;&gt;상품명&lt;/span&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/section&gt;

&lt;!-- 로딩 완료: skeleton 클래스 제거 --&gt;
&lt;section class=&quot;card-section&quot;&gt;
  ...
&lt;/section&gt;</code></pre>
<p><strong>CSS</strong></p>
<pre><code class="language-css">/* 스켈레톤 상태일 때 하위 요소를 덮어씀 */
.skeleton .card-title,
.skeleton .card-rank,
.skeleton .card-name {
  position: relative;
  overflow: hidden;
  background: #ececec;
  color: transparent;        /* 텍스트 숨기기 */
  border-radius: 4px;
  pointer-events: none;      /* 클릭 막기 */
}

.skeleton .card-title  { width: 80px; height: 18px; }
.skeleton .card-rank   { width: 16px; height: 14px; }
.skeleton .card-name   { width: 74%; height: 14px; }

/* shimmer */
.skeleton .card-title::after,
.skeleton .card-rank::after,
.skeleton .card-name::after {
  content: &quot;&quot;;
  position: absolute;
  top: 0; left: -120px;
  width: 80px; height: 100%;
  background: linear-gradient(
    90deg,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,0.7) 50%,
    rgba(255,255,255,0) 100%
  );
  animation: shimmer 1.4s infinite;
}

@keyframes shimmer {
  0%   { left: -120px; }
  100% { left: 100%; }
}</code></pre>
<p><strong>JS</strong></p>
<pre><code class="language-js">async function loadCard() {
  section.classList.add(&#39;skeleton&#39;);

  const data = await fetchData();
  renderCard(data);

  section.classList.remove(&#39;skeleton&#39;);
}</code></pre>
<p><strong>장점</strong></p>
<ul>
<li>별도 스켈레톤 HTML이 필요 없다</li>
<li>마크업이 바뀌어도 HTML은 수정 불필요 (CSS만 조정)</li>
<li>PHP처럼 서버에서 마크업을 미리 그리는 환경에 특히 적합</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>CSS가 길어진다 (하위 요소 전부 스타일 덮어써야 함)</li>
<li><code>color: transparent</code>, <code>pointer-events: none</code> 등 예외 처리가 많아진다</li>
</ul>
<hr>
<h2 id="5-방법-선택-기준">5. 방법 선택 기준</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천 방법</th>
</tr>
</thead>
<tbody><tr>
<td>AJAX로 콘텐츠 전체를 동적으로 삽입</td>
<td>방법 A (마크업 교체)</td>
</tr>
<tr>
<td>PHP 등 서버에서 마크업을 미리 렌더링</td>
<td>방법 B (클래스 토글)</td>
</tr>
<tr>
<td>컴포넌트 구조가 자주 바뀜</td>
<td>방법 B</td>
</tr>
<tr>
<td>스켈레톤 모양을 세밀하게 커스텀해야 함</td>
<td>방법 A</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-shimmer-애니메이션-원리">6. shimmer 애니메이션 원리</h2>
<p>스켈레톤의 핵심인 shimmer(반짝임) 효과는 <code>::after</code> 가상 요소로 구현한다.</p>
<pre><code>[회색 박스 - overflow: hidden]
       ↑
  [흰색 빛줄기 - position: absolute]
  left: -120px → left: 100% 로 이동 (animation)</code></pre><p><code>overflow: hidden</code> 덕분에 빛줄기가 박스 밖으로 나가면 잘려서 보이지 않는다.<br>이 빛줄기가 왼쪽에서 오른쪽으로 반복 이동하면서 shimmer 효과가 만들어진다.</p>
<hr>
<h2 id="7-실무-팁">7. 실무 팁</h2>
<p><strong>키워드 너비를 다르게 줘서 자연스럽게 만들기</strong></p>
<p>모든 항목의 너비가 똑같으면 부자연스러워 보인다.<br><code>nth-child</code>로 항목마다 너비를 다르게 주면 실제 텍스트처럼 보인다.<br>방법 B(클래스 토글)에서는 요소에 <code>color: transparent</code>와 <code>background</code>가 이미 적용되어 있으므로, <code>width</code>를 직접 덮어쓰면 된다.</p>
<pre><code class="language-css">.skeleton .card-name                           { width: 74%; }
.skeleton .card-item:nth-child(2n) .card-name  { width: 62%; }
.skeleton .card-item:nth-child(3n) .card-name  { width: 84%; }
.skeleton .card-item:nth-child(5n) .card-name  { width: 56%; }</code></pre>
<p><strong>로딩 완료 후 부드럽게 전환</strong></p>
<p>스켈레톤에서 실제 콘텐츠로 바뀔 때 transition을 주면 덜 튀어보인다.</p>
<pre><code class="language-css">.card-section {
  transition: opacity 0.2s ease;
}
.card-section.skeleton {
  opacity: 0.8;
}</code></pre>
<p><strong>접근성 처리</strong></p>
<p>스켈레톤 UI에서 접근성의 핵심은 스켈레톤 요소 하나하나를 스크린 리더에게 설명하는 게 아니라,<br><strong>컨테이너가 현재 로딩 중이라는 상태만 전달하는 것</strong>이다.<br><code>aria-busy=&quot;true&quot;</code>는 &quot;이 영역은 아직 준비 중&quot;이라는 신호를 스크린 리더에 보내고, 내부 콘텐츠 읽기를 보류하게 한다.</p>
<pre><code class="language-html">&lt;section aria-busy=&quot;true&quot; aria-label=&quot;콘텐츠 로딩 중&quot;&gt;</code></pre>
<pre><code class="language-js">// 완료 후: 로딩 끝났음을 알림
section.setAttribute(&#39;aria-busy&#39;, &#39;false&#39;);
section.removeAttribute(&#39;aria-label&#39;);</code></pre>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>정의</td>
<td>로딩 중 실제 콘텐츠 형태를 흉내낸 회색 플레이스홀더</td>
</tr>
<tr>
<td>목적</td>
<td>체감 속도 향상, Layout Shift 방지, UX 완성도</td>
</tr>
<tr>
<td>핵심 기술</td>
<td><code>::after</code> 가상 요소 + <code>shimmer</code> 애니메이션</td>
</tr>
<tr>
<td>구현 방식</td>
<td>별도 마크업 교체 / CSS 클래스 토글</td>
</tr>
<tr>
<td>선택 기준</td>
<td>동적 삽입이면 마크업 교체, 서버 렌더링이면 클래스 토글</td>
</tr>
</tbody></table>
<p>스켈레톤 UI는 단순한 로딩 효과가 아니다.<br>화면을 미리 채워두는 것 자체가 사용자에게 &quot;지금 잘 되고 있다&quot;는 신호를 주는 UX 장치다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 타입스크립트 - ! (느낌표) 이게 뭐길래? Non-Null Assertion Operator]]></title>
            <link>https://velog.io/@wow_da65/TIL-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%8A%90%EB%82%8C%ED%91%9C-%EC%9D%B4%EA%B2%8C-%EB%AD%90%EA%B8%B8%EB%9E%98-Non-Null-Assertion-Operator</link>
            <guid>https://velog.io/@wow_da65/TIL-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%8A%90%EB%82%8C%ED%91%9C-%EC%9D%B4%EA%B2%8C-%EB%AD%90%EA%B8%B8%EB%9E%98-Non-Null-Assertion-Operator</guid>
            <pubDate>Fri, 06 Dec 2024 09:40:44 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-공부한것--기억하고-싶은-내용">오늘 공부한것 &amp; 기억하고 싶은 내용</h2>
<p>TypeScript와 Next.js로 코드를 작성하다 보니, 종종 낯선 느낌표(<code>!</code>)를 마주쳤다.
내가 아는 느낌표는 <code>!=</code> 같은 연산자에 쓰이는 &quot;not&quot; 정도였는데, 갑자기 변수 뒤에 붙는 느낌표가 등장하니 궁금증이 생겼다. 그래서 찾아보았다.</p>
<pre><code class="language-tsx">const dislikePost = async () =&gt; {
    &quot;use server&quot;;
    try {
      const session = await getSession();
      await db.like.delete({
        where: {
          id: {
            postId: id,
            userId: session.id!,
          },
        },
      });
      revalidatePath(/post/${id});
    } catch (e) {}
  };</code></pre>
<p><code>session.id!</code>의 느낌표는 TypeScript에서 <strong>Non-Null Assertion Operator</strong>라고 불린다.</p>
<h3 id="non-null-assertion-operator">Non-Null Assertion Operator</h3>
<ul>
<li>이 연산자는 값이 <code>null</code> 또는 <code>undefined</code>가 아닐 것이라고 개발자가 확신하는 경우에 사용한다.</li>
<li>TypeScript는 기본적으로 값이 <code>null</code> 이나 <code>undefined</code>일 가능성을 엄격하게 체크하기 때문에, 해당값이 확실히 존재한다고 알려주지 않으면 에러를 발생시킬 수 있다.</li>
</ul>
<h3 id="왜-sessionid에-느낌표를-붙였을까">왜 <code>session.id!</code>에 느낌표를 붙였을까?</h3>
<p><code>session.id</code>가 타입 정의상 <code>string | undefined</code>와 같이 <code>undefined</code>가 포함될 수 있는 경우, TypeScript는 안전성을 위해 에러를 발생시킬 수 있다.</p>
<pre><code class="language-tsx">const session: { id?: string } = { id: undefined }; // id가 없을 수도 있는 상황</code></pre>
<p>이런 상황에서 <code>session.id!</code>는 &quot;내가 개발자인 내가 확신하는데, 이 값은 절대 <code>undefined</code>가 아니야!&quot;라고 TypeScript에게 알려주는 것이다.</p>
<p>즉, <code>!</code>를 붙임으로써 타입 검사기를 속이고 해당 값이 반드시 존재한다고 강제한다.</p>
<blockquote>
<p><strong><code>undefined</code>를 처리하기 위한 의미</strong>
<code>undefined</code>와 관련이 있지만 정확히는 TypeScript의 타입 검사기가 <code>undefined</code> 가능성을 인지하고, 그로 인해 발생할 수 있는 오류를 방지하기 위해 사용하는 것이다. <code>undefined</code>일 수도 있는 값을 개발자가 확실히 존재한다고 보장하기 위한 도구라고 이해하면 쉽다.</p>
</blockquote>
<h3 id="주의점">주의점</h3>
<ul>
<li><code>!</code>를 사용할 때는 정말 해당 값이 존재한다는 확신이 있을 때만 사용해야 한다. 그렇지 않으면 런타임 오류가 발생할 수 있다.
만약 값이 <code>undefined</code>일 가능성을 제대로 처리하고 싶다면 다음과 같이 조건문을 추가하는 것이 더 안전하다.</li>
</ul>
<pre><code class="language-tsx">if (!session?.id) {
  throw new Error(&quot;Session ID is not available!&quot;);
}
await db.like.delete({
  where: {
    id: {
      postId: id,
      userId: session.id,
    },
  },
});</code></pre>
<h2 id="배운점--느낀점">배운점 &amp; 느낀점</h2>
<p>유용해 보이지만, 함부로 쓰면 위험할 수 있는 연산자(?)를 또 하나 알게 되었다.
css에서는 <code>!important</code> / typescript <code>any</code>가 있었는데 또 하나 추가되었따.
강제로 선언하는 역할을 하기 때문에 사용하는게 신중함이 필요하다고 했으니 .... 
간단한 프로젝트에서는 <code>!</code>의 남발을 피해야겠지만, 프로젝트 규모가 커지면 어쩔 수 없이 사용하는 경우도 생길 것 같다..... 하지만 typescript는 안전성이 중요하기 때문에 안쓰려고 노력해야겠다</p>
<p><del>실제로 css에서 !important 많이 사용하고 있다 .. 코드가 방대해지면 어쩔 수 없을 거 같다 ^^</del></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Next.js 이미지 업로드 - createObjectURL, arrayBuffer, CloudFlare Images]]></title>
            <link>https://velog.io/@wow_da65/TIL-Next.js-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-createObjectURL-arrayBuffer-CloudFlare-Images</link>
            <guid>https://velog.io/@wow_da65/TIL-Next.js-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%97%85%EB%A1%9C%EB%93%9C-createObjectURL-arrayBuffer-CloudFlare-Images</guid>
            <pubDate>Thu, 05 Dec 2024 11:52:04 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-공부한것--기억하고-싶은-내용">오늘 공부한것 &amp; 기억하고 싶은 내용</h2>
<h3 id="createobjecturl">createObjectURL</h3>
<ul>
<li><code>URL.createObjectURL</code>은 브라우저에서 로컬 파일을 로드하여 미리보기를 제공하는 간단한 방법이다.</li>
</ul>
<pre><code class="language-jsx">export default function AddProduct() {
  const [preview, setPreview] = useState(&quot;&quot;); // 이미지 미리보기 URL 저장
  const onImageChange = (event: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    const {
      target: { files },
    } = event;
    if (!files) return; // 파일이 없으면 종료

    const file = files[0];
    const url = URL.createObjectURL(file); // 파일 객체로 URL 생성
    setPreview(url); // URL 상태 업데이트
  };

  return (
    &lt;div&gt;
      &lt;input type=&quot;file&quot; onChange={onImageChange} /&gt;
      {preview &amp;&amp; &lt;img src={preview} alt=&quot;Preview&quot; /&gt;}
    &lt;/div&gt;
  );
}</code></pre>
<ul>
<li>브라우저에서 로컬 파일을 간단히 로드 가능</li>
<li>이미지 파일을 업로드하기 전에 사용자에게 미리보기를 제공함</li>
<li>메모리 누수를 방지하려면 사용 후 URL을 해제해야함. (URL.revokeObjectURL(url);)</li>
</ul>
<h3 id="arraybuffer">arrayBuffer</h3>
<ul>
<li><code>arrayBuffer</code>는 파일 데이터를 버퍼로 변환하여 서버에 저장하는 방법이다.</li>
<li>파일 데이터를 직접 변환하고 저장할 수 있음</li>
<li>주로 테스트나 임시 파일 작업에 사용(이미지 업로드한척 해야할때 사용)</li>
<li>보안 문제: 클라이언트가 악의적인 파일 이름이나 데이터를 보낼 경우 서버가 위험에 노출될 수 있기에 <code>실제 이미지 업로드로는 권장하지 않음</code></li>
</ul>
<pre><code class="language-jsx">if (data.photo instanceof File) {
  const photoData = await data.photo.arrayBuffer(); // 파일 데이터를 ArrayBuffer로 변환
  await fs.appendFile(`./public/${data.photo.name}`, Buffer.from(photoData)); // 서버에 파일 저장
  data.photo = `/${data.photo.name}`; // 저장된 경로를 photo에 할당
}</code></pre>
<h3 id="cloudflare-images-사용-추천">CloudFlare Images (사용 추천)</h3>
<ul>
<li><code>Cloudflare Images</code>는 이미지 업로드와 관리를 위한 유료 서비스</li>
<li>월 $5로 간단한 이미지 처리와 저장 가능.</li>
<li>API를 사용하여 이미지를 업로드하고 URL로 접근 가능</li>
</ul>
<h4 id="cloudflare-images-설정-과정">Cloudflare Images 설정 과정</h4>
<ol>
<li>Cloudflare 계정 생성 및 로그인</li>
</ol>
<ul>
<li>Cloudflare에서 계정 생성 후 로그인.</li>
</ul>
<ol start="2">
<li>Images 서비스 활성화</li>
</ol>
<ul>
<li>Images → Overview로 이동.</li>
<li>결제 설정 후 서비스 활성화.</li>
</ul>
<ol start="3">
<li>API 토큰 생성</li>
</ol>
<ul>
<li>Images → API → Create API Token으로 이동.</li>
<li>&quot;Read and write to Cloudflare Stream and Images&quot; 템플릿 사용. (Use template 버튼)</li>
<li>Analytics 권한 삭제.</li>
<li>Create Token을 클릭하고 생성된 토큰을 복사.</li>
</ul>
<ol start="4">
<li>.env 파일에 설정 추가</li>
</ol>
<ul>
<li>Images - Overview  &gt; 생성된 API 토큰, Account ID, Account Hash를 .env 파일에 저장<pre><code class="language-env">//env
CLOUDFLARE_API_TOKEN=your_api_token
CLOUDFLARE_ACCOUNT_ID=your_account_id
CLOUDFLARE_ACCOUNT_HASH=your_account_hash</code></pre>
</li>
</ul>
<h2 id="배운점--느낀점">배운점 &amp; 느낀점</h2>
<p>아직 유료서비스를 결제할 단계는 아닌거 같아서 간단하게 arrayBuffer나 createObjectURL를 사용해서 이미지 업로드를 해보았다. 보안 이슈로 인해 권장하지는 않지만 어떤식으로 동작하는지에 대해 자세히 알게 되는 시간이었고, 나중에 포트폴리오나 다른 작업을 하게 된다면 유료 서비스도 한번 사용해보고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Zod - superRefine, z.NEVER]]></title>
            <link>https://velog.io/@wow_da65/%EC%97%85%EB%A1%9C%EB%93%9C%EA%B0%80%EB%8A%A5-TIL-Zod-superRefine-z.NEVER</link>
            <guid>https://velog.io/@wow_da65/%EC%97%85%EB%A1%9C%EB%93%9C%EA%B0%80%EB%8A%A5-TIL-Zod-superRefine-z.NEVER</guid>
            <pubDate>Wed, 04 Dec 2024 00:00:30 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-공부한것--기억하고-싶은-내용">오늘 공부한것 &amp; 기억하고 싶은 내용</h2>
<h3 id="superrefine">.superRefine</h3>
<p><code>ctx.addIssue</code>를 통해 원하는 만큼 이슈를 추가할 수 있다.
함수 실행 중에 <code>ctx.addIssue</code>가 호출되지 않으면 유효성 검사가 통과</p>
<p><code>fatal: true</code>설정 시, 그 다음 refine이 실행되는 것을 방지
<code>z.NEVER</code> 설정 시, 반환 값 자체를 사용하기 위해서가 아닌, 타입 시스템을 맞추기 위함
(함수가 특정한 타입 검사를 통과시키면서도, 그 결과 값을 반환할 필요가 없을 때 사용)
<a href="https://zod.dev/?id=superrefine">https://zod.dev/?id=superrefine</a></p>
<pre><code class="language-jsx">import { z } from &quot;zod&quot;;

const schema = z.object({
  password: z.string(),
  confirmPassword: z.string(),
}).superRefine(({ password, confirmPassword }, ctx) =&gt; {
  if (password !== confirmPassword) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: &quot;Passwords do not match&quot;,
      fatal: true, // 추가 검사 중단
    });
  }
});

schema.parse({
  password: &quot;12345&quot;,
  confirmPassword: &quot;123&quot;,
}); // 에러: &quot;Passwords do not match&quot;</code></pre>
<h3 id="znever">z.NEVER</h3>
<ul>
<li>기본적으로 어떤 값도 통과시키지 않음 → &quot;절대 유효하지 않음&quot;을 명시.</li>
<li>타입 시스템에서 특정 값이나 조건을 명시적으로 차단하려는 경우 사용.</li>
<li>기본적으로 모든 입력을 거부하는 스키마</li>
<li>API 스키마에서 특정 필드의 사용을 차단</li>
<li>타입 유효성 검사에서 never 타입을 활용하여 실수를 방지.</li>
</ul>
<pre><code class="language-typescript">import { z } from &quot;zod&quot;;

// 특정 필드 차단
const userSchema = z.object({
  id: z.string(),
  role: z.enum([&quot;admin&quot;, &quot;user&quot;]),
  extra: z.NEVER, // 이 필드는 절대 허용되지 않음
});

userSchema.parse({
  id: &quot;123&quot;,
  role: &quot;admin&quot;,
  extra: &quot;unexpected_field&quot;, // 에러 발생: &#39;extra&#39; 필드는 허용되지 않음
});

// 타입만 검사 (값을 반환하지 않음)
const neverSchema = z.NEVER;
const result = neverSchema.safeParse(&quot;anything&quot;); // 항상 실패</code></pre>
<h2 id="배운점--느낀점">배운점 &amp; 느낀점</h2>
<p>Zod를 배워서 유효성검사 하는 법을 배웠는데, 이 개념들은 좀 심화과정인거 같다. 커스텀 유효성 검사와 타입 안정성을 높이는데 중요한 개념인거 같아서 나중에 타입스크립트도 함께 더 작업을 하게 된다면 꼭 사용해야 할 필수 개념같다.
지금은 아직 초보 이슈로... 더 공부하는걸로~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Next.js - Image 컴포넌트]]></title>
            <link>https://velog.io/@wow_da65/TIL-Next.js-Image-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</link>
            <guid>https://velog.io/@wow_da65/TIL-Next.js-Image-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</guid>
            <pubDate>Tue, 03 Dec 2024 10:04:56 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-공부한것--기억하고-싶은-내용">오늘 공부한것 &amp; 기억하고 싶은 내용</h2>
<h3 id="image-컴포넌트">Image 컴포넌트</h3>
<p>기본 jsx img가 지원하지 않는 여러가지 강력한기능을 지원한다.</p>
<ul>
<li>로딩 전후로 컴포넌트 위치가 밀리는 content shift를 방지한다.</li>
<li>압축률이나, 화면 크기별 압축 옵션을 제공한다.</li>
<li>필수 prop으로 src, width, height, alt를 입력해야한다.</li>
<li>width, height를 모른다면, fill을 써주면 된다.</li>
<li>fill은 이미지를 자동으로 부모컴포넌트의 크기로 맞춰주는 역할을 한다.</li>
</ul>
<pre><code class="language-jsx">    &lt;Image fill src={photo} alt={title} /&gt; //quality 속성도 있음 (1이면 화질이 저하됨)</code></pre>
<h3 id="image-hostnames">Image Hostnames</h3>
<ul>
<li>NextJS의 Image는 이미지를 자동으로 최적화를 해주어 성능을 향상시키고 빠른 로딩이 되도록 해 준다.
하지만 외부 호스트의 이미지(다른 사이트의 이미지 링크 등)를 불러올 때는 보안 상의 이유로 이 기능이 허용되지 않는다.
따라서 next.config.mjs에서 hostname들을 등록해 주어야 한다.
(nextConfig &gt; images &gt; remotePatterns &gt; hostname)</li>
</ul>
<pre><code class="language-jsx">import type { NextConfig } from &quot;next&quot;;

const nextConfig = {
  images: {
    remotePatterns: [
      {
        hostname: &quot;avatars.githubusercontent.com&quot;,
      },
    ],
  },
};

export default nextConfig;
</code></pre>
<h2 id="배운점--느낀점">배운점 &amp; 느낀점</h2>
<p>매번 이미지 크기로 고통 받았는데, Next.js는 알아서 이미지도 맞게 넣어주고 최적화도 해준다니 정말 갓벽한거 같다.. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Next.js - Middleware, Matcher, Edge Runtime]]></title>
            <link>https://velog.io/@wow_da65/TIL-Next.js-Middleware-Matcher-Edge-Runtime</link>
            <guid>https://velog.io/@wow_da65/TIL-Next.js-Middleware-Matcher-Edge-Runtime</guid>
            <pubDate>Mon, 02 Dec 2024 09:00:53 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-공부한것--기억하고-싶은-내용">오늘 공부한것 &amp; 기억하고 싶은 내용</h2>
<h3 id="middleware">Middleware</h3>
<ul>
<li>말 그대로 중간에서 동작하는 일종의 소트프웨어를 뜻한다. 어떤 것과 다른것 사이에서 실행되는 코드.</li>
<li>미들웨어를 사용하면 request가 완료되기 전에 코드를 실행할 수 있다.</li>
<li>이미지, CSS, JS, Favicon 요청 등 웹 사이트의 모든 단일 request 하나하나 마다 미들웨어가 실행</li>
</ul>
<p>예를들어 user가 profile 페이지로 이동하면 그 request는 profile 페이지로 가고 user에게 profile 페이지를 줘야한다.
그러다 미들웨어를 활성화하면 그 사이에서 실행될 어떤 코드를 갖게 된다. 따라서 profile 페이지로 이동하기 전에 임의의 코드를 실행할 수 있다.</p>
<h4 id="미들웨어-사용-케이스">미들웨어 사용 케이스</h4>
<ol>
<li>인증 및 권한 부여: 특정 페이지나 API 라우트에 대한 액세스 권한을 부여하기 전에 사용자 신원을 확인하고 세션 쿠키를 확인할 때 사용할 수 있다.</li>
<li>서버 사이드 리디렉션: 특정 조건(예: local, 사용자 조건)에 따라 서버에서 사용자를 리디렉션함.</li>
<li>경로 Rewriting: request 속성을 기반으로 API 라우트 또는 페이지에 대한 라우트를 동적으로 재작성하여 A/B 테스트, 기능 출시 또는 레거시 경로를 지원한다.</li>
<li>봇 탐지: 봇 트래픽을 탐지하고 차단하여 리소스를 보호한다.</li>
<li>로깅 및 분석</li>
<li>기능 플래그 지정</li>
</ol>
<h4 id="response">Response</h4>
<p>Fetch API의 Response 인터페이스는 request에 대한 response를 나타낸다.</p>
<h3 id="matcher">Matcher</h3>
<ul>
<li>matcher를 사용하면 matcher에 지정한 특정 경로들에서만 미들웨어가 실행되도록 할 수 있다.<pre><code>matcher: [&quot;/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)&quot;]</code></pre></li>
</ul>
<h3 id="runtimes">Runtimes</h3>
<p>Next.js에는 애플리케이션에서 사용할 수 있는 두 가지 서버 런타임이 있다.</p>
<ol>
<li>Node.js 런타임(기본값): 생태계의 모든 Node.js API 및 호환 패키지에 액세스</li>
<li>Edge 런타임: 제한된 API를 지원하는 Edge 런타임</li>
</ol>
<h4 id="edge-runtime">Edge Runtime</h4>
<ul>
<li>Edge Runtime는 일종의 제한된 버전의 node.js로 생각하면 된다.</li>
<li>미들웨어는 현재 Edge Runtime과 호환되는 API만 지원함</li>
</ul>
<h3 id="배운점--느낀점">배운점 &amp; 느낀점</h3>
<p>페이지 이동 전에 보여져야 할 것과 안 보여져야 할 것을 미리 구분할 수 있다는 점이 정말 편리하게 느껴졌다. 단순히 &quot;중간에 끼어드는 코드&quot;라는 개념을 넘어서, 보안, 최적화, 유저 경험 같은 다양한 측면에서 큰 역할을 할 수 있다는 점이 인상 깊었다.</p>
<p>실제로 사용하면 정말 유용할 것 같지만... 솔직히 아직은 조금 많이 어렵다...😅 그래도 배우면서 &quot;언젠가 이걸 자유자재로 다룰 수 있다면 정말 멋질 것 같다!&quot;라는 생각이 들었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Database Validation, bcrypt, iron-session]]></title>
            <link>https://velog.io/@wow_da65/TIL-Database-Validation-bcrypt-iron-session</link>
            <guid>https://velog.io/@wow_da65/TIL-Database-Validation-bcrypt-iron-session</guid>
            <pubDate>Sun, 01 Dec 2024 23:27:01 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-공부한것--기억하고-싶은-내용">오늘 공부한것 &amp; 기억하고 싶은 내용</h2>
<h3 id="bcrypt">bcrypt</h3>
<ul>
<li>비밀번호를 해시할 때 사용하는 라이브러리</li>
<li>해시함수는 단방향 멱등성 함수</li>
<li>데이터베이스에 비밀번호를 그대로 저장하지 않는다.(해킹의 위험성)</li>
<li>정해진 입력에 따라 정해진 난수를 출력함<pre><code>npm i bcrypt
npm i @types/bcrypt</code></pre></li>
</ul>
<pre><code class="language-jsx">12345 =&gt; hashFunction(12345) =&gt; 4rejglkdhlk4-gdfh-096   (O) //단방향임

hashFunction(4rejglkdhlk4-gdfh-096) =&gt; 12345            (X)</code></pre>
<h4 id="bcrypt하는-이유">bcrypt하는 이유</h4>
<ul>
<li>보안상 데이터가 유출되어도, 원본 비밀번호를 알 수 없으니 해킹당하지않음</li>
<li>정형데이터로 정해진 양식, 정해진 길이로 맞출 수 있음</li>
</ul>
<pre><code class="language-jsx">bcrypt.hash(비밀번호, saltRounds, function(err, hash) {
// 비밀번호 DB에 해시를 저장하세요.
});

---
const hashedPassword = await bcrypt.hash(result.data.password, 12); 
//해싱 알고리즘을 12번 실행하겠다</code></pre>
<h3 id="iron-session">iron-session</h3>
<ul>
<li>JavaScript에서 사용되는 세션 관리 라이브러리로, 안전성과 stateless(무상태) 특징을 갖춘 쿠키 기반 세션을 제공함</li>
<li>쿠키 기반 세션 : 사용자 브라우저에 쿠키를 전송하고, 이 쿠키를 통해 세션을 관리, 사용자가 요청을 보낼 때마다 쿠키가 자동으로 서버로 전송</li>
<li>Stateless 설계 : 세션 데이터를 서버에 저장하지 않고 쿠키에 저장하므로, 서버는 상태를 유지하지 않아도 됨 =&gt; 서버의 부하를 줄이고 확장성을 높이는데 유리함</li>
<li>데이터 암호화 및 복호화 : 단순히 사용자 데이터를 쿠키에 저장하면 보안 문제가 발생할 수 있어 Iron-Session은 데이터를 암호화하여 쿠키에 저장하고, 서버에서 복호화하여 사용함</li>
</ul>
<pre><code>npm i iron-session
https://github.com/vvo/iron-session</code></pre><blockquote>
<p>1password password generator (비밀번호 생성기)
<a href="https://1password.com/password-generator">https://1password.com/password-generator</a></p>
</blockquote>
<h2 id="배운점--느낀점">배운점 &amp; 느낀점</h2>
<p>지금까지 회원가입 프로세스를 공부해보았는데.. 어떤분이 요약해주신게 있어서 올려본다..</p>
<ol>
<li>zod를 이용해서 회원가입 폼을 검증
a. 검증 실패 시, 오류 메세지를 띄움</li>
<li>검증 성공 시, bcrypt를 이용해서 유저가 입력한 비밀번호 해싱
a. 해싱된 비밀번호가 데이터베이스에 저장됨</li>
<li>유저가 입력한 유저명, 이메일, 해싱된 비밀번호를 이용해서 DB에 유저를 생성</li>
<li>유저를 성공적으로 생성했다면 브라우저에 쿠키를 반환
a. iron session을 통해 설정한 cookieName에 해당하는 쿠키가 있는 지 확인하고, 없다면 세션 데이터를 암호화하고 쿠키를 설정함
(쿠키를 설정할 때는 쿠키에 저장할 데이터를 암호화하여 저장함)</li>
<li>위 단계를 모두 통과했다면 특정 페이지로 리다이렉트 처리</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Validation - validator, coerce]]></title>
            <link>https://velog.io/@wow_da65/TIL-Validation-validator-coerce</link>
            <guid>https://velog.io/@wow_da65/TIL-Validation-validator-coerce</guid>
            <pubDate>Sat, 30 Nov 2024 06:39:17 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-공부한것--기억하고-싶은-내용">오늘 공부한것 &amp; 기억하고 싶은 내용</h2>
<h3 id="validator">validator</h3>
<ul>
<li>문자열 유효성 검사 라이브러리<pre><code>npm i validator
npm i --save-dev @types/validator</code></pre></li>
</ul>
<pre><code class="language-jsx">
import validator from &#39;validator&#39;;

const isEmailValid = validator.isEmail(&#39;test@example.com&#39;); // true
console.log(isEmailValid);

-----

const phoneSchema = z
  .string()
  .trim()
  .refine(
    (phone) =&gt; validator.isMobilePhone(phone, &quot;ko-KR&quot;),
    &quot;Wrong phone format&quot;
  );

</code></pre>
<h3 id="coerce">coerce</h3>
<ul>
<li>coerce는 기본 값을 강제로 변환</li>
<li>내장 생성자(String(input), Number(input), new Date(input) 등))를 사용하여 모든 입력을 강제함</li>
</ul>
<pre><code class="language-jsx">import { z } from &#39;zod&#39;;

const schema = z.object({
  phoneNumber: z.coerce.number(),
});

const result = schema.parse({ phoneNumber: &quot;1234567890&quot; });
console.log(result); // { phoneNumber: 1234567890 }

-----

const tokenSchema = z.coerce.number().min(100000).max(999999);</code></pre>
<h2 id="배운점--느낀점">배운점 &amp; 느낀점</h2>
<ul>
<li>input의 값은 항상 string으로 오기 때문에 전화번호 같은건 꼭 number로 가져와야 하므로 coerce는 zod와 함께 잘 쓰일 거 같다.</li>
<li>앞으로 폼 데이터 처리할 때 적극 활용해봐야겠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Prisma 기초]]></title>
            <link>https://velog.io/@wow_da65/TIL-Prisma-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@wow_da65/TIL-Prisma-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Thu, 28 Nov 2024 10:27:51 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-공부한것--기억하고-싶은-내용">오늘 공부한것 &amp; 기억하고 싶은 내용</h2>
<h3 id="prisma">Prisma</h3>
<ul>
<li>데이터베이스와 애플리케이션 사이에서 통역사 역할을 해주는 도구</li>
<li>내가 사용하는 데이터베이스에 맞춰 필요한 파일이나 마이그레이션 파일을 자동으로 생성해주기 때문에 정말 유용함</li>
<li>SQL을 몰라도 데이터베이스를 효율적으로 다룰 수 있게 도와줘서 개발 초보자에게도 유용함</li>
</ul>
<h4 id="prisma-기능-및-장점">Prisma 기능 및 장점</h4>
<ol>
<li>데이터베이스 스키마 정의</li>
<li>자동 마이그레이션</li>
<li>타입 안전성 제공</li>
<li>다양한 데이터베이스 지원</li>
<li>빠른 생산성</li>
</ol>
<hr>
<h3 id="1-prisma-설치와-초기화">1. Prisma 설치와 초기화</h3>
<ul>
<li>vscode 확장 프로그램에서 &quot;Prisma&quot; 설치하면 도움됨</li>
<li>설치 <pre><code>npm i prisma</code></pre></li>
<li>초기화 <pre><code>npx prisma init</code></pre></li>
</ul>
<blockquote>
<p>초기화 결과</p>
</blockquote>
<ul>
<li>prisma/schema.prisma 파일 생성: 이 파일에서 데이터베이스의 구조와 규칙(스키마)을 정의.</li>
<li>.env 파일 생성: 데이터베이스에 접속하기 위한 DATABASE_URL 환경 변수가 여기에 저장.</li>
<li>중요: .env 파일에는 중요한 정보가 들어 있으므로 절대 깃헙에 올리지 않도록 .gitignore에 포함해야 합니다.</li>
</ul>
<h4 id="env-파일-수정">.env 파일 수정</h4>
<pre><code>DATABASE_URL=&quot;file:./database.db&quot;</code></pre><h3 id="2-sqlite와-prisma">2. SQLite와 Prisma</h3>
<ul>
<li>초보자가 배우기엔 SQLite가 쉽고, 로컬에서 실행할 수 있어 추천함</li>
<li>추가로 VSCode에서 <strong>SQLite Viewer</strong> 확장 프로그램을 설치하면 데이터베이스를 시각적으로 볼 수 있어 편리함</li>
</ul>
<h3 id="3-prisma-schema와-models">3. Prisma Schema와 Models</h3>
<ul>
<li>애플리케이션 도메인의 엔터티를 나타냅다.</li>
<li>데이터베이스의 테이블(PostgreSQL과 같은 관계형 데이터베이스) 또는 컬렉션(MongoDB)에 매핑</li>
<li>모델(Model)은 데이터베이스에서 테이블과 비슷한 개념으로, 우리가 다룰 데이터를 어떻게 저장할지 정의한다.</li>
</ul>
<h4 id="schema-파일-schemaprisma">Schema 파일 (schema.prisma)</h4>
<ul>
<li>schema.prisma는 데이터베이스에 어떤 데이터가 저장될지 설명한다.<pre><code>model User {
id             Int     @id @default(autoincrement())//기본 키, 자동 증가
username         String     @unique                     .//고유값
email         String? @unique                        // 선택적 필드 (?)
password         String?
phone         String? @unique
githubId         String? @unique
avatar         String?                                //이미지 넣을때(깃허브면 프로필 사진 저장 가능)
createdAt     DateTime @default(now())            //생성 시간
updatedAt     DateTime @updatedAt                    //수정 시간
}</code></pre></li>
</ul>
<p>&lt;save 시 릴레이션 자동완성 방법&gt;</p>
<pre><code>플러그인 prisma 다운
cmd + shift + p로 JSON settings 파일을 열고

&quot;[prisma]&quot;: {
&quot;editor.defaultFormatter&quot;: &quot;Prisma.prisma&quot;
}</code></pre><h3 id="4-마이그레이션migrations">4. 마이그레이션(Migrations)</h3>
<ul>
<li><strong>스키마 파일을 수정한 후</strong>, 데이터베이스에 반영하려면 <strong>마이그레이션</strong>이 필요하다.<pre><code class="language-bash">npx prisma migrate dev
</code></pre>
</li>
</ul>
<p>Enter a name for the new migration: -&gt; 이름을 정해주고 엔터 (예시: add_user, smstoken)</p>
<pre><code>- 새로운 마이그레이션 파일을 생성하고 변경 사항을 데이터베이스에 적용
- 개발 환경에서만 사용! 프로덕션 환경에서는 사용 금지!
- Prisma는 mpx prisma generate 명령어도 같이 실행한다.

### 5. Prisma Client
- Prisma Client는 Prisma가 자동으로 생성해주는 타입 안전한 쿼리 빌더이다.
- 데이터베이스와 상호작용할 때 사용할 수 있다.
- 설치</code></pre><p>npm install @prisma/client</p>
<pre><code>- 사용 예시
``` jsx
  import { PrismaClient } from &quot;@prisma/client&quot;;
  const db = new PrismaClient();

  async function createUser() {
    await db.user.create({
      data: {
        username: &quot;홍길동&quot;,
        phone: &quot;010-1234-5678&quot;,
      },
    });
  }

  createUser();

//export default db;
</code></pre><h3 id="6-prisma-studio">6. Prisma Studio</h3>
<ul>
<li>Prisma Studio는 데이터베이스를 시각적으로 관리할 수 있는 UI(편집기)</li>
<li>Prisma 스키마 파일에 정의된 모든 모델 목록을 확인하고 데이터베이스를 관리할 수 있다.</li>
<li>Client의 어드민을 위한 UI  <pre><code>npx prisma studio</code></pre></li>
<li><a href="http://localhost:5555/">http://localhost:5555/</a> 로 열림</li>
<li>새로운 사용자를 추가하거나 새로운 모델을 만드는 것과 같은 것을 하고 싶다면 schema.prisma 파일을 수정하고 migrate한 다음 변경사항을 studio에 반영하고 싶다면 studio를 끄고 <code>npx prisma studio</code> 재시작하는걸 추천<blockquote>
<p>schema.prisma 파일 수정
npx prisma migrate dev로 마이그레이션 적용
Studio를 다시 실행 (npx prisma studio)</p>
</blockquote>
</li>
</ul>
<h3 id="7-relations">7. Relations</h3>
<ul>
<li>Relation는 Prisma 스키마의 두 모델 간의 연결이다.</li>
<li>Relation(관계)는 데이터의 구조를 설계하고, 관련된 데이터를 효율적으로 조회하고나 조작할 수 있도록 도와준다.</li>
<li><code>@relation(fields, references)</code> : 관계를 정의하고, 외래 키 필드와 참조되는 모델의 필드를 연결.</li>
</ul>
<pre><code class="language-jsx">model SMSToken {
  // id         Int      @id @default(autoincrement())
  id Int @id @default(autoincrement())
  token      String   @unique
  created_at DateTime @default(now())
  updated_at DateTime @updatedAt
  user       User     @relation(fields: [userId], references: [id])
  userId     Int
}

//user       User 까지만 작성하면 자동완성으로 relation은 나옴!! -편안-</code></pre>
<h3 id="8-ondelete">8. onDelete</h3>
<ul>
<li>Relations(관계)로 작업을 할 때 꼭 알아야 함.</li>
<li>위 SMSToken 코드를 보면 user는 필수값이다. (물음표가 없기 때문)</li>
<li>필수값인 경우에는 삭제를 못한다. </li>
<li>서로 연관된 모델이 지워질때 삭제가 가능해진다.</li>
<li>onDelete 행위는 늘 고려해야한다.</li>
</ul>
<pre><code class="language-jsx">model SMSToken {
  // id         Int      @id @default(autoincrement())
  id Int @id @default(autoincrement())
  token      String   @unique
  created_at DateTime @default(now())
  updated_at DateTime @updatedAt
  user       User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId     Int
}</code></pre>
<h4 id="ondelete">onDelete</h4>
<p>Cascade: 참조 레코드를 삭제하면 참조 레코드의 삭제가 트리거된다. (사용자가 삭제됐을때 사용자가 연결된 모든 SMSToken들도 같이 지워진다)
Restrict: 참조 레코드가 있는 경우 삭제를 방지한다.
NoAction: Restrict과 유사하지만 사용 중인 데이터베이스에 따라 다르다.
SetNull: 참조 필드가 NULL로 설정된다. (optional일 때만 정상 작동/사용자가 삭제됐을때 null로 설정)
SetDefault: 참조 필드가 기본값으로 설정된다.</p>
<h2 id="배운점--느낀점">배운점 &amp; 느낀점</h2>
<p>초기 설정이 좀 복잡해보이기는 하는데 다른 데이터베이스를 다루는 프로그램(?)보다는 단순하다고 하니 열심히 연습해봐야겠다.
db 초기 설계만 잘끝낸다면 분명 좋은 프로젝트가 완성되지 않을까 하는 기대가 생긴다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Next.js - Zod (refine, 정규표현식 표현)]]></title>
            <link>https://velog.io/@wow_da65/TIL-Next.js-Zod-refine-%EC%A0%95%EA%B7%9C%ED%91%9C%ED%98%84%EC%8B%9D-%ED%91%9C%ED%98%84</link>
            <guid>https://velog.io/@wow_da65/TIL-Next.js-Zod-refine-%EC%A0%95%EA%B7%9C%ED%91%9C%ED%98%84%EC%8B%9D-%ED%91%9C%ED%98%84</guid>
            <pubDate>Wed, 27 Nov 2024 14:34:55 GMT</pubDate>
            <description><![CDATA[<h2 id="오늘-공부한것--기억하고-싶은-내용">오늘 공부한것 &amp; 기억하고 싶은 내용</h2>
<p>Zod에는 몇 가지 문자열 관련 유효성 검사가 포함되어 있다.
(<a href="https://zod.dev/?id=strings">https://zod.dev/?id=strings</a>)</p>
<p>문자열 스키마를 만들 때 몇 가지 오류 메시지를 지정할 수 있다.
const name = z.string({
required_error: &quot;Name은 필수입니다.&quot;,
invalid_type_error: &quot;Name은 문자열이어야 합니다.&quot;,
});</p>
<p>유효성 검사 메서드를 사용할 때 추가 인수를 전달하여 사용자 지정 오류 메시지를 제공할 수 있다.
z.string().min(5, { message: &quot;5글자 이상 되어야합니다.&quot; });</p>
<p>.refine 메서드를 통해 사용자 지정 유효성 검사를 할 수 있다.
(<a href="https://zod.dev/?id=refine">https://zod.dev/?id=refine</a>)
z.string().refine((val) ⇒ val.length ≤ 255, {message: “255이하의 문자열이어야 합니다.”});</p>
<p>.refine 은 2개의 인수를 받습니다.</p>
<ol>
<li>유효성 검사 함수</li>
<li>몇가지 옵션
제공되는 옵션은 다음과 같습니다.</li>
</ol>
<ul>
<li>message: 에러 메세지 지정</li>
<li>path: 에러 경로 지정</li>
<li>params: 에러시 메세지를 커스텀하기 위해 사용되는 객체</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>