<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>(O)1_choi.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 10 Dec 2025 14:36:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>(O)1_choi.log</title>
            <url>https://velog.velcdn.com/images/o1_choi/profile/26589765-9ac4-4b26-9d70-689bd84b98cf/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. (O)1_choi.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/o1_choi" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[이메일은 검증하면서 파일은 왜 그냥 믿었을까? (매직바이트 도입기)]]></title>
            <link>https://velog.io/@o1_choi/%EC%9D%B4%EB%A9%94%EC%9D%BC%EC%9D%80-%EA%B2%80%EC%A6%9D%ED%95%98%EB%A9%B4%EC%84%9C-%ED%8C%8C%EC%9D%BC%EC%9D%80-%EC%99%9C-%EA%B7%B8%EB%83%A5-%EB%AF%BF%EC%97%88%EC%9D%84%EA%B9%8C-%EB%A7%A4%EC%A7%81%EB%B0%94%EC%9D%B4%ED%8A%B8-%EB%8F%84%EC%9E%85%EA%B8%B0</link>
            <guid>https://velog.io/@o1_choi/%EC%9D%B4%EB%A9%94%EC%9D%BC%EC%9D%80-%EA%B2%80%EC%A6%9D%ED%95%98%EB%A9%B4%EC%84%9C-%ED%8C%8C%EC%9D%BC%EC%9D%80-%EC%99%9C-%EA%B7%B8%EB%83%A5-%EB%AF%BF%EC%97%88%EC%9D%84%EA%B9%8C-%EB%A7%A4%EC%A7%81%EB%B0%94%EC%9D%B4%ED%8A%B8-%EB%8F%84%EC%9E%85%EA%B8%B0</guid>
            <pubDate>Wed, 10 Dec 2025 14:36:57 GMT</pubDate>
            <description><![CDATA[<p>폼(Form)을 개발할 때 <code>input type=&quot;text&quot;</code>에 들어오는 이메일 주소 하나를 검증하기 위해 정규표현식을 쓰고, 길이 제한을 걸곤 합니다. </p>
<p>유저가 입력한 텍스트를 <strong>&quot;무조건 신뢰해서는 안되는 데이터&quot;</strong>라고 생각하기 때문이죠.</p>
<p><strong>그런데, 파일 입력은 어떻게 하고 계신가요?</strong></p>
<pre><code class="language-ts">&lt;input type=&quot;file&quot; accept=&quot;image/*&quot; /&gt;

if (!file.type.startsWith(&#39;image/&#39;)) {
  throw new Error(&#39;이미지 파일만 가능합니다&#39;);
}

if (file.size &gt; 5 * 1024 * 1024) {
  throw new Error(&#39;5MB 이하만 가능합니다&#39;);
}</code></pre>
<p><code>accept</code> 속성으로 필터링하고, <code>file.type</code>으로 MIME 타입 체크하고, <code>file.size</code>로 용량 제한 걸면 끝!
이렇게 하지 않으셨나요? (사실 제가 그랬습니다🥹)</p>
<p>이미지 파일 업로드 구현 시 <code>accept</code>로 충분하다고 생각했습니다.</p>
<p><strong>하지만 이 검증은 겉모습만 보는 겁니다.</strong></p>
<p><code>resume.pdf</code>의 확장자를 <code>resume.jpg</code>로 바꾸면?<br><code>file.type</code>은 <code>&quot;image/jpeg&quot;</code>가 됩니다.<br><code>accept=&quot;image/*&quot;</code>도 통과합니다.</p>
<p><strong>하지만 실제 내용은 여전히 PDF입니다.</strong></p>
<hr>
<h2 id="❌-accept는-보안-기능이-아닙니다">❌ accept는 보안 기능이 아닙니다.</h2>
<p>브라우저의 <code>accept</code> 속성은 <strong>OS 파일 선택 다이얼로그에 필터링 힌트만 제공</strong>합니다. 
실제 파일 내용에 대한 검증은 전혀 없습니다.</p>
<p>파일 선택 창에서 &quot;모든 파일 (<em>.</em>)&quot;을 선택하면 <code>resume.pdf</code>나 <code>malware.exe</code>도 선택할 수 있습니다. 
그리고 더욱 치명적인 건 위의 예시처럼 <strong>파일 확장자만 <code>.jpg</code>로 바꾸면</strong> 브라우저는 이를 <strong>&quot;이미지 파일&quot;</strong>로 인식하고 통과시킵니다.</p>
<p>이런 파일이 서버로 넘어가면 어떻게 될까요?</p>
<p>서버에서 그대로 스토리지에 업로드 하지 않는 이상 당연히 <strong>서버에서 에러가 발생</strong>합니다.
하지만 그건 사용자가 업로드를 기다린 후의 이야기입니다.</p>
<p>프론트엔드에서 <strong>바로 확인 가능한 문제</strong>를, 유저에게 <strong>업로드 시간을 기다리게 한 뒤</strong> &quot;실패&quot; 메시지로 알려주는 셈이죠.</p>
<hr>
<h2 id="🔍-진짜-파일-형식은-매직바이트로-알-수-있습니다">🔍 진짜 파일 형식은 &#39;매직바이트&#39;로 알 수 있습니다.</h2>
<p>모든 파일은 헤더(맨 앞부분)에 자신이 어떤 포맷인지 알려주는 고유한 시그니처, <strong>매직바이트(Magic Bytes)</strong>를 가지고 있습니다. 파일 전체를 읽을 필요 없이 <strong>처음 24바이트(헤더)</strong>만 읽어보면 정체가 드러납니다.</p>
<p>재미있는 점은 파일 포맷의 세대에 따라 이 매직바이트를 저장하는 방식이 다르다는 것입니다.</p>
<h3 id="1-고전적인-방식">1. 고전적인 방식</h3>
<p>JPEG, PNG, GIF 같은 전통적인 포맷은 <strong>파일의 0번 바이트</strong>부터 자신이 누구인지 바로 선언합니다.</p>
<table>
<thead>
<tr>
<th>포맷</th>
<th>매직바이트 (16진수)</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>JPEG</strong></td>
<td><code>FF D8 FF</code></td>
<td>시작부터 바로 시그니처 등장</td>
</tr>
<tr>
<td><strong>PNG</strong></td>
<td><code>89 50 4E 47 ...</code></td>
<td><code>.PNG</code>를 텍스트로 표현</td>
</tr>
<tr>
<td><strong>PDF</strong></td>
<td><code>25 50 44 46</code></td>
<td><code>%PDF</code> (이미지가 아님!)</td>
</tr>
</tbody></table>
<p>구조가 단순해서 첫 몇 바이트만 비교하면 바로 판별이 가능합니다.</p>
<h3 id="2-컨테이너-방식">2. 컨테이너 방식</h3>
<p>단순히 맨 앞만 보는 것이 아니라, <strong>&#39;택배 상자(컨테이너)&#39;</strong> 구조를 가진 포맷들입니다. 
이들은 맨 앞에는 &quot;나는 상자다&quot;라고 적혀 있고, <strong>중간에 있는 사이즈 정보를 건너뛰어야</strong> 진짜 이름표(포맷)가 나옵니다.</p>
<p>대표적으로 <strong>WebP</strong>와 <strong>HEIC/AVIF</strong>가 있습니다.</p>
<h4 id="a-webp-riff-구조">A. WebP (RIFF 구조)</h4>
<p>WebP는 <strong>RIFF</strong>라는 컨테이너를 사용합니다.</p>
<pre><code class="language-text">52 49 46 46  .. .. .. ..  57 45 42 50
[  R I F F ]  [  사이즈  ]  [ W E B P ]
 0~3 byte      4~7 byte      8~11 byte</code></pre>
<ul>
<li><strong>0~3 byte:</strong> <code>RIFF</code> (컨테이너 선언)</li>
<li><strong>4~7 byte:</strong> 파일 크기 (검증 시 무시)</li>
<li><strong>8~11 byte:</strong> <code>WEBP</code> (진짜 포맷) → <strong>여기를 확인해야 합니다.</strong></li>
</ul>
<h4 id="b-heic--avif-isobmff-구조">B. HEIC / AVIF (ISOBMFF 구조)</h4>
<p>최신 포맷인 HEIC와 AVIF는 <strong>ISOBMFF</strong>라는 더 복잡한 컨테이너를 사용합니다.</p>
<pre><code class="language-text">00 00 00 1C  66 74 79 70  68 65 69 63 ...
[  사이즈  ]  [  f t y p ]  [  h e i c ]
 0~3 byte      4~7 byte      8~11 byte</code></pre>
<ul>
<li><strong>4~7 byte (<code>ftyp</code>):</strong> &quot;나는 미디어 컨테이너야&quot;</li>
<li><strong>8~11 byte (Brand):</strong> &quot;내 진짜 정체는 <strong>HEIC</strong>야&quot;</li>
</ul>
<blockquote>
<p><strong>💡 공통점은 무엇인가요?</strong>
둘 다 <strong>&quot;단일 시그니처&quot;가 아닙니다.</strong>
JPEG처럼 맨 앞만 보고 판단하는 게 아니라, <strong>특정 위치(Offset)로 점프해서</strong> 진짜 정보를 확인해야 한다는 점이 핵심입니다.</p>
<p><strong>💡 왜 방식이 바뀌었나요? (확장성 때문!)</strong></p>
<p><strong>JPEG</strong> 같은 고전 포맷은 &#39;이미지&#39;라는 단일 목적을 위해 만들어졌습니다. 
편지 봉투에 내용물이 바로 들어있는 것과 같습니다.</p>
<p>반면 <strong>HEIC/AVIF/MP4</strong>는 &#39;택배 상자(컨테이너)&#39;와 같습니다. 
이 상자 안에는 이미지뿐만 아니라 비디오, 오디오, 자막, 메타데이터 등 무엇이든 담을 수 있어야 합니다.</p>
<p>그래서 <strong>&quot;나는 규격화된 상자(ftyp)입니다&quot;</strong>라고 먼저 밝히고, <strong>&quot;내용물은 HEIC(brand)입니다&quot;</strong>라고 구체적으로 명시하는 구조가 된 것입니다.</p>
<p><strong>⚠️ 주의할 점</strong></p>
<p><strong>MP4 동영상 파일</strong>도 ISOBMFF구조로 <code>ftyp</code>로 시작합니다. 
따라서 <code>ftyp</code>만 확인하고 통과시키면 <strong>동영상이 이미지 업로더에 올라가는 사고</strong>가 터집니다. 
반드시 <strong>8~11번째 바이트(Brand)</strong>까지 확인해서 <code>heic</code>, <code>avif</code> 인지 콕 집어내야 합니다.</p>
</blockquote>
<hr>
<h2 id="무조건적인-신뢰는-그만-최소한의-무결성을-챙기자">무조건적인 신뢰는 그만, &#39;최소한의 무결성&#39;을 챙기자</h2>
<p>매직바이트를 알고 나니 문득 부끄러움이 밀려옵니다.</p>
<p><strong>왜 <code>input type=&quot;file&quot;</code>로 들어오는 파일은 순진하게 신뢰하고 있었을까요?</strong></p>
<p><code>accept=&quot;image/*&quot;</code>라는 얇은 방패 뒤에 숨어서, 파일 검증에 대해 안일하게 생각했던 제 자신을 반성하게 됩니다. 
파일의 확장자는 언제든 거짓말을 할 수 있는데 말이죠.</p>
<p>물론 이 검증이 보안의 전부가 될 수는 없습니다. </p>
<p>하지만 <strong>매직바이트 검증</strong>을 통해</p>
<p><strong>거짓말하는 확장자(오염된 파일)</strong>를 걸러냄으로써,
<strong>&#39;검증되지 않은 데이터는 처리하지 않는다&#39;</strong>는 기본 원칙을 지킬 수 있게 됩니다.</p>
<p>지금 서비스하고 있는 프로젝트의 파일 업로드 로직을 한 번 열어보세요.
확장자만 믿고 정체불명의 파일들에게 문을 열어주고 있진 않나요?</p>
<hr>
<h3 id="🔗-참고-자료-references">🔗 참고 자료 (References)</h3>
<ul>
<li><a href="https://en.wikipedia.org/wiki/List_of_file_signatures">Wikipedia - List of file signatures</a></li>
<li><strong>ISOBMFF (HEIC/AVIF)</strong>: <a href="https://www.loc.gov/preservation/digital/formats/fdd/fdd000079.shtml">Library of Congress - ISO Base Media File Format</a></li>
<li><strong>WebP (RIFF)</strong>: <a href="https://developers.google.com/speed/webp/docs/riff_container">Google Developers - WebP Container Specification</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[같은 마크업 에러인데 왜 브라우저 반응은 다를까? HTML 파서의 숨겨진 규칙 (feat. Hydration Error)]]></title>
            <link>https://velog.io/@o1_choi/%EA%B0%99%EC%9D%80-%EB%A7%88%ED%81%AC%EC%97%85-%EC%97%90%EB%9F%AC%EC%9D%B8%EB%8D%B0-%EC%99%9C-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%B0%98%EC%9D%91%EC%9D%80-%EB%8B%A4%EB%A5%BC%EA%B9%8C-HTML-%ED%8C%8C%EC%84%9C%EC%9D%98-%EC%88%A8%EA%B2%A8%EC%A7%84-%EA%B7%9C%EC%B9%99-with-hydration-error</link>
            <guid>https://velog.io/@o1_choi/%EA%B0%99%EC%9D%80-%EB%A7%88%ED%81%AC%EC%97%85-%EC%97%90%EB%9F%AC%EC%9D%B8%EB%8D%B0-%EC%99%9C-%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%B0%98%EC%9D%91%EC%9D%80-%EB%8B%A4%EB%A5%BC%EA%B9%8C-HTML-%ED%8C%8C%EC%84%9C%EC%9D%98-%EC%88%A8%EA%B2%A8%EC%A7%84-%EA%B7%9C%EC%B9%99-with-hydration-error</guid>
            <pubDate>Wed, 19 Nov 2025 15:33:08 GMT</pubDate>
            <description><![CDATA[<p>최근 &quot;밑바닥부터 시작하는 웹 브라우저&quot; 북스터디에서 HTML 파서를 간이로 구현해볼 기회가 있었습니다.</p>
<p>&quot;실제 브라우저는 엉망인 HTML을 어떻게 처리할까?&quot; 라는 궁금증이 생겨,의도적으로 유효하지 않은 마크업들을 <a href="https://validator.w3.org/">W3C Validator</a>에 넣어봤습니다.</p>
<p><strong>🥸 아래 코드에서 어떤 마크업 유효성 위반이 있을까요?</strong></p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body&gt;
  &lt;!-- 케이스 1 --&gt;
  &lt;p&gt;
    문단 시작
    &lt;div&gt;이것은 블록 요소입니다&lt;/div&gt;
    문단 끝
  &lt;/p&gt;

  &lt;!-- 케이스 2 --&gt;
  &lt;span&gt;
    인라인 텍스트
    &lt;div&gt;여기도 블록 요소입니다&lt;/div&gt;
  &lt;/span&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>두 케이스 모두 <strong>phrasing content</strong>만 허용하는 요소 안에 <strong>flow content(블록 요소)</strong> 인 <code>&lt;div&gt;</code>를 넣었으니 비슷한 에러가 나올 거라 예상했는데…</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/970e9785-c52d-4ec0-af01-114867fc8cb2/image.png" alt=""></p>
<p><strong>&quot;어? 이상하네요?&quot;</strong></p>
<ul>
<li><code>&lt;span&gt;&lt;div&gt;&lt;/div&gt;&lt;/span&gt;</code>: <strong>&quot;div는 span의 자식으로 허용되지 않는다&quot;</strong> ✅ 예상대로!</li>
<li><code>&lt;p&gt;&lt;div&gt;&lt;/div&gt;&lt;/p&gt;</code>: <strong>&quot;p 요소가 없는데 p 닫는 태그가 보인다&quot;</strong> 🤔 ???</li>
</ul>
<p>왜 비슷한 이슈인데 에러 메시지가 완전히 다를까?</p>
<p>이 이상한 에러 메시지가 <strong>HTML 파서의 동작을 이해하는 핵심 단서</strong>였습니다!</p>
<h2 id="🔍-브라우저는-어떻게-파싱할까">🔍 브라우저는 어떻게 파싱할까?</h2>
<p>&quot;여는 태그 없이 닫는 태그만 있다&quot;는 에러의 의미를 이해하기 위해, 순수 HTML에서 브라우저가 어떻게 동작하는지 테스트해봤습니다.</p>
<h3 id="html-파일">HTML 파일</h3>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;body&gt;
  &lt;div&gt;
    &lt;p&gt;
      문단 시작
      &lt;div&gt;이것은 블록 요소입니다&lt;/div&gt;
      문단 끝
    &lt;/p&gt;
  &lt;/div&gt;

  &lt;div&gt;
    &lt;span&gt;
      인라인 텍스트
      &lt;div&gt;여기도 블록 요소입니다&lt;/div&gt;
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p><strong>브라우저가 실제로 생성한 DOM (개발자 도구 Elements 탭 기준):</strong></p>
<pre><code class="language-html">&lt;!-- p 케이스: 구조가 완전히 바뀜 --&gt;
&lt;p&gt;문단 시작&lt;/p&gt;
&lt;div&gt;블록 요소&lt;/div&gt;
문단 끝
&lt;p&gt;&lt;/p&gt;  &lt;!-- 빈 p가 하나 더 생김! --&gt;

&lt;!-- span 케이스: 그대로 유지됨 --&gt;
&lt;span&gt;
  인라인 텍스트
  &lt;div&gt;블록 요소&lt;/div&gt;
&lt;/span&gt;</code></pre>
<p><code>&lt;p&gt;</code> 케이스는 구조가 완전히 바뀌었고, 심지어 빈 <code>&lt;p&gt;&lt;/p&gt;</code>까지 생겼는데
<code>&lt;span&gt;</code> 케이스는 유효하지 않은 마크업임에도 작성한 구조 그대로 유지되었습니다.</p>
<p>왜 이런 차이가 발생하는지 알아보겠습니다!</p>
<h2 id="1️⃣-optional-end-tags-정상-동작">1️⃣ Optional End Tags (정상 동작)</h2>
<p>HTML 파서는 <code>&lt;p&gt;</code> 바로 뒤에 <code>&lt;div&gt;</code>, <code>&lt;section&gt;</code>, <code>&lt;article&gt;</code> 같은 블록 요소가 나오면<br><strong><code>&lt;/p&gt;</code>를 아예 생략해도 된다</strong>고 규정하고 있습니다.</p>
<p>→ <a href="https://html.spec.whatwg.org/multipage/syntax.html#optional-tags">WHATWG HTML Living Standard - 8.1.2.4 Optional tags</a></p>
<blockquote>
<p>A p element’s end tag may be omitted if the p element is immediately followed by an address, article, aside, blockquote, div …</p>
</blockquote>
<p>즉, 개발자가 <code>&lt;/p&gt;</code>를 안 써도 되고,<br>파서도 <strong>자동으로 <code>&lt;/p&gt;</code>를 삽입해서 <code>&lt;p&gt;</code>를 닫아버립니다</strong>.  
이건 <strong>에러가 아니라 완전히 정상적인 파싱 규칙</strong>입니다.</p>
<pre><code class="language-html">&lt;!-- 개발자가 이렇게만 작성해도 유효한 HTML --&gt;
&lt;p&gt;Hello
&lt;div&gt;World&lt;/div&gt;</code></pre>
<p>→ 브라우저가 만든 실제 DOM  </p>
<pre><code class="language-html">&lt;p&gt;Hello&lt;/p&gt;
&lt;div&gt;World&lt;/div&gt;</code></pre>
<p>✅ 완벽한 HTML, 아무 문제 없음!</p>
<h2 id="2️⃣-parse-error--에러-복구-이게-진짜-문제">2️⃣ Parse Error + 에러 복구 (이게 진짜 문제)</h2>
<p>그런데 <code>&lt;/p&gt;</code>를 <strong>실제로 작성</strong>했죠.</p>
<p><strong>파서가 보는 흐름</strong></p>
<ol>
<li><code>&lt;p&gt;</code> 열림  </li>
<li><code>&lt;div&gt;</code> 만남 → Optional End Tags 규칙 발동 → 자동으로 <code>&lt;/p&gt;</code> 삽입 → p 닫힘  </li>
<li><code>&lt;div&gt;블록 요소&lt;/div&gt;</code> 처리  </li>
<li>텍스트 “문단 끝” 처리  </li>
<li><strong>우리가 쓴 실제 <code>&lt;/p&gt;</code>를 만남</strong> → 그런데 지금 열린 <code>&lt;p&gt;</code>가 없음!</li>
</ol>
<p>여기서부터 진짜 <strong>Parse Error</strong>가 발생합니다.</p>
<p>→ <a href="https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody">WHATWG HTML - “in body” insertion mode, “An end tag whose tag name is “p””</a></p>
<blockquote>
<p>If the stack of open elements does not have a p element in scope, this is a parse error;<br><strong>insert an HTML element for a “p” start tag token</strong> (with no attributes), and then pop that p element off the stack.</p>
</blockquote>
<p>“열린 p가 없으면 파싱 에러야. 그럼 빈 <code>&lt;p&gt;</code> 하나 삽입하고 바로 닫아버려.”</p>
<p>그래서 우리가 개발자 도구에서 보는 그 <strong>의문의 빈 <code>&lt;p&gt;&lt;/p&gt;</code>가 생기는 겁니다</strong>!</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>메커니즘</th>
<th>에러인가?</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><code>&lt;div&gt;</code> 만나서 <code>&lt;p&gt;</code> 닫힘</td>
<td>Optional End Tags 규칙</td>
<td>아니오</td>
<td>정상 동작 (자동 종료)</td>
</tr>
<tr>
<td>실제 <code>&lt;/p&gt;</code> 만남</td>
<td>Unmatched end tag → Parse Error + 복구</td>
<td>예</td>
<td>빈 <code>&lt;p&gt;&lt;/p&gt;</code> 삽입</td>
</tr>
</tbody></table>
<p>이제 W3C Validator 에러 메시지도 완전히 이해가 되죠?</p>
<ul>
<li><code>&lt;span&gt;&lt;div&gt;</code> → 단순 Content Model 위반 (구조는 그대로 유지)</li>
<li><code>&lt;p&gt;&lt;div&gt;&lt;/div&gt;&lt;/p&gt;</code> → Optional End Tags로 일찍 닫히고 → 남은 <code>&lt;/p&gt;</code>가 Parse Error → 복구로 빈 <code>&lt;p&gt;</code> 생성</li>
</ul>
<h2 id="💡-nextjs-hydration-error">💡 Next.js Hydration Error</h2>
<p>이 HTML 파서의 원리를 이해하고 나니, 예전에 겪었던 경험이 떠올랐습니다.
Next.js 프로젝트에서 AI가 생성한 코드에서 이런 에러를 마주쳤던기억이 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/9abd68bf-8d27-4c8b-be46-7fd0cfe1500b/image.png" alt=""></p>
<p>당시에는 <strong>&quot;마크업 유효성이 실패한게 hydration에 왜 걸리지?&quot;</strong></p>
<p>정도로 가볍게 생각하고 넘겼는데, 이제 그 이유를 명확하게 알게 되었습니다.</p>
<h2 id="🎯-nextjs-ssr--hydration-과정">🎯 Next.js SSR + Hydration 과정</h2>
<p>먼저 중요한 전제를 이해해야 합니다.</p>
<h3 id="전제-react는-dom-api를-직접-사용한다">전제: React는 DOM API를 직접 사용한다</h3>
<pre><code class="language-jsx">&lt;p&gt;&lt;div&gt;내용&lt;/div&gt;&lt;/p&gt;

// ↓ JSX 컴파일

React.createElement(&#39;p&#39;, null,
  React.createElement(&#39;div&#39;, null, &#39;내용&#39;)
)

// ↓ 클라이언트에서 실제 DOM 생성 시

const p = document.createElement(&#39;p&#39;);  // DOM API 직접 사용
const div = document.createElement(&#39;div&#39;);
div.textContent = &#39;내용&#39;;
p.appendChild(div);  // HTML 파서를 거치지 않음!</code></pre>
<p>React는 <strong>DOM API를 직접 사용</strong>하므로 HTML 파싱 규칙을 우회합니다!</p>
<p>이제 SSR + Hydration 과정을 단계별로 살펴봅시다.</p>
<h3 id="1단계-서버-사이드-렌더링">1단계: 서버 사이드 렌더링</h3>
<pre><code class="language-javascript">// 서버에서
const html = renderToString(&lt;p&gt;&lt;div&gt;내용&lt;/div&gt;&lt;/p&gt;);
// 결과: &quot;&lt;p&gt;&lt;div&gt;내용&lt;/div&gt;&lt;/p&gt;&quot; (HTML 문자열)

// 브라우저로 전송</code></pre>
<h3 id="2단계-브라우저가-html-파싱">2단계: 브라우저가 HTML 파싱</h3>
<pre><code class="language-html">&lt;!-- 받은 HTML 문자열 --&gt;
&lt;p&gt;&lt;div&gt;내용&lt;/div&gt;&lt;/p&gt;

&lt;!-- HTML 파서가 처리 --&gt;
&lt;p&gt;&lt;/p&gt;          &lt;!-- &lt;div&gt; 만나서 자동 종료 --&gt;
&lt;div&gt;내용&lt;/div&gt;
&lt;p&gt;&lt;/p&gt;          &lt;!-- 일치하지 않는 &lt;/p&gt; 태그가 빈 요소로 변환 --&gt;</code></pre>
<h3 id="3단계-클라이언트-하이드레이션">3단계: 클라이언트 하이드레이션</h3>
<pre><code class="language-javascript">// React가 DOM API로 직접 생성
hydrateRoot(
  document.getElementById(&#39;root&#39;),  // ← 파싱된 DOM
  &lt;p&gt;&lt;div&gt;내용&lt;/div&gt;&lt;/p&gt;             // ← React 예상 구조
);

// React 예상: &lt;p&gt;&lt;div&gt;내용&lt;/div&gt;&lt;/p&gt;
// 실제 DOM: &lt;p&gt;&lt;/p&gt;&lt;div&gt;내용&lt;/div&gt;&lt;p&gt;&lt;/p&gt;
// → Mismatch! Hydration Error!</code></pre>
<h2 id="💡-그냥-넘겼던-에러의-진짜-이유">💡 그냥 넘겼던 에러의 &#39;진짜&#39; 이유</h2>
<p>Next.js에서 하이드레이션 에러가 떴을 때 개선이 쉬운 문제라, <strong>&quot;하이드레이션에서 마크업 유효성을 체크하나?&quot;</strong> 하고 대수롭지 않게 넘겼었는데 브라우저의 HTML파서가 어떻게 동작하는지 공부하고 나서야 왜 에러가 발생했는지 깨달았습니다.</p>
<p>단순히 마크업이 틀려서가 아니라, <strong>HTML파서의 동작으로 인해 서버랑 클라이언트가 만든 DOM이 다르기 때문에 필연적으로 발생하는 에러</strong>였다니... 😅</p>
<p>그 바탕이 되는 브라우저의 동작 원리와 기본기를 이해하는 것이 얼마나 중요한지 느끼는 계기가 되었습니다!</p>
<p>해결은 1초면 끝나는 정말 사소한 이슈였지만, 그 이면에는 이런 디테일한 원리가 숨어있었네요.
역시 &quot;그런가 보다&quot; 하고 넘기는 것보다, 가끔은 &quot;도대체 왜 그럴까?&quot; 하고 파보는 게 개발의 재미인 것 같습니다! 🚀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1개가 사라지고 30개가 생겼습니다 - 예측불가 2024년 회고 💫]]></title>
            <link>https://velog.io/@o1_choi/1%EA%B0%9C%EA%B0%80-%EC%82%AC%EB%9D%BC%EC%A7%80%EA%B3%A0-30%EA%B0%9C%EA%B0%80-%EC%83%9D%EA%B2%BC%EC%8A%B5%EB%8B%88%EB%8B%A4-%EC%98%88%EC%B8%A1%EB%B6%88%EA%B0%80-2024%EB%85%84-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@o1_choi/1%EA%B0%9C%EA%B0%80-%EC%82%AC%EB%9D%BC%EC%A7%80%EA%B3%A0-30%EA%B0%9C%EA%B0%80-%EC%83%9D%EA%B2%BC%EC%8A%B5%EB%8B%88%EB%8B%A4-%EC%98%88%EC%B8%A1%EB%B6%88%EA%B0%80-2024%EB%85%84-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 05 Jan 2025 08:48:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/o1_choi/post/91ba9c55-655a-46be-9f53-c017376c23ad/image.png" alt=""></p>
<p>2025년의 첫 주말, 부트캠프 동기들과 오랜만에 만났습니다. 각자 다른 회사에서 일하면서도 여전히 이렇게 모여 이야기를 나눌 수 있다는 게 참 감사했습니다. 💝</p>
<p>다들 회사에서 2년 차를 보내며 겪은 이야기들을 나누었습니다. 각자의 회사 개발 환경은 어떤지, 우리가 생각하는 잘하는 개발자는 어떤 모습인지, 앞으로의 커리어를 위해 차별화된 이력서는 어떻게 준비해야 할지 등 진지한 이야기를 나누었습니다. 🗣️</p>
<p>업무 얘기를 하다 보니 자연스럽게 스트레스 해소 방법에 대한 이야기로 이어졌고, 각자의 취미 생활도 공유하게 되었습니다. 특히 애완복어를 키운다는 친구의 이야기가 너무 신기했는데... 제 이야기를 들은 친구들의 반응도 비슷했던 것 같네요. 그 이유는 지금부터 시작되는 제 2024년 회고에서 들려드리도록 하겠습니다. 💭</p>
<h2 id="의미-있는-성과와-인정-🏆">의미 있는 성과와 인정 🏆</h2>
<p>4월, 입사 1주년을 맞이하며 연봉 협상을 진행했습니다. 앱으로만 서비스되던 커머스 플랫폼을 웹으로 확장하는 프로젝트를 맡아 진행했는데, 매출 영향도가 높은 기능을 우선순위로 두고 개발 일정을 전략적으로 조정한 결과 계획보다 2개월 일찍 출시할 수 있었습니다. </p>
<p>웹 서비스 출시 이후 전체 매출이 400% 증가했습니다. 물론 이는 앱 설치 없이도 구매가 가능한 웹의 접근성과 때마침 진행된 인플루언서 마케팅의 시너지 효과가 컸습니다. 그래도 결제 로직에서 특별한 이슈 없이 안정적으로 운영된 점은 개발자로서 작지만 확실한 자부심이 되었습니다. 💪</p>
<p>인상 깊었던 또 다른 작업은 체험단 모집을 위한 인스타그램 DM 자동화 개발이었습니다. Python Selenium으로 개발한 매크로는 실제 사용자의 행동 패턴을 최대한 반영하려 노력했습니다. 계정 제재를 피하기 위해 여러 계정을 번갈아 사용해야 했지만, 인건비 절감 효과와 마케팅팀의 긍정적인 피드백을 받으며 즐겁게 진행했던 프로젝트였습니다. 🤖</p>
<p>이런 다양한 성과들을 바탕으로 진행된 연봉 협상에서 25% 인상이라는 결과를 얻을 수 있었습니다. 처음 협상을 앞두고 긴장되었지만, 그동안의 프로젝트들이 회사에 가져온 실질적인 가치를 차근차근 설명드렸고, 기술적인 성장과 함께 비즈니스 임팩트도 보여드릴 수 있었던 것이 주효했던 것 같습니다. 🎉</p>
<h2 id="있었는데-없어졌어요-📚">있었는데, 없어졌어요? 📚</h2>
<p>하지만 상승세가 계속될 것이라는 기대와 달리, 현실은 녹록지 않았습니다. 인플루언서 관련 예상치 못한 이슈와 판매처의 물량 수급 문제 등이 겹치면서 어려움을 겪었고, 결국 서비스 종료가 결정되었습니다.</p>
<p>첫 회사에서 처음으로 맡은 프로젝트였기에 더욱 특별했습니다. 초기 기획 단계부터 참여하여 프로젝트 세팅과 아키텍처를 고민하고, 밤낮으로 코드를 작성하며 애정을 쏟았던 만큼 서비스 종료 소식은 큰 충격이었습니다. 특히 사용자들의 피드백을 반영하며 조금씩 개선해나가던 과정이 무척 즐거웠기에, 더 발전시키지 못하게 된 아쉬움이 컸습니다.</p>
<p>Next.js pages router에서 app router로의 마이그레이션, 모노레포를 통한 어드민 시스템 통합 등 서비스가 안정화된 만큼 새로운 기술적 도전을 계획했었는데, 이를 실현해보지 못한 아쉬움이 남습니다. 쿠팡이나 네이버 스마트스토어 같은 오픈 플랫폼을 활용하지 않고 폐쇄적으로 운영한 전략적 선택에 대한 아쉬움도 있었지만, 동시에 개발자로서 어떤 기술적 개선점들이 있을지 깊이 고민해보는 계기가 되었는데, 이렇게 더 나은 기술과 구조에 대한 갈망이 생겼다는 것은 그만큼 제가 성장했다는 의미일 것 같습니다.</p>
<h2 id="새로운-균형점의-발견-⚖️">새로운 균형점의 발견 ⚖️</h2>
<p>개발자로 전향할 때 &#39;3년 차까지는 취미 없이 개발에만 집중하자&#39;는 다짐을 했었습니다. 하지만 1년이 지나며 업무 외적인 삶의 균형이 필요하다는 것을 깨달았습니다. 악기나 게임 등 여러 취미를 고민하던 중, 우연한 기회에 친구와 함께 간 파충류 박람회에서 새로운 관심사를 발견했습니다. 🦎</p>
<p>강아지나 고양이는 많은 시간과 관심이 필요한데 비해, 파충류는 상대적으로 독립적인 특성이 개발자의 생활 패턴과 잘 맞았습니다. 첫 반려 파충류의 이름을 TypeScript를 따서 &#39;타스&#39;라고 지었는데, 이후 새롭게 익숙해지는 기술 스택의 이름을 따서 입양하는 재미있는 룰을 만들었습니다. </p>
<p>처음에는 하나로 시작했지만, 앞으로 배우고 싶은 기술 스택의 이름을 미리 붙여가며 가족이 늘어나 어느새 30마리에 이르렀습니다. 최근에는 알을 낳기 시작한 아이들도 있어 인큐베이터를 설치하며 더 깊이 있게 파충류에 대해 공부하고 있습니다. 🥚
혹시 이 글을 보시는 분들 중 도마뱀을 키우시는 분들이 계시다면 정보도 나누고 이야기도 나눠봤으면 좋겠네요!</p>
<h2 id="마치며-💫">마치며 💫</h2>
<p>2024년은 전문성을 인정받은 기쁨과 서비스 종료라는 아쉬움을 동시에 경험한 해였습니다. 하지만 이 과정에서 얻은 기술적 경험과 교훈들은 분명 의미 있는 자산이 되었습니다. 예상치 못한 방향에서 찾은 취미를 통해 개발자로서의 성장과 개인의 삶 사이의 균형점도 찾아가고 있습니다.</p>
<p>현재 회사는 새로운 도전을 준비하며 바쁜 나날을 보내고 있습니다. 외주 프로젝트를 진행하면서 동시에 새로운 서비스 기획에 대한 회의를 이어가고 있는데, 이전의 경험들이 새로운 도전에 큰 자산이 되고 있음을 느낍니다. 특히 서비스 종료를 경험하며 배운 교훈들이 새로운 기획을 검토할 때 더욱 세심한 관점을 제공해주고 있습니다.</p>
<p>회사 외적으로는 12월 말부터 20명의 개발자들과 함께하는 사이드 프로젝트에 합류하게 되었습니다. 서로 다른 배경을 가진 개발자들과의 협업은 제게 큰 도전이 될 것 같습니다. 코드 리뷰를 통해 다양한 관점의 피드백을 주고받으며 기술적으로 성장하는 것은 물론, 효과적인 커뮤니케이션 방법과 협업 프로세스에 대해서도 많이 배울 수 있을 것으로 기대됩니다.</p>
<p>또한 2년째 이어오고 있는 북스터디의 스터디장 역할도 계속해서 해나갈 예정입니다. 함께 공부하고 성장하는 과정에서 얻는 인사이트와 네트워킹의 가치를 새삼 느끼고 있기 때문입니다. 스터디를 통해 배운 내용들이 실제 프로젝트에 적용되는 순간들이 특히 보람찼습니다.</p>
<p>2025년에는 이런 다양한 경험과 도전들을 통해 더 단단한 성장을 이어가고 싶습니다. 새로운 프로젝트에서도 기술적 완성도를 높이는 동시에 비즈니스적 가치를 만들어내는 개발자로 성장하고자 합니다. 🌱</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🚀 속도에 진심인 Next.js 프로젝트 NextFaster 파헤치기]]></title>
            <link>https://velog.io/@o1_choi/%EC%86%8D%EB%8F%84%EC%97%90-%EC%A7%84%EC%8B%AC%EC%9D%B8-Next.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-NextFaster-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@o1_choi/%EC%86%8D%EB%8F%84%EC%97%90-%EC%A7%84%EC%8B%AC%EC%9D%B8-Next.js-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-NextFaster-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Sun, 24 Nov 2024 11:53:18 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/o1_choi/post/dd2b9627-1c1d-4916-a1ee-2ffcf8336117/image.png" alt=""></p>
<p>얼마 전, 유튜브를 통해 흥미로운 프로젝트 하나를 발견했습니다. 바로 <a href="https://github.com/ethanniser/NextFaster">NextFaster</a>라는 프로젝트인데요, 이름에서 느껴지듯이 오로지 하나의 목표를 향해 달리고 있었습니다.</p>
<p>&quot;어떻게 하면 Next.js 애플리케이션을 가장 빠르게 만들 수 있을까?&quot;</p>
<p>이 프로젝트는 Next.js 15를 기반으로, 인프라 비용이나 리소스 사용량보다는 오로지 &#39;속도&#39;라는 하나의 가치에 모든 초점을 맞추고 있습니다. 어떻게 하면 사용자에게 가장 빠른 경험을 제공할 수 있을지, 그 극한의 최적화를 추구했죠. 특히 두 가지 전략이 눈에 띄었습니다.</p>
<h2 id="1-aggressive-prefetching-방문할-것-같은-페이지는-전부-미리-가져온다">1. Aggressive Prefetching: &quot;방문할 것 같은 페이지는 전부 미리 가져온다&quot;</h2>
<p>Next.js에서는 Link 컴포넌트를 통해 기본적인 프리패칭을 제공합니다. 하지만 Next.js의 기본 Link 컴포넌트를 확장한 커스텀 컴포넌트를 사용하여 더욱 적극적인 프리패칭 전략을 구현했습니다.</p>
<pre><code class="language-typescript">//src/components/ui/link.tsx

// 이미지 프리패치 함수
async function prefetchImages(href: string) {
  // 외부 링크나 특정 경로(/order, /)는 프리패치 제외
  if (!href.startsWith(&quot;/&quot;) || href.startsWith(&quot;/order&quot;) || href === &quot;/&quot;) {
    return [];
  }

  // API를 통해 이미지 정보 가져오기 (낮은 우선순위로 요청)
  const url = new URL(href, window.location.href);
  const imageResponse = await fetch(`/api/prefetch-images${url.pathname}`, {
    priority: &quot;low&quot;,
  });

  const { images } = await imageResponse.json();
  return images as PrefetchImage[];
}

// 이미 프리패치된 이미지 추적을 위한 Set
const seen = new Set&lt;string&gt;();

export const Link: typeof NextLink = (({ children, ...props }) =&gt; {
  const [images, setImages] = useState&lt;PrefetchImage[]&gt;([]); // 프리패치할 이미지 목록
  const [preloading, setPreloading] = useState&lt;(() =&gt; void)[]&gt;([]); // 현재 프리로딩 중인 이미지들의 cleanup 함수
  const linkRef = useRef&lt;HTMLAnchorElement&gt;(null); 
  const router = useRouter();
  let prefetchTimeout: NodeJS.Timeout | null = null; // 프리패치 타이머 ID

  useEffect(() =&gt; {
    // prefetch prop이 false면 프리패치 비활성화
    if (props.prefetch === false) {
      return;
    }

    const linkElement = linkRef.current;
    if (!linkElement) return;

    const observer = new IntersectionObserver(
      (entries) =&gt; {
        const entry = entries[0];
        if (entry.isIntersecting) {
          // 링크가 뷰포트에 보이면 300ms 후 프리패치 시작
          prefetchTimeout = setTimeout(async () =&gt; {
            router.prefetch(String(props.href));
            await sleep(0); // 문서 프리패치가 먼저 일어나도록 지연
            void prefetchImages(String(props.href)).then((images) =&gt; {
              setImages(images);
            }, console.error);
            observer.unobserve(entry.target);
          }, 300);
        } else if (prefetchTimeout) {
          // 링크가 뷰포트에서 사라지면 프리패치 취소
          clearTimeout(prefetchTimeout);
          prefetchTimeout = null;
        }
      },
      { rootMargin: &quot;0px&quot;, threshold: 0.1 } // 10% 이상 보이면 활성화
    );

    observer.observe(linkElement);

    return () =&gt; {
      observer.disconnect();
      if (prefetchTimeout) {
        clearTimeout(prefetchTimeout);
      }
    };
  }, [props.href, props.prefetch]);

  return (
    &lt;NextLink
      ref={linkRef}
      prefetch={false} // 기본 프리패치 비활성화 (useEffect 내부 커스텀 구현 사용)
      onMouseEnter={() =&gt; {
        // 마우스가 링크 위로 오면 즉시 프리패치
        router.prefetch(String(props.href));
        if (preloading.length) return;
        const p: (() =&gt; void)[] = [];
        for (const image of images) {
          const remove = prefetchImage(image);
          if (remove) p.push(remove);
        }
        setPreloading(p);
      }}
      onMouseLeave={() =&gt; {
        // 마우스가 떠나면 프리로딩 취소 및 정리
        for (const remove of preloading) {
          remove();
        }
        setPreloading([]);
      }}
      onMouseDown={(e) =&gt; {
        // 내부 링크일 경우 기본 동작 방지 및 라우터 사용
        const url = new URL(String(props.href), window.location.href);
        if (
          url.origin === window.location.origin &amp;&amp;
          e.button === 0 &amp;&amp;          // 좌클릭만
          !e.altKey &amp;&amp;               // 수정자 키 없이
          !e.ctrlKey &amp;&amp;
          !e.metaKey &amp;&amp;
          !e.shiftKey
        ) {
          e.preventDefault();
          router.push(String(props.href));
        }
      }}
      {...props}
    &gt;
      {children}
    &lt;/NextLink&gt;
  );
}) as typeof NextLink;

// 개별 이미지 프리패치 함수
function prefetchImage(image: PrefetchImage) {
  // lazy 로딩이거나 이미 프리패치된 이미지는 스킵
  if (image.loading === &quot;lazy&quot; || seen.has(image.srcset)) {
    return;
  }

  // 새 이미지 객체 생성 및 설정
  const img = new Image();
  img.decoding = &quot;async&quot;;           // 비동기 디코딩
  img.fetchPriority = &quot;low&quot;;        // 낮은 우선순위로 가져오기
  img.sizes = image.sizes;
  seen.add(image.srcset);           // 중복 프리패치 방지
  img.srcset = image.srcset;
  img.src = image.src;
  img.alt = image.alt;

  let done = false;
  // 로드 완료 또는 에러 시 완료 표시
  img.onload = img.onerror = () =&gt; {
    done = true;
  };

  // cleanup 함수 반환
  return () =&gt; {
    if (done) return;
    img.src = img.srcset = &quot;&quot;;      // 로드 취소
    seen.delete(image.srcset);      // Set에서 제거
  };
}</code></pre>
<p>이 커스텀 Link 컴포넌트는 단순히 페이지 콘텐츠뿐만 아니라, 해당 페이지에 있는 이미지들까지 미리 가져옵니다. <code>IntersectionObserver</code>를 사용해 뷰포트에 링크가 10%만 보여도 말이죠. 마치 &quot;이 링크가 조금이라도 보이네? 그럼 미리 가져와야지!&quot;라는 느낌입니다.</p>
<p>하지만 무작정 가져오지는 않습니다. 300ms의 지연 시간을 두어 불필요한 프리페칭을 방지하고, 사용자가 마우스를 링크에서 떼면 즉시 프리페칭을 취소합니다. 또한 seen이라는 <code>Set 객체</code>를 사용해 이미 프리패치된 이미지는 다시 가져오지 않도록 하고, <code>lazy loading</code>이 설정된 이미지는 프리페칭에서 제외시킵니다. 모든 이미지는 <code>fetchPriority: &quot;low&quot;</code>로 설정되어 중요한 리소스의 로딩을 방해하지 않도록 했습니다.</p>
<h2 id="2-aggressive-caching-한-번-가져온-데이터는-다시-요청하지-않는다">2. Aggressive Caching: &quot;한 번 가져온 데이터는 다시 요청하지 않는다&quot;</h2>
<p>먼저 모든 다이나믹 경로에 generateStaticParams를 활용해 빌드 타임에 가능한 경로를 미리 생성했습니다.</p>
<pre><code class="language-typescript">export async function generateStaticParams() {
  return await db.select({ collection: collections.slug }).from(collections);
}</code></pre>
<p>여기에 더해 Next.js의 unstable_cache와 React의 cache를 결합한 전략이 특히 인상적이었습니다.</p>
<pre><code class="language-typescript">export const unstable_cache = &lt;Inputs extends unknown[], Output&gt;(
  callback: (...args: Inputs) =&gt; Promise&lt;Output&gt;,
  key: string[],
  options: { revalidate: number },
) =&gt; cache(next_unstable_cache(callback, key, options));</code></pre>
<p>React의 <code>cache</code>로 메모리 내 중복 요청을 방지하고, Next.js의 <code>unstable_cache</code>로 영구적인 데이터 캐싱을 구현했습니다. </p>
<p>결과적으로? 런타임에서 발생하는 불필요한 데이터베이스 쿼리가 없습니다. 모든 것이 미리 준비되어 있고, 캐시되어 있고, 최적화되어 있는 거죠.</p>
<h2 id="마무리">마무리</h2>
<p>성능 최적화를 위해 수많은 프리패치 요청을 생성하는 접근 방식은 비용 측면에서 실무에 적용하기는 어렵겠지만, 오로지 &#39;속도&#39;만을 위해 만들어진 재미있는 프로젝트였습니다.</p>
<p>프로젝트 코드를 구경하며 <code>Route Segment Config(ex: dynamic = &#39;force-static&#39;)</code>가 페이지나 레이아웃 파일에서만 사용 가능하다고 알고 있었는데, route handler에서도 활용할 수 있다는 것을 알게되었습니다. </p>
<p>또한 React의 <code>cache</code>와 Next.js의 <code>unstable_cache</code>에 대해서도 다시 한 번 학습할 수 있었습니다. 처음에는 unstable_cache가 내부적으로 React의 cache를 사용할 것이라 생각했지만, 실제로는 완전히 다른 동작 방식을 가진 별개의 기능이라는 것을 알게 되었습니다. 
(<a href="https://nextjs.org/docs/app/api-reference/functions/unstable_cache">unstable_cache</a>는 이미 legacy API가 되었으며, <a href="https://nextjs.org/docs/canary/app/api-reference/directives/use-cache">use cache</a> 사용을 권장)</p>
<h2 id="참고">참고</h2>
<blockquote>
</blockquote>
<p><a href="https://github.com/ethanniser/NextFaster">https://github.com/ethanniser/NextFaster</a>
<a href="https://www.youtube.com/watch?v=7bfTpZxRGto">https://www.youtube.com/watch?v=7bfTpZxRGto</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 동시성 모델: Lane 이해하기]]></title>
            <link>https://velog.io/@o1_choi/React-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AA%A8%EB%8D%B8-Lane-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@o1_choi/React-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AA%A8%EB%8D%B8-Lane-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 10 Nov 2024 11:45:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/o1_choi/post/1007991d-829f-41bc-ba48-182415292355/image.png" alt=""></p>
<p>그동안 React의 내부 동작을 깊이 있게 다룬 <a href="https://goidle.github.io/">리액트 톺아보기</a> 시리즈도 여러 번 읽고 React 소스코드도 살펴보았지만, 세부적인 구현을 완벽히 이해하기란 쉽지 않았습니다. 그래서 이번 포스팅에서는 너무 깊은 구현 상세보다는, React 18에서 동시성의 큰 흐름을 정리해보고자 합니다. </p>
<p>동시성의 핵심은 더 나은 사용자 경험을 위한 화면 전환과 높은 응답성 유지에 있습니다. 
이를 가능하게 하는 핵심 메커니즘인 Lane 모델을 중심으로 알아보겠습니다.</p>
<h2 id="업데이트의-이해">업데이트의 이해</h2>
<p>React에서 발생하는 업데이트는 종류와 중요도가 다양합니다. 일상적으로 접하는 몇 가지 상황을 통해 이해해보겠습니다.</p>
<ol>
<li><p><strong>검색어 자동완성</strong></p>
<ul>
<li>검색어 입력 - 사용자가 입력하는 텍스트는 즉시 화면에 표시되어야 함</li>
<li>추천 검색어 목록 - 입력된 텍스트를 기반으로 서버에서 데이터를 받아와 표시</li>
<li>일정 시간 내에 추천 검색어가 표시되지 않으면 사용자 경험이 저하됨</li>
</ul>
</li>
<li><p><strong>SNS 게시글 작성</strong></p>
<ul>
<li>텍스트 입력 - 사용자가 입력하는 내용이 즉시 화면에 표시되어야 함</li>
<li>해시태그 추천 - 입력한 &#39;#&#39; 다음의 텍스트를 기반으로 추천 목록 표시</li>
<li>이미지 미리보기 - 첨부한 이미지의 썸네일 생성과 표시</li>
</ul>
</li>
<li><p><strong>쇼핑몰 상품 필터링</strong></p>
<ul>
<li>필터 선택 UI - 체크박스나 라디오 버튼의 상태는 즉시 변경되어야 함</li>
<li>상품 목록 업데이트 - 수천 개의 상품 중 조건에 맞는 상품을 필터링하고 정렬</li>
<li>가격 범위 설정 - 슬라이더 움직임은 부드럽게, 실제 상품 필터링은 적절한 타이밍에</li>
</ul>
</li>
</ol>
<p>위의 사례에서 볼 수 있듯이, 업데이트는 크게 두 가지 특성으로 분류할 수 있습니다.</p>
<ul>
<li><p><strong>즉각적인 반응이 필요한 업데이트</strong></p>
<ul>
<li>사용자의 직접적인 조작에 대한 반응</li>
<li>지연되면 &quot;버벅임&quot;을 느끼게 되는 업데이트</li>
<li>예시:<ul>
<li>검색창에 타이핑하는 텍스트 표시</li>
<li>게시글 작성 시 텍스트 입력</li>
<li>체크박스 선택/해제 상태 변경</li>
<li>슬라이더 핸들 이동</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>지연 가능한 전환 업데이트</strong></p>
<ul>
<li>현재 화면에서 다른 상태로의 전환</li>
<li>계산이나 데이터 처리가 필요한 무거운 업데이트</li>
<li>예시:<ul>
<li>검색어 추천 목록 표시</li>
<li>해시태그 추천 목록 업데이트</li>
<li>쇼핑몰 필터링된 상품 목록 표시</li>
<li>이미지 썸네일 생성과 미리보기</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="업데이트-처리의-문제점">업데이트 처리의 문제점</h3>
<p>기존의 방식(Expiration Time)에서는 이러한 업데이트들이 모두 동일한 우선순위로 처리되었습니다. 모든 작업이 동시에 처리되려다 보니 입력이 버벅거리는 현상이 발생했습니다.</p>
<h2 id="expiration-time-모델의-한계">Expiration Time 모델의 한계</h2>
<p>React 18 이전에는 Expiration Time 모델을 사용했습니다. 이 모델은 다음과 같은 한계를 가지고 있었습니다.</p>
<ol>
<li><p><strong>단일 시간 데이터의 한계</strong></p>
<ul>
<li>우선 순위와 배치 여부가 하나의 시간 데이터에 함께 존재</li>
<li>단순한 대소 비교만으로 우선순위와 배치 여부를 결정</li>
</ul>
</li>
<li><p><strong>Suspense와의 호환성 문제</strong></p>
<ul>
<li>Suspense 등장 이전에 설계되어, 우선순위 외의 이유로 작업 순서를 결정하는 유연성 부족</li>
</ul>
</li>
<li><p><strong>IO-Bound와 CPU-Bound 작업의 충돌</strong></p>
<ul>
<li>IO-Bound 작업(예: 데이터 페칭)은 외부 리소스 대기 시간이 있어 완료까지 시간이 더 걸림</li>
<li>IO-Bound의 경우 CPU-Bound가 느릴 수 밖에 없는데 짧은 간격으로 IO-Bound 이후 CPU-Bound가 발생할 경우 IO - CPU 시간 범위를 지정하고 구분할 수 있어야하는데 불가능</li>
</ul>
</li>
</ol>
<h2 id="lane-모델">Lane 모델</h2>
<p>이러한 한계를 해결하기 위해 React 18에서는 Lane 모델을 도입했습니다.</p>
<h3 id="업데이트-우선순위">업데이트 우선순위</h3>
<p>Lane은 비트 필드로 구현되어 있으며, 다음과 같은 우선순위를 가집니다.</p>
<ol>
<li><p><strong>SyncLane</strong></p>
<ul>
<li>가장 높은 우선순위</li>
<li>개별적 물리 이벤트 (click, input, submit)</li>
<li>긴급한 에러 처리</li>
</ul>
</li>
<li><p><strong>InputContinuousLane</strong></p>
<ul>
<li>연속적 물리 이벤트 (drag, scroll)</li>
</ul>
</li>
<li><p><strong>DefaultLane</strong></p>
<ul>
<li>일반적인 비동기 업데이트</li>
<li>외부 이벤트 (setTimeout, Promise)</li>
</ul>
</li>
<li><p><strong>TransitionLane</strong></p>
<ul>
<li>개발자가 정의한 전환 이벤트</li>
<li>startTransition, useTransition 사용</li>
</ul>
</li>
<li><p><strong>기타 Lanes</strong></p>
<ul>
<li>RetryLane: 에러 복구</li>
<li>SelectiveLane: 선택적 하이드레이션</li>
<li>IdleLane: 유휴 시간 작업</li>
</ul>
</li>
</ol>
<h3 id="lane의-동작-방식">Lane의 동작 방식</h3>
<ol>
<li><p><strong>인터럽트 처리</strong></p>
<ul>
<li>현재 렌더링 중인 Lane보다 높은 우선순위의 업데이트가 발생하면 인터럽트 발생</li>
<li>wipLanes를 통해 현재 진행 중인 렌더링 추적 (인터럽트가 필요한지 결정을 위해 필요)</li>
</ul>
</li>
<li><p><strong>배치 처리</strong></p>
<ul>
<li>동일한 우선순위(같은 Lane)의 업데이트는 자동으로 배치 처리</li>
</ul>
</li>
<li><p><strong>Time Slicing</strong></p>
<ul>
<li>우선순위에 따라 작업을 나누어 비동기적으로 처리</li>
<li>브라우저의 메인 스레드를 주기적으로 양보하여 다른 작업(사용자 입력 등) 처리 가능</li>
</ul>
</li>
</ol>
<h2 id="suspense와-transition의-조합">Suspense와 Transition의 조합</h2>
<ol>
<li><p><strong>Fallback 처리 최적화</strong></p>
<ul>
<li>Suspense만 사용할 경우 데이터 로딩 시 즉시 fallback이 표시됨</li>
<li>Transition과 함께 사용하면 불필요한 로딩 상태 깜빡임 방지</li>
</ul>
</li>
<li><p><strong>사용자 경험 개선</strong></p>
<ul>
<li>기존 컨텐츠를 유지하면서 새로운 데이터를 준비</li>
<li>준비가 완료된 후에만 새로운 컨텐츠를 표시</li>
<li>전환 중에도 UI의 응답성 유지</li>
</ul>
</li>
</ol>
<h2 id="동시성을-위한-핵심-요구사항">동시성을 위한 핵심 요구사항</h2>
<ol>
<li><p><strong>렌더링 제어</strong></p>
<ul>
<li>작업의 중지와 재개가 가능해야 함</li>
</ul>
</li>
<li><p><strong>렌더링 독립성</strong></p>
<ul>
<li>렌더링 간 의존성 없음</li>
<li>멱등성 보장</li>
<li>current와 workInProgress 더블 버퍼링</li>
</ul>
</li>
<li><p><strong>브라우저 차단 방지</strong></p>
<ul>
<li>렌더 페이즈: 비동기 처리</li>
<li>커밋 페이즈: 동기 처리</li>
<li>효율적인 작업 스위칭</li>
</ul>
</li>
</ol>
<h2 id="마무리">마무리</h2>
<p>Lane이 어떻게 우선순위 기반의 렌더링을 가능하게 하는지, 정리하며 어느정도 큰 그림은 감을 잡은 듯 하네요. 세부 구현을 파악하기에는 아직 갈 길이 멀지만요 ㅠㅠ
항상 소스 코드를 분석하다가 방대한 양에 지레 겁먹고 흐지부지 넘어간 것 같는데 곧 React 19의 안정된 버전이 나오면 다시 한 번 소스 코드 분석 도전!!!</p>
<h2 id="참고">참고</h2>
<blockquote>
</blockquote>
<p><a href="https://goidle.github.io/">https://goidle.github.io/</a>
<a href="https://ko.react.dev/reference/react/Suspense#preventing-already-revealed-content-from-hiding">https://ko.react.dev/reference/react/Suspense#preventing-already-revealed-content-from-hiding</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Node.js로 만드는 작은 마법들: 소소한 DX 개선기]]></title>
            <link>https://velog.io/@o1_choi/Node.js%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-%EC%9E%91%EC%9D%80-%EB%A7%88%EB%B2%95%EB%93%A4-%EC%86%8C%EC%86%8C%ED%95%9C-DX-%EA%B0%9C%EC%84%A0%EA%B8%B0</link>
            <guid>https://velog.io/@o1_choi/Node.js%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-%EC%9E%91%EC%9D%80-%EB%A7%88%EB%B2%95%EB%93%A4-%EC%86%8C%EC%86%8C%ED%95%9C-DX-%EA%B0%9C%EC%84%A0%EA%B8%B0</guid>
            <pubDate>Sun, 27 Oct 2024 06:17:36 GMT</pubDate>
            <description><![CDATA[<p>최근 혁신적인 <strong>DX 개선</strong> 사례들을 유튜브나 컨퍼런스에서 자주 접하게 됩니다.
이 글는 그에 비하면 너무 귀여운 수준이지만 <strong>Node.js</strong>를 활용해 개발 과정에서 마주치는 작은 불편함들을 개선한 사례를 공유하고자 합니다.
작은 불편함을 해소하는 것부터 시작해도 팀의 생산성과 만족도를 높일 수 있다고 생각합니다!!!</p>
<h2 id="1-폴더-생성-자동화">1. 폴더 생성 자동화</h2>
<pre><code>feature/특정 feature명
  ├── components/     # UI 컴포넌트 
  │                   # 해당 기능에 특화된 재사용 가능한 컴포넌트들을 포함
  │
  ├── constants/      # 상수 정의 
  │                   # 기능별 분리된 상수들을 체계적으로 관리
  │
  ├── models/         # 타입 정의 
  │                   # 인터페이스, API 응답/요청 타입 등 타입 시스템 관리
  │
  ├── pages/          # 페이지 컴포넌트 
  │                   # 라우팅 대상이 되는 페이지 컴포넌트들을 포함
  │
  ├── services/       # 비즈니스 로직 
  │                   # API 통신 로직 및 데이터 처리(query/mutation)를 담당
  │
  ├── hooks/          # 커스텀 훅 
  │                   # 재사용 가능한 로직을 훅으로 모듈화
  │
  ├── store/          # 상태 관리 
  │                   # 기능별 격리된 상태 관리 로직을 포함
  │
  └── utils/          # 유틸리티 
                      # feature 내 헬퍼 함수들을 모듈화</code></pre><p>저희 팀은 <strong>feature 기반의 폴더 구조</strong>를 사용하고 있습니다. 프로젝트를 초기 단계부터 구축하고 있었기에 신규 feature를 자주 만들어야 했는데요. 매번 이 폴더 구조를 만드는 일이 단순하면서도 반복적이었습니다.</p>
<p>이런 불편함을 느끼던 중 문득 <strong>Nest.js의 CLI 도구</strong>가 떠올랐습니다. Nest.js를 사용해보신 분들이라면 <strong><em>nest g resource</em></strong> 명령어로 필요한 폴더와 파일을 한 번에 생성했던 경험이 있으실 텐데요. 
이전에 이 기능을 사용하면서 느꼈던 편리함이 떠올라, Node.js의 파일시스템을 활용해 비슷한 도구를 만들어보기로 했습니다. 
목표는 간단했습니다. 터미널에서 feature 이름만 입력하면 필요한 모든 폴더 구조가 자동으로 생성되도록 만드는 것이었죠. </p>
<pre><code class="language-javascript">const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);

const createFeatureFolders = (featureName) =&gt; {
  try {
    const featuresDir = path.join(process.cwd(), &quot;src&quot;, &quot;features&quot;);
    const featureDir = path.join(featuresDir, featureName);

    const subFolders = [&quot;components&quot;, &quot;constants&quot;, &quot;models&quot;, &quot;pages&quot;, &quot;layouts&quot;, &quot;services&quot;, &quot;hooks&quot;, &quot;store&quot;, &quot;utils&quot;];

    // features 폴더 생성
    if (!fs.existsSync(featuresDir)) {
      fs.mkdirSync(featuresDir);
    }

    // 하위 폴더 생성
    subFolders.forEach((folder) =&gt; {
      const subFolderPath = path.join(featureDir, folder);
      if (!fs.existsSync(subFolderPath)) {
        fs.mkdirSync(subFolderPath);
        console.log(`하위 폴더 생성 완료: ${subFolderPath}`);
      } else {
        console.log(`하위 폴더가 이미 존재합니다: ${subFolderPath}`);
      }
    });
  } catch (e) {
    console.error(e.message);
  }
};

// 커맨드 라인 인자로 feature 이름 받기
const featureName = process.argv[2];

if (!featureName) {
  console.error(&quot;신규 기능 이름을 넣어주세요!&quot;);
  process.exit(1);
}

createFeatureFolders(featureName);
</code></pre>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/648e4441-d0fc-4c0c-9d50-283867bbc279/image.gif" alt=""></p>
<blockquote>
<p>신규 feature를 개발할 때마다 단순 반복 작업에서 벗어나 실제 기능 구현에만 집중할 수 있게 되었습니다.
(mkdir로 하면되지 라고 하신다면 슬퍼요ㅠㅠ) 
feature 기반의 폴더 구조를 사용하고 계시다면, 한 번쯤 시도해보시면 좋을 것 같네요!</p>
</blockquote>
<h2 id="2-아이콘-컴포넌트-자동화">2. 아이콘 컴포넌트 자동화</h2>
<p>UI 개발에서 아이콘은 빠질 수 없는 요소입니다. 저희 팀은 <strong>SVGR</strong>을 활용해 SVG 아이콘들을 React 컴포넌트로 변환하여 사용하고 있는데요. 이 과정에서 두 가지 불편함이 있었습니다.</p>
<p>첫째는 SVG 파일을 직접 import할 때 <strong>IDE의 자동 완성 기능</strong>을 사용할 수 없다는 점이었고, 둘째는 다크모드 대응을 위해 <strong>CSS 변수를 주입</strong>해야 하는 번거로움이었습니다.</p>
<p>이 작업을 Node.js를 통해 자동화했습니다. kebab-case로 작성된 SVG 아이콘 파일을 PascalCase 네이밍을 가진 컴포넌트로 변환하고, CSS 변수를 자동으로 주입한 뒤, 배럴 파일까지 자동으로 생성하도록 구현했습니다.
(실제로는 아이콘 별 별도 컴포넌트를 생성하고 추가 작업들이 있지만 코드 소개가 주된 내용이 아니기에 배럴 파일화 하는 로직만 작성했습니다.)</p>
<pre><code class="language-javascript">const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);

const SVG_DIR = path.join(process.cwd(), &quot;public&quot;, &quot;assets&quot;, &quot;icons&quot;);
const OUTPUT_FILE = path.join(process.cwd(), &quot;src&quot;, &quot;shared&quot;, &quot;assets&quot;, &quot;icons&quot;, &quot;index.ts&quot;);

// kebab-case를 PascalCase로 변환하는 함수
const toPascalCase = (str) =&gt;
  str
    .split(&quot;-&quot;)
    .map((word) =&gt; word.charAt(0).toUpperCase() + word.slice(1))
    .join(&quot;&quot;);

// SVG 파일들을 스캔하고 배럴 파일 생성
const generateBarrelFile = () =&gt; {
  const svgFiles = fs.readdirSync(SVG_DIR).filter((file) =&gt; file.endsWith(&quot;.svg&quot;));

  const exports = svgFiles.map((file) =&gt; {
    const componentName = toPascalCase(file.replace(&quot;.svg&quot;, &quot;&quot;));
    return `export { default as ${componentName} } from &#39;./${file}&#39;;`;
  });

  fs.writeFileSync(OUTPUT_FILE, exports.join(&quot;\n&quot;));
  console.log(&quot;아이콘 컴포넌트화 완료!&quot;);
};

generateBarrelFile();
</code></pre>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/0a3a34a0-cc1b-4cc0-9c65-0cd7d23ce970/image.gif" alt=""></p>
<blockquote>
<p>이렇게 생성된 배럴 파일 덕분에 아이콘을 사용할 때 IDE의 자동 완성 기능을 활용할 수 있게 되었고, 코드의 일관성도 높아졌습니다.
새로운 아이콘이 추가될 때마다 스크립트만 실행하면 되니, 이전보다 훨씬 효율적으로 아이콘을 관리할 수 있게 되었답니다!</p>
</blockquote>
<h2 id="3-excel과-i18n-json-파일-간의-변환-자동화">3. Excel과 i18n JSON 파일 간의 변환 자동화</h2>
<p>최근 외주 프로젝트에 참여하면서 <strong>i18n 관리</strong> 프로세스의 문제점을 경험했는데요. 상황이 꽤나 복잡했습니다.
디자이너분이 피그마에서 번역기로 임시 번역한 문구를 사용하고 있었고, 이후 엑셀을 통해 문구가 지속적으로 수정되고 있었습니다. 여기에 문구 자체가 변경되어 모든 언어의 번역을 수정해야 하는 경우도 빈번했죠. 게다가 실제로는 문구가 변경됐지만 수정이 필요하다는 표시를 안해주시는 휴먼 에러가 발생하면서 다른 개발 작업의 흐름이 자주 끊기곤 했습니다. </p>
<p>근본적으로는 공유 문서 템플릿 자체의 개선이 필요해보였지만, 당장의 고통을 벗어나기 위해 간단한 자동화를 해보기로 했습니다. <strong>XLSX 라이브러리</strong>를 활용해 <strong>Excel파일을 JSON 파일로 변환</strong>하는 자동화하는 스크립트를 작성했습니다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/a6f110dc-3593-47ac-8cd4-e8fc5ca4954b/image.png" alt="">
(실제 문서를 공개할 수 없기에 매우 간략한 버전으로 코드를 작성해보겠습니다!)</p>
<pre><code class="language-javascript">const XLSX = require(&quot;xlsx&quot;);
const fs = require(&quot;fs&quot;);
const path = require(&quot;path&quot;);

const EXCEL_FILE = path.join(process.cwd(), &quot;translations.xlsx&quot;);
const LOCALES_DIR = path.join(process.cwd(), &quot;public&quot;, &quot;locales&quot;);

const convertExcelToJson = () =&gt; {
  try {
    // Excel 파일 읽기
    const workbook = XLSX.readFile(EXCEL_FILE);
    const sheet = workbook.Sheets[workbook.SheetNames[0]];
    const data = XLSX.utils.sheet_to_json(sheet);

    // 언어별로 데이터 분류
    const translations = data.reduce((acc, row) =&gt; {
      const key = row.Key; // Excel의 Key 열

      // 각 언어별로 데이터 정리
      Object.entries(row).forEach(([lang, value]) =&gt; {
        if (lang === &quot;Key&quot;) return; // Key일 경우 건너뛰기
        if (!acc[lang]) acc[lang] = {};
        acc[lang][key] = value;
      });

      return acc;
    }, {});

    // 각 언어별 폴더 생성 및 JSON 파일 저장
    Object.entries(translations).forEach(([lang, trans]) =&gt; {
      const langDir = path.join(LOCALES_DIR, lang);

      // 언어별 폴더가 없으면 생성
      if (!fs.existsSync(langDir)) {
        fs.mkdirSync(langDir, { recursive: true });
      }

      // JSON 파일 생성
      const jsonContent = JSON.stringify(trans, null, 2);
      fs.writeFileSync(path.join(langDir, &quot;index.json&quot;), jsonContent);
    });

    console.log(&quot;✨ 번역 파일이 성공적으로 생성되었습니다!&quot;);
  } catch (error) {
    console.error(&quot;❌ 번역 파일 생성 중 오류가 발생했습니다:&quot;, error);
  }
};

convertExcelToJson();
</code></pre>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/026ad2c4-b5b1-45a0-99ad-802b0edd6a86/image.gif" alt=""></p>
<blockquote>
<p>번역 파일이 수정될 때마다 스크립트를 실행하기만 하면 되니, 수작업으로 인한 실수를 줄이고 작업 시간도 단축할 수 있게 되었습니다.
물론 임시방편일 수 있지만, 때로는 이런 작은 자동화만으로도 업무 효율을 크게 높일 수 있다는 걸 새삼 깨달았네요!</p>
</blockquote>
<h2 id="마무리">마무리</h2>
<p>너무나도 소소한 개선이지만, 이런 작은 변화들이 생각보다 <strong>핵심 작업</strong>에 더 집중할 수 있게 할 수 있는 경험들이였습니다. 특히 Node.js의 file system 모듈과 같은 기본적인 도구만으로도 충분히 의미 있는 <strong>DX 개선</strong>을 이룰 수 있다는 점이 인상적이었습니다.</p>
<p>처음에는 단순히 &#39;귀찮은 일을 줄여보자&#39;는 생각으로 시작했지만, 반복적인 작업에서 벗어나 본연의 문제 해결에 집중할 수 있게 되었고, 휴먼 에러도 줄어들었습니다.</p>
<p>이런 작은 자동화의 경험들이 모여 다른 불편한 지점들도 개선할 수 있다는 생각이 자연스럽게 들게 되었고, 팀원들과 함께 더 나은 개발 환경을 만들어가는 여정이 즐거워졌습니다. </p>
<p>혹시 이 글을 읽으시는 분들은 어떤 방식으로 개발 과정의 불편함을 해소하고 계신가요? 
여러분들의 개선 경험이 궁금합니다!!!🙂</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[어제보다 나은 오늘 (글또)]]></title>
            <link>https://velog.io/@o1_choi/%EC%96%B4%EC%A0%9C%EB%B3%B4%EB%8B%A4-%EB%82%98%EC%9D%80-%EC%98%A4%EB%8A%98-%EA%B8%80%EB%98%90</link>
            <guid>https://velog.io/@o1_choi/%EC%96%B4%EC%A0%9C%EB%B3%B4%EB%8B%A4-%EB%82%98%EC%9D%80-%EC%98%A4%EB%8A%98-%EA%B8%80%EB%98%90</guid>
            <pubDate>Fri, 11 Oct 2024 05:59:58 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/o1_choi/post/4d73fbe2-b5a3-4655-b0af-9e3483a02240/image.png" alt=""></p>
<p>개발자로서 많은 고민과 경험을 해왔지만, 그것을 글로 표현하는 것에 두려움이 있습니다. 
&#39;이 글이 얼마나 의미 있을까?&#39;라는 생각에 주저하다 결국 아무것도 쓰지 않고 넘어간 적이 많았어요. 때로는 문제를 해결하고 나서도 그 과정이나 결과가 대단치 않다고 여겨 &#39;글로 남길 정도인가?&#39;라는 생각에 지나쳐 버리기도 했습니다. 하지만 이제는 그런 순간들이 성장의 한 조각이었음을 깨닫고 글을 작성하지 않은 것을 반성하며 글또 활동을 하며 어제보다 나은 오늘을 살기로 했어요!</p>
<hr>
<h2 id="⛳️-이루고-싶은-목표">⛳️ 이루고 싶은 목표</h2>
<h3 id="1-글쓰기의-습관화">1. 글쓰기의 습관화</h3>
<p>첫 번째 목표는 글쓰기에 대한 두려움을 극복하고 <strong>글쓰기를 일상적인 습관</strong>으로 만드는 것입니다. 
지금까지는 노션에 가끔씩 정리하는 정도였지만, 이제는 <strong>정기적으로 글을 쓰는 루틴</strong>을 만들고 싶습니다.
글또 활동을 통해 2주에 한 번씩 글을 제출하며 자연스럽게 제 생각과 경험을 표현하는 능력을 기를 수 있을 거라 기대합니다.
처음에는 어색하고 어려울 수 있지만, 꾸준히 실천하다 보면 글쓰기가 일상의 한 부분으로 자리 잡을 수 있겠죠...?
더불어, 작성한 글들 중 일부를 <strong>공유하고 피드백을 받으며 더 나은 방향으로 발전</strong>시켜 나가고 싶습니다.
글쓰기를 혼자만의 활동이 아닌, 함께 성장하는 과정으로 만들어 가는 것이 꼭 이루고 싶은 목표입니다.</p>
<h3 id="2-네트워킹">2. 네트워킹</h3>
<p>두 번째 목표는 <strong>적극적인 네트워킹 참여</strong>입니다.
개발자로서 성장하기 위해서는 혼자만의 노력보다는 <strong>함께 할 때 효율적</strong>임을 깨달았습니다.
다른 개발자들과 고민을 나누고, 실무에서의 다양한 접근 방식을 배우고 싶은 마음이 커졌어요.
이전의 네트워킹 경험에서 얻은 긍정적인 에너지를 글또에서도 계속 이어나가고 싶습니다.
다른 개발자들의 이야기를 들으며 동기부여를 받고, 그분들의 <strong>좋은 습관을 배우는 것</strong>이 제 목표입니다.
<strong>매달 최소 두 번의 커피챗</strong>을 통해 의미 있는 대화를 나누는 것을 목표로 삼아보려 합니다.</p>
<hr>
<h2 id="🙏-마무리">🙏 마무리</h2>
<p>글또 10기로 활동한지 2주밖에 되지 않았지만, 벌써 많이 경험하고 배웠습니다. 3번의 커피챗을 통해 다양한 개발자분들을 만나면서, 저와 비슷한 고민을 가진 분들도 있었고 저에게 큰 영감을 주신 분들도 계셨습니다.</p>
<p>특히 인상 깊었던 것은, 저와 마찬가지로 글쓰기를 두려워했지만 이제는 글쓰기의 즐거움을 발견하고 설레는 마음으로 글을 공유하고 싶어 하시는 분을 만난 것입니다. 또한, 6개월 동안 제출할 글감을 미리 정해놓은 분의 계획성에 크게 감명받았습니다. 
커피챗을 통해 만난 대부분의 개발자분들이 메모하는 습관을 가지고 계시고, 글을 쓰는 것뿐만 아니라 읽는 것도 즐기시는 모습을 보며 좋은 자극을 받았습니다.</p>
<p>더불어 개발 외적으로 메타인지에 대해 중요하게 생각하시는 분들이 많다는 점도 흥미로웠습니다. 단순히 기술적인 성장뿐만 아니라, 개인으로서의 전반적인 성장을 추구하고 있다는 것 같아 인상적이었습니다.</p>
<p>이제 시작에 불과하지만, 이렇게 많은 것을 배우고 느낄 수 있었다는 점에 감사함을 느낍니다. 
앞으로의 글또 활동이 더욱 기대되네요 ㅎㅎ
함께 배우고, 성장하며, 서로에게 영감을 주는 멋진 여정이 되기를 진심으로 희망합니다!!!🔥</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[스토리북 - interaction Test]]></title>
            <link>https://velog.io/@o1_choi/%EC%8A%A4%ED%86%A0%EB%A6%AC%EB%B6%81-interaction-Test</link>
            <guid>https://velog.io/@o1_choi/%EC%8A%A4%ED%86%A0%EB%A6%AC%EB%B6%81-interaction-Test</guid>
            <pubDate>Tue, 28 Feb 2023 02:10:44 GMT</pubDate>
            <description><![CDATA[<h2 id="storybook-도입">Storybook 도입</h2>
<p>기존 Javascript로 진행했던 Hello world 프로젝트를 Next.js로 마이그레이션하는 프로젝트를 진행하는 중이다.
이번 프로젝트에서 테스트 경험을 쌓자는 팀원들의 공통된 의견이 있었고 <strong>UI 테스트를 위해 storybook을 도입</strong>하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/7f37d39a-da9c-4284-997f-363e1aa2c3ed/image.png" alt=""></p>
<p>우선 피그마로 작업한 공통 컴포넌트들 중 Button, Input, Wrapper, Title 이렇게 분리하여 storybook 코드를 작성했다.
공식 Docs에서 알려주는 대로 _npx storybook init_으로 설치 시 기본 템플릿을 제공해 줘 코드 작성이 어렵진 않았다.</p>
<pre><code>// Button.stories.tsx

import { ComponentStory, ComponentMeta } from &#39;@storybook/react&#39;;
import { within, userEvent } from &#39;@storybook/testing-library&#39;;
import Button from &#39;./Button&#39;;

export default {
  title: &#39;Common/Button&#39;,
  component: Button,
  argTypes: {},
} as ComponentMeta&lt;typeof Button&gt;;

const Template: ComponentStory&lt;typeof Button&gt; = args =&gt; &lt;Button {...args} /&gt;;

export const Base = Template.bind({});

Base.args = {
  color: &#39;#ffffff&#39;,
  background: &#39;#464B68&#39;,
  children: &#39;test&#39;,
  onClick: () =&gt; {
    alert(&#39;test&#39;);
  },
};

Base.play = async ({ canvasElement }) =&gt; {
  const canvas = within(canvasElement);

  await userEvent.click(canvas.getByRole(&#39;button&#39;, { name: &#39;test&#39; }));
};

export const Neon = Template.bind({});

Neon.args = {
  color: &#39;#252525&#39;,
  background: &#39;#C5FB6D&#39;,
  children: &lt;div&gt;123&lt;/div&gt;,
};

export const Disabled = Template.bind({});

Disabled.args = {
  color: &#39;#252525&#39;,
  background: &#39;#D9D9D9&#39;,
  children: &lt;input /&gt;,
};
</code></pre><p><img src="https://velog.velcdn.com/images/o1_choi/post/f93b0d98-0085-418e-b556-6ff7e7ad9d6e/image.png" alt=""></p>
<h2 id="storybook-test">Storybook Test</h2>
<p>예상한 대로 UI가 잘 보이는 것을 알 수 있었다. UI가 잘 보이는 것은 알겠으나 테스트는 어떻게 연결이 되는 것인지가 궁금했다. 
다양한 테스트 방법이 있는듯하지만 우선 두 가지 테스트를 진행했다.</p>
<ul>
<li>*<em>test파일에서 story를 불러와 테스트 *</em>
일반적인 *.test.tsx 파일에서 render에 컴포넌트를 가상으로 렌더링하는 대신 _@storybook/testing-react_의 _composeStories_를 통해 스토리?를 가상으로 렌더링하여 테스트</li>
<li><strong>stories파일에서 jest관련 메서드를 불러와 테스트</strong></li>
<li>.stories.tsx 파일에서 _@storybook/jest_에서 jest관련 메서드들을 사용하여 테스트</li>
</ul>
<blockquote>
<p>첫 번째 테스트 방법의 경우 test파일을 따로 만들어야 하기에 굳이 스토리를 가져와서 테스트할 때의 이점이 무엇인지를 아직 모르겠다. 
전달해야 하는 props가 있는 경우 스토리에 <strong>원하는 props로 입력된 컴포넌트를 render 하기에 간결한 코드</strong>의 느낌이 나는 정도 (적고 보니 큰 메리트 같기도?)</p>
<p>두 번째 테스트의 경우는 <a href="https://storybook.js.org/docs/react/writing-tests/interaction-testing">공식 Docs</a>에서 로그인 과정을 테스트하는 영상이 있는데 <strong>사용자가 직접 동작하는 것처럼 보여줘 시각적 효과</strong>가 좋았다.</p>
</blockquote>
<p>이 글에서는 <strong>stories파일에서 interaction 테스트</strong>를 진행한 경험을 말하고자 한다.
처음 storybook으로 구현한 Button, Input, Wrapper, Title을 통해 SignIn 컴포넌트를 임시로 만들었다.</p>
<pre><code>import { ComponentStory, ComponentMeta } from &#39;@storybook/react&#39;;
import { within, userEvent } from &#39;@storybook/testing-library&#39;;
import { expect } from &#39;@storybook/jest&#39;;
import SignIn from &#39;./index&#39;;

export default {
  title: &#39;Page/SignIn&#39;,
  component: SignIn,
  argTypes: {},
} as ComponentMeta&lt;typeof SignIn&gt;;

const Template: ComponentStory&lt;typeof SignIn&gt; = () =&gt; &lt;SignIn /&gt;;

export const SignInBox = Template.bind({});

// 스토리에 인터랙션 스토리를 재생하는 속성을 추가한다.
SignInBox.play = async ({ canvasElement }) =&gt; {

// 직접 screen API를 쓸 수도 있지만 스토리북에서는 within(canvasElement) 로 캔버스를 가져올 것을 권장한다.
  const canvas = within(canvasElement);

  await userEvent.type(canvas.getByLabelText(&#39;이메일&#39;), &#39;abcemail.com&#39;, { delay: 100 });
  await userEvent.type(canvas.getByLabelText(&#39;비밀번호&#39;), &#39;12ㅣㅑ더ㅣㅑㅁ너이ㅑㅁㄴ&#39;, { delay: 100 });

  expect(canvas.getByText(&#39;아이디 형식에 맞게 입력해주세요.&#39;)).toBeInTheDocument();

  await userEvent.clear(canvas.getByLabelText(&#39;이메일&#39;));
  await userEvent.type(canvas.getByLabelText(&#39;이메일&#39;), &#39;abc@email.com&#39;, { delay: 100 });

  expect(canvas.queryByText(&#39;아이디 형식에 맞게 입력해주세요.&#39;)).toBeNull();
};
</code></pre><p>이메일, 비밀번호를 유효성 검사 실패로 입력 시킨 후 이메일만 유효성 검사 통과하도록 Event 추가 후 유효성 실패 문구가 화면에서 사라지는지 테스트를 했다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/d37b0e8c-f7e4-4950-b56e-c47edaf70208/image.gif" alt=""></p>
<p>보여지는 것처럼 입력에 맞게 화면이 변하고 아래에 테스트가 제대로 동작하는지 알 수 있었다.</p>
<blockquote>
<p>스토리북을 사용해보니 확실히 <strong>기획자나 디자이너와 협업을 하게 되면 큰 도움</strong>이 되겠다는 생각이 들었다. 
처음에는 굳이 공수가 더 들어가는 것이 아닌가 했지만 인터렉션 테스트를 해보니 <strong>시각적으로 느껴지는 부분</strong>이 확실히 좋았다.</p>
<p>마이그레이션을 하며 UI테스트는 스토리북, 로직테스트는 jest, E2E는 cypress 이렇게 진행하려한다.
아직까진 테스트를 진행해보며 어디까지가 UI로 봐야하고 어디까지가 로직으로 봐야하는지의 경계가 명확히 보이진 않는 것 같다.
그래도 테스트의 중요성은 점점 느끼고 있고 재밌다. 정답은 없어보이지만 경험이 확실히 중요해보인다.</p>
</blockquote>
<h3 id="참고">참고</h3>
<blockquote>
<p><a href="https://storybook.js.org/docs/react/writing-tests/interaction-testing">https://storybook.js.org/docs/react/writing-tests/interaction-testing</a>
<a href="https://ui.toast.com/posts/ko_20220111">https://ui.toast.com/posts/ko_20220111</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[함수형 프로그래밍과 Javascript ES6+ (4)]]></title>
            <link>https://velog.io/@o1_choi/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-Javascript-ES6-4</link>
            <guid>https://velog.io/@o1_choi/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-Javascript-ES6-4</guid>
            <pubDate>Tue, 27 Dec 2022 07:51:01 GMT</pubDate>
            <description><![CDATA[<p><strong>map이나 filter는 함수를 합성하여 배열이나 이터레이터를 유지하는 역할</strong>로 지연성을 가질 수 있다.
<strong>reduce나 take는 배열이나 이터레이터의 내부 값으로 결국에는 최종적으로 결과를 만드는</strong> 함수다. </p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/e4a58b41-51de-465a-a180-5a8ea71b76c9/image.png" alt=""></p>
<p>L.entries는 제너레이터를 활용한 기존 entries와 달리 지연성있는 함수이고 join은 Array.prototype.join 보다 다형성이 높은 함수로 reduce를 통해 결과를 만드는 함수다.
두 함수를 통해 지연평가되며 다형성이 높은 queryStr 함수를 만들 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/40d34d2a-4a49-4713-95f0-b2f1e4da001b/image.png" alt=""></p>
<p>마찬가지로 L.filter와 take를 활용하여 지연평가되며 다형성이 높은 find 함수를 만들 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/518216cb-1b5b-483b-bad1-e65e260ec90d/image.png" alt=""></p>
<p>이번엔 지연성 있는 flat 함수를 만들어보자. 전체 배열을 가져올 수 있는 takeAll이라는 함수와 이터러블인지 확인하는 isIterable이라는 보조함수가 있다.
for of문으로 순회하며 이터러블일 경우 다시 for of문으로 순회하며 값을 하나씩 가져올 수 있도록한다. 
또한 takeAll을 활용해 flat된 전체 배열을 가져올 수 있다.
참고로** yield <em>iterable은 for (const val of iterable) yield val; 과 같다*</em>. 즉, L.flatten의 코드를 더욱 간결하게 쓸 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/f4d040da-6e05-413d-95d0-aa51145dc3a1/image.png" alt=""></p>
<p>for of 문으로 특별한 동작없이 단순 순회하는 경우 단축표현을 잘 활용하자!</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/758af37c-83c1-4f2b-9f1a-3e9e81368023/image.png" alt=""></p>
<p>앞서 만든 L.flatten을 활용한 지연성 있는 L.flatMap도 구현이 가능하다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/c9e01bc0-9717-4d11-a7c1-7c3f99258f5e/image.png" alt=""></p>
<p>이제 실무적으로 만든 함수들이 어떻게 사용될 수 있는지 확인해보자.
해당 코드는 users 배열에서 family만 뽑아 flat 후 age가 20세 초과인 사람들의 나이를 첫번째 부터 4개 뽑아 더하는 코드다. 
특별할 것 없어보이는 코드지만 해당코드를 지연성 없는 함수로 사용했다면 전체 반복을 여러번 돌게 되는 코드다. 하지만 해당 코드는 지연성을 가진 함수들로 동작되기에 4개를 뽑아오는 순간 더 이상의 반복은 돌지 않고 계산을 한다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/d5b04764-8706-41dd-9a79-31d712d6fa9f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/50eeb0f0-c8ff-4ed9-9ac0-67d3d696cc06/image.png" alt=""></p>
<p>일반 함수와 지연성 있는 함수를 비교 시 결괏값은 동일하지만 동작되는 시간 차이를 확인할 수 있다.</p>
<p>객체 지향은 데이터를 우선적으로 정리하고 메소드를 이후에 작성해 나간다.
함수형 프로그래밍은 <strong>이미 만들어진 함수조합이 있다면 함수 조합에 맞는 데이터를 구성</strong>하고 보다 함수가 우선 순위에 있다.</p>
<h2 id="참고">참고</h2>
<blockquote>
<p>유인동님의 함수형 프로그래밍과 JavaScript ES6+ 강의</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[함수형 프로그래밍과 Javascript ES6+ (3)]]></title>
            <link>https://velog.io/@o1_choi/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-Javascript-ES6-3</link>
            <guid>https://velog.io/@o1_choi/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-Javascript-ES6-3</guid>
            <pubDate>Mon, 26 Dec 2022 10:41:34 GMT</pubDate>
            <description><![CDATA[<h2 id="지연성">지연성</h2>
<ul>
<li>제때 계산법</li>
<li>느긋한 계산법</li>
<li>제너레이터/이터레이터 프로토콜을 기반으로 구현</li>
</ul>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/322ef8a9-29c4-4605-9bab-f114449a653f/image.png" alt=""></p>
<p>range 함수는 0부터 인수로 넣어준 값 만큼 순차적으로 숫자가 들어가 있는 배열을 만드는 함수다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/0edb8176-3499-4018-9f22-c35b7e32f36b/image.png" alt=""></p>
<p>이번에는 제너레이터를 활용한 L.range함수를 만들었다. list2의 값은 제너레이터 객체가 나오고 밑의 reduce로 동작한 값은 동일하게 6이 나옴을 알 수 있다.</p>
<p>하지만 두 함수의 <strong>동작 방식에는 큰 차이</strong>가 있다. 
위의 <strong>range함수는 호출 시 즉시 평가</strong>가 되어 [0,1,2,3] 이라는 배열이 생성된다.
<strong>L.range의 경우에는 제너레이터 객체</strong> 
즉, 이터레이터이기 때문에 <strong>list2.next()를 하기 전까지는 어떤 코드도 동작 되지 않는다.</strong> 그렇기에 reduce 함수로 <strong>순회를 하는 시점에 값을 꺼내 사용</strong>한 것이다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/973d0906-d0f8-4466-a07c-1923c9053324/image.png" alt=""></p>
<p>이번엔 길이와 이터레이터를 인자로 받아 전달해준 길이만큼 이터레이터를 리턴하는 take 함수를 만들었다.
이후 range와 L.range의 시간 차이를 console로 찍어보았다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/598a0103-91be-42d6-95fc-277b08cef1d1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/a6ff5b98-5f63-44a6-bc89-eff814b17e2e/image.png" alt=""></p>
<p>보다시피 엄청난 시간차이를 보이고 있다. range함수에 전달하는 길이 값이 커지면 커질수록 시간 차이는 엄청날 것이다. 앞에 설명한 것과 같이 <strong>range의 경우 배열을 생성하고 그 후에 take로 길이 만큼 출력</strong>한다.
반면 <strong>L.range의 경우는 연산을 미루다가 take함수로 순회할 때 필요한 값만 사용</strong>을 했기때문에 range의 인수로 Infinity가 오더라도 동작 시간의 차이가 없다.</p>
<p>위의 예로 보다시피 <strong>지연평가는 굉장히 영리</strong>하다.
이전에 만든 함수들도 지연성을 가진 제너레이터 함수로 변경해보자!</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/46927464-e4ee-41e0-8941-91d0cb49407f/image.png" alt=""></p>
<p>map함수를 제너레이터 함수로 구현했다. 
마찬가지로 L.map 자체로는 새로운 array를 반환하지 않는다. 
필요한 시점에 순회하며 값을 뽑아쓸 수 있도록 연산을 미룬것이다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/a3d87265-93bb-4d67-ae7d-5d624063d5d9/image.png" alt=""></p>
<p>지연성을 가진 filter 함수도 마찬가지로 동작한다.
최종적으로 L.map과 L.filter는 curry함수로 감싸져 있다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/67030e06-2fb2-4c96-9dcc-eb4b223351d8/image.png" alt=""></p>
<p>이제 앞서 만든 함수들을 활용해 지연성을 가진 함수의 동작 차이를 보려고 한다. 
위의 go는 순차적으로 즉시 평가가 되어 예상대로 진행이 된다.
아래의 go는 <strong>L.map과 L.filter가 실행되지만 어떤 연산도 하지 않고 take함수 내부</strong>로 들어가게 된다. 이후 <strong>take함수의 iter.next() 메서드가 실행되면서 L.filter 함수로 들어가게된다</strong>. 마찬가지로 filter 내부의 iter.next()가 실행되는 순간 L.map 함수로 들어가고 L.map에서 L.range로 들어간다. 이후 L.range의 <strong>yield로 값이 평가되면 아래 순서로 흐른다</strong>.  </p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/9525882c-6474-4073-84ea-8e9c18f5d915/image.png" alt=""></p>
<h3 id="map-filter-함수들이-가지는-결합-법칙">map, filter 함수들이 가지는 결합 법칙</h3>
<ul>
<li>사용하는 데이터가 무엇이든지</li>
<li>사용하는 보조 함수가 순수 함수라면 무엇이든지</li>
<li>결합해서 사용해도 결과 같다.</li>
</ul>
<pre><code>[mapping, mapping], [filtering, filtering], [mapping, mapping]] = [[mapping, filtering, mapping], [mapping, filtering, mapping]]</code></pre><h2 id="참고">참고</h2>
<blockquote>
<p>유인동님의 함수형 프로그래밍과 JavaScript ES6+ 강의</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[함수형 프로그래밍과 Javascript ES6+ (2)]]></title>
            <link>https://velog.io/@o1_choi/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-Javascript-ES6-2</link>
            <guid>https://velog.io/@o1_choi/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-Javascript-ES6-2</guid>
            <pubDate>Fri, 23 Dec 2022 07:43:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/o1_choi/post/e93faf00-14d2-4dc1-b8a0-c41be8a0d7e2/image.png" alt=""></p>
<p>이전 글에서 만든 map, filter, reduce를 활용하여 20,000원 이하 금액의 product 금액을 모두 합산하는 코드이다. </p>
<p>해당 코드는 우측에서 좌측으로 아래에서부터 위로 코드를 읽어야하고 함수가 중첩되어 있어 가독성이 좋지는 않다.
위의 코드를 리팩토링 해보자!</p>
<h2 id="코드를-값으로-다루어-표현력-높이기">코드를 값으로 다루어 표현력 높이기</h2>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/6d0fbfae-f0ce-409d-a37b-09a53d2ee997/image.png" alt=""></p>
<p>이전에 만든 reduce함수를 활용하여 <strong>연속적으로 실행되는 함수</strong>인 go라는 함수를 만들었다.</p>
<p>여기서의 go함수는 첫번째 인자로 함수의 인수로 사용할 값을 받고 이후 인자로는 함수가 들어와 연속적으로 실행되는 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/66977ba7-bba3-4e11-850a-936128e9ed49/image.png" alt=""></p>
<p>pipe함수는 go함수와 다르게 함수를 return 하는 함수이다. 
즉, <strong>함수들이 나열되어있는 합성된 함수를 만드는 함수</strong>이다.</p>
<p>첫번째 함수에 여러개의 인자를 받을 수 있기 때문에 Rest파라미터로 받아 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/b54efe1f-ffcf-4dfd-9a93-ea623f63c763/image.png" alt=""></p>
<p>curry함수는 고차함수이며   <strong>원하는 개수의 인자가 들어왔을 때 평가 시키는 함수</strong>다.</p>
<hr>
<p>이제 만든 go, pipe, curry 함수를 활용하여 상단 코드를 리팩토링 해보자!</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/90c1743b-00be-4108-b2da-ec5f6b96a16c/image.png" alt=""></p>
<p>go함수로 전과 달리 위에서 아래로 순차적으로 읽을 수 있는 코드가 되었다. </p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/71fae053-94c6-440b-941a-0b4f20c1c6b0/image.png" alt=""></p>
<p>이후 작성한 curry 함수로 이전 map, filter, reduce 함수를 감싸줬다.
이로 인해 인자로 넘겨주는 이터러블 프로토콜을 따르는 값을 분리할 수 있게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/f01ecdbc-9ea1-465d-ba53-a5ced749636e/image.png" alt=""></p>
<p>curry함수를 통해 보다 간결하게 리팩토링된 코드를 볼 수 있다.</p>
<blockquote>
<p>이 부분이 정말 어렵다. curry함수를 통해 원하는 인수 개수가 맞춰졌을 때 실행하도록하고 go함수 내부에 있는 reduce로 인해 첫번째 인수로 받은 products가 2번째 인수로 들어감으로써 실행이되는? 이후는 계속 실행된 값으로 이후의 함수를 또 실행 시켜주는?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/257c2a3c-2400-44eb-bbf4-b6535a4c6acc/image.png" alt=""></p>
<p>또한 pipe함수를 통해 고차함수들을 함수의 조합으로 만들어가며 코드 중복을 제거할 수 있다.</p>
<blockquote>
<p><del>끝. 인줄 알았겠지만</del>
여기서 하나 더 리팩토링을 하면 가격이 사용되는 특정 도메인에서만 사용할 수 있는 함수가 아닌 좀 더 범용적으로 더할 수 있는 함수로 리팩토링도 가능하다. </p>
<p>함수를 짤 때 이런 부분들을 잘 고려해서 짜야겠다는 생각이 들었다. 
당장 코드로 옮기긴 어려워 보이지만 반복 숙달만이 살 길이다. </p>
</blockquote>
<h2 id="참고">참고</h2>
<blockquote>
<p>유인동님의 함수형 프로그래밍과 JavaScript ES6+ 강의</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[함수형 프로그래밍과 Javascript ES6+ ]]></title>
            <link>https://velog.io/@o1_choi/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-Javascript-ES6</link>
            <guid>https://velog.io/@o1_choi/%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-Javascript-ES6</guid>
            <pubDate>Thu, 22 Dec 2022 10:13:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/o1_choi/post/93eb9505-8488-4ad3-b9e9-b9e62c8c28af/image.png" alt=""></p>
<p><a href="https://velog.io/@teo/functional-programming#%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%84-%EC%95%8C%EC%95%84%EC%95%BC-%EC%9D%B4%EC%9C%A0%EB%8A%94-%EB%AD%98%EA%B9%8C%EC%9A%94">테오의 함수형 프로그래밍에 관한 블로그 포스트</a>를 보며 프론트엔드 개발을 잘 하기 위해서는 함수형으로 사고하는 패러다임을 잘 이해할 필요가 있음을 알게 되었다.</p>
<p>함수형 프로그래밍을 경험하고 보다 나은 코드를 짜기 위해 유인동 님의 함수형 프로그래밍과 JavaScript ES6+ 강의를 통해 학습하며 기록하고자 한다.</p>
<h3 id="일급-객체">일급 객체</h3>
<p>자바스크립트에서의 함수는 일급 객체다.
즉  자바스크립트의 함수는</p>
<ul>
<li>값으로 다룰 수 있다.</li>
<li>변수에 담을 수 있다.</li>
<li>함수의 인자로 사용될 수 있다.</li>
<li>함수의 결과로 사용될 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/983c42c5-ce57-4762-880a-c99acddd6812/image.png" alt=""></p>
<h3 id="고차-함수">고차 함수</h3>
<p>함수를 값으로 다루는 함수</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/ea331232-9e25-4dae-be4e-0bba221e613d/image.png" alt=""></p>
<h3 id="es6에서의-리스트-순회">ES6에서의 리스트 순회</h3>
<p>for문에서 for of문으로 코드가 간결해지고 보다 선언적으로 변경</p>
<ul>
<li>for문 - 숫자로된 키와 그에 해당되는 값을 매핑하여 순회</li>
<li>for of문 - 이터러블 프로토콜을 따라 순회</li>
</ul>
<h3 id="이터러블이터레이터-프로토콜">이터러블/이터레이터 프로토콜</h3>
<ul>
<li>이터러블: 이터레이터를 리턴하는 Symbol.iterator메서드를 가진 값</li>
<li>이터레이터: {value, done} 객체를 리턴하는 next메서드를 가진 값</li>
<li>이터러블/이터레이터 프로토콜: 이터러블을 for ...of, 스프레드 연산자 등과 함께 동작하도록한 규약</li>
</ul>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/122cc484-2a16-4b89-99a5-91882a26625d/image.png" alt=""></p>
<h3 id="사용자-정의-이터러블">사용자 정의 이터러블</h3>
<p>잘 구현된 이러터블은 이터레이터를 진행하다가 순회할 수도 있고 이터레이터를 그대로 순회할 수 있도록 되어 있다. 
즉, 얼마나 반환되었는지를 알기위해 Symbol.iterator를 실행했을 때 자기 자신을 반환한다.
<img src="https://velog.velcdn.com/images/o1_choi/post/103285e7-e94f-4810-b0cb-31f350d978f5/image.png" alt=""></p>
<h3 id="제너레이터">제너레이터</h3>
<ul>
<li>이터레이터이자 이터러블을 생성하는 함수</li>
<li>순회 시에는 return 값은 반환하지 않으며 순회가 끝나도 done이 true로 바뀔 때 value의 값이 된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/2884fe9a-01b4-46c9-8874-2ba8acb0ae04/image.png" alt=""></p>
<p>제너레이터는 문을 값으로 만들 수 있고 순회할 수 있기 때문에 자바스크립트에서는 어떠한 상태나 어떠한 값도 순회할 수 있게 만들 수 있다.
즉, 함수형 프로그래밍 관점에서 다형성이 높다.</p>
<h3 id="다형성-있는-map-fiter-reduce">다형성 있는 map, fiter, reduce</h3>
<p>Array를 상속받지 않은 다른 이터러블 프로토콜을 따르는 값들도 map, filter, reduce를 사용할 수 있게 구현해보자. 
<img src="https://velog.velcdn.com/images/o1_choi/post/4e095bf3-7022-423a-806a-38a449a13e58/image.png" alt=""></p>
<p>이터러블이자 이터레이터인 제너레이트로도 map, filter, reduce가 Array의 prototype 메서드 처럼 동작하는 걸 확인함으로써 다형성이 더 좋아짐을 알 수 있다.</p>
<h3 id="딥다이브-추가-학습-내용">딥다이브 추가 학습 내용</h3>
<ul>
<li>for in문 - 프로퍼티 어트리뷰트[[Eumerable]]의 값이 true인 프로퍼티를 순회(프로퍼티 키가 심벌인 프로퍼티는 열거 x)</li>
<li>유사 배열 객체는 Symbol.iterator 메서드가 없다.</li>
<li>arguments, NodeList, HTMLCollection은 유사 배열 객체이지만 이터러블이 도입되면서 Symbol.iterator 메서드를 구현하여 이터러블이 되었다. 
즉, 유사 배열 객체이자 이터러블이다.</li>
<li>제너레이터 함수는 화살표 함수로 정의할 수 없다.</li>
<li>제너레이터 함수를 호출하여 반환한 제너레이터 객체는 return과 throw 메서드를 가지고 있다.<blockquote>
<p>return: 인수로 전달받은 값을 value로, true를 done 프로퍼티 값으로 하는 이터레이터 리절트 객체를 반환
throw: 인수로 전달받은 에러를 발생시키고 undefined를 value 프로퍼티 값, true를 done 프로퍼티 값으로 하는 이터레이터 리절트 객체를 반환</p>
</blockquote>
</li>
</ul>
<h3 id="참고">참고</h3>
<blockquote>
<p>함수형 프로그래밍과 JavaScript ES6+ 강의
모던 자바스크립트 Deep Dive</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[리액트 테스팅 - React Testing Library]]></title>
            <link>https://velog.io/@o1_choi/ReactTestingLibrary</link>
            <guid>https://velog.io/@o1_choi/ReactTestingLibrary</guid>
            <pubDate>Tue, 20 Dec 2022 02:35:54 GMT</pubDate>
            <description><![CDATA[<h2 id="🛠-테스트-자동화-왜-해야-할까">🛠 테스트 자동화 왜 해야 할까?</h2>
<p><strong>테스트</strong>란 <strong>코드가 정상적으로 작동하는지 검증하는 작업</strong>을 의미한다.</p>
<p>테스트 자동화를 하지 않는 다면 구현한 기능들을 직접 사용해보는 것이 가장 기본적인 방법일 것이다. 
그런데 사람이 수동으로 하나하나 체크하는 것은 굉장히 번거로울 것이다. 
하지만 테스트하는 코드를 작성해 <strong>테스트 시스템을 갖춘다면</strong> 프로젝트를 개발하는 과정에서 코드가 기존의 기능들을 <strong>실수로 망가뜨리는것을 효과적으로 방지</strong> 할 수 있다.</p>
<blockquote>
<p><strong>유닛테스트 &amp; 통합테스트</strong>
유닛테스트 - 프로젝트의 작은 기능들이 작동하는지 확인하는 테스트
통합테스트 - 기능들이 전체적으로 작동되는지 확인하는 테스트</p>
</blockquote>
<h3 id="jest란-무엇인가">Jest란 무엇인가?</h3>
<p>Facebook이 만든 테스팅 프레임 워크
<strong>최소한의 설정으로 동작</strong>하며 <strong>단위(Unit)테스트를 위해 사용</strong>한다.</p>
<blockquote>
<p><strong>Jest가 Test 파일을 찾는 방법</strong>
    - filename.test.js
    - filename.spec.js
    - tests(폴더명)</p>
</blockquote>
<h3 id="react-testing-library란-무엇인가">React Testing Library란 무엇인가?</h3>
<p>리액트 공식문서에서 사용을 권장하는 라이브러리로 <strong>Behavior Driven Test(행위 주도 테스트)</strong> 방법론이 대두 되면서 함께 주목 받기 시작한 테스팅 라이브러리다. 
Behavior Driven Test에서는 사용자가 애플리케이션을 이용하는 관점에서 <strong>사용자의 실제 경험 위주로 테스트</strong>를 작성한다.
React Testing Library는 _jsdom_이라는 라이브러리를 통해 <strong>실제 브라우저 DOM을 기준으로 테스트를 작성</strong>한다. 
따라서 어떤 React 컴포넌트를 사용하는지는 의미가 없으며, 결국 사용자 브라우저에서 랜더링하는 실제 HTML 마크업의 모습이 어떤지에 대해서 테스트하기 용이하다.</p>
<blockquote>
<p>CRA로 생성된 프로젝트는 Jest, React Testing Library 자동으로 설치가 되어 있으며 그렇지 않은 경우 npm으로 추가가 가능하다. </p>
</blockquote>
<pre><code>npm i -D jest @testing-library/react @testing-library/jest-dom</code></pre><h2 id="📊-예시로-알아보는-테스트-코드">📊 예시로 알아보는 테스트 코드</h2>
<p>간단한 counter 테스트코드를 작성했다.</p>
<h4 id="srcappjs">src/App.js</h4>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/2d61fb02-e7bb-40bb-84a2-e66fbe0036c2/image.png" alt=""></p>
<h4 id="srcapptestjs">src/App.test.js</h4>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/b3a6542b-a8fb-470e-a424-3c825ce5db24/image.png" alt=""></p>
<p><strong>①</strong> <strong>describe</strong> - 여러 관련 테스트를 그룹화</p>
<p><strong>②</strong> <strong>test(it)</strong> - 개별 테스트로 query, expect, matcher로 이루어져 있다.</p>
<p><strong>③</strong> <strong>query</strong> - 페이지에서 요소를 찾기 위해 테스트 라이브러리가 제공하는 방법</p>
<ul>
<li><strong>getBy</strong> - 쿼리에 대해 일치하는 노드를 반환, 일치하는 요소가 없거나 둘 이상 일시 오류 발생 (둘 이상의 경우 getAllBy 사용)</li>
<li><strong>queryBy</strong> - 쿼리에 대해 일치하는 노드를 반환하며 일치하는 요소가 없을 시 null을 반환, 둘 이상의 경우 오류 발생 (둘 이상의 경우 queryAllBy 사용)</li>
<li><strong>findBy</strong> - 쿼리와 일치하는 요소가 발견되면 해결되는 Promise를 반환, 요소가 발견되지 않거나 제한 시간(기본 1s) 후 둘 이상의 요소가 발견되면 오류 발생. (둘 이상의 경우 findAllBy 사용)</li>
</ul>
<p><strong>④ expect</strong> - 값을 테스트할 때 사용</p>
<p><strong>⑤ matcher</strong> - 다양한 방법으로 값을 테스트하도록 사용</p>
<hr>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/9c59b3ce-3df3-43b3-bcee-33716ee8a9ea/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/ffdfd8a9-2e0a-4475-90dc-c5281381330c/image.png" alt=""></p>
<blockquote>
<p>최종테스트 코드와 테스트 결과다.
5개의 테스트 코드가 통과됨을 확인 할 수 있다. 이로써 직접 버튼을 클릭해보지 않고도 코드에 문제가 없음을 알 수 있었다.</p>
</blockquote>
<h2 id="➕-추가">➕ 추가</h2>
<h3 id="query-사용-우선-순위">query 사용 우선 순위</h3>
<ol>
<li><strong>모든 사용자가 액세스 할 수 있는 사용자의 경험을 반영하는 쿼리</strong>
  getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue</li>
<li><strong>시멘틱 쿼리</strong>
  getByAltText, getByTitle</li>
<li><strong>테스트ID</strong>
  getByTestId</li>
</ol>
<blockquote>
<p><a href="https://testing-library.com/docs/queries/about#priority">https://testing-library.com/docs/queries/about#priority</a></p>
</blockquote>
<p>쿼리 사용 우선 순위로는 최하위이지만 비대화형 요소의 경우 text의 변동이 너무 잦기 때문에 <strong>실무에서는 testID를 자주 사용하는듯 하다?</strong> 
(프론트엔드 오픈채팅방 통계로 불확실)</p>
<h3 id="eslint-testing-plugins">ESLint Testing Plugins</h3>
<pre><code>npm i -D eslint-plugin-testing-library eslint-plugin-jest-dom</code></pre><blockquote>
<p><a href="https://testing-library.com/docs/ecosystem-eslint-plugin-testing-library">https://testing-library.com/docs/ecosystem-eslint-plugin-testing-library</a>
<a href="https://testing-library.com/docs/ecosystem-eslint-plugin-jest-dom">https://testing-library.com/docs/ecosystem-eslint-plugin-jest-dom</a></p>
</blockquote>
<h4 id="eslintrcjson">.eslintrc.json</h4>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/ebd28b9a-14ce-4829-95b2-9984f52e38e9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WhatSsub 회고]]></title>
            <link>https://velog.io/@o1_choi/WhatSsub-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@o1_choi/WhatSsub-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Wed, 07 Dec 2022 15:27:23 GMT</pubDate>
            <description><![CDATA[<p><strong>왔썹(WhatSsub)은 서브웨이 주문 시 재료를 선택해서 음식을 주문하는 방식에 익숙하지 않은 사람들을 위한 재료 조합 생성 및 제공하는 서비스</strong>다.</p>
<blockquote>
<p>배포: <a href="https://what-ssub.vercel.app/">https://what-ssub.vercel.app/</a>
github: <a href="https://github.com/harseille/WhatSsub">https://github.com/harseille/WhatSsub</a></p>
</blockquote>
<p>프로젝트를 진행하며 느낀 경험을 <strong>4L(4Ls) 방식으로 회고</strong>하고자 한다. </p>
<h2 id="😲-liked---좋았던-점은-어떤-것이-있나요">😲 Liked - 좋았던 점은 어떤 것이 있나요?</h2>
<hr>
<h3 id="보여주기-위한-프로젝트가-아닌-불편함을-해결하기-위한-아이디어">보여주기 위한 프로젝트가 아닌 불편함을 해결하기 위한 아이디어</h3>
<p> 매주 스터디를 하며 먹기 깔끔한 서브웨이를 자주 배달시켜 먹었는데 그때마다 <strong>꿀조합을 검색</strong>하기도 하고 <strong>선택 자체에 시간이 오래 걸리기</strong>도 했다. 
실제로 주변 지인들 중에 <strong>서브웨이의 주문 시 어려움을 겪어 서브웨이 자체를 이용하지 않는 경우</strong>도 더러 있었다. 
위의 <strong>불편함을 해결하고자 왔썹(WhatSssub) 프로젝트를 시작</strong>하게 되었다.</p>
<p> 왔썹은 있어 보이기 위한 아이디어로 시작한 프로젝트가 아닌 스터디를 하며 <strong>실제로 겪은 불편함을 해결하고자 필요한 아이디어를 바탕으로 시작한 프로젝트</strong>여서 더욱 애착이 가고 즐겁게 개발을 진행했다. </p>
<h3 id="프로젝트-규모나-기능에-맞는-기술-스택-도입">프로젝트 규모나 기능에 맞는 기술 스택 도입</h3>
<p> 좋았던 점이자 가장 아쉬운 부분이 아닐까 생각한다. </p>
<p> 이전 프로젝트의 기술 스택은 대체로 현재 사용할 줄 아는 기술이라는 이유로 프로젝트의 규모나 기술 스택 자체의 장단점은 자체는 고려하지 않고 프로젝트를 진행했다.
 하지만 이번 프로젝트에서는 <strong>기획의도, 규모, 기능에 대한 부분을 고민하며 나름대로의 이유를 가지고 기술 스택을 도입</strong>했다.</p>
<blockquote>
</blockquote>
<p>** 구현 기능이 캐주얼하고 짧은 시간 내 완성을 목표로 함
기술 변화가 빠른 프론트엔드 환경을 고려하여 최신 버전의 제품에 대응할 수 있는 역량을 갖추고자 함**</p>
<ul>
<li><p><a href="mailto:react-router-dom@6.4">react-router-dom@6.4</a> - 새로 추가된 <em>loader, action, errorElement</em> 등 기능을 활용해 컴포넌트 구현부의 코드를 clean하게 하고자 함</p>
</li>
<li><p>Recoil - <em>React</em> 자체 라이브러리로 상태를 외부에서 처리하지 않는 점, 비동기 처리를 위한 추가적인 라이브러리도 필요 없음</p>
</li>
<li><p>Emotion - <em>styled-component</em> 대비 가볍고 작은 번들 사이즈</p>
</li>
<li><p>Firebase - 프론트엔드로만 이루어진 프로젝트였기에 serverless 서비스 사용, OAuth 및 정적 파일 저장소 제공</p>
<h3 id="이전의-스프린트-협업-시-좋았던-bdd-sdd-도입">이전의 스프린트 협업 시 좋았던 BDD, SDD 도입</h3>
<p>테오의 스프린트를 진행하며 BDD, SDD를 방식을 사용해 효율적으로 함께 설계를 했던 좋은 경험이 있었다. 
이전의 긍정적인 경험을 얘기하며 설계에 대한 아이디어를 낼 수 있었고 <strong>BDD, SDD를 통해 서비스 기능에 대한 공통된 이해</strong>를 할 수 있었다.
다른 개발자분께 <strong>공유 받은 지식과 경험을 통해 팀원들에게 전파하고 활용</strong>할 수 있어 뿌듯했다.</p>
</li>
</ul>
<h2 id="🫠-lacked---어떤-아쉬움이-있었나요">🫠 Lacked - 어떤 아쉬움이 있었나요?</h2>
<hr>
<h3 id="한글-코딩-컨벤션---집현전-프로젝트">한글 코딩 컨벤션 - 집현전 프로젝트</h3>
<p>Toss에서 적용 중인 <a href="https://tosspayments-dev.oopy.io/chapters/frontend/posts/hangul-coding-convention">세종대왕 컨벤션</a>을 참고하여 한글 컨벤션이 <strong>코드 가독성</strong>과 <strong>유지 보수</strong>에 이점이 있는지 확인하고자 도입을 했다. 
Toss에서는 예시로 <mark style='background-color: #ccc'> <em>국가지방자치단체공공단체금융사여부</em> </mark> &nbsp; 같은 <strong>어려운 도메인 용어</strong>나 <strong>비즈니스 로직</strong>에 한글 컨벤션을 적용했다.</p>
<p>왔썹 프로젝트의 경우 사실 영어로 표현하기 어려운 단어 자체는 없었지만 서비스의 핵심인 <strong>&quot;꿀조합&quot;이라는 한국만의 밈</strong>을 살리고자 했다.
그렇게 Toss의 세종대왕 컨벤션을 베이스로 <strong><a href="https://absorbed-leek-405.notion.site/daaf58b9e2fa48048ff98c858253bfae">집현전 프로젝트</a>라는 우리만의 컨벤션</strong>을 만들었다.</p>
<p>한글 컨벤션을 사용하다 보니 <strong>어디까지 한글 컨벤션을 적용하는 것이 옳은가</strong>를 많이 고민했다. 
<strong>익숙하지 않아 어색</strong>하게 보이기도 했고 밈을 살리기 위해 쓰기엔 <strong>굳이 꿀조합 하나를 위해 한글을? 이라는 의문</strong>도 많이 생겼던 것 같다.
비즈니스 로직만 하자니 <strong>각자가 생각하는 비즈니스 로직의 기준이 달라</strong> 컨벤션이 지켜지지 않는 경우도 많았다.
대표적으로 로그인과 유저정보, 댓글 등 <strong>타 프로젝트에도 대부분 들어가는 비즈니스 로직의 경우가 애매</strong>했다. 
핵심 비즈니스 로직이냐라고 하기엔 아니라고 대답할 수 있고 비즈니스 로직이 아니냐라는 질문엔 그건 또 아니었기 때문이다.
이런 이슈로 <strong>주기적으로 회의를 하고 라이브쉐어로 코드를 살펴보고 조율하는 시간을 가지며 컨벤션을 통일</strong>했다.</p>
<p>처음 도입한 이유인 <strong>코드 가독성과 유지 보수의 이점</strong>을 생각해 본다면 많은 아쉬움이 남는다. 
도입 시 무분별한 한글 사용으로 보일까를 걱정했는데 <strong>다른 개발자의 시각</strong>에서 본다면 적절한 한글 사용으로 이점을 잘 가져갔다라기보단 굳이 <strong>왜 한글을 했을까라는 의문</strong>이 더 클 것 같다. 
하지만 한글 컨벤션을 사용함으로써 <strong>보다 나은 추상화를 위해 식별자 명을 더 고민</strong>할 수 있었고 통일을 하는 과정에서 <strong>팀원의 코드에도 함께 집중</strong>할 수 있는 뜻깊은 경험이였다. </p>
<h3 id="firebase가-제공하는-기능-파악-miss">Firebase가 제공하는 기능 파악 Miss</h3>
<p>꿀조합 Pick 페이지에서 <strong>선택한 속성으로 필터링 된 꿀조합을 제공해 주는 기능</strong>이 이번 프로젝트의 핵심 기능이었다.<br>최대 7개의 속성까지 들어가는 경우가 있어 <strong>and 조건의 필터링이 필요</strong>했는데 <strong>FireSotre에서 해당 기능이 제공되지 않았다</strong>.</p>
<blockquote>
<p><mark style='background-color: #ccc'> <em>array-contain</em> </mark> &nbsp; 사용 시 쿼리당 최대 하나의 <mark style='background-color: #ccc'> <em>array-contain</em> </mark> 절을 사용할 수 있어 해당 문제를 해결할 수 없었다.
 <mark style='background-color: #ccc'> <em>not in(!=)</em> </mark> 을 사용하여 선택 속성 외의 속성으로 필터링도 시도를 해봤지만 최대 10개의 같지않는 경우만 제공되고 있어 여러 번의 요청이 필요해 해결이 어려웠다.</p>
</blockquote>
<p>최종적으로 해당 이슈를 해결하지 못함으로써 모든 꿀조합을 받아온 후 <strong>client에서 필터링하여 제공</strong>하게 되어 아쉽다.</p>
<h3 id="어디까지가-전역-상태인가">어디까지가 전역 상태인가?</h3>
<p>recoil을 상태관리 라이브러리로 사용을 했다. recoil의 atom의 경우 리얼 전역적인 상태, 즉 <strong>어떤 페이지든 알아야 하는 상태를 기준으로 사용</strong>을 했다. 
그렇다 보니 왔썹 프로젝트의 경우 <strong>로그인 여부, 유저정보</strong>를 제외하곤 전역적으로 가지고 있을 필요가 없었다. 
꿀조합 리스트를 불러오는 페이지들은 많았지만 페이지마다 <strong>필터링이 되거나 정렬 기준이 달랐기 때문에 지역 상태로 관리</strong>를 했다.
이로 인한 <strong>props drilling이 발생</strong>하긴 했지만 컴포넌트 depth가 깊지 않아 나쁘지 않았다. 
하지만 프로젝트를 진행하다 보니 위의 아쉬운 점이었던 firebase의 and Filter에 대한 기능이 제공되지 않아 <strong>전체 꿀조합을 불러온 뒤 로컬에서 가공하는 케이스</strong>가 존재하게 됐다. 
그렇다 보니 전체 꿀조합을 불러오는 경우에는 atom에 할당하여 전역 상태로 가지고 있고 나머지 꿀조합 요청 시에도 꿀조합에 대한 변경사항이 없는 경우 서버에 요청하지 않고 atom에 저장된 꿀조합을 가공하여 사용할까도 고민을 했지만 실제 애플리케이션이라 생각을 했을 때 애초에 모든 데이터를 불러오는 케이스가 없기 때문에 불필요한 최적화라 생각되어 수정하지 않았다. </p>
<p>최적화를 고려해 atom으로 관리하는 게 맞는가? 전역상태의 본질을 유지하는게 맞는가?
정말 어려운 문제인 것 같다. 
정답은 없겠지만 <strong>상황에 따라 최적화를 고려해 전역 상태로 관리 하는게 맞는지 아닌지 판단할 수 있는 경험치</strong>가 필요함을 많이 느꼈다.</p>
<h2 id="🧐-learned---어떤-것을-배웠나요">🧐 Learned - 어떤 것을 배웠나요?</h2>
<hr>
<h3 id="webpack설정---cra는-위대하다">Webpack설정 - CRA는 위대하다.</h3>
<p>CRA를 사용하지 않고 Webpack을 구성하며 <strong>CRA의 편리함</strong>을 느꼈다.
Webpack 세팅을 하며 CRA를 사용할 땐 당연하게 생각했던 부분들이 직접 설정을 해줘야 하는 경우가 많았다.</p>
<p><strong>env 파일</strong>을 위해 <mark style='background-color: #ccc'> <strong>dotenv-webpack</strong> </mark> <strong>플러그인</strong>도 필요했고 <strong>dynamic-import</strong>를 위해 <mark style='background-color: #ccc'> <strong>@babel/plugin-syntax-dynamic-import</strong> </mark> <strong>바벨 플러그인</strong>을 추가하기도 했다.
그 외에도 <strong>alias로 절대 경로</strong>를 지정해 사용하기도 하고 <strong>splitChunks</strong>로 파일을 분리해 최적화를 하는 경험도 했다.</p>
<p>처음엔 마냥 어렵게만 느껴졌는데 필요한 설정을 하나하나 추가를 해가며 퍼즐 맞추는 느낌이 들기도 하고 흥미로웠다.</p>
<h3 id="최적화">최적화</h3>
<p><strong>접근성</strong> 향상을 위해 WAI-ARIA의 <strong>aria-label</strong>로 이미지만 있는 버튼의 내용을 명시해다. 
Pick 페이지와 Custom 페이지의 경우 버튼에서 <strong>색상대비율 개선이 필요</strong>한 경우도 있었지만 <strong>디자인을 고려해 타협</strong>한 경우도 있었다.</p>
<p><strong>퍼포먼스</strong> 향상을 위해 <strong>splitChunks로</strong> react와 firebase 관련 파일을 <strong>Code Splitting</strong>하기도 하고 React.lazy를 활용해 route 단위 <strong>Dynamic Imports</strong>를 하기도 했다.
(react-router-dom V6.4에 새로 추가된 data APIs에서 lazy가 작동하지 않는 이슈로 기존의 router를 사용했다. )</p>
<p><strong>SEO</strong>를 위해 <strong>description, OG</strong> 등의 meta 태그를 추가하며 meta 태그에 대해서도 배울 수 있었다.  </p>
<p>가장 큰 깨달음은 개념조차 몰랐던 <a href="https://velog.io/@o1_choi/tree-shaking"><strong>Tree Shaking</strong></a> 이었다. 
(이 부분은 따로 정리를 했기에 링크도 대체한다.)</p>
<blockquote>
<p><strong>lighthouse와</strong> <strong>webpack-bundle-analyze</strong>로 최적화 체크
<img src="https://velog.velcdn.com/images/o1_choi/post/481764ee-ff15-4b85-9f58-ae7229714c18/image.png" alt="light house" title="light house"></p>
</blockquote>
<h2 id="🙏-longed-for---앞으로-바라는-점이-있나요">🙏 Longed for - 앞으로 바라는 점이 있나요?</h2>
<hr>
<h3 id="프로젝트-리팩토링">프로젝트 리팩토링</h3>
<p>recoil을 사용했지만 atom을 제외하곤 사용되지 않았다.
로그인 여부, 유저 정보, 유저가 좋아요한 꿀조합 정보는 <strong>selector를 활용</strong>해 좀 더 recoil의 장점을 살릴 수 있지 않을까 생각된다.</p>
<h3 id="다른-프로젝트에도-사용-가능한-컴포넌트">다른 프로젝트에도 사용 가능한 컴포넌트</h3>
<p>공통적인 컴포넌트의 경우 <strong>다른 프로젝트에서도 사용 가능</strong> 하도록 개발해 <strong>생산성을 높이는 것</strong>이 중요하다고 생각했다.
이번 프로젝트에서도 나름 공통적인 컴포넌트를 나누긴 했지만 그 외에도 분리해 재사용 가능한 부분이 있음을 깨달으며 부족함을 느꼈다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Webpack - Tree Shaking으로 최적화]]></title>
            <link>https://velog.io/@o1_choi/tree-shaking</link>
            <guid>https://velog.io/@o1_choi/tree-shaking</guid>
            <pubDate>Thu, 01 Dec 2022 08:59:45 GMT</pubDate>
            <description><![CDATA[<p>CRA를 사용하지 않고 직접 _Webpack_과 _Babel_을 세팅한 프로젝트를 진행중이다. 
프로젝트의 구현이 마무리 단계가 되어 최적화를 위해 라이트하우스를 돌려보았다.
<img src="https://velog.velcdn.com/images/o1_choi/post/46aa3a8c-3df3-4df9-9f1a-598eb81f2351/image.jpg" alt=""></p>
<p>위 사진에서 보다시피 성능적인 부분에서의 개선이 필요했다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/467ff182-52a4-4838-8451-6870150e1513/image.png" alt=""></p>
<p><em>webpack-bundle-analyzer</em> 플러그인을 사용해 빌드 시 번들에서 어떤 요소가 용량을 많이 차지하고 있는지 확인을 해보니 firebase와 lodash가 압도적인 크기를 자랑하고 있었다. </p>
<p><em>lodash_의 경우 스크롤 이벤트의 잦은 발생을 막기 위해 _debounce</em> 메소드만을 사용하고 있었는데 엄청 아까운 용량이였다.</p>
<p>라이브러리의 필요한 부분만 남기기위한 기능이 존재하겠지라는 생각으로 구글링을 해보니 <strong>웹팩에서 사용하지 않는 코드를 제거해주는 Tree Shaking 기능</strong>이 있었다.</p>
<h2 id="tree-shaking이-머야">Tree Shaking이 머야?</h2>
<p>나무흔들기? 
나무를 흔들어 죽은 잎을 떨어뜨리 듯 <strong>빌드 시에 사용하지 않는 코드를 제거하는 최적화 과정</strong>을 뜻한다. </p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/2003fb0c-2231-45e2-aa47-e44d48827a81/image.png" alt=""></p>
<blockquote>
<p><a href="https://webpack.kr/guides/tree-shaking/">Webpack의 공식 문서</a>에 따르면 webpack2에서 사용하지 않는 모듈의 export를 감지하는 기능이 제공되고 <strong>webpack4에서 사이드이펙트에 관한 기능이 확장되며 보다 적합한 최적화가 가능해진 것</strong>으로 보인다. 
( TMI. 현재 프로젝트는 webpack5를 사용 중이다. )</p>
</blockquote>
<h2 id="그래서-tree-shaking-어떻게-하는건데">그래서 Tree Shaking 어떻게 하는건데?</h2>
<p>웹팩 공식 문서의 예시를 보니 <strong><em>named export</em></strong> 여야 하고   <strong><em>package.json</em></strong>의 <strong><em>&quot;sideEffects&quot;</em></strong> <strong>에 추가되지 않은 파일</strong>이면 빌드 시 자동으로 최적화를 해주는 듯해 보였다. 
(<em>defalut export</em> 경우도 가능하나 sideEffect 관련 설정을 해야 함)</p>
<p>그래서 <em>lodash_의 _debounce</em> 메서드를 사용한 코드를 확인해 보았다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/122151b7-d265-4370-81d5-880aced167b8/image.png" alt=""></p>
<p><del>응? 지금도 뽑아썼는데?????</del> </p>
<h3 id="tree-shaking이-작동되지-않을-때">Tree Shaking이 작동되지 않을 때</h3>
<p>Tree Shaking이 작동되지 않는 대표적인 예시로 <em>lodash</em> 가 많이 사용되고 있었다. 
<strong>import 해서 사용하는 해당 모듈의 export가 ES2015(export)로 내보내지고 있지 않기 때문</strong>이다. </p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/e6737c3c-3912-4481-aed7-b2961163a20f/image.png" alt=""></p>
<p><a href="https://github.com/lodash/lodash">lodash_github</a>에서 <strong>UMD module로 export</strong> 하고 있음을 알 수 있었다.</p>
</blockquote>
<p>그럼 ES 모듈로 내보내지 않은 모듈은 방법이 없나 해서 찾아보니 
webpack-common-shake 플러그인을 추가하는 방법과 
<strong>import 시에 모듈에서 메서드까지 한 번에 import? 하는 방법</strong>이 있었다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/d2767ee3-6823-473d-9486-aa0361a54790/image.png" alt=""></p>
<p>후자의 방법으로 코드를 변경 후 다시 용량 테스트해보자! </p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/2a7f4258-ed74-47d8-b175-a77513662138/image.jpg" alt=""></p>
<p>Before &amp; After의 차이가 엄청나다. 제대로 Tree Shacking이 동작함을 알 수 있었다. </p>
<blockquote>
<p>lodash 라이브러리의 경우 ES모듈로 export한 lodash-es 라이브러리가 있고
<a href="https://github.com/lodash/lodash">lodash_github</a>에서 권장하기에 lodash-es 라이브러리로 최종 수정을 했다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/9bac5b21-6e60-4ed3-960b-f83b301a4eb0/image.png" alt=""></p>
</blockquote>
<h3 id="tree-shaking-조건">Tree Shaking 조건</h3>
<ul>
<li><strong>ES모듈 구문을 사용</strong>해야 한다. (import, export)</li>
<li>컴파일러가 ES모듈을 commonJS 모듈로 변환하지 않도록 해야한다.<ul>
<li><strong>@babel/preset-env의 기본 설정</strong>으로 주의!</li>
</ul>
</li>
<li><strong>mode 설정</strong>이 development가 아닌 <strong>production</strong> 이여야 한다.</li>
<li><strong>Side Effect</strong>를 고려하자<ul>
<li>Tree Shaking으로 사용하지 않는 코드를 제거 시 이로 인해 사이드 이펙트가 발생할 수 있다. 
옵션을 명시하지 않으면 Tree Shaking 시에 사이드 이펙트가 발생할 수 있다고 판단하여 해당 패키지는 Tree Shaking의 대상에서 제외된다. 
(빌드 된 js파일 확인 시 sideEffects: false 설정이 defalut로 적용되는 듯하다?)</li>
</ul>
</li>
</ul>
<h2 id="참고">참고</h2>
<blockquote>
<p><a href="https://ui.toast.com/weekly-pick/ko_20180716">https://ui.toast.com/weekly-pick/ko_20180716</a>
<a href="https://webpack.kr/guides/tree-shaking/">https://webpack.kr/guides/tree-shaking/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[키보드 이벤트의 isComposing (Feat: React )]]></title>
            <link>https://velog.io/@o1_choi/isComposing</link>
            <guid>https://velog.io/@o1_choi/isComposing</guid>
            <pubDate>Thu, 10 Nov 2022 15:37:43 GMT</pubDate>
            <description><![CDATA[<h2 id="⌨️-키보드-이벤트의-iscomposing">⌨️ 키보드 이벤트의 isComposing</h2>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/3e29f7d0-5873-40cf-b385-79e42c52f80a/image.gif" alt="">
자동완성 기능을 구현하던 중 추천 키워드로 이동 시 기존 입력한 검색어의 <strong>마지막 글자가 포함되어 출력되는 문제</strong>가 발생했다.
테스트를 해보니 <strong>크롬 브라우저에서 한글을 사용하는 경우</strong>만 해당 문제가 존재했다.</p>
<p>검색해 보니 한글 입력 시 입력 중인 글자 아래 검은 밑줄이 생기는데 해당 밑줄이 있는 상황에서 <strong>키보드 이벤트 발생 시 이벤트핸들러가 두 번 호출</strong> 되는 문제가 존재했다. </p>
<p>한글의 경우 자음과 모음의 조합으로 한 음절이 만들어지는 조합 문자이기 때문에 <strong>글자가 조합 중인지, 조합이 끝난 상태인지를 알 수 없기 때문</strong>이다. 
이로 인해 키보드 이벤트에는 <strong><em>isComposing</em></strong> 이라는 <strong>입력 문자가 조합 문자인지 아닌지를 boolean값으로 반환</strong>하는 프로퍼티가 있었다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/12e6fb10-edef-4a22-b87d-3b6bd2a6391c/image.png" alt=""></p>
<blockquote>
<p><strong>chrome 브라우저</strong> 사용 시 keydown 이벤트 핸들러 내부에서 console로 찍어보면 <strong>이벤트가 두 번 발생</strong>하는 것을 알 수 있다. 
참고로 firefox로 확인 시 isComposing의 값은 false로 이벤트가 한 번만 발생한다. </p>
</blockquote>
<h2 id="♻️-리액트는-합성-이벤트-syntheticevent">♻️ 리액트는 합성 이벤트 (SyntheticEvent)</h2>
<p>그럼 이제 문제가 isComposing 때문임을 알게 되었다.
<del>오잉? isComposing을 사용해서 문제 해결해야지 했는데 이벤트 객체에 isComposing 속성이 없었다.</del>
<img src="https://velog.velcdn.com/images/o1_choi/post/6be4c453-cf7c-4c9e-adda-79bbd5998360/image.png" alt="">
<strong>리액트의 이벤트가 합성 이벤트(<em>SyntheticEvent</em>)</strong>임은 알고 있었지만 실질적으로 처음 체감해 봤다.
리액트의 공식 문서에서 봤던 기억이 있어 공식 문서를 보니 떡하니 &quot;<strong><em>브라우저의 고유 이벤트가 필요하다면 nativeEvent 어트리뷰트를 참조하세요.</em></strong>&quot; 라는 문구가 있었다.
<strong><em>navtiveEvent</em></strong> 어트리뷰트를 사용해 브라우저의 고유 키보드 이벤트에 접근하여 isComposing 프로퍼티를 사용하여 문제를 해결했다.</p>
<pre><code>
 const handleKeyDown = (event: React.KeyboardEvent) =&gt; {
    if (event.nativeEvent.isComposing) return;

    ...

  };</code></pre><h3 id="참고">참고</h3>
<blockquote>
<p><a href="https://ko.reactjs.org/docs/events.html#overview">https://ko.reactjs.org/docs/events.html#overview</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[구글 스프린트를 경험하다!]]></title>
            <link>https://velog.io/@o1_choi/%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8%EB%A5%BC-%EA%B2%BD%ED%97%98%ED%95%98%EB%8B%A4</link>
            <guid>https://velog.io/@o1_choi/%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8%EB%A5%BC-%EA%B2%BD%ED%97%98%ED%95%98%EB%8B%A4</guid>
            <pubDate>Tue, 18 Oct 2022 13:57:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Velog에 올라오는 <strong>테오의 스프린트</strong> 관련 블로그를 보며 <strong>5일간 실제 구글의 스프린트 방식</strong>으로 게더타운으로 다 함께 스프린트를 진행하며 모여 있는 모습이 재밌어 보였고 부러웠다. 
프로젝트 경험이 많지 않아 협업을 경험해 보고 개발자분들과 소통하며 다양한 인사이트를 얻고 싶어 이번 기회에 테오의 스프린트를 참여했다.
<br/>
자세한 스프린트 과정은 <a href="https://velog.io/@teo/google-sprint-11">테오의 블로그</a>를 확인!</p>
</blockquote>
<h2 id="🐱-아이디어의-순수함">🐱 아이디어의 순수함</h2>
<blockquote>
<h3 id="가치있는-서비스">가치있는 서비스</h3>
<p>다들 거창한 아이디어보다 정말 필요로 하는 아이디어들이 많아 공감이 되고 아이디어의 순수함이 보여 좋았다.
개발 공부를 시작하며 서비스를 이용할 때 개발적인 부분을 많이 떠올렸는데 잘 쓰는 서비스들은 개발을 잘해서가 아니라 그 <strong>서비스가 가치를 주기 때문에</strong> 라는 내용이 인상 깊었다. 
<strong>항상 개발자이기 이전에 한 명의 유저인 개발자를 꿈꿨는데 놓치고 있던 부분을 다시 리마인드</strong> 할 수 있었다. </p>
</blockquote>
<h3 id="초기-아이디어의-구체화-금지">초기 아이디어의 구체화 금지</h3>
<p>5일이라는 짧은 시간이기에 구체화된 아이디어가 있어야 기획 단계의 시간을 줄이고 개발에 몰입할 수 있을 거라 생각했지만 큰 착각이었다. 
함께하는 스프린트에서 구체화된 아이디어는 다른 분들의 좋은 아이디어가 들어갈 자리가 없었다. 
실제로 다양한 아이디어를 보며 너무 구체화된 아이디어는 목적성이 분명해 다른 추가적인 아이디어가 생각나지 않았다. </p>
<h3 id="나쁜-아이디어는-없다">나쁜 아이디어는 없다</h3>
<p>팀을 결성 후 선택한 아이디어에 대해 생각해온 모든 아이디어를 공유했는데 중간중간 정리 하지 말아달라는 요청이 있었다. 
초기 기획의도와 다른 부분이 있으면 즉각적인 피드백으로 어느 정도 정리를 했는데 테오의 조언을 듣고 모든 아이디어를 얘기할 수 있었다. 
실제 그렇게 다시 살린 아이디어에서 기획을 다른 시각으로도 볼 수 있었고 <strong>모든 아이디어가 소중함</strong>을 느꼈다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/33b03466-b607-4522-b820-58149176bbe6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/53ed568b-a730-488f-9610-5ec72bbb49d3/image.png" alt=""></p>
<h2 id="🐱-같이의-가치">🐱 같이의 가치</h2>
<blockquote>
<h3 id="좋은-협업을-위한-주파수-맞추기">좋은 협업을 위한 주파수 맞추기</h3>
<p>하나의 아이디어를 만들기 위해 모였지만 각자가 생각한 <strong>아이디어 구현의 디테일한 생각이 달랐다.</strong>
실제 우리 팀은 뽀모도로 타이머 + 투두의 아이디어였는데 뽀모도로와 투두 두 가지의 컨텐츠 중 어떤 컨텐츠에 중점을 두고 있는지에 대한 부분과 뽀모도로 타이머와 투두가 개별로 동작할지 연동되게 동작할지에 대한 디테일의 차이가 있었다. 
이러한 부분에 대해 각자의 생각을 논의한 이후 <strong>워드클라우딩</strong>과 <strong>어떻게 하면 ~ 질문</strong>, <strong>지도그리기</strong>를 진행하며 점점 아이디어에 대한 키워드 만으로 <strong>각자의 생각이 우리의 생각으로 변해감을 느낄 수 있었다.</strong></p>
<h3 id="bdd와-sdd">BDD와 SDD</h3>
</blockquote>
<p><em>BDD - 사용자의 행동을 중심으로 개발을 진행하는 방식
SDD - 데이터를 중심으로 추려내고 개발을 진행하는 방식</em></p>
<blockquote>
</blockquote>
<p>항상 프로젝트를 진행하면 <strong>어떻게 해야 효율적으로 다 같이 개발을 설계할 수 있을까</strong>를 고민하게 된다. 
이번 스프린트를 통해 BDD와 SDD를 경험하며 BDD를 통해 사용자의 행동에 따른 서비스의 동작을 같이 고민함으로써 <strong>본인이 맡은 부분의 개발만이 아닌 서비스 전체에 대한 개발이 함께 공유</strong>되고 있음을 몸소 체험했다. 
SDD의 경우 초기 설계에서도 함께 고민을 많이 했지만 구조가 달라진 부분이 많아 아쉬움으로 남았다.</p>
<p><img src="https://velog.velcdn.com/images/o1_choi/post/7a77e1b5-cace-44bf-8876-5e9b411d5366/image.png" alt=""></p>
<p align="center">
<img src="https://velog.velcdn.com/images/o1_choi/post/ba16569d-745a-4349-aed9-47e3eb139255/image.png" width="700px">
</p>

<h2 id="😃-마무리">😃 마무리</h2>
<blockquote>
<p>완성보다 협업이 중점인 스프린트였기에 부담감보다는 즐거움으로 가득 찬 5일이었다. 
다른 기수보다 많은 인원으로 어려움이 있어 테오가 미안함을 전달했지만 같은 목적으로 좋은 분들을 만나 긴 시간이 전혀 힘들지 않았다. 그래서 &quot;덕분에 1시간이나 더 함께할 수 있어 좋아요&quot;라고 했는데 기억에 남으셨는지 공지 메일에 언급해 주셔서 <strong>뿌듯</strong>함과 감사함을 느꼈다.
<img src="https://velog.velcdn.com/images/o1_choi/post/96a44f01-8403-46e4-8d57-c5a2d143bc06/image.png" alt="">
협업의 소통에 있어 <strong>용어의 중요성</strong>을 다시 한번 느꼈고 수면 관리에 대한 부분은 아쉬웠다. 
미련 갖지 않기라는 말이 참 와닿았다. 그날에 못한다고 죽는 일이 아니라면 내일로 <strong>미룰 줄 아는 용기도 필요함</strong>을 느꼈다. 안 좋은 컨디션의 2시간이 좋은 컨디션의 30분만 못할 수도 있다. 잊지 말자!<br/> 
그리고 무엇보다 스프린트를 통해 만난 6명의 팀원들에게 배울 점이 많아 성장할 수 있었다. 
다들 <strong>주도적으로 의견</strong>을 내고 반대 의견이 있어도 소통을 통해 수긍하고 <strong>긍정적인 방향으로 피드백</strong>을 주고받아서 감사했다.
1일 1커밋으로 잔디로 축구장을 만드신 분, 라이브러리를 자유자재로 다루시는 분, 디자인의 신, 제대로 아이스브레이킹해준 분, 개발 열정을 되살려 주신 분 등등 열정 넘치는 팀원과 함께해 개발 스킬뿐 아니라 협업의 소통까지 잘 배워갑니다. (자기반성은 플러스!!!)
앞으로 <strong>뽀모도로 팀원의 반만 해도 협업왕</strong>이 될지어다.
<br />
단톡방 입장할 때 매번 인사해 주시던 모습이 참 인상적이었는데 좋은 경험을 하게 해주신 테오에게도 다시 한번 감사를 전합니다. 기회가 된다면 성장해서 꼭 다시 참여하고 싶은 좋은 경험이였습니다.</p>
</blockquote>
<h3 id="🚀-프로젝트-링크">🚀 프로젝트 링크</h3>
<blockquote>
<p>GitHub Repository : <a href="https://github.com/Time-Catcher/timecatcher">https://github.com/Time-Catcher/timecatcher</a> 
Service Page : <a href="https://time-catcher.netlify.app/">https://time-catcher.netlify.app/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[WAI-ARIA - 베스트 사례 분석!!!]]></title>
            <link>https://velog.io/@o1_choi/WAI-ARIA</link>
            <guid>https://velog.io/@o1_choi/WAI-ARIA</guid>
            <pubDate>Thu, 14 Jul 2022 05:40:08 GMT</pubDate>
            <description><![CDATA[<h1 id="wai-aria란">WAI-ARIA란?</h1>
<blockquote>
<p><strong>Web Accessibility Initiative – Accessible Rich Internet Applications</strong></p>
</blockquote>
<p><strong>WAI-ARIA</strong>는 장애가 있는 사람들이 웹 콘텐츠와 웹 응용 프로그램에 더 쉽게 액세스할 수 있도록 하는 방법을 정의한다. </p>
<p> <strong>WAI</strong>는 W3C에서 웹 접근성을 담당하는 기관
 <strong>ARIA</strong>는 RIA 환경의 웹 접근성에 대한 표준 기술 규격을 의미</p>
<h2 id="aria가-등장한-이유는">ARIA가 등장한 이유는?</h2>
<p>페이지 중심의 정적이던 사이트들은 동적으로 변화하고 있다.
이런 변화는 사용성과 반응형 향상에는 극적인 도움을 주지만 스크린리더 같은 보조기술을 사용하는 유저들에게는 <strong>접근성 격차가 발생</strong>했다. 이런 격차에 대해 보다 <strong>일관된 접근성 모델을 형성하고 상호 운용성 향상</strong>을 위해 등장했다. </p>
<h2 id="aria는-어떻게-사용-하는가">ARIA는 어떻게 사용 하는가?</h2>
<p>ARIA는 마크업에서 특별한 속성을 추가하여 위젯의 디테일한 정보를 제공할 때 사용한다. 
이를 위한 ARIA의 속성(roles, states, properties)을 알아보자.</p>
<ul>
<li>역할(Role) : 컴포넌트, 요소 내 역할을 정의<pre><code>&lt;ol role=&quot;tablist&quot;&gt;
  &lt;li id=&quot;ch1Tab&quot; role=&quot;tab&quot;&gt;
    &lt;a href=&quot;#ch1Panel&quot;&gt;Chapter 1&lt;/a&gt;
  &lt;/li&gt;
&lt;/ol&gt;</code></pre>aria 구조 중 역할을 명시해주면 단지 링크 태그로서의 의미만을 전달해주는것이 아닌, 버튼의 역할이 Tab이라고 명시해준다.</li>
</ul>
<hr>
<ul>
<li>속성(Property) : 컴포넌트의 특징이나 상황을 정의 
   (속성명으로 aria-xx라는 접두사 사용)<pre><code>   &lt;button class=&quot;btn_search&quot; aria-label=&quot;검색&quot;&gt;</code></pre>   검색이라는 안내 텍스트 없이 버튼을 나타낼 때, 스크린리더 사용자는 어떤 버튼인지 알 수 없다. aria-label을 이용하여 버튼 요소에 검색이라는 설명을 추가하여 정보를 전달할 수 있다.</li>
</ul>
<hr>
<ul>
<li>상태(State) : 해당 컴포넌트의 상태 정보를 정의메뉴의 활성 여부를 보여주는 aria-expanded, aria-selected와 같이 현재 상태 또는 변화된 값을 알려준다.<pre><code>&lt;ul class=&quot;btnList&quot;&gt;
  &lt;li&gt;
    &lt;button aria-controls=&quot;accordion-region&quot; aria-expanded=&quot;true&quot;&gt;btn&lt;/button&gt;
  &lt;/li&gt;
&lt;/ul&gt;</code></pre>아코디언 메뉴의 활성 상태 값인 aria-expanded를 명시하면 스크린 리더기가 상태정보(확장 또는 축소)를 읽어줄 수 있다.</li>
</ul>
<blockquote>
<p>더 많은 ARIA의 역할, 속성, 상태는 (<a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques">MDN-ARIA</a>) 참고</p>
</blockquote>
<h2 id="aria는-사용-시-주의사항은">ARIA는 사용 시 주의사항은?</h2>
<ul>
<li><p>제공되는 의미 체계 및 동작이 있는 HTML요소를 먼저 사용할 것 </p>
<pre><code>&lt;!-- Do Not --&gt;
&lt;!-- nav태그로 의미 전달이 가능한데 먼저 사용하지 않은경우--&gt;
&lt;div role=&quot;navigation&quot;&gt; navi &lt;/div&gt;
&lt;!-- 중복되는 역할을 전달한 경우 --&gt;
&lt;nav role=&quot;navigation&quot;&gt; navi &lt;/nav&gt;

&lt;!-- Do --&gt;
&lt;nav&gt; &lt;/nav&gt;</code></pre></li>
<li><p>필요한 경우가 아니면 의미를 가진 HTML 요소를 변경하지 말 것</p>
<pre><code>&lt;!-- Do Not --&gt;
&lt;h2 role=tab&gt;heading tab&lt;/h2&gt;

&lt;!-- Do --&gt;
&lt;div role=tab&gt;&lt;h2&gt;heading tab&lt;/h2&gt;&lt;/div&gt;</code></pre></li>
<li><p>키보들 이용하여 접근할 수 있게 할 것</p>
<pre><code>&lt;!-- 버튼으로 사용하기위해 role=&quot;button&quot;속성 부여시 
tabindex도 반드시 설정해서 접근이 가능하도록 한다. --&gt;

&lt;!-- Do Not --&gt;
&lt;span role=&quot;button&quot;&gt; button &lt;/span&gt;

&lt;!-- Do --&gt;
&lt;span role=&quot;button&quot; tabindex=&quot;0&quot;&gt; button &lt;/span&gt;</code></pre></li>
</ul>
<ul>
<li><p>모든 대화형 요소에는 액세스 가능한 이름을 가질 것</p>
<pre><code>&lt;!-- Do Not --&gt;
user name &lt;input type=&quot;text&quot;&gt;

&lt;!-- Do --&gt;
&lt;input type=&quot;text&quot; aria-label=&quot;User Name&quot;&gt;

&lt;span id=&quot;p1&quot;&gt;user name&lt;/span&gt; &lt;input type=&quot;text&quot; aria-labelledby=&quot;p1&quot;&gt;</code></pre><ul>
<li>포커스 가능한 요소 에 role=&quot;presentation&quot;또는 aria-hidden=&quot;true&quot;를 사용하지 말것</li>
</ul>
</li>
<li><p>사용자의 브라우저와 보조기기가 WAI-ARIA를 지원하는지 확인할 것</p>
</li>
</ul>
<h2 id="wai-aria를-사용하여-구현해본-combobox-ui">WAI-ARIA를 사용하여 구현해본 COMBOBOX UI</h2>
<p>WAI-ARIA 미적용 사례</p>
<p><a href="https://codesandbox.io/embed/combo-wai-aria-ojn9fz?fontsize=14&amp;hidenavigation=1&amp;theme=dark">https://codesandbox.io/embed/combo-wai-aria-ojn9fz?fontsize=14&amp;hidenavigation=1&amp;theme=dark</a></p>
<p>WAI-ARIA 적용 사례</p>
<p><a href="https://codepen.io/Won-Oh-Choi/pen/QWQQjop">https://codepen.io/Won-Oh-Choi/pen/QWQQjop</a></p>
<h3 id="어떤-부분에서-접근성이-문제가-있을까br-스크린리더는-어떻게-읽어줄까">어떤 부분에서 접근성이 문제가 있을까?<br> 스크린리더는 어떻게 읽어줄까?</h3>
<ul>
<li><p>역할 명시가 없기 때문에 어떤 용도의 리스트인지 이해하기 어려울 것 같다.</p>
</li>
<li><p>포커스가 잡히지 않기 때문에 한번에 모든 리스트를 읽는 문제점이 있을 것 같다.</p>
<h3 id="스크린리더-테스트-후-예상과-어떻게-다른가">스크린리더 테스트 후 예상과 어떻게 다른가?</h3>
</li>
<li><p>포커스가 잡히지 않는 예상은 했지만 하나만 알고 둘은 몰랐다. 기본적으로 포커스가 안되기 때문에 마우스클릭으로만 동작이 가능해 스크린리더 이용자가 전혀 사용할 수 없는 콤보 박스였다.</p>
<h3 id="베스트-사례-분석-후-깨달은-점은">베스트 사례 분석 후 깨달은 점은?</h3>
</li>
<li><p><code>role</code>을 이용하여 역할 명시 ul태그에 <code>listbox</code>를 주고 li태그에 <code>option</code>을 준다.</p>
</li>
<li><p><code>aria-autocomplete</code>의 속성값으로 <code>&#39;list&#39;</code>를 줘서 목록형 자동완성 편집 요소임을 명시
(우선, 스크린 리더 사용자가 자동완성이 가능한 편집 요소임을 알 수 있도록 하려면 사용자가 편집할 input 요소에 aria-autocomplete를 사용하고, 브라우저에서 제공하는 autocomplete 속성을 비활성 상태로 변경해야 한다.)</p>
</li>
<li><p><code>aria-owns</code> 속성은 서로 구조상 관계가 없는 요소를 연결하는 역할을 수행한다.
input 요소는 닫는 태그가 별도로 없는 셀프 클로징 요소로 자손 요소를 둘 수 없다.
따라서 이 aria-owns를 사용하여 input의 자손처럼 스크린 리더가 인식할 수 있도록 listbox 요소를 연결하여 사용한다.</p>
</li>
<li><p><code>aria-activedescendant</code> 는 aria-owns와 마찬가지로 HTML id 값을 받는 속성으로 스크린 리더 사용자의 실제 초점과 가상 커서는 편집창에 위치하고 있으나, 마치 초점이 어떠한 요소로 탐색하는 것과 같이 속성값에 제공한 id와 일치하는 요소를 읽게끔 전달한다.</p>
</li>
<li><p>상위 메뉴가 하위 메뉴를 가지고 있는 경우 <code>aria-haspopup=&quot;true&quot;</code> 속성을 삽입하여 하위 메뉴가 있음을 스크린리더에서 읽게 하고, 하위 메뉴가 펼쳐졌을 때는 <code>aria-expanded=&quot;true&quot;</code>를 삽입하고 <code>aria-controls</code> 속성으로 하위 메뉴의 id 연결한다.</p>
</li>
</ul>
<h2 id="wai-aria-학습-후-무엇을-느꼈는가">WAI-ARIA 학습 후 무엇을 느꼈는가?</h2>
<p>WCAG에 이어 WAI-ARIA를 학습하며, 시맨틱 태그를 사용하지 않을 때 적절한 역할 속성을 어떻게 지정해 의미를 부여할지에 대해 고민해야 한다는 것을 깨달았다. 또한 웹 페이지를 보며 단순히 어떤 레이아웃을 갖고 어떤 태그를 사용했는지가 아니라 해당 태그를 사용하여 어떻게 접근성에 대해 고민하였고 풀어냈는지에 대해서도 관심 있게 바라보게 되었다. 거기에 더해 자바스크립트로 ARIA 속성 값을 어떻게 지정해줘야 하는지에 대해 고민해보는 시간도 가질 수 있어 좋았다. ARIA를 사용해 일관된 접근성을 사용자에게 제공할 수 있도록 신경쓰자</p>
<blockquote>
<p>** 참고 ** </p>
</blockquote>
<ul>
<li><a href="https://github.com/niawa/ARIA">https://github.com/niawa/ARIA</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/Accessibility/An_overview_of_accessible_web_applications_and_widgets">https://developer.mozilla.org/ko/docs/Web/Accessibility/An_overview_of_accessible_web_applications_and_widgets</a></li>
<li><a href="https://w3c.github.io/using-aria/">https://w3c.github.io/using-aria/</a></li>
<li><a href="https://aoa.gitbook.io/skymimo/">https://aoa.gitbook.io/skymimo/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[WCAG - 직접 체험해본 접근성!!!]]></title>
            <link>https://velog.io/@o1_choi/WCAG%EC%99%80-%EC%A7%81%EC%A0%91-%EC%B2%B4%ED%97%98%ED%95%B4%EB%B3%B8-%EC%A0%91%EA%B7%BC%EC%84%B1</link>
            <guid>https://velog.io/@o1_choi/WCAG%EC%99%80-%EC%A7%81%EC%A0%91-%EC%B2%B4%ED%97%98%ED%95%B4%EB%B3%B8-%EC%A0%91%EA%B7%BC%EC%84%B1</guid>
            <pubDate>Thu, 30 Jun 2022 12:25:27 GMT</pubDate>
            <description><![CDATA[<h1 id="wcag란">WCAG란?</h1>
<p>W3C 웹 콘텐츠 접근성 가이드라인 표준 권고안으로 , 웹 서비스를 제작하는 사람들이 기획 , 디자인 , 개발 과정에서 고려해야 할 요구사항이다.</p>
<h2 id="왜-필요한가">왜 필요한가?</h2>
<p>지침을 준수함으로써 저시력 , 전맹 , 청각 , 운동 , 학습 및 인지 등 다양한 장애가 있는 사람들이 더 쉽게 콘텐츠에 접근할 수 있기 때문이다.</p>
<p>그러므로 웹 콘텐츠 생산자는 모든 사용자가 어떤 기기에서, 어떠한 상황에서든,개인적인 제약이 있던, 상
황적인 제한 상황이던 사용하기 쉬운 서비스를 제공을 목표로 해야 한다. 그러므로 일반적으로 UI/UX 가
좋다는 심미적, 인식적인 디자인 개념을 넘어서 모두가 사용하기 좋은 디자인을 고려해야 한다.</p>
<p>누구나 어떠한 환경에서 유려하게 사용 가능해야 하며 , 그 기준이 되는 것이 WCAG 이다 .</p>
<h2 id="wcag-대원칙-4가지">WCAG 대원칙 4가지</h2>
<blockquote>
<p>모든 콘텐츠를 <strong>인식</strong>해서 파악할 수 있어야 하고
파악한 콘텐츠를 사용할 수 있도록 <strong>운영</strong>하고 있어야 하며 ,
콘텐츠의 내용이 명확하여 <strong>이해</strong>하기 쉬워야 하며 ,
모든 기기 및 브라우저에서 접근 , 사용할 수 있도록 <strong>견고</strong>해야 한다 .</p>
</blockquote>
<ol>
<li><p><strong>인식</strong></p>
<ul>
<li>대체 텍스트: 텍스트가 아닌 콘텐츠가 그 의미나 용도를 동등하게 인식할 수 있도록 적절한 텍스트를 제공해야 한다 .<ul>
<li>멀티미디어 대체 수단 : 자막, 대본 또는 수화를 통해 멀티미디어와 동등한 콘텐츠를 제공해야한다.</li>
<li>명료성: 색에 관계없이 인식될 수 있어야 한다.</li>
</ul>
</li>
</ul>
</li>
<li><p>*<em>조작(운용) *</em></p>
<ul>
<li>입력 장치 접근성: 마우스 이외의 입력 장치로도 모든 기능을 사용할 수 있어야 한다.</li>
<li>충분한 시간 제공: 시간 제한이 있는 컨텐츠는 가급적 포함하지 않는 것이 바람직하며 보안 등의 사유로 시간 제한이 필요한 경우, 사용자에게 시간 제한을 연장할 수 있는 수단을 제공해야한다.</li>
<li>광과민성 발작 예방 : 깜빡거리는 콘텐츠로 발작을 일으키지 않도록 초당 3~50 회 주기로 컨텐츠를 제공하지 않는다.</li>
<li>쉬운 내비게이션 : 메뉴처럼 같은 반복 영역을 바로 건너뛰어 핵심 영역으로 직접 이동할 수 있는 수단을 제공해야 한다.</li>
</ul>
</li>
<li><p><strong>이해</strong></p>
<ul>
<li>가독성: 텍스트 콘텐츠의 언어 정보를 낭독 프로그램으로 전달하기 위해 콘텐츠에 적용되는 기본 언어를 명시해주어야 한다.</li>
<li>예측 가능성: 사용자 입력이 의도하지 않는 기능이 자동적으로 실행되지 않도록 해야한다.            <br> ex. 새 창이나 팝업 창 등이 사용자가 인지하고 있는 상황에서 열리도록 해야한다.</li>
<li>콘텐츠의 논리성: 선형 구조로 작성되어 사용자가 내용을 이해할 수 있어야 한다.</li>
<li>입력 도움: 사용자가 입력하는 기능을 사용할 때 기능 주변에 해당 사용법을 알려주는 레이블이 제공되어야 한다.</li>
</ul>
</li>
<li><p><strong>견고</strong></p>
<ul>
<li>문법 준수: 마크업의 규칙에 맞게 열고 닫음 및 중첩 관계 등의 오류가 없어야 한다.  →  이와  같은 규칙이 지켜지지 않으면 웹 브라우저나 보조 기술이 작동을 멈추거나 컨텐츠를 명확하게 전달할 수가 없다.</li>
<li>웹 애플리케이션 접근성: 웹 콘텐츠나 기능을 사용하는 데 필요한 부수적인 것들이 사용자가 웹 페이지를 이용하는 것을 방해해서는 안된다.</li>
</ul>
</li>
</ol>
<h2 id="지침">지침</h2>
<p>4가지 원칙에는 각각 지침이 있다. 다양한 장애 상태에 있는 사용자에게 보다 접근성이 좋은 콘텐츠를 제공하기 위해 제작자가 지향해할 할 기본 목표이다.</p>
<h2 id="적합성-수준">적합성 수준</h2>
<p>적합성 수준은 포함관계이다. 즉, Level AAA 적합성의 경우, Level A, Level AA, Level AAA 적합 기준을 모두 만족해야 한다.</p>
<p><strong>A : 최소 수준</strong>
이러한 항목을 다루지 않으면 보조 기술로 극복할 수 없는 장벽이 존재하게 된다. (가장 많은 혜택을 제공하는 광범위한 그룹에 영향을 미침.)</p>
<p><strong>AA : 접근성 향상</strong>
가장 일반적인 수준의 적합성 (대부분의 법률 및 공식 요구 사항으로 준수를 권장) 데스크톱 및 모바일 장치에서 대부분의 보조 기술과 함께 작동해야 하는 접근성 수준을 설정 해당 레벨 기준을 해결하면 페이지의모양에 영향을 미치거나 사이트 논리에 더 큰 영향을 미친다.</p>
<p><strong>AAA : 접근성 향상</strong>
해당 기준은 모든 곳에 적용할 수 없으므로 일반적으로 필요하지 않는다. 해당 수준을 충족하더라도 모든
사람의 웹페이지에 액세스할 수 있는 것이 아니다. 업무적 특성에 따라 WCAG 지침 이외 사항을 구현할
필요가 있다. *필수 사항이 아님.</p>
<h3 id="적합성-수준-성공-이해시-고려할-점">적합성 수준 성공 이해시 고려할 점</h3>
<ul>
<li>성공 기준이 필수적인지 (즉, 충족되지 않으면 보조 기술로도 콘텐츠에 액세스할 수 없음)</li>
<li>모든 웹사이트의 성공 기준 및 성공 기준이 적용될 콘텐츠 유형 (ex.  다양한 주제, 콘텐츠 유형, 웹 기술 유형 ) 을 충족할 수 있는지</li>
<li>콘텐츠 작성자가 합리적으로 달성할 수 있는 기술이 성공 기준에 필요한지 (즉, 성공 기준을 충족하
기 위한 지식과 기술은 1 주일 이내의 교육으로 습득할 수 있음)</li>
<li>성공 기준이 웹 페이지의 &quot;모양 및 느낌&quot; 또는 기능에 제한을 부과하는지 (성공 기준이 저자에게 부여할 수 있는 기능, 프레젠테이션, 표현의 자유, 디자인 또는 미학에 대한 제한)</li>
<li>성공 기준이 충족되지 않은 경우 해결 방법이 없는지<h3 id="어떤-적합성-실패-사례가-많은가">어떤 적합성 실패 사례가 많은가?</h3>
<img src="https://velog.velcdn.com/images/o1_choi/post/40968e57-5878-448e-89c4-79ad2436d749/image.png
" width="700" /><blockquote>
<p>전체 감지된 실패 사례의 96.5%가 이 6 가지 범주에 속한다.
<a href="https://webaim.org/projects/million/#errors">https://webaim.org/projects/million/#errors</a></p>
</blockquote>
</li>
</ul>
<h3 id="적합성-실패-사례의-해결-방법은-무엇인가">적합성 실패 사례의 해결 방법은 무엇인가?</h3>
<ul>
<li>낮은 대비 텍스트
텍스트 또는 배경의 색상을 변경하여 낮은 대비 접근성 오류를 수정
Lighthouse 테스트를 실행하여 웹사이트에서 색상 대비가 문제인지 확인</li>
<li>이미지의 대체 텍스트 누락
단순 img태그의 alt 속성값 추가로 접근성 표준 충족을 위한게 아니라 스크린리더 사용자의 입장에서 충분한 설명되는 대체 텍스트가 필요</li>
<li>빈 링크
로고를 링크로 사용시 스크린 리더 사용자를 고려하여 텍스트요소를 작성 후 css로 처리</li>
<li>누락된 양식 입력 레이블
(나중에 알게되면 채워넣자)</li>
<li>빈 버튼
이미지버튼으로 사용시 스크린 리더 사용자를 고려하여 텍스트요소를 작성 후 css로 처리</li>
<li>문서 언어 누락
화면 낭독기의 주 언어 설정을 위해 홈페이지에서 주로 사용하는 언어를 명시
lang 속성 값인 Language tag 문법은 언어 하위태그(Language subtag, 필수)와 지역 하위태그(Region subtag, 선택사항)으로 구성</li>
</ul>
<h2 id="-직접-체험해-본-접근성">+ 직접 체험해 본 접근성</h2>
<p><a href="https://nuli.navercorp.com/">Naver Nuli</a>에서 저시력, 전맹 시각장애, 손, 중증 운동장애 접근성 체험 후 느낀점</p>
<ol>
<li>저시력 체험</li>
</ol>
<ul>
<li>화면을 아무리 확대해도 식별이 쉽지 않았다.</li>
<li>명도 대비가 낮은 경우 텍스트가 배경과 혼합되어 인식이 어려웠다.</li>
</ul>
<ol start="2">
<li>전맹 체험</li>
</ol>
<ul>
<li>스크린 리더 사용자의 불편함을 조금이나마 이해하게 되었다.</li>
<li>페이지 내용을 한 번에 인식할 수 없어 어떤 내용이 나올지 긴장 상태를 유지하게 됐다.</li>
<li>보안 문자를 한 번에 기억하고 기록해야 해고 input 창을 제대로 찾지 못해 조작 및 운용에 어려움을 겪었다.</li>
<li>이미지에 포커스 시 충분하지 않은 설명은 어떤 이미지인지 전혀 알 수 없었다.</li>
<li>문서 언어를 사용하여 단어의 발음이 달라지기에 언어 표시의 중요성을 느꼈다.</li>
</ul>
<ol start="3">
<li>손 운동 장애</li>
</ol>
<ul>
<li>충분한 버튼 영역을 확보해야 하는 이유를 알게 되었다.</li>
</ul>
<ol start="4">
<li>중증 운동 장애</li>
</ol>
<ul>
<li>키보드를 누르고 모니터 보기를 반복해야 했다.</li>
<li>페이지를 원하는 만큼 내리는 것이 힘들었다.</li>
</ul>
<h2 id="개발자로서-생각해야-할-것은-무엇일까">개발자로서 생각해야 할 것은 무엇일까?</h2>
<p><strong>모든 사용자들을 위한 배려</strong>
WCAG를 공부하다보니 불편함이 없는 입장에서 접근성 필요에 대한 인지 부족, 접근성보다 디자인적 요소를 중요하게 생각하는 사회적 인식, 접근성에 대한 제도적인 기준 미흡 등의 이유로 생각보다 어렵지 않게 수정 가능한 오류가 전체 오류의 높은 비율을 차지하는 것이 안타까웠다. 
웹 접근성의 목표 대상은 &#39;모든 유저&#39;로, 장애를 가진 사용자 뿐 아니라 일시적인 환경적 장애로 웹 접근이 불편한 사용자, 제한된 하드웨어만으로 웹에 접근해야 하는 사용자 등 모든 사용자가 차별없이 컨텐츠를 이용할 수 있도록 접근성을 고려하는 태도를 가지자</p>
]]></description>
        </item>
    </channel>
</rss>