<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>mini.log</title>
        <link>https://velog.io/</link>
        <description>성장하는 웹 프론트엔드 개발자 입니다.</description>
        <lastBuildDate>Sun, 29 Mar 2026 12:42:11 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>mini.log</title>
            <url>https://velog.velcdn.com/images/sang-mini/profile/1a2cc697-7307-4285-a439-c550401ab8fb/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. mini.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sang-mini" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Frontend Fundamentals 모의고사 2회차 후기]]></title>
            <link>https://velog.io/@sang-mini/Frontend-Fundamentals-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC-2%ED%9A%8C%EC%B0%A8-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@sang-mini/Frontend-Fundamentals-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC-2%ED%9A%8C%EC%B0%A8-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 29 Mar 2026 12:42:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sang-mini/post/87667197-ba05-4799-a4bb-1e6bfa6c2344/image.png" alt=""></p>
<p>Frontend Fundamentals 모의고사 2회차에 참여했다. 저번 1회차에 운좋게 참여하게 되어 유익한 시간을 보내고, 이번에도 이어서 참여하게 되었다. </p>
<blockquote>
<p><a href="https://velog.io/@sang-mini/Frontend-Fundamentals-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC-%ED%9B%84%EA%B8%B0-1zkdospu">1회차 후기</a></p>
</blockquote>
<p>이번에는 개인적인 사유로 많은 시간을 할애하지는 못해서 아쉬움이 있었지만 1회차 때 배웠던 것들을 바탕으로 빠르게 한 번 진행해보았다. 1회차와는 다르게 구현 과제가 아닌 리팩토링 과제여서 조금 더 수월하게 진행할 수 있었고, 컴포넌트 추상화에만 조금 더 집중할 수 있는 시간이었다.</p>
<h3 id="느낀점-3줄-요약">느낀점 3줄 요약</h3>
<ol>
<li>이전에 배웠던 UI가 드러나게 추상화하기를 잘 해낸 것 같았다.</li>
<li>라이브 해설강의를 듣고난 후, 추출과 추상화를 여전히 잘 구분짓지 못하고 있었다는 걸 깨달았다.</li>
<li>1회차에는 다른 참가자들 코드도 많이 뜯어보고 리뷰도 남겼었는데, 그런 시간을 가지지 못해서 아쉬움이 크게 남았다.</li>
</ol>
<h1 id="과제-진행">과제 진행</h1>
<h3 id="1-ui만-보고-임시-코드-작성하기">1. UI만 보고 임시 코드 작성하기</h3>
<p>1회차 때 배운대로 기존 코드를 보지 않고 UI만 보고 페이지 컴포넌트의 대략적인 형태를 임시 코드로 작성했다. 그리고 추상화 하기로 한 컴포넌트들의 인터페이스를 정해보았다.
여기서 집중했던 것은 UI가 드러나도록 하는 것이었는데, 내가 생각하기에 중요했던 부분은 섹션들마다 있는 타이틀, 레이아웃을 위한 <code>spacing</code> 컴포넌트 들이었다. 이것들을 페이지 컴포넌트에 드러내야 페이지 컴포넌트를 보았을 때 UI와 바로바로 매칭이 될 거라고 생각했다. </p>
<h3 id="2-현재-구현-확인하기">2. 현재 구현 확인하기</h3>
<p>주어진 코드는 모든 페이지의 로직과 jsx가 하나의 컴포넌트에 들어가있는 방식이었다. 일단은 많은 로직들이 있었지만 이걸 기준으로 추상화하지는 않고 UI기준으로 내가 작성한 컴포넌트들로 추상화하고 나면 로직들이 책임에 맞는 컴포넌트로 위치하게 될거라고 생각하고 현재 구현에서 jsx 부분을 위주로 추상화하겠다고 계획을 세웠다.</p>
<h3 id="3-리팩토링-시작">3. 리팩토링 시작</h3>
<p>정말 눈에 보이는 UI대로 컴포넌트화를 진행했다. 여기서 나는 추출과 추상화에 대해서 조금 더 고민이 필요했다고 생각한다. </p>
<p>내가 기존에 생각했던 인터페이스대로 리펙토링이 가능한지를 codex를 이용해서 빠르게 검증해보고 인터페이스만 가지고 이래저래 고민해보았다. 대부분의 컴포넌트들은 section이라는 단위로 분리를 하면 해당 section에서 필요한 상태값과 변환 함수, 그리고 제목이 props로 드러나기 때문에 적절해보였다. </p>
<p>한가지 고민했던 부분은 회의실의 예약 현황을 보여주는 section이었는데 단순히 추출만 하고 props를 구성하면 다음과 같은 인터페이스를 가지게 된다.</p>
<pre><code>      &lt;ReservationTimelineSection
        title=&quot;예약 현황&quot;
        rooms={rooms}
        reservations={reservations}
        activeReservationId={activeReservation}
        onToggleReservation={setActiveReservation}
      /&gt;</code></pre><p>하지만 이 컴포넌트의 실제 UI를 보면 섹션 title안에 회의실 별 타임 테이블리스트가 있고 그 타임 테이블은 header와 timelinelist로 이루어져 있다. 그래서 title을 제외한 다른 것들은 사실 타임 테이블 리스트를 위한 것이다. 그래서 이런 UI적 요소들을 드러내고 왜 이런 props들이 필요한지 더 잘 나타내기 위해 합성컴포넌트로 나타내보았다. </p>
<pre><code>      &lt;ReservationTimelineSection
        title=&quot;예약 현황&quot;
        timeHeader={&lt;ReservationTimelineSection.TimeHeader /&gt;}
        timeline={
          &lt;ReservationTimelineSection.RoomTimelineList
            rooms={rooms}
            reservations={reservations}
            activeReservationId={activeReservation}
            onToggleReservation={setActiveReservation}
          /&gt;
        }
      /&gt;</code></pre><h3 id="4-해설강의를-듣고">4. 해설강의를 듣고..</h3>
<p>짧은 시간이었지만 나름 잘 해냈다라고 생각했는데 조금 더 추상화에 대해서 생각해보았어야한다는 것을 해설강의를 통해 느꼈다. 오히려 긴 시간 고민했던 1회차때의 코드가 조금 더 나았나? 싶기도 할 정도로 이번에는 거의 추출에 가깝게 리팩토링을 했다는 것을 해설 강의를 진행하는 내내 느꼈다. </p>
<p>먼저 섹션 컴포넌트를 추출할 때 title까지 추출할 필요가 있었을까 싶다. title을 제외한 내용이 들어있는 부분을 컴포넌트화 하겠다고 생각했다면 오히려 추상화에 집중할 수 있었을 것 같다. 
가장 오래 고민했던 <code>ReservationTimelineSection</code>의 경우를 예로 들자면, title부분은 페이지 컴포넌트에 그냥 드러내고 타임 테이블 부분만 추상화하겠다고 하면 자연스럽게 예약 현황이라는 도메인 지식을 컴포넌트에서 분리했을거다. 
<code>rooms</code>, <code>reservations</code>가 아닌 <code>rows</code>, <code>timeRange</code> 등의 props를 받도록 설계했다면 그냥 타임 테이블을 나타내는 컴포넌트로 추상화 할 수 있었다. 그러면 타임테이블을 나타내는 컴포넌트를 페이지컴포넌트에서 가져다썼고, 그안에 들어가는게 회의실과 예약이라는 것이 props로 표현되었을 것이다.</p>
<h3 id="5-배운점">5. 배운점</h3>
<p>잘못된 코드 의심하는 방법 하나가 기억에 남는다. </p>
<pre><code>&lt;ReservationTimelineSection.RoomTimelineList
    rooms={rooms}
    reservations={reservations}
    activeReservationId={activeReservation}
    onToggleReservation={setActiveReservation}
 /&gt;</code></pre><p>이 props들을 보면 rooms에 rooms를 넘겨준다. 이러면 의심해볼만 하다. 이건 추상화가 맞나? 추출아닌가? 
잘 추상화 되었다면 아래와 같을거다. (물론 props 이름은 더 고민이 필요하다.)</p>
<pre><code>&lt;TimeTable
    rows={rooms}
    timeRange={reservations}
    activeTimeId={activeReservation} 
    onToggleTimeRange={setActiveReservation}
 /&gt;</code></pre><p>이렇게 2회차를 마무리 했는데, 리팩토링을 하면서 조금 더 고민과 연습을 해봐야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Frontend Fundamentals 모의고사 후기]]></title>
            <link>https://velog.io/@sang-mini/Frontend-Fundamentals-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC-%ED%9B%84%EA%B8%B0-1zkdospu</link>
            <guid>https://velog.io/@sang-mini/Frontend-Fundamentals-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC-%ED%9B%84%EA%B8%B0-1zkdospu</guid>
            <pubDate>Tue, 25 Nov 2025 16:18:36 GMT</pubDate>
            <description><![CDATA[<h1 id="frontend-fundamentals-모의고사란">Frontend Fundamentals 모의고사란?</h1>
<blockquote>
<p><a href="https://frontend-fundamentals.com/code-quality/">Frontend-fundamentals</a></p>
</blockquote>
<p><strong>Frontend Fundamentals 모의고사</strong>는 토스에서 실제로 출제되었던 과제에 참여하면서 토론하고 해설을 들을 수 있는 프로그램이다. 링크드인에서 이런 프로그램을 진행한다는 것을 보고 선착순 50명 안에 들기 위해 알람을 맞춰놓고 기다렸다. </p>
<p>올해 초, Frontend Fundamentals 문서를 읽고 회사에서도 개인 프로젝트에서도 어떤 코드가 더 좋을까에 대해서 많이 고민하고 개선할 수 있었다. 이번 기회로 토스에서 코드를 작성할 때 중요하게 생각하는 부분이 어떤 것인지 더 많이 배우기를 기대하며 모의고사에 참여했다.</p>
<h3 id="느낀점-3줄-요약">느낀점 3줄 요약</h3>
<p>먼저 간략하게 느낀점부터 적어보자면,</p>
<ol>
<li>과제를 풀면서 평소에 고민하던 부분들을 많이 마주할 수 있어서 좋았다. </li>
<li>제출 후에는 다른 참여자들의 코드를 보면서 좋은 구현 아이디어도 얻어가고, 다른 분들은 어떤 걸 중요하게 생각하며 코드를 작성하는지도 엿볼 수 있었다.</li>
<li>라이브 해설강의가 수능 모의고사 풀이 강의를 떠오르게 했는데, 무릎을 탁치게하는 깨달음도 얻고 나도 나름 Frontend-fundamentals에서 배운 점들을 잘 써먹고 있구나 싶은 자신감도 생겼다. </li>
</ol>
<h1 id="과제-풀어보기">과제 풀어보기</h1>
<p>처음 과제를 받았을 때, 요구사항의 기능 구현 난이도가 높지는 않아보였고 확장성을 고려한 설계와 추상화 관점에 집중해달라는 문구가 있었다. 
그래서 익숙한 기술들을 사용해서 기능은 빠르게 구현하고, 설계에 고민을 많이 해보자는 마음으로 시작했다.</p>
<h2 id="고민했던-부분">고민했던 부분</h2>
<p>폼 상태 관리, 필터링 로직 가독성 고민 같은 부분도 있었지만 추상화가 주요 고민이었다. 
여러 입력 필드들, 탭 전환, 리스트 등 별 생각 없이 구현하다보면 그냥 관성적으로 컴포넌트로 분리할까? 싶은 부분들이 있었다.
하지만 이번 만큼은 이유 없이 추상화 하지 않겠다고 다짐하며 고민 끝에 나름의 기준을 세울 수 있었다. 고민을 하며 세운 정말 나름의 기준이고 정답은 아니다.</p>
<ul>
<li>단순히 의미상으로 비슷한 것끼리 묶는 컴포넌트를 만들지 말자.</li>
<li>UI 라이브러리에서 제공하는 컴포넌트 가공하여 재사용 위한 컴포넌트로 만들지말자.</li>
<li>레이아웃은 페이지에서 책임지고 하위 컴포넌트에 숨기지 말자.</li>
<li>로직을 책임 단위로 보고 컴포넌트를 추상화 하자.</li>
</ul>
<h3 id="입력-필드를-form으로">입력 필드를 form으로?</h3>
<p>여러 입력 필드들이 있고, 이 입력 필드들은 연관된 상태를 가지기 때문에 하나의 form state로 다루는 상황이었다. 이 필드들을 form이라는 컴포넌트로 추상화할지 고민되었다. 그래서 일단 컴포넌트를 만들어보니 몇가지 문제점이 눈에 보였다.</p>
<ol>
<li><code>&lt;Spacing /&gt;</code>으로 필드사이 간격을 조정하고 있었는데, 하나의 form으로 묶어보니 페이지 컴포넌트에서 레이아웃이 전혀 보이지 않았다. </li>
<li>그리고 formContext로 상태를 주입받다보니 props도 없어서 정말 어떤 역할을 하는 컴포넌트인지 전혀 알 수 없었다. </li>
<li>form 안에 여러 필드 입력을 위한 로직들이 전부 혼재한다.</li>
</ol>
<p>이 3가지 문제점을 보고 필드별로 추상화하기로 결정했다. 
여기서 추상화를 안 할순 없었냐하면, 그럴수도 있었지만 입력 필드 로직, 스타일 등이 모두 페이지 컴포넌트에 존재했기 때문에 이 입력 필드 하나만을 위한 코드들은 숨기는게 좋다고 생각해서 추상화는 꼭 필요하다고 생각했다.</p>
<h3 id="탭-전환-기능을-공통화">탭 전환 기능을 공통화?</h3>
<p>탭 전환을 위한 UI 컴포넌트를 탭 내용을 렌더링 하는 기능까지 갖춘 컴포넌트로 추상화할지가 다음 고민이었다. 
하지만 이를 추상화 해서 얻는 점은 탭 렌더링 로직을 숨긴다 정도인데 탭 UI는 있으니 상태만 관리하고 조건부 렌더링을 하면 되는 건데 이걸 숨겨야할까? 싶었다. 
그리고 UI 컴포넌트의 인터페이스를 변경하지 않고 최대한 활용하는게 일관성 있게 쓸 수 있을 거라 생각하여 페이지 컴포넌트에 탭 렌더링 로직을 드러냈다.</p>
<h1 id="토론">토론</h1>
<blockquote>
<p><a href="https://github.com/toss-fe-interview/frontend-fundamentals-mock-exam-1/pull/13">제가 올린 PR</a>에도 언제든 편하게 의견 주시면 좋아요~</p>
</blockquote>
<p>모든 분의 PR을 다 보고 싶었지만 게으름 이슈로 다 보지는 못했다. 
점심시간에 잠깐 짬을 내서 코드 리뷰도 달아보고 여러 아이디어를 얻어보았다. 
기억에 남는 아이디어는 nummeric input을 다루는 hook을 통해 포메팅과 상태 로직을 추상화 한 것이었는데, 그냥 util로 만들어서 상태 변경 때마다 포메팅 하는 것보다 훨씬 좋다고 생각했다. </p>
<p>그리고 <a href="https://github.com/toss-fe-interview/frontend-fundamentals-mock-exam-1/discussions/83">&quot;단일 책임&quot; 경계에 대한 discussion</a>도 아주 재미있었다.</p>
<h1 id="해설을-듣고-난-뒤">해설을 듣고 난 뒤</h1>
<p>라이브코딩으로 진행해주신 해설을 통해 요구사항을 분석하고 접근하는 법부터 설계의 관점을 배울 수 있었다.
해설을 통해 배운점, 잘 해왔던 점을 정리해본다.</p>
<h2 id="배운점">배운점</h2>
<ul>
<li><p>추출과 추상화는 다르다. (<a href="https://jbee.io/articles/etc/%EC%A2%8B%EC%9D%80%20%EC%BD%94%EB%93%9C%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C">좋은 코드란?</a>) </p>
</li>
<li><p>요구사항을 보고 인터페이스 위주로 pseudo code를 작성하면 구현하며 흐름을 잃지 않을 수 있다.</p>
</li>
<li><p>UI가 드러나게 페이지 컴포넌트를 구성하다보면 추상화할 것과 아닌 것이 명확하게 드러난다.</p>
</li>
<li><p>Suspense fallback에 들어가는 컴포넌트를 합성 컴포넌트로 만들어서 의존성이 드러나게 할 수도 있다.</p>
</li>
<li><p>추상화를 할 때에는 엣지케이스를 내부에서 처리하고 내부 컴포넌트의 인터페이스를 유지하자.</p>
</li>
<li><p>책임 단위로 추상화를 하면 재사용성은 따라온다.</p>
</li>
<li><p>컴포넌트 인터페이스를 설계할 때 일반적인 props를 사용하자. </p>
</li>
<li><p>useState는 구현의 영역이다. 이것만으로 추상화할 가치가 있다. useState가 아닌 다른 방식으로 구현하게 될 수도 있으니.</p>
<pre><code class="language-tsx">// 이렇게 단순한 상태만 관리하는 훅이지만 
// 이 내부 구현이 전역 상태가 될지, storage를 사용할지, reducer를 사용할지, setQueryState를 사용할지 등
// 구현 방식의 선택사항일 뿐, 단순하니 추상화할 가치가 없다고 생각하지말자. 
function useView(...) {
const [view, setView] = useState&lt;&quot;view1&quot; | &quot;view2&gt;(...)

return [view, setView] as const
}</code></pre>
</li>
</ul>
<h2 id="잘한점">잘한점</h2>
<ul>
<li>페이지 컴포넌트에서 UI가 드러나게끔 추상화를 했다. 해설을 통해 기준을 더 명확하게 할 수 있었다.</li>
<li>대부분의 컴포넌트 인터페이스에서 의도가 드러나게 해서 사용하는 곳에서 예측이 되도록 했다. 이건 좀 더 연습이 필요하다. </li>
</ul>
<h1 id="끝으로">끝으로..</h1>
<p>오늘을 계기로 코드를 보는 시각이 달라질 것 같다. 많은 고민과 연습이 필요하겠지만 좋은 코드의 기준들이 체화되도록 노력해야겠다. </p>
<p>좋은 시간 만들어주신 토스 프론트엔드 개발자 분들께 감사드립니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[함수형 프로그래밍으로 검색 기능 만들기]]></title>
            <link>https://velog.io/@sang-mini/%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%9C%BC%EB%A1%9C-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@sang-mini/%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%9C%BC%EB%A1%9C-%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 19 Aug 2025 16:17:04 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>검색 기능을 구현하면서 점진적으로 함수형 프로그래밍 스타일로 개선해나간 과정을 기록해보려고 한다. 
코드가 적힌 순서대로 실행되어 로직이 잘 읽히도록 노력했다.</p>
</blockquote>
<h2 id="최초-코드">최초 코드</h2>
<p>처음엔 그냥 기본적인 자바스크립트 방식으로 검색을 구현했다. 띄어쓰기를 고려해서 <code>replace</code>로 공백을 제거하는 방식을 썼다.</p>
<pre><code class="language-typescript">// 함수형 프로그래밍을 위해 FxTS 라이브러리를 사용했다.
import { filter, pipe, toArray, isUndefined, unless } from &#39;@fxts/core&#39; 

...

  // 기존 카테고리 필터 로직에 검색 로직을 추가했다.
  const search = Route.useSearch()

  const { data: policies } = useSuspenseQuery({
    ...getPoliciesQueryOptions(),
    select: ({ data: policies }) =&gt; {
      pipe(
        policies,
        unless(
          () =&gt; isUndefined(search.category),
          filter((policy) =&gt; policy.category === search.category!)
        ),
        // 여기 부터 개선하게 될 코드
        unless(
          () =&gt; isUndefined(search.keyword),
          filter(
            (policy) =&gt;
              policy.title
                .toLowerCase()
                .replace(/\s+/g, &#39;&#39;)
                .includes(search.keyword!.toLowerCase().replace(/\s+/g, &#39;&#39;)) ||
              policy.description
                .toLowerCase()
                .replace(/\s+/g, &#39;&#39;)
                .includes(search.keyword!.toLowerCase().replace(/\s+/g, &#39;&#39;))
          )
        ),
        toArray
      )
    },
  })</code></pre>
<p>동작은 하지만 코드가 잘 읽히지 않고 저 <code>||</code> 비교연산자를 기준으로 딱봐도 같은 코드가 반복되고 있다.  이 두가지 부분을 어떻게든 개선해 보고 싶었다.</p>
<h2 id="step-1-공통-로직-분리">Step 1: 공통 로직 분리</h2>
<p>가장 먼저 텍스트 정규화 로직을 함수로 분리했다. 일단 중복을 제거해보려는 시도였다.</p>
<pre><code class="language-typescript">// 텍스트 정규화 함수 분리
const normalizeText = (text: string) =&gt; text.toLowerCase().replace(/\s+/g, &#39;&#39;)

filter((policy) =&gt; {
  const normalizedKeyword = normalizeText(search.keyword!)
  return (
    normalizeText(policy.title).includes(normalizedKeyword) ||
    normalizeText(policy.description).includes(normalizedKeyword)
  )
})</code></pre>
<p>이제 <code>normalizeText</code>라는 함수명으로 의도가 명확해졌고, 키워드도 한 번만 정규화 하니까 조금 나아보였다.
그런데 return 이후의 조금 극단적이지만  코드를 순서 그대로 읽어보면 이렇게 된다.</p>
<blockquote>
<p>정규화 하는 함수에 title을 넘기고 그 결과에 정규화된 키워드가 포함되거나 정규화 하는 함수에 description을 넘기고 그 결과에 정규화된 키워드가 포함되는 policy를 필터링한다.</p>
</blockquote>
<h2 id="step-2-체이닝-방식으로-복귀">Step 2: 체이닝 방식으로 복귀</h2>
<p>Step 1에서의 함수 분리는 좋았지만, 읽기가 어려웠고 여전히 <code>||</code> 연산자를 기준으로 같은 코드가 반복된다. 차라리 체이닝 방식이 읽는 순서대로 실행되기 때문에 더 낫다고 생각했다. </p>
<pre><code class="language-typescript">filter(
  (policy) =&gt;
    policy.title
      .toLowerCase()
      .replace(/\s+/g, &#39;&#39;)
      .includes(search.keyword!.toLowerCase().replace(/\s+/g, &#39;&#39;)) ||
    policy.description
      .toLowerCase()
      .replace(/\s+/g, &#39;&#39;)
      .includes(search.keyword!.toLowerCase().replace(/\s+/g, &#39;&#39;))
)</code></pre>
<h2 id="step-3-pipe로-공통로직-순서대로-실행하기">Step 3: pipe로 공통로직 순서대로 실행하기</h2>
<p>텍스트 정규화 로직을 함수화 한건 좋은 시도였고, 문제는 함수의 실행순서라고 생각했다.
그래서 반복되고 있던 <code>search.keyword</code> 부터 일단 <code>pipe</code>의 첫 번째 인자로 넣어보기로 했다.</p>
<pre><code class="language-typescript">filter((policy) =&gt;
  pipe(
    search.keyword!,
    normalizeText,
    (normalizedKeyword) =&gt;
      some((field) =&gt; normalizeText(field).includes(normalizedKeyword), [
        policy.title,
        policy.description,
      ])
  )
)</code></pre>
<p>이제 키워드는 한 번만 정규화되고, 로직의 흐름도 더 명확해졌다. 키워드를 정규화한 다음, 그 결과를 가지고 필드들과 비교하는 구조가 되었다.</p>
<h2 id="step-4-필드-배열을-기준으로-pipe-실행">Step 4: 필드 배열을 기준으로 pipe 실행</h2>
<p>그런데 <code>some</code> 함수를 보다보니 괜히 함수로 감싸야하고, 비교 대상 필드 배열도 두번째 인자로 위치시켜야했다. 다시 생각해보니 비교 대상 필드 배열을 <code>pipe</code>의 첫번째 인자로 하는게 자연스러울 것 같았다.</p>
<pre><code class="language-typescript">filter((policy) =&gt;
  pipe(
    [policy.title, policy.description],
    map(normalizeText),
    some((field) =&gt; field.includes(normalizeText(search.keyword!)))
  )
)</code></pre>
<p>이제 로직이 정말 읽는 순서대로 진행된다</p>
<ol>
<li>비교 대상 필드들을 배열로 만들어</li>
<li>모든 배열 원소를 정규화하고  </li>
<li>정규화된 키워드가 포함된 필드가 있는지 확인한다</li>
</ol>
<p>하지만 여전히 some안의 콜백 함수 로직이 눈에 거슬렸다. 간단하니까 읽어지는 것 같지만 함수가 중첩되어 순서에 맞지 않다. </p>
<h2 id="step-5-한번-더-pipe-실행하기">Step 5: 한번 더 pipe 실행하기</h2>
<p>이쯤 되니 함수 실행 순서가 반대일 때는 <code>pipe</code>를 사용하면 되는구나라는 생각이 저절로 들었다.
<code>some</code> 안의 콜백도 <code>pipe</code>를 이용해 순서대로 읽게 했다. </p>
<pre><code class="language-typescript">filter((policy) =&gt;
  pipe(
    [policy.title, policy.description],
    map(normalizeText),
    some((field) =&gt; pipe(
      search.keyword!, 
      normalizeText, 
      (keyword) =&gt; field.includes(keyword)
    ))
  )
)</code></pre>
<p>이제 모든 부분이 순서대로 실행되어 데이터가 어떻게 변화되는지 잘 읽을 수 있게 되었다. 키워드를 2번 정규화하게 되지만, 성능에 영향을 미치지는 않을거라 가독성을 높이는게 더 좋다고 생각했다.</p>
<h2 id="최종-결과">최종 결과</h2>
<pre><code class="language-typescript">const { data: policies } = useSuspenseQuery({
  ...getPoliciesQueryOptions(),
  select: ({ data: policies }) =&gt;
    pipe(
      policies,
      unless(
        () =&gt; isUndefined(search.category),
        filter((policy) =&gt; policy.category === search.category!)
      ),
      unless(
        () =&gt; isUndefined(search.keyword),
        filter((policy) =&gt;
          pipe(
            [policy.title, policy.description],
            map(normalizeText),
            some((field) =&gt; pipe(
              search.keyword!, 
              normalizeText, 
              (keyword) =&gt; field.includes(keyword)
            )
           )
          )
        )
      ),
      toArray
    ),
})</code></pre>
<h2 id="마지막으로">마지막으로..</h2>
<pre><code class="language-typescript"> some((field) =&gt; pipe(
   search.keyword!, 
   normalizeText, 
   field.includes // 화살표 함수가 아닌 메서드를 pipe에 넘기면?
 )</code></pre>
<p>마지막 화살표 함수를 쓴 부분을 꼭 저렇게 써야하나 라는 생각이 들어 메서드를 넘겨보았다.
하지만 동작을 하지 않았는데, 이유는 <code>this</code> 바인딩 때문이었다.
공부할 때 정말 많이 봤던 내용이라 알고 있다고 생각했는데 전혀 아니었다. 
this 바인딩에 대해서는 다시 잘 정리해서 다음 글로 써야겠다.</p>
<h2 id="느낀점">느낀점</h2>
<ul>
<li>함수형으로 로직을 작성하면 코드를 읽는 순서와 실행 순서를 일치시킬 수 있다.</li>
<li>새로운 비교 대상 필드가 생겨도 배열에 넣으면 되니 확장성도 좋다.</li>
<li>카테고리 필터링에 검색 필터링을 쉽게 추가한 것도 <code>pipe</code>의 힘이라고 생각한다.</li>
<li>의도적으로라도 <code>pipe</code>와 <code>map</code>, <code>filter</code>, <code>some</code> 같은 함수들로 표현하다보니 다양한 로직들을 함수형으로 작성할 수 있겠다는 자신감이 생겼다.</li>
</ul>
<p>오늘 작성한 코드들은 모두 <a href="https://fxts.dev/">FxTS</a>라는 라이브러리를 사용했다. 오늘 내가 사용한 함수들 외에도 아주 많은 API를 제공하는데, 함수형 프로그래밍에 대한 두려움의 장벽을 많이 허물어 준 라이브러리다. 사용법이 정말 간단하면서 타입추론도 잘 되고, 일관성 있게 코드를 작성할 수 있게 도와준다.
유인동님의 도서 <a href="https://product.kyobobook.co.kr/detail/S000216318962">멀티패러다임 프로그래밍</a>의 리뷰어로 참여하면서 함수형 프로그래밍과 FxTS에 대해 알게 되었는데 돌아보니 정말 감사한 경험이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Prettify 타입으로 복잡한 타입 쉽게 보기]]></title>
            <link>https://velog.io/@sang-mini/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%8C%81</link>
            <guid>https://velog.io/@sang-mini/%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%8C%81</guid>
            <pubDate>Sun, 17 Aug 2025 14:17:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sang-mini/post/d6d18a16-166b-429c-8042-d0952386316d/image.png" alt=""></p>
<p>복잡한 제네릭 타입들을 중첩해서 쓰다 보면 결과 타입이 뭔지 도저히 알 수가 없다. </p>
<pre><code class="language-ts">Omit&lt;Pick&lt;SomeType, &#39;a&#39; | &#39;b&#39;&gt; &amp; OtherType, &#39;c&#39;&gt;</code></pre>
<p>IDE에서 보면 이런 식으로 나와서 매번 최종 타입까지 찾아가서 봐야한다. </p>
<p>그러던 중에 유튜브 영상에서 이런 헬퍼 타입을 만들어서 사용하는 방법을 보게 되었다.  </p>
<pre><code class="language-typescript">
/**

 * @description 복잡한 교차/중첩 타입을 펼쳐서 읽기 쉽게 표시하는 헬퍼 타입

 * 타입의 의미는 그대로 유지하면서, IDE에서 보기 좋은 형태로 변환한다.

 */

export type Prettify&lt;T&gt; = {

  [K in keyof T]: T[K]

} &amp; {}
</code></pre>
<h3 id="사용-예시">사용 예시</h3>
<pre><code class="language-typescript">
// Before: 복잡한 타입

type Complex = Omit&lt;{ a: string; b: number; c: boolean }, &#39;c&#39;&gt; &amp; { d: Date }

// IDE에서 &quot;Omit&lt;{...}, &#39;c&#39;&gt; &amp; { d: Date }&quot; 로 표시됨



// After: Prettify 적용

type Clean = Prettify&lt;Complex&gt;

// IDE에서 &quot;{ a: string; b: number; d: Date }&quot; 로 깔끔하게 표시됨



// 유니온 타입도 잘 동작함

type PolicyCategories = Prettify&lt;NonNullable&lt;PoliciesSearch[&#39;category&#39;]&gt; | &#39;전체&#39;&gt;

// IDE에서 &quot;일자리&quot; | &quot;주거&quot; | &quot;금융&quot; | &quot;교육&quot; | &quot;전체&quot; 로 깔끔하게 표시됨</code></pre>
<h3 id="동작-원리">동작 원리</h3>
<p>타입을 &quot;예쁘게&quot; 만들어주는 마법 같은 느낌이어서 원리를 찾아봤다.</p>
<h4 id="1-mapped-type으로-재구성">1. Mapped Type으로 재구성</h4>
<pre><code class="language-typescript">
{ [K in keyof T]: T[K] }
</code></pre>
<p>이 부분이 TypeScript에게 &quot;T의 모든 키를 순회하면서 새로운 객체 타입을 만들어라&quot;라고 지시한다. 교차 타입이나 복잡한 조건부 타입을 단일 객체 리터럴로 재구성하는 역할이다.</p>
<h4 id="2-빈-교차-타입으로-강제-평가">2. 빈 교차 타입으로 강제 평가</h4>
<pre><code class="language-typescript">
&amp; {}
</code></pre>
<p>이게 핵심이다. 빈 교차 타입을 추가하면:</p>
<ul>
<li><p>TypeScript 컴파일러가 타입을 즉시 평가하도록 강제함</p>
</li>
<li><p>지연 계산을 방지하고 타입 캐시를 무효화함</p>
</li>
<li><p>내부 표현을 사용자가 읽기 쉬운 형태로 정규화함</p>
</li>
</ul>
<h3 id="왜-타입스크립트는-알아서-이렇게-안해줄까">왜 타입스크립트는 알아서 이렇게 안해줄까?</h3>
<p>AI의 답변은 다음과 같다.</p>
<blockquote>
<p>TypeScript는 성능을 위해 복잡한 타입 계산을 가능한 한 미루려고 한다. 그래서 IDE에서 보면 <code>Omit&lt;Pick&lt;...&gt;, &#39;...&#39;&gt; &amp; {...}</code> 같은 내부 표현 그대로 보여준다.</p>
</blockquote>
<p><code>Prettify</code>는 이런 지연 계산을 강제로 실행시켜서 최종 결과를 보여주는 방식이다.</p>
<h3 id="언제-사용하면-좋을까">언제 사용하면 좋을까?</h3>
<ul>
<li><p>여러 제네릭 타입을 조합해서 만든 복잡한 타입</p>
</li>
<li><p>유니온 타입이 복잡하게 중첩된 경우  </p>
</li>
<li><p>라이브러리 타입들을 조합해서 만든 커스텀 타입</p>
</li>
<li><p>동료들이 타입을 이해하기 쉽게 만들고 싶을 때</p>
</li>
</ul>
<p>타입을 볼 때 타입 선언부까지 가지 않고 확인할 수 있어서 좋은 것 같다. 이렇게 타입 계산을 강제로 시키는 방식이 IDE에서 성능을 실제로 떨어뜨리는지는 확인해볼 필요가 있다. </p>
<h3 id="참고">참고</h3>
<p><a href="https://youtu.be/q5DFpyIN5Xs?si=qhd1vV67EOa4w5MI&amp;t=241">https://youtu.be/q5DFpyIN5Xs?si=qhd1vV67EOa4w5MI&amp;t=241</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TanStack Router 파일 라우팅 깔끔하게 관리하기 (feat. Private folder)]]></title>
            <link>https://velog.io/@sang-mini/TanStack-Router-%EA%BF%80%ED%8C%81-%ED%8C%8C%EC%9D%BC-%EB%9D%BC%EC%9A%B0%ED%8C%85-%EA%B9%94%EB%81%94%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-feat.-Private-%ED%8F%B4%EB%8D%94</link>
            <guid>https://velog.io/@sang-mini/TanStack-Router-%EA%BF%80%ED%8C%81-%ED%8C%8C%EC%9D%BC-%EB%9D%BC%EC%9A%B0%ED%8C%85-%EA%B9%94%EB%81%94%ED%95%98%EA%B2%8C-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-feat.-Private-%ED%8F%B4%EB%8D%94</guid>
            <pubDate>Sun, 17 Aug 2025 14:00:06 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sang-mini/post/09990a7e-489d-4846-bc80-7904e85a60f6/image.png" alt=""></p>
<p>요즘 TanStack Router를 아주 잘 사용하고 있는데 이걸 선택한 이유는 크게 두가지다.</p>
<ol>
<li>Type Safe하게 router 기능을 사용할 수 있다. (React Router를 쓰면서 가장 불편했던 점..)</li>
<li>파일 기반 라우팅이 가능하다.</li>
</ol>
<p>나는 NextJS를 많이 사용해보지 않아서 파일 기반 라우팅을 할 때에는 디렉토리 구조를 어떻게 구성해야할지 고민이 되었다.</p>
<blockquote>
<p>&quot;특정 라우트에서만 사용하는 컴포넌트나 훅은 어디 둬야 하지..?&quot;</p>
</blockquote>
<p><code>/policies</code> 경로에서만 쓰는 <code>policy-card</code> 컴포넌트가 있다면, <code>/policies/components/policy-card</code> 이런 식으로 route 하위의 <code>components</code> 디렉토리를 만들고 싶었다. 
하지만 이렇게 할 경우 <code>components</code>를 sub route로 인식해서 routeTree에 포함이 된다.</p>
<h3 id="🤔-routefileignoreprefix-vs-routefileignorepattern">🤔 routeFileIgnorePrefix vs routeFileIgnorePattern</h3>
<p>NextJS에서는 <a href="https://nextjs.org/docs/app/getting-started/project-structure#private-folders">Private folder</a>를 이용해서 <code>_components</code>와 같이 route로 인식하지 않게 하는 방법이 있었다. 그래서 이와 같은 기능이 있을 거라고 생각하고 문서를 찾아보니 명시적으로 라우팅에서 특정 파일/폴더를 무시하는 두 가지 옵션을 찾을 수 있었다.</p>
<ol>
<li><a href="https://tanstack.com/router/latest/docs/api/file-based-routing#routefileignoreprefix"><strong>routeFileIgnorePrefix</strong></a>: 파일/폴더 이름 앞에 특정 접두사(기본값: <code>-</code>)가 붙으면 무시하는 방식.</li>
<li><a href="https://tanstack.com/router/latest/docs/api/file-based-routing#routefileignorepattern"><strong>routeFileIgnorePattern</strong></a>: 정규식 패턴에 맞는 파일/폴더를 무시하는 방식.</li>
</ol>
<p>처음엔 <code>routeFileIgnorePattern</code>을 사용하려고 했다. <code>(components|hooks|utils)</code> 이런 식으로 정규식을 설정 파일에 한 번만 등록해두면, 폴더 이름을 <code>-</code> 없이 깔끔하게 쓸 수 있기 때문이었다.</p>
<pre><code class="language-plaintext">// Pattern 방식
src/routes/
└── policies/
    ├── components/ 
    │   └── policy-card.tsx
    ├── hooks/
    │   └── use-policy-filter.ts
    └── index.tsx</code></pre>
<h3 id="👍-prefix-방식-선택-이유">👍 Prefix 방식 선택 이유</h3>
<p>하지만 나는 <code>routeFileIgnorePrefix</code>를 쓰기로 했고 기본값인 <code>-</code>를 prefix로 쓰기로 했다.</p>
<h4 id="1-명확성">1. 명확성</h4>
<p>폴더 이름에 <code>-</code>가 붙어있으면 <strong>누가 봐도, 언제 봐도 &quot;아, 이건 라우트가 아니구나&quot;</strong> 라고 바로 알 수 있다.</p>
<pre><code class="language-plaintext">// Prefix 방식
src/routes/
└── policies/
    ├── -components/ 
    │   └── policy-card.tsx
    ├── -hooks/
    │   └── use-policy-filter.ts
    └── index.tsx</code></pre>
<p><code>routeFileIgnorePattern</code> 방식은 디렉토리 구조만 봐서는 얘가 라우트인지 아닌지 알 수가 없다. 항상 <code>vite.config.ts</code> 같은 설정 파일을 열어서 정규식을 확인해야만 한다.</p>
<h4 id="2-설정-파일-변경-최소화">2. 설정 파일 변경 최소화</h4>
<p><code>routeFileIgnorePrefix</code> 방식은 그냥 규칙에 따라 디렉토리를 만들면 된다. prefix를 바꾸지 않는다면 설정 파일을 건드릴 필요가 전혀 없다. (prefix를 <code>_</code>로 바꾸려고 했지만 <code>_</code>는 TanStack Router에서 레이아웃 파일명으로 쓰이고 있기 때문에 기본값을 선택했다.) </p>
<p>하지만 <code>routeFileIgnorePattern</code>은 나중에 <code>utils</code> 디렉토리가 필요해지면? <code>constants</code> 디렉토리가 필요해지면? 그때마다 설정 파일 열어서 정규식에 <code>|utils</code>, <code>|constants</code>를 추가해줘야 한다. 관리 포인트가 늘어나는 걸 피하고 싶었다.</p>
<h3 id="결론">결론</h3>
<ul>
<li><strong><code>routeFileIgnorePattern</code></strong>: 설정에 의존해야 하고 관리 포인트가 늘어난다.</li>
<li><strong><code>routeFileIgnorePrefix</code></strong>: 단순하고, 명확하며, 확장성이 좋다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[vscode로 디버깅하기]]></title>
            <link>https://velog.io/@sang-mini/vscode%EB%A1%9C-%EB%94%94%EB%B2%84%EA%B9%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sang-mini/vscode%EB%A1%9C-%EB%94%94%EB%B2%84%EA%B9%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 08 Jul 2024 08:03:37 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>웹 브라우저에서와 같이 vscode에서도 디버거를 사용할 수 있다. </p>
</blockquote>
<h2 id="break-point-설정">break point 설정</h2>
<h2 id="메뉴-알아보기">메뉴 알아보기</h2>
<h3 id="variables">variables</h3>
<ul>
<li>local</li>
<li>global</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/4e589b6b-ebd7-4969-8047-8dda569952bc/image.png" alt=""></p>
<h3 id="watch">watch</h3>
<ul>
<li>변수 추적</li>
<li>문장도 입력 가능</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/7e253e86-fdcc-4c6a-8442-4a7bc6b18f06/image.png" alt=""></p>
<h3 id="call-stack">call stack</h3>
<p>함수 실행순서를 확인할 수 있다.</p>
<h3 id="loading-script">loading script</h3>
<p>현재 로드된 모든 스크립트(소스 파일)를 보여준다.</p>
<h3 id="break-point-확인">break point 확인</h3>
<p>break point중 지금 사용하지 않을 것을 일시 비활성화 할 수 있다.</p>
<h2 id="디버거-조작하기">디버거 조작하기</h2>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/fd3bc7a3-694d-441d-9064-607e24b8594e/image.png" alt=""></p>
<h3 id="continue">continue</h3>
<p>클릭하면 다음 브레이크 포인트로 실행</p>
<h3 id="step-over">step over</h3>
<p>코드 한줄씩 실행하는데, 다른함수를 호출할 때 건너뛰면서 실행됨</p>
<h3 id="step-into">step into</h3>
<p>함수 안으로 들어가고 싶을때</p>
<h3 id="step-out">step out</h3>
<p>함수 밖으로 빠져나옴</p>
<h2 id="활용팁">활용팁</h2>
<ul>
<li>variable 동적으로 조작</li>
<li>edit break point로 특정 시점에 멈추게 하기</li>
<li>콜스택에서 몇번째줄에서 호출되었는지 확인하기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Error log] Failed to fetch dynamically imported module]]></title>
            <link>https://velog.io/@sang-mini/Error-log-Failed-to-fetch-dynamically-imported-module</link>
            <guid>https://velog.io/@sang-mini/Error-log-Failed-to-fetch-dynamically-imported-module</guid>
            <pubDate>Mon, 20 May 2024 04:33:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/sang-mini/post/fcfe53c6-4084-4e57-a4de-d9e9fcb84f18/image.png" alt=""></p>
<blockquote>
<p>사이드 프로젝트를 하는 와중에 storybook을 실행했을 때 위와 같은 에러가 발생했다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/c35393fa-50d6-4a09-9c23-cfdc930ad485/image.png" alt="">
절대 경로로 import 한 파일을 찾을 수 없다는 에러여서 vite와 관련되어 있을 거라고 생각하고 찾아보았다.</p>
<h2 id="builder-vite">builder-vite</h2>
<p><a href="https://storybook.js.org/docs/builders/vite">공식문서의 vite설정</a> 부분을 찾아보니 아래와 같이 패키지 설치가 필요했다.
<img src="https://velog.velcdn.com/images/sang-mini/post/84ea0df7-156d-47fe-9b64-93059ef1ddf7/image.png" alt=""></p>
<pre><code class="language-js">core: {
    builder: &#39;@storybook/builder-vite&#39;,
  },</code></pre>
<h2 id="viteconfig">vite.config</h2>
<p>패키지 설치를 마쳤는데도 안되길래 vite 경로를 조금 더 찾아보았다.
tsconfig에서 paths 설정을 해주었고 import도 잘 되어서 몰랐는데 vite에서도 기본 path 설정이 필요했다.</p>
<pre><code class="language-ts">import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react-swc&quot;;
import path from &quot;path&quot;;

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
   resolve: {
    alias: {
      &quot;~/atoms&quot;: path.resolve(__dirname, &quot;./src/components/atoms&quot;),
      &quot;~/molecures&quot;: path.resolve(__dirname, &quot;./src/components/molecules&quot;),
      &quot;~/organics&quot;: path.resolve(__dirname, &quot;./src/components/organisms&quot;),
      &quot;~/templates&quot;: path.resolve(__dirname, &quot;./src/components/templates&quot;),
      &quot;~/pages&quot;: path.resolve(__dirname, &quot;./src/pages&quot;),
      &quot;~/style&quot;: path.resolve(__dirname, &quot;./src/style&quot;),
      &quot;~&quot;: path.resolve(__dirname, &quot;./src&quot;),
    },
  },
});</code></pre>
<p>위와 같이 설정하고 나니 정상적으로 작동한다.</p>
<h2 id="vite-tsconfig-paths">vite-tsconfig-paths</h2>
<p>예전 프로젝트에 따로 alias 설정이 없길래 괜찮은 줄 알았는데 잘 보니 그때는 <a href="https://www.npmjs.com/package/vite-tsconfig-paths">패키지</a>를 사용한거였다.</p>
<pre><code class="language-ts">import { defineConfig } from &quot;vite&quot;;
import tsConfigPaths from &quot;vite-tsconfig-paths&quot;;

import react from &quot;@vitejs/plugin-react-swc&quot;;

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), tsConfigPaths()],
});
</code></pre>
<p>위의 alias 설정으로 잘 되기 때문에 이번에는 따로 패키지 설치없이 진행했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Supabase로 프로젝트 살리기]]></title>
            <link>https://velog.io/@sang-mini/Supabase%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%82%B4%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@sang-mini/Supabase%EB%A1%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%82%B4%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Thu, 16 May 2024 08:54:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>항상 팀 프로젝트를 하고 나서 디벨롭을 하려하면 <strong>&quot;서버비용 때문에 서비스를 중지합니다&quot;</strong> 라는 말을 듣게 된다.
이런 프로젝트를 살리기 위해 Supabase를 사용해보았다.</p>
</blockquote>
<h2 id="선택-이유">선택 이유</h2>
<p><code>Supabase</code>의 문서를 들어가자 마자 <code>Firebase</code>의 대안인 오픈소스라는 소개가 있다. 
이처럼 <code>Firebase</code>라는 선택지도 있었지만 <code>Supabase</code>를 사용한 이유는 <code>Postgres</code> 기반의 <strong>관계형 DB</strong>를 사용할 수 있다는 점이었다.</p>
<p>기존 프로젝트를 살려서 디벨롭하는게 목적이기 때문에 이전의 API 자원을 그대로 사용하기를 원했는데, <code>Supabase</code>의 관계형 DB 덕분에 ERD를 참고해서 빠르게 DB를 구축할 수 있었다.</p>
<p>그리고 추가로 <code>Edge Functions</code>, <code>Storage</code>, <code>Auth</code> 같은 기능을 제공하는 것도 <code>Supabase</code>의 장점인데 Auth는 일단 사용하지 않을 것이기 때문에 이것 빼고 설명해보겠다.</p>
<h2 id="프로젝트-만들기">프로젝트 만들기</h2>
<p>프로젝트를 만드는 것이 Supabase의 시작인데 아주 간단하다.
아래와 같이 이름과 Organization을 지정하고 새 프로젝트를 생성하면 된다.
<img src="https://velog.velcdn.com/images/sang-mini/post/afe30fc8-2ab9-4eef-b942-3e7712902da6/image.png" alt=""></p>
<h2 id="📚-database">📚 Database</h2>
<p>Database를 만드는 것도 아주 직관적인데, Table Editor로 가서 new Table만 누르면 아래와 같이 명령어 하나 없이 테이블을 만들 수 있다.
<img src="https://velog.velcdn.com/images/sang-mini/post/41349c42-6a8c-4787-ae2b-0b035c823174/image.png" alt="">
또는 SQL Editor에서 SQL문으로 여러 작업들을 할 수 있는데 이것도 정말 편한게 AI Assistant가 있어서 SQL문을 잘 몰라도 사용할 수 있다.
<img src="https://velog.velcdn.com/images/sang-mini/post/0d46f98b-03ff-457b-bff7-242fe1bce024/image.png" alt=""></p>
<h3 id="rls">RLS</h3>
<p>RLS는 깊게 알지는 못하지만 DB에 접근할 수 있는 사용자를 지정하는 역할을 한다고 알고 있다.
나중에 Edge functions로 API를 만들다가 알게되었는데 아무리 요청을 해도 data가 빈값으로 오길래 찾아보니 RLS를 지정하지 않아서였다. </p>
<p>RLS는 테이블에서 policy를 선택하면 나오는데 이것도 아래와 같이 템플릿을 제공하기 때문에 아주 편하게 사용할 수 있다.
<img src="https://velog.velcdn.com/images/sang-mini/post/ce7b1e7a-fb25-4a47-adf0-f1af3237c9b8/image.png" alt=""></p>
<h2 id="🔨-edge-functions">🔨 Edge Functions</h2>
<p>사용하면서 정말 좋다고 생각한 것이 <code>Edge Functions</code>인데 DB만 있다면 이를 호출할 때 SQL문 비슷하게 접근해서 필요한 데이터를 요청하거나 삽입해야한다. </p>
<p><code>Edge Functions</code>은 서버리스 함수 기능 중 하나로, 전 세계의 엣지 네트워크에 분산되어 사용자에게 빠른 서비스를 제공한다. 
이를 활용하여 기존과 동일하게 API 통신을 할 수 있는 백엔드 서비스를 구축할 수 있다.</p>
<p><code>Edge Functions</code>은 <code>Deno</code> 환경에서 개발할 수 있는데 개발 환경을 세팅하고 사용하는 건 <a href="https://www.youtube.com/watch?v=5OWH9c4u68M">Supabase Youtube</a>와 <a href="https://supabase.com/docs/guides/functions/quickstart">공식문서</a>를 보면 너무 잘 알려주기 때문에 사용법은 생략하겠다.</p>
<p>Deno를 통해 함수를 작성하고 배포하고 요청을 보내봤다.
<img src="https://velog.velcdn.com/images/sang-mini/post/4e4f2bb1-552c-45a0-bf93-0cdf7a8e0dbf/image.png" width="400px" /></p>
<h2 id="📦-storage">📦 Storage</h2>
<p><code>Storage</code>는 사진 같은 파일을 저장할 수 있는 저장소이고 이미지 url을 생성해주기 때문에 이것도 정말 쉽게 이용해서 API를 만들 수 있었다. </p>
<h2 id="🧐-회고">🧐 회고</h2>
<p>예전에 프로젝트에 사용을 고려했던 적이 있었는데 DB에 대한 이해도가 부족해서 설계부터 막히다보니 그만뒀었다. 
하지만 DB나 API 함수에 대한 이해도가 생기고 나니 정말 쉽게 백엔드를 구축할 수 있게 해주는 서비스라는 걸 느낄 수 있었다. 
supabase로 <a href="https://github.com/Sang-minKIM/waglewagle_with_supabase/tree/main/supabase">지하철 SNS 서비스</a>의 백엔드를 아주 간단한 것부터 살려보았다.
그리고 공식문서가 너무 잘 되어 있어서 끝난 프로젝트를 살리는데에 쓰기 정말 좋은 것 같다.</p>
<h2 id="참고">참고</h2>
<p><a href="https://supabase.com/docs">supabase 공식문서</a>
<a href="https://www.youtube.com/watch?v=FbLzqoENTsg&amp;t=111s">생활코딩 유튜브</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vite를 알아보자]]></title>
            <link>https://velog.io/@sang-mini/Vite%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@sang-mini/Vite%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 18 Apr 2024 10:37:22 GMT</pubDate>
            <description><![CDATA[<h1 id="🥲-개요">🥲 개요</h1>
<blockquote>
<p><strong>면접관</strong>: Vite를 사용한 이유가 있나요?
<strong>나</strong>: ESM 방식을 사용하기 때문에 빌드속도가 빨라서 선택했습니다.
<strong>면접관</strong>: 음.. 배포환경에서는 Rollup을 사용하고 개발환경에서만 esbuild를 사용하지 않나요?
<strong>나</strong>: 아.. 그 부분까지는 잘 몰랐습니다.</p>
</blockquote>
<p>사실 첫번째 답변에서 “ESM 방식을 사용하기 때문에 개발 서버 구동이 빠릅니다.” 라는 말을 하고 싶었던 거였다.
Vite와 빌드에 대해 제대로 알고 있지 않아서 답을 잘못했었던 것 같다.
그래서 Vite를 조금 더 이해하기 위해 정리해보려 한다.</p>
<h1 id="📦-번들링">📦 번들링</h1>
<p>모듈화 되어있는 여러 자바스크립트 파일을 브라우저에서 실행할 수 있는 파일로 합치는 과정</p>
<p><strong><a href="https://webpack.js.org/">Webpack</a></strong>, <strong><a href="https://rollupjs.org/">Rollup</a></strong> 그리고 <strong><a href="https://parceljs.org/">Parcel</a></strong>과 같은 번들링 도구들이 있다.</p>
<blockquote>
<p><strong>번들링 vs 빌드</strong>
번들링은 빌드의 한 부분이다.
번들링, 압축, 난독화, 트랜스파일링 등의 과정을 빌드라고 한다.
현대의 번들링 도구들은 빌드 과정까지 함께 지원하기 때문에 헷갈렸다.</p>
</blockquote>
<h2 id="문제">문제</h2>
<p>프론트엔드 애플리케이션이 발전함에 따라 JS 모듈의 개수도 많아지다 보니 번들링 작업 후 개발 서버를 가동하는데에 너무 오래걸리고, HMR을 사용하더라도 변경 사항이 적용되는 데에 수 초 이상 걸리는 문제가 있었다.
Vite는 ESM을 활용해서 개발 서버 구동 시간을 줄이고 이런 문제를 해결한다.</p>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/98de75ef-2da8-44a0-a592-70ea9ac6d9c9/image.png" alt=""></p>
<h1 id="⚡️vite">⚡️Vite</h1>
<h2 id="개발환경">개발환경</h2>
<h3 id="서버구동">서버구동</h3>
<p>Vite는 모듈을 <strong>Dependencies</strong>와 <strong>Source code</strong>로 나누어 위의 문제점을 개선했다.</p>
<ul>
<li>Dependencies
개발 할 때 바뀌지 않는 부분<br><a href="https://esbuild.github.io/">esbuild</a>를 이용한 사전 번들링을 제공해서 기존 번들러보다 빠른 속도를 제공한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/94ff3572-6eaf-452e-a44a-29436c08c0df/image.png" alt=""></p>
<ul>
<li><p>Source code
JSX, CSS 같이 컴파일링이 필요하고 수정이 잦은 것들
 <strong><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">Native ESM</a></strong>을 이용해서 브라우저를 번들러로 사용한다.</p>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/6f725520-4113-4fb5-8027-b3de52c7e4a7/image.png" alt=""></p>
</li>
</ul>
<h3 id="소스-코드-갱신">소스 코드 갱신</h3>
<ul>
<li><p>Webpack
소스 코드를 갱신하면 기존의 경우 번들링 과정을 다시 거쳐야했고 이에 대한 대안으로 나온게 Webpack의 <a href="https://webpack.kr/concepts/hot-module-replacement">HMR(Hot Module Replacement)</a>이다. 이는 모듈 전체를 다시 로드하지 않고 변경된 부분만 업데이트 하는 방식이다.
하지만 Webpack의 경우 소스코드가 하나의 파일로 번들링 되어 있기 때문에, 모듈의 의존성을 파악하기 위해 모든 파일을 해석해야했다.<br>그래서 프로젝트 규모가 커지면 HMR 시간이 길어지는 문제가 있었다.</p>
</li>
<li><p>Vite
ESM을 이용한 HMR을 구현해서 변경된 모듈과 직접적으로 관련된 부분만 업데이트 해서 기존의 HMR을 개선했다.
이렇게 해서 프로젝트 규모가 커져도 HMR로 업데이트 하는 시간에는 영향이 없도록 했다.</p>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/3b45fbb7-edd5-4ec0-b601-2ab2e7a81442/image.png" alt="이렇게 Vite도 HMR을 사용한다는걸 확인할 수 있다."><em>이렇게 Vite도 HMR을 사용한다는걸 확인할 수 있다.</em></p>
</li>
</ul>
<h2 id="배포환경">배포환경</h2>
<p>ESM이 대부분의 브라우저에서 지원되지만 배포 시에는 <a href="https://rollupjs.org/introduction/#the-why">Rollup</a>으로 번들링한다.</p>
<p>ESM을 배포환경에서 사용하면 각 모듈 파일을 별도의 HTTP 요청을 통해 가져와야하기 때문에 네트워크 지연이 될 수 있다. 그리고 트리 셰이킹이나 코드 압축, 난독화, 청크파일 분할을 이용해서 번들링하는게 좋기 때문에 배포환경에서는 번들링을 한다.</p>
<p>번들링에 esbuild를 사용하지 않는 이유는 Rollup의 플러그인을 이용하기 위해서라고 한다. esbuild는 주로 성능 최적화에 초점을 두고 있기 때문에 Rollup만큼 다양한 기능을 제공하지 않는다고 한다.</p>
<h2 id="특징">특징</h2>
<p><code>index.html</code> 이 <code>public</code>이 아닌 프로젝트 루트에 위치한다.</p>
<p>이건 추가적인 번들링 과정 없이 <code>index.html</code>이 앱의 진입점이 되게끔 하기 위함이라고 한다.</p>
<ul>
<li><code>&lt;script type=&quot;module&quot; src=&quot;...&quot;&gt;</code> 태그를 이용해 JavaScript 소스 코드를 가져온다.</li>
<li><code>index.html</code> 내에 존재하는 URL에 대해 <code>%PUBLIC_URL%</code>과 같은 자리 표시자 없이 사용할 수 있도록 URL 베이스를 자동으로 맞춰준다</li>
</ul>
<h1 id="👍🏻-유용한-기능">👍🏻 유용한 기능</h1>
<p><code>vite.config.js</code> 파일에서 여러 플러그인을 사용하거나 설정할 수 있다. 그리고 Rollup 기반의 플러그인도 사용가능하다.</p>
<p>기본적으로 해주는 것들이 많아서 설정을 건드릴 일을 많이 없었다.</p>
<ul>
<li>Vitest 설정</li>
<li>proxy 설정</li>
<li>@vitejs/plugin-react-swc 설정 
(빌드 중에는 esbuild를 사용하지만 개발 중에는 Babel을 SWC로 대체해서 바벨을 사용할 때 보다 최대 20배 빠른 새로고침)</li>
</ul>
<h1 id="📚-참조">📚 참조</h1>
<p><a href="https://ko.vitejs.dev/guide/why.html">https://ko.vitejs.dev/guide/why.html</a></p>
<p><a href="https://velog.io/@sehyunny/is-it-time-to-say-goodbye-to-webpack">https://velog.io/@sehyunny/is-it-time-to-say-goodbye-to-webpack</a></p>
<p><a href="https://webpack.js.org/">https://webpack.js.org/</a></p>
<p><a href="https://joshua1988.github.io/vue-camp/vite/intro.html#esm-%E1%84%8C%E1%85%A1%E1%84%87%E1%85%A1%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%B8%E1%84%90%E1%85%B3-%E1%84%82%E1%85%A6%E1%84%8B%E1%85%B5%E1%84%90%E1%85%B5%E1%84%87%E1%85%B3-%E1%84%86%E1%85%A9%E1%84%83%E1%85%B2%E1%86%AF">https://joshua1988.github.io/vue-camp/vite/intro.html#esm-자바스크립트-네이티브-모듈</a></p>
<p><a href="https://fe-developers.kakaoent.com/2022/220217-learn-babel-terser-swc/">https://fe-developers.kakaoent.com/2022/220217-learn-babel-terser-swc/</a></p>
<p><a href="https://jaiboy.tistory.com/16">https://jaiboy.tistory.com/16</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vitest로 단위 테스트 도전]]></title>
            <link>https://velog.io/@sang-mini/Vitest%EB%A1%9C-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%84%EC%A0%84</link>
            <guid>https://velog.io/@sang-mini/Vitest%EB%A1%9C-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8F%84%EC%A0%84</guid>
            <pubDate>Tue, 05 Mar 2024 09:47:43 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>vanilla js 서비스에 <strong>단위테스트</strong>를 도입해보자.</p>
</blockquote>
<p>2달간의 소프티어 부트캠프가 끝나서 이제 면접 준비와 함께 해보고 싶었던 공부를 시작했다.
와글와글 프로젝트를 발전시키면서 테스트의 필요성을 느꼈고 테스트 코드를 작성해보고 싶었는데, 테스트에 대한 개념에 대해 교육 받은 후에 간단한 예제 테스트 작성만 해보고 프로젝트에 적용을 못해서 아쉬움이 있었다.</p>
<h1 id="테스트-대상">테스트 대상</h1>
<p>워밍업 프로젝트로 진행한 <a href="https://www.waglewagle.store/">와글와글</a>에 테스트를 해보려고 한다.
vanilla js로 만들었기 때문에 처음 단위테스트를 도입하기 좋다고 생각했다.</p>
<blockquote>
<p>참고로 <strong>와글와글</strong>은 소프티어 부트캠프에서 5일간의 프로젝트를 통해 만들어진 지하철에서 떠드는 서비스다.
프로젝트 기간이 끝나고도 팀원들과 함께 계속해서 서비스를 만들어가고 있고 지금은 기능 보완과 ts로 마이그레이션을 거친 후에 ver.2를 배포한 상태다.</p>
</blockquote>
<h1 id="⚡️-vitest">⚡️ Vitest</h1>
<p>vitest는 jest와 같이 테스트를 도와주는 라이브러리다. 
빌드도구로 vite를 사용하고 있어서 vitest로 첫 단위테스트를 해보려한다.
vite를 사용하고 있다면 설정도 간단하고 사용법도 간단해서 좋은 것 같다.</p>
<h1 id="⚙️-setting">⚙️ Setting</h1>
<pre><code class="language-bash">npm install -D vitest @vitest/ui</code></pre>
<pre><code class="language-js">// package.json

&quot;scripts&quot;: {
        &quot;test&quot;: &quot;vitest --ui&quot;,
        &quot;test:run&quot;: &quot;vitest run&quot;,
        &quot;coverage&quot;: &quot;vitest run --coverage&quot;
    },</code></pre>
<p>이렇게 하면 기본적인 테스트 세팅이 끝난다.
그리고 @vitest/ui는 아래와 같이 테스트 상황을 ui로 보여준다. 
<code>--ui</code>를 test script에 추가해서 사용할 수 있다.
<img src="https://velog.velcdn.com/images/sang-mini/post/4ecbaf26-769a-4227-be2e-b58eeba93ddb/image.png" alt=""></p>
<h2 id="추가-세팅">추가 세팅</h2>
<p>테스트는 <strong>node.js</strong>환경에서 실행되기 때문에 브라우저의 <strong>DOM API</strong>에 접근할 수가 없어서 window.location이나 localstorage등에 접근하는 코드가 있다면 mocking을 해줘야하는 문제가 있다. 
그래서 이를 해결하기 위해서 DOM API를 사용할 수 있게 해주는 <strong>jsdom</strong>이나 <strong>happy-dom</strong>을 사용한다.
happy-dom이 조금 더 경량화된 버전이라고 해서 이를 사용했다.</p>
<pre><code class="language-bash">npm i -D happy-dom</code></pre>
<pre><code class="language-ts">// vite.config.ts
// vite 환경이 아니라면 vitest.config.ts에 아래 설정을 추가해줘도 된다.
import { defineConfig } from &#39;vitest/config&#39;

export default defineConfig({
  test: {
    environment: &#39;happy-dom&#39;, // or &#39;jsdom&#39;, &#39;node&#39;
  },
})</code></pre>
<h1 id="🧪-테스트-작성하기">🧪 테스트 작성하기</h1>
<p>먼저 테스트를 진행하기전에 기준과 순서를 정했다.</p>
<h3 id="1-최소단위-함수부터-테스트하기">1. 최소단위 함수부터 테스트하기</h3>
<ul>
<li>반환값이 명확히 존재하고, 다른 함수를 호출하지 않는 함수부터 테스트 한다.(의존성이 없는 함수)</li>
<li>단위 테스트 구현을 하면서 원래 코드도 테스트 가능한 코드로 개선해나간다.(리팩토링)</li>
</ul>
<h3 id="2-given---when---then-패턴">2. given -&gt; when- &gt; then 패턴</h3>
<p>어떤 상황에서 어떻게 동작하는지를 나타내는게 테스트코드의 핵심이다.</p>
<p>일관된 방식의 테스트 코드 구현을 위해서
<strong>given(테스트에 필요한 값 셋팅) -&gt; when(실행) -&gt; then(테스트)</strong>
방식으로 테스트를 수행한다. </p>
<blockquote>
<p>이 방법이 가장 많이 쓰인다고 함.
<a href="https://martinfowler.com/bliki/GivenWhenThen.html">https://martinfowler.com/bliki/GivenWhenThen.html</a></p>
</blockquote>
<p>그래서 <strong>util</strong> 함수부터 해보기로 했다.</p>
<h2 id="첫번째-테스트">첫번째 테스트</h2>
<p>테스트할 함수는 아래와 같다.
url에서 역의 id를 가져오는 함수다.</p>
<pre><code class="language-ts">export const getStationId = (): string =&gt; {
  const { pathname } = window.location;
  const stationId = pathname.split(&quot;/&quot;)[2];
  return stationId;
};</code></pre>
<h3 id="😅-첫번째-난관">😅 첫번째 난관..</h3>
<p>순수함수가 아니라서 window.location에 대한 <strong>mocking</strong>이 필요했다. 
(이때는 happy-dom을 몰랐음)</p>
<p>공식문서에서 방법을 찾아보니 <code>stubGlobal</code>을 사용하면 된다고 했다. 
그래서 아래와 같이 테스트 코드를 작성해서 통과했다.</p>
<pre><code class="language-ts">import { vi, describe, it, expect } from &quot;vitest&quot;;
import { getStationId } from &quot;./getStationId&quot;;

const mockLocation = {
    pathname: &quot;www.waglewagle.store/station/12&quot;,
};

// Given: window.location의 pathname을 모의 설정
vi.stubGlobal(&quot;location&quot;, mockLocation);

describe(&quot;getStationId&quot;, () =&gt; {
    it(&quot;URL에서 역 ID를 추출하여 반환한다&quot;, () =&gt; {
        // When: getStationId 함수가 호출될 때
        const stationId = getStationId();
        // Then: 예상되는 역 ID가 반환되어야 한다
        expect(stationId).toBe(&quot;12&quot;);
    });</code></pre>
<h3 id="😎-happy-dom">😎 happy-dom</h3>
<p>하지만 이렇게 순수함수가 아닌 함수가 더 많을텐데 <strong>DOM API</strong>를 사용하는 코드는 전부 mocking을 한다는게 말이 안된다고 생각했다.
그래서 공식문서를 조금 더 찾아보니 environment setting에서 <strong>happy-dom</strong>을 알게 되었다.
<strong>mocking</strong>을 제거한 코드는 아래와 같다.</p>
<pre><code class="language-ts">import { describe, it, expect, beforeEach } from &quot;vitest&quot;;
import { getStationId } from &quot;./getStationId&quot;;

describe(&quot;getStationId with happy-dom&quot;, () =&gt; {
    // Given: 사전 조건으로 window.location.href를 설정
    beforeEach(() =&gt; {
        window.location.href = &quot;http://www.waglewagle.store/station/12&quot;;
    });

    it(&quot;URL에서 역 ID를 추출하여 반환한다&quot;, () =&gt; {
        // When: getStationId 함수가 호출될 때
        const stationId = getStationId();

        // Then: 예상되는 역 ID가 반환되어야 한다
        expect(stationId).toBe(&quot;12&quot;);
    });
});</code></pre>
<h3 id="비교">비교</h3>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/6c71c5bb-7bb3-4f76-95d4-ef1ce7e1b33b/image.png" alt=""></p>
<h2 id="두번째-테스트">두번째 테스트</h2>
<p>다음은 신고 게시물을 임시처리하는 코드다.
게시글 id를 localStorage에 저장하는 함수와
localStorage에서 신고 게시물 리스트를 불러오는 함수 2개가 있다.</p>
<pre><code class="language-ts">export const getReports = () =&gt; {
  return [...JSON.parse(localStorage.getItem(&quot;reportList&quot;)!)];
};

export const saveReport = (postId: number) =&gt; {
  const reportList = getReports();
  reportList.push(postId);
  localStorage.setItem(&quot;reportList&quot;, JSON.stringify(reportList));
};</code></pre>
<p>두가지 함수를 테스트하기 위해 <code>describe</code>로 묶어서 테스트 코드를 짜봤다.</p>
<pre><code class="language-ts">import { describe, it, expect, beforeEach } from &quot;vitest&quot;;
import { getReports, saveReport } from &quot;./reportFn&quot;;

describe(&quot;localStorage를 사용한 report 관리&quot;, () =&gt; {
    beforeEach(() =&gt; {
        // Given: 각 테스트 실행 전에 localStorage 초기화
        window.localStorage.clear();
    });

    it(&quot;saveReport 함수는 postId를 localStorage에 저장한다&quot;, () =&gt; {
        // Given: 초기 상태에서 reportList 검증
        expect(getReports()).toEqual([]);

        // When: postId 저장
        saveReport(1);
        saveReport(2);

        // Then: 저장된 postId 배열 반환 검증
        expect(getReports()).toEqual([1, 2]);
    });

    it(&quot;getReports 함수는 저장된 postId 배열을 반환한다&quot;, () =&gt; {
        // Given: 여러 postId 저장
        saveReport(1);
        saveReport(2);
        saveReport(3);

        // When: getReports 함수 호출
        const reports = getReports();

        // Then: 저장된 postId 배열 반환 검증
        expect(reports).toEqual([1, 2, 3]);
    });
});</code></pre>
<h3 id="🥲-두번째-난관">🥲 두번째 난관</h3>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/fc17b1fc-df16-41d0-80bd-336deb1cbf1f/image.png" alt=""></p>
<p><code>reportList</code>가 <code>localStorage</code>에 없을 수도 있어서 타입에러가 났다.
사실 ts로 마이그레이션 할 때도 문제가 있었는데 이걸 안고치고 타입 단언하는 바람에 이제서야 문제가 발견된거다.
이래서 테스트 코드를 짜는구나라는 생각이 든 순간이었다.</p>
<h3 id="😁-리펙토링">😁 리펙토링</h3>
<p>예외를 처리하는 코드를 추가해서 함수를 수정했다.</p>
<pre><code class="language-ts">export const getReports = () =&gt; {
    try {
        const data = localStorage.getItem(&quot;reportList&quot;);
        if (!data) return [];

        const reportList = JSON.parse(data);
        if (!Array.isArray(reportList)) return [];

        return reportList;
    } catch (error) {
        console.error(&quot;Parsing error in getReports:&quot;, error);
        return [];
    }
};</code></pre>
<blockquote>
<p>가독성이 조금 떨어지는 코드가 된 것 같은데 이건 추후에 리펙토링 해보겠다.
유효성을 검증하는 순수함수를 사용하면 가독성을 높일 수 있을 것 같다.</p>
</blockquote>
<h3 id="결과">결과</h3>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/316cd6db-e4d8-4c66-aa3f-22dadec7be04/image.png" alt="">
<img src="https://velog.velcdn.com/images/sang-mini/post/0f772397-6f27-4b48-9a3d-dfcf47482bf2/image.png" alt=""></p>
<h1 id="회고">회고</h1>
<p>아직 테스트에 대해 알아갈게 많고 문법도 잘 알지 못하지만 하나씩 해봐야겠다.
참고로 문법은 공식문서 <a href="https://vitest.dev/api/">API reference</a>에 잘 나와 있다.</p>
<h1 id="참고">참고</h1>
<p><a href="https://vitest.dev/">https://vitest.dev/</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP 파헤치기]]></title>
            <link>https://velog.io/@sang-mini/HTTP-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@sang-mini/HTTP-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Mon, 04 Mar 2024 02:58:32 GMT</pubDate>
            <description><![CDATA[<h1 id="📖-http의-발전">📖 HTTP의 발전</h1>
<h2 id="📑-http09">📑 HTTP/0.9</h2>
<h3 id="문제점">문제점</h3>
<ul>
<li>GET method만 존재</li>
<li>header 없음</li>
<li>HTML만 가져오도록 설계(이미지는 처리할 수 없고 텍스트만 다룰 수 있었음)</li>
</ul>
<h2 id="📑-http10">📑 HTTP/1.0</h2>
<h3 id="개선점">개선점</h3>
<ul>
<li>header 도입</li>
<li>응답 코드</li>
<li>리다이렉트</li>
<li>오류</li>
<li>조건부 요청</li>
<li>콘텐츠 압축</li>
<li>다양한 요청 메서드</li>
</ul>
<h3 id="문제점-1">문제점</h3>
<ul>
<li><p>한 연결당 하나의 요청만 처리</p>
<p>  ⇒ RTT 증가</p>
</li>
<li><p>서버가 하나의 호스트만 가진다고 가정함</p>
<p>  ⇒ Host header 필수 아님</p>
</li>
<li><p>캐싱 옵션 빈약</p>
</li>
</ul>
<h2 id="📑-http11">📑 HTTP/1.1</h2>
<h3 id="개선점-1">개선점</h3>
<ul>
<li><p>Keep-Alive default (접속 재사용)</p>
<p>  ⇒ 응답 후에 연결을 끊지 않음, 모든 요청마다 TCP 연결을 재수립하지 않아도 됨</p>
<pre><code class="language-jsx">  import express from &quot;express&quot;;

  const app = express();
  app.get(&quot;/&quot;, (req, res) =&gt; {
      res.json({ title: &quot;Hello I&#39;m Sangmin&quot; });
  });

  const server = app.listen(4000);
  server.keepAliveTimeout = 30 * 1000;</code></pre>
</li>
<li><p>Host header</p>
<p>  ⇒ 하나의 IP 주소로 다수의 웹 프로퍼티 제공(하나의 서버가 여러개의 호스트를 가질 수 있음)</p>
</li>
<li><p>대역폭 최적화</p>
<p>  ⇒ 파일을 다운로드 받다가 연결이 끊겨도 다운로드 재개 요청을 할 수 있는 헤더 추가(Range:bytes=5000-)</p>
</li>
<li><p>XMLHttpRequest</p>
</li>
<li><p>TLS 암호화 통신</p>
</li>
</ul>
<h3 id="문제점-2">문제점</h3>
<ul>
<li><p>리소스 우선순위 없이 받아짐. 렌더링 비효율</p>
</li>
<li><p>헤더 크기가 너무 큼(쿠키 같은 메타데이터들 때문)</p>
</li>
<li><p><strong>keep-alive를 사용하더라도</strong> 단일 연결에서 처리할 수 있는 요청의 수가 제한되어 있고, HOL문제가 있음</p>
<blockquote>
<p><strong>HOL(Head of Line Bloking)</strong>
  네트워크에서 같은 큐에 있는 패킷이 앞의 지연 패킷 때문에 발생하는 성능 저하 현상</p>
</blockquote>
</li>
<li><p>HTML 파일, CSS 파일, 이미지 등을 동시에 요청하고 받기 위해 여러개의 연결(connection)을 사용해야만 함</p>
</li>
</ul>
<h3 id="rtt를-줄이기-위한-기술">RTT를 줄이기 위한 기술</h3>
<ul>
<li>이미지 스프라이트</li>
<li>코드압축</li>
<li>이미지 Base64 인코딩</li>
</ul>
<h2 id="📑-spdy">📑 SPDY</h2>
<ul>
<li>다중화, 프레이밍, 헤더압축</li>
</ul>
<h2 id="📑-http2">📑 HTTP/2</h2>
<ul>
<li>구글의 SPDY에서 출발</li>
<li>HTTP 1.1 까지의 핵심기능 유지하며 확장.</li>
</ul>
<h3 id="개선점-2">개선점</h3>
<ul>
<li><p>멀티플렉싱
리소스를 작은 프레임으로 나누고 이를 스트림으로 프레임을 전달. 
각각의 프레임은 스트림id, 청크의 크기를 전달하여 병력적으로 다운하고 재조립 가능
⇒ HOL 해결   </p>
<ul>
<li><p>여러개의 스트림을 동시에 전송.</p>
</li>
<li><p>하나의 스트림은 여러개의 프레임으로 구성.</p>
<ul>
<li>1번 스트림: 프레임 A, 프레임 B, 프레임 C</li>
<li>2번 스트림: 프레임 X, 프레임 Y, 프레임 Z</li>
</ul>
</li>
<li><p>프레임은 각각의 고유한 ID를 가지며 독립적으로 전송.</p>
</li>
<li><p>이처럼 HTTP/2 에서는 병렬로 전송되는 스트림과 프레임을 통해 HTTP/2은 다중 요청 및 응답을 동시에 처리하고, 성능을 개선.</p>
<blockquote>
<p><strong>스트림</strong>: 스트림은 독립적인 양방향 데이터 흐름. 각 스트림에는 고유한 식별자가 있으며, 하나의 연결 내에서 병렬로 여러 스트림이 동시에 활성화 가능
<strong>프레임</strong>: 프레임은 스트림에 속하는 가장 작은 통신 단위로, 헤더 프레임, 데이터 프레임 등 다양한 종류가 있음. 각 스트림은 하나 이상의 프레임으로 구성</p>
</blockquote>
</li>
</ul>
</li>
<li><p>Header 압축
서버에서 중복되는 헤더는 제외하고 공통 필드로 헤더를 재구성
중복되지 않은 헤더 값은 허프만 인코딩 압축 후 전송</p>
<blockquote>
<p>허프만 인코딩: 문자열을 문자 단위로 쪼개서 빈도수를 세어 빈도가 높은 정보는 적은 비트수, 낮은 정보는 많은 비트 수를 사용해서 전체 비트 수를 줄이는 알고리즘</p>
</blockquote>
</li>
<li><p>Server Push
  서버가 클라이언트에 리소스를 푸시할 수 있다.</p>
</li>
<li><p>우선순위
  서버가 원하는 순서대로 우선순위를 정해 리소스를 전달</p>
</li>
</ul>
<h2 id="📑-http3">📑 HTTP/3</h2>
<p><strong><em>UDP의 단순하고, 낮은 오버헤드의 장점을 살리자!</em></strong></p>
<ul>
<li><p>QUIC(Quick UDP Internet Connection)위에서 동작</p>
<p>  QUIC는 HTTP/2에 상응하는 다중화와 흐름제어, TLS에 상응하는 보안성, TCP에 상응하는 연결 의미, 신뢰성, 혼잡제어를 제공한다.</p>
</li>
<li><p>QUIC은 UDP 위에서 동작.</p>
</li>
<li><p>HTTP/2의 경우 클라이언트와 서버간의 연결을 맺어 세션을 만드는데 필요한 핸드셰이크와 TLS 핸드셰이크가 각각 필요.
HTTP/3에서는 TLS 1.3 암호화를 프로토콜에 내장하여, 한 번의 핸드셰이크로 연결과 암호화통신 모두 구축
<strong>⇒ 3-RTT에서 1-RTT로</strong></p>
</li>
<li><p><strong>HTTP/3</strong>도 여러개의 stream 을 사용하고 그 안에서 여러개의 프레임이 존재함.</p>
<p>  따라서 <strong>HTTP/2</strong>과 같이 여러 개의 요청과 응답이 하나의 연결(커넥션)에서 동시에 처리될 수 있음.</p>
<p>  다만 <strong>HTTP/3</strong>은 <strong>HTTP/2</strong>과 달리 <strong>프레임이 UDP 패킷단위로 나눠져 있음.</strong></p>
</li>
<li><p><strong>HTTP/3</strong>에서는 UDP 그리고 부족한 신뢰성을 극복하기 위해 패킷 손실등의 방안을 추가.</p>
<p>  QUIC 프로토콜은 패킷에 일련번호를 부여함 
  TCP의 재전송 메커니즘과 유사하게 패킷 재전송</p>
</li>
<li><p><strong>UDP 패킷 레벨에서 데이터를 분할하고 전송</strong>.</p>
<p>  TCP 패킷보다 더 작은 패킷사용해서 패킷 손실시 영향이 적음</p>
</li>
</ul>
<h1 id="tcp-vs-udp">TCP vs UDP</h1>
<h2 id="🤔-tcp">🤔 TCP....</h2>
<ul>
<li><p>3-way handshake 및 4-way handshake 과정을 거치기 때문에 추가적인 오버헤드가 발생</p>
</li>
<li><p>TCP는 데이터의 신뢰성과 네트워크 혼잡제어 알고리즘 사용
  <img src="https://velog.velcdn.com/images/sang-mini/post/17a96701-600d-4b43-9f4d-004c5e5cff66/image.png" alt=""></p>
<ul>
<li><p><strong>느린 시작 (Slow Start)</strong></p>
<p>  연결 초기에 TCP는 네트워크의 혼잡 수준을 알 수 없으므로, 작은 양의 데이터로 시작하여 네트워크의 용량을 점진적으로 탐색. </p>
<p>  데이터 전송률은 지수적으로 증가하며, 이는 &quot;혼잡 윈도우(congestion window)&quot;의 크기가 각 RTT(Round-Trip Time)마다 두 배로 증가</p>
<blockquote>
<p><strong>혼잡 윈도우:</strong> 수신자가 확인(ACK)하기 전까지 송신자가 전송할 수 있는 TCP 패킷의 수</p>
</blockquote>
</li>
<li><p><strong>혼잡 회피 (Congestion Avoidance)</strong></p>
<p>  느린 시작 단계에서 혼잡 윈도우가 일정 임계값(ssthresh, slow start threshold)에 도달하면, TCP는 혼잡 회피 모드로 전환.</p>
<p>  이 모드에서는 혼잡 윈도우의 크기를 선형적으로 증가시켜 네트워크 혼잡을 회피</p>
<p>  윈도우 사이즈를 늘려나가다 패킷이 전송되지 않거나 <code>TIME_OUT</code> 이 발생하면 늘려놨던 윈도우 사이즈를 절반으로 줄임</p>
</li>
<li><p><strong>빠른 재전송 (Fast Retransmit) 및 빠른 회복 (Fast Recovery)</strong></p>
<p>  패킷 손실을 감지할 때, TCP는 빠른 재전송 알고리즘을 사용하여 해당 패킷을 즉시 재전송.</p>
<p>  빠른 회복 알고리즘은 혼잡 윈도우의 크기를 조정하여 네트워크의 혼잡 상태를 빠르게 극복</p>
</li>
</ul>
</li>
</ul>
<h2 id="🧐-udp">🧐 UDP..?</h2>
<ul>
<li>다른 방법은? UDP...? 그건 신뢰성을 보장하지 못하는데.. 
DNS 같은 단순 프로토콜이나 음성채팅에서 사용</li>
</ul>
<h2 id="🥊-tcp-vs-udp">🥊 TCP vs UDP</h2>
<p>UDP 기반으로 프로토콜을 바꾸려면, 연결이라는 개념을 구현하고, 신뢰성을 높이고, 혼잡제어도 있어야함. 즉 TCP의 많은 것을 다시 구현해야함</p>
<h3 id="but">But,</h3>
<p>TCP 스택은 커널 공간에 구현되어 있음. 즉 OS 개발사가 OS를 업데이트 해야 커널을 변경할 수 있음</p>
<p>OS를 업데이트 하는 건 브라우저를 업데이트 하는 것에 비해 복잡한 일</p>
<p>따라서 UDP 기반으로 사용자 공간(브라우저 내부)에서 TCP 스택을 다시 구현하면 브라우저 업데이트 만으로 사용자가 새 버전을 사용할 수 있음</p>
<h3 id="🥊-커널-공간-vs-사용자-공간">🥊 커널 공간 vs 사용자 공간</h3>
<p>TCP인가, UDP인가?가 아니라 커널 공간인가, 사용자 공간인가?를 생각하는게 논쟁의 핵심!</p>
<h2 id="🪖-http-header">🪖 HTTP Header</h2>
<p>Body를 설명하는 정보를 포함헤서 여러가지 정보가 담긴 정보 묶음. </p>
<p>콜론으로 구분되는 key-value 형태</p>
<p>3가지 헤더가 자동으로 생김</p>
<ul>
<li><p>일반헤더<br>  <img src="https://velog.velcdn.com/images/sang-mini/post/e0373aa7-9706-42d0-b4a6-8a6135b2f317/image.png" alt=""><br>  요청 URL, 메서드, 상태코드, Refferrer Policy(자원을 요청할 때 출처 노출을 시킬지 말지 정하는 보안 정도 설정) 등</p>
</li>
<li><p>요청헤더
  <img src="https://velog.velcdn.com/images/sang-mini/post/3c319791-ba14-42e4-bc15-dea67960fde5/image.png" alt=""></p>
<p>  클라이언트에서 설정하거나 자동으로 설정됨
  메서드, 클라이언트 OS, 브라우저 정보</p>
</li>
<li><p>응답헤더
  <img src="https://velog.velcdn.com/images/sang-mini/post/87ca55b2-80e5-4927-ab03-fb2d908aaab2/image.png" alt=""></p>
<p>  서버에서 설정하는 헤더
  서버의 소프트웨어 정보(프록시 서버 등)</p>
<h1 id="참고">참고</h1>
<p>러닝 HTTP/2</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 푸시알림 적용하기(with.FCM)]]></title>
            <link>https://velog.io/@sang-mini/%EC%9B%B9-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0with.FCM</link>
            <guid>https://velog.io/@sang-mini/%EC%9B%B9-%ED%91%B8%EC%8B%9C%EC%95%8C%EB%A6%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0with.FCM</guid>
            <pubDate>Tue, 20 Feb 2024 16:20:01 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>작년에 했던 프로젝트에서 실패했던 web push 알림을 드디어 성공시켰다. 어떻게 동작하는지를 이해하고 헷갈리는 부분만 넘어가니까 쉽게 할 수 있었다.
블로그들을 찾아봐도 구버전 세팅 방법만 나오고 원리를 알 수 없어서 공식문서를 정말 열심히 봤다.</p>
</blockquote>
<h1 id="📂-fcm이란">📂 FCM이란?</h1>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/f5466fb8-cf20-4d83-ac00-b2a34da93f10/image.png" alt="">
<strong>Firebase Cloud Messaging(FCM)</strong>은 메시지를 안정적으로 무료 전송할 수 있는 크로스 플랫폼 메시징이다.
<strong>웹 푸시알림을 사용하기 위해서는 FCM을 이용한다.</strong>
클라이언트 앱을 사용하는 기기가 알림을 허용하면 고유한 토큰을 FCM에서 발급받고 이를 통해 FCM에 등록하면 FCM을 통해 메시지를 수신할 수 있다. 
즉, <strong>FCM</strong>은 서버의 요청에 따라 클라이언트에게 메시지를 보낼 수 있도록 하는 <strong>중간관리자</strong>라고 생각하면 된다.</p>
<h1 id="fcm-세팅하기">FCM 세팅하기</h1>
<h2 id="1-프로젝트-만들기">1. 프로젝트 만들기</h2>
<p>먼저 <a href="https://firebase.google.com/?hl=ko">firebase</a>로 가서 콘솔로 이동하여 그리고 프로젝트 추가를 누르고 시키는데로 하면 된다.
이 글은 firebase v.10.8.0으로 진행했다.</p>
<h2 id="2-sdk-추가">2. SDK 추가</h2>
<p>이 부분도 시키는 데로 붙여넣기를 하면 되는데 파일명은 아무거나 하면 되고 위치도 꼭 루트 디렉토리일 필요 없다. src 안에 그냥 하나 만들자.</p>
<pre><code class="language-js">// src/service/initFirebase.js
import { initializeApp } from &quot;firebase/app&quot;;

const firebaseConfig = {
    apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
    authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
    projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
    storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
    appId: import.meta.env.VITE_FIREBASE_APP_ID,
    measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
};

export const app = initializeApp(firebaseConfig);</code></pre>
<p>그리고 config안의 키 들을 환경변수로 설정한다.
이렇게 하면 app이 firebaseConfig에 따라 초기화된다.
<code>import { initializeApp } from &quot;firebase/app&quot;;</code>
firebase 9 버전 이상이라면 꼭 이 코드로 진행하면 되고 8 버전 이하는 코드가 조금 달라진다. 공식문서의 모듈 api가 9버전 이상에서 동작하는 코드다. firebase github snippet은 아직 8버전 코드로 되어있다.</p>
<h2 id="3-notification-권한-받기">3. Notification 권한 받기</h2>
<p>사용자에게 알림을 허용 받아서 고유 토큰을 발급받는 단계다.
이때 먼저 vapid key라는 것을 발급받아야한다.</p>
<ol>
<li>프로젝트 들어가기</li>
<li>설정 아이콘 &gt; ‘프로젝트 설정’ 들어가기</li>
<li>‘클라우드 메시징’ 들어가기</li>
<li>‘웹 구성’에서 Generate key pair 버튼 누르기
<img src="https://velog.velcdn.com/images/sang-mini/post/182105a0-874e-47a7-aeb6-a126b3ce2601/image.png" alt="">
검은색으로 가린 부분이 vapid key이다. 
직접 복사해야함! 키쌍 보기 하면 안됨!(여기서 삽질 조금 했음..🥲)
vapid key도 env에 넣고 사용한다. 그리고 서버 개발자에게도 알려줘야함<pre><code class="language-js">// src/service/notificationPermission.js
import { getToken } from &quot;firebase/messaging&quot;;
import { sendTokenToServer } from &quot;./api&quot;;
import { registerServiceWorker } from &quot;./registerServiceWorker&quot;;
</code></pre>
</li>
</ol>
<p>export async function handleAllowNotification() {
    registerServiceWorker(); // 나중에 설명
    try {
        const permission = await Notification.requestPermission();</p>
<pre><code>    if (permission === &quot;granted&quot;) {
        const token = await getToken(messaging, {
            vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY
        });
        if (token) {
            sendTokenToServer(token);// (토큰을 서버로 전송하는 로직)
        } else {
            alert(
                &quot;토큰 등록이 불가능 합니다. 생성하려면 권한을 허용해주세요&quot;
            );
        }
    } else if (permission === &quot;denied&quot;) {
        alert(
            &quot;web push 권한이 차단되었습니다. 알림을 사용하시려면 권한을 허용해주세요&quot;
        );
    }
} catch (error) {
    console.error(&quot;푸시 토큰 가져오는 중에 에러 발생&quot;, error);
}</code></pre><p>}</p>
<pre><code>`handleAllowNotification` 이라는 함수를 만들어서 로그인 한 유저의 첫 화면에서 토큰 발급을 요청하도록 했다.
토큰을 출력해서 복사해두면 fcm에서 알림 받기 테스트를 해볼 수도 있다.

# 📨 메세지 수신 설정
먼저 메시지를 수신할 때 두가지 버전이 있다는 걸 잘 이해해야 헤메지 않는다.
앱이 켜져 있을 때 메시지를 수신하는 foreground message, 그리고 앱이 백그라운드에 있을 때 메시지를 수신하는 background message가 있다.
## foreground message
foreground의 경우에는 만들고 있는 서비스 코드에서 동작하기 때문에 그 전처럼 src에서 설정을 한다.
```js
// src/service/foregroundMessage.js
import { getMessaging, onMessage } from &quot;firebase/messaging&quot;;
import { app } from &quot;./initFirebase&quot;;

const messaging = getMessaging(app);

onMessage(messaging, (payload) =&gt; {
    // console.log(&quot;알림 도착 &quot;, payload);
    const notificationTitle = payload.notification.title;
    const notificationOptions = {
        body: payload.notification.body
    };

    if (Notification.permission === &quot;granted&quot;) {
        new Notification(notificationTitle, notificationOptions);
    }
});</code></pre><h2 id="background-message">background message</h2>
<p>background는 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API">service worker</a>에 의해서만 컨트롤 된다.
<strong>service worker 파일은 항상 public 폴더에 있어야한다.</strong>
firebase 문서에는 이 부분에 대한 설명이 부족해서 한참을 헤맸다..
네임스페이스(8버전 이하)코드를 보면 아래와 같이 사용할 수 있는 코드가 나온다.</p>
<pre><code class="language-js">// /public/firebase-messaging-sw.js
importScripts(
    &quot;https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js&quot;
);
importScripts(
    &quot;https://www.gstatic.com/firebasejs/10.8.0/firebase-messaging-compat.js&quot;
);

self.addEventListener(&quot;install&quot;, function (e) {
    self.skipWaiting();
});

self.addEventListener(&quot;activate&quot;, function (e) {
    console.log(&quot;fcm service worker가 실행되었습니다.&quot;);
});

const firebaseConfig = {
    apiKey: &quot;&quot;,
    authDomain: &quot;&quot;,
    projectId: &quot;&quot;,
    storageBucket: &quot;&quot;,
    messagingSenderId: &quot;&quot;,
    appId: &quot;&quot;,
    measurementId: &quot;&quot;
};

firebase.initializeApp(firebaseConfig);

const messaging = firebase.messaging();

messaging.onBackgroundMessage((payload) =&gt; {
    const notificationTitle = payload.title;
    const notificationOptions = {
        body: payload.body
        // icon: payload.icon
    };
    self.registration.showNotification(notificationTitle, notificationOptions);
});
</code></pre>
<p>public 폴더에서는 모듈이 동작하지 않기 때문에 일반적인 import는 할 수 없고 import script를 이용한다. 그리고 env도 사용할 수 없기 때문에 string으로 직접 넣어준다.
여기서도 한 번 더 app을 초기화 해줘야 동작하는데 이 부분도 문서에는 없어서 에러코드를 보면서 하나씩 고치다가 알게되었다. 
아마 더 좋은 방법이 있을수도..</p>
<p>vite에서 pwa plugin을 설치하면 서비스 워커 코드도 src에서 다룰 수 있는 것 같은데 아직 안해봐서 잘 모르겠다.</p>
<p>그리고 위의 코드를 실행시켜 주는 서비스 워커를 등록해준다..</p>
<pre><code class="language-js">export function registerServiceWorker() {
    if (&quot;serviceWorker&quot; in navigator) {
        window.addEventListener(&quot;load&quot;, function () {
            navigator.serviceWorker
                .register(&quot;/firebase-messaging-sw.js&quot;)
                .then(function (registration) {
                    console.log(
                        &quot;Service Worker가 scope에 등록되었습니다.:&quot;,
                        registration.scope
                    );
                })
                .catch(function (err) {
                    console.log(&quot;Service Worker 등록 실패:&quot;, err);
                });
        });
    }
}
</code></pre>
<p>좀 찾아보면 직접 script 코드에 넣어서 서비스가 시작될 때 동작하도록 하라는데 나는 로그인 된 사용자에게만 push 알림을 보낼거라 알림 권한 설정할 때 실행되도록 했다. (나중에 바꿀 수도..😁)</p>
<p>이렇게 하고 나서 <a href="https://firebase.google.com/docs/cloud-messaging/js/first-message?hl=ko">백그라운드 앱에 테스트 메시지 보내기</a> 문서를 참고해서 테스트 메시지를 보내면 이렇게 알림이 오는 걸 볼 수 있다.
<img src="https://velog.velcdn.com/images/sang-mini/post/7877a025-d733-4369-9bb8-431da1d1d96a/image.png" alt=""></p>
<h1 id="회고">회고</h1>
<p>PWA에 관심을 갖고 부터 알고는 있었지만 직접 만드는 것에 막연한 두려움이 있었는데 문서를 차근차근 읽으면서 하다보니 생각보다 별거 아니라는 것을 느꼈다.
다음에는 PWA로 만들어서 진짜 앱처럼 동작하게 만들어 볼 생각이다.
그리고 service worker가 어떻게 동작하는지도 조금 더 알아보면 좋을 것 같다.</p>
<h1 id="참고">참고</h1>
<p><a href="https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko">https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers">https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 로그인 어떻게 하지??]]></title>
            <link>https://velog.io/@sang-mini/React-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EC%A7%80</link>
            <guid>https://velog.io/@sang-mini/React-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%95%98%EC%A7%80</guid>
            <pubDate>Mon, 12 Feb 2024 13:19:15 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>react로 진행하는 팀 프로젝트에서 로그인을 구현하면서 마주했던 고민들을 정리해본다. </p>
</blockquote>
<h1 id="jwt-vs-session">JWT vs Session</h1>
<p>가장 먼저 마주한 선택지는 로그인 인증 방식이었다. 
JWT(JSON Web Token)을 이용한 토큰기반 방식과 Session 방식이 있었다.
각각의 특징과 인증 절차는 많은 자료가 있어서 생략하고, 결론부터 말하면 팀원들간의 논의 끝에 <strong>JWT</strong>를 사용하기로 했다.</p>
<p>여기에는 몇가지 이유가 있다. </p>
<ul>
<li><p>Session 방식은 <strong>백엔드의 부담</strong>이 커진다. 
유저의 로그인 상태를 DB에서 모두 다루어야하기 때문에 부담이 크고 확장도 쉽지 않다.
또한 보안 처리를 위해서 백엔드에서 해주어야 할 설정들도 많아서 기간이 정해진 프로젝트에서 적합하지 않다고 판단했다.</p>
</li>
<li><p>JWT는 <strong>SPA의 비동기적 특성</strong>에 부합한다.
react 프로젝트에서 JWT를 사용한 사람들이 많아보여서 찾아본 결과, SPA의 비동기적 특성에 부합한다는 것을 알게 되었다. 
SPA는 필요한 데이터만 서버에 비동기 요청을 해서 화면에 반영하는데, JWT는 이런 요청에 인증 토큰을 쉽게 첨부할 수 있어서 페이지 리로드 없이 토큰을 갱신하거나 검증할 수 있는 점이 서로 잘 어울린다.</p>
</li>
</ul>
<h1 id="accesstoken--refreshtoken">accessToken &amp; refreshToken</h1>
<p>JWT 방식은 토큰이 탈취 당할 수 있다는 단점이 있는데 이를 보완하기 위해 두 종류의 토큰을 사용한다.</p>
<ul>
<li>accessToken: 실제 인증에 필요한 token, 만료시간을 짧게해서 탈취당하더라도 재사용을 못하도록 한다.</li>
<li>refreshToken: accessToken이 만료 되었을 때 재발급 받기 위한 token, 만료 시간을 길게 해서 사용자가 로그인을 유지할 수 있도록 한다. </li>
</ul>
<h2 id="accesstoken-재발급-방식">accessToken 재발급 방식</h2>
<p>accessToken이 만료된 것을 확인하고 재발급하는 방식도 두가지가 있다는 것을 알게되었다.</p>
<h3 id="서버에서-만료-확인">서버에서 만료 확인</h3>
<p>서버에 accessToken을 포함한 요청을 했을 때 토큰 만료로 실패했다는 응답을 받으면 
refreshToken으로 토큰 재발급을 하고 새로운 토큰으로 다시 요청을 보내는 방식이다.</p>
<h3 id="클라이언트에서-만료-확인">클라이언트에서 만료 확인</h3>
<p>토큰을 발급 받을 때마다 클라이언트에 발급 시간을 기록해두고 요청을 보내기 전에 토큰이 만료 되었는지 확인 후 요청을 보내는 방식이다.
이렇게 하면 불필요한 요청을 줄일 수 있다.</p>
<h3 id="그래서-어떤-방식">그래서 어떤 방식?</h3>
<p>일단은 서버에서 만료를 확인하는 방식으로 코드를 작성했다.
어떤 방식이 좋은지 조금 더 알아본 뒤에 다시 수정할 예정!</p>
<h1 id="토큰-저장위치">토큰 저장위치</h1>
<p>이제 서버에서 발급받은 토큰을 어디다가 저장할지에 고민이 생겼다.</p>
<h2 id="global-state">Global State</h2>
<p>전역 상태에 저장해서 쓸 수 있을 것 같은데, 이렇게 하면 새로고침 했을 때 토큰이 사라지므로 결국 Web Storage를 이용한 redux-persist, recoil-persist등을 사용해야해서 좋은 방법이 아니다.</p>
<h2 id="cookie-vs-web-storage">Cookie vs Web Storage</h2>
<h3 id="xss-공격">XSS 공격</h3>
<ul>
<li><p>Web Storage의 경우 script를 삽입하는 XSS 공격을 받으면 token을 열람할 수 있다.</p>
</li>
<li><p>Cookie는 httpOnly 옵션을 통해 xss 공격을 방어할 수 있기 때문에 webStorage보다 안전하다.
하지만 xss는 필수적으로 방어해야 하는 공격이므로 딱히 cookie의 장점이라 보긴 어렵다.
<em>또한 React는 자체적으로 사용자 입력에 대해 <strong>escape처리</strong>를 하고 있다.</em></p>
</li>
<li><p>만약 XSS가 뚫린다면??
js로 위조된 request를 보낼 수 있으므로 자동으로 request에 실리는 쿠키의 특성 상 안전하지 못한 건 똑같다.</p>
</li>
</ul>
<h3 id="추가-비용">추가 비용</h3>
<ul>
<li>또한 cookie를 사용하려면 백엔드 api에서 내가 사용하는 cookie를 위한 설정을 해야한다. 
보안 때문에 쿠키를 request에 싣지 못하도록 막는게 fetch API의 기본값이다.</li>
</ul>
<h3 id="mdn의-입장">mdn의 입장</h3>
<ul>
<li><strong>mdn에서도 쿠키에 담는 것은 적절하지 않다고 한다.</strong><br><img src="https://velog.velcdn.com/images/sang-mini/post/2496a687-ac85-45b9-97cd-e0ad0aa2b6c2/image.png" alt=""></li>
</ul>
<h2 id="결론">결론</h2>
<p>위에서 본 이유들을 바탕으로 우리는 localStorage에 저장하는 방식을 택했다.
<a href="https://medium.com/swlh/whats-the-secure-way-to-store-jwt-dd362f5b7914">Cookie를 지지하는 사람들</a>도 있지만 XSS 취약점만 빼면 가장 적절한 방식이라고 판단했기 때문이다.</p>
<h1 id="로그인-구현">로그인 구현</h1>
<h2 id="token-가져오기">token 가져오기</h2>
<p>최대한 선언적인 코드스타일을 지향하는 react의 특성을 살리기 위해 localStorage에서 토큰을 다루는 util 함수를 만들었다.
이런 helper function을 쓰면 컴포넌트 안에서 재사용 할 때 선언적으로 코드를 짤 수 있어서 좋다고 생각한다.</p>
<pre><code class="language-js">export function getAccessToken() {
    return localStorage.getItem(&quot;accessToken&quot;);
}
export function getRefreshToken() {
    return localStorage.getItem(&quot;refreshToken&quot;);
}
export function setToken(key, token) {
    localStorage.setItem(key, token);
}
</code></pre>
<h2 id="로그인-요청">로그인 요청</h2>
<p>로그인 요청을 보내고 성공 여부와 응답을 반환하는 비동기 함수를 따로 분리해서 로그인 컴포넌트안의 로직을 단순화 했다.</p>
<pre><code class="language-js">// api.js
export async function login({ id, password }) {
    try {
        const response = await fetch(&quot;http://localhost:8080/user/login&quot;, {
            method: &quot;POST&quot;,
            headers: {
                &quot;Content-Type&quot;: &quot;application/json&quot;
            },
            body: JSON.stringify({ id, password })
        });

        if (response.ok) {
            const data = await response.json();
            return {
                success: true,
                data
            };
        } else {
            // 서버 에러코드에 따라 에러처리
            return { success: false, message: &quot;Login failed&quot; };
        }
    } catch (error) {
        console.error(&quot;Login error:&quot;, error);
        return { success: false, message: error.toString() };
    }
}</code></pre>
<p>로그인 요청을 통해 응답을 받으면 토큰을 저장하고 리다이렉트 한다.
이렇게 하면 로그인이 완료된다.</p>
<pre><code class="language-js">// login component

...
const handleLogin = async (e) =&gt; {
        e.preventDefault();
        const { success, data, message } = await login({
            id: userId,
            password
        });

        if (success) {
            setToken(&quot;accessToken&quot;, data.accessToken);
            setToken(&quot;refreshToken&quot;, data.refreshToken);
            navigate(&quot;/&quot;);
        } else {
            console.error(message);
        }
    };
...</code></pre>
<h2 id="페이지-보호">페이지 보호</h2>
<p>react-router v6 data-router의 <strong><a href="https://reactrouter.com/en/main/route/loader">loader</a></strong> 사용하면 페이지에 접근하기 전에 함수를 실행할 수 있다.</p>
<p>따로 Wrapper 컴포넌트를 만들어서 인증 처리를 해야하나 하다가 알게 되었는데 아주 유용한 것 같다.</p>
<p>우리 프로젝트에서는 매 요청마다 토큰 만료를 확인하긴 하지만, 사용자가 권한이 있어야 접근할 수 있는 페이지로 들어왔을 때 최소한의 페이지 보호는 필요하다고 생각했다. </p>
<pre><code class="language-js">export function checkAuthLoader() {
    const token = getAccessToken();

    if (!token) {
        return redirect(&quot;/login&quot;);
    }
}

// app.jsx 로그인 필요한 페이지 예시
&lt;Route path=&quot;subscription/form&quot; loader={checkAuthLoader} element={&lt;Subscribe /&gt;} /&gt;</code></pre>
<p>권한이 필요한 페이지의 경우 페이지 접근시에 <strong>토큰이 없다면 login 페이지로 redirect</strong> 시키도록 했다.</p>
<h2 id="인증된-사용자-요청">인증된 사용자 요청</h2>
<p>인증이 필요한 요청을 보낼 때 마다 Bearer 토큰 타입으로 accessToken을 같이 보내는데 이 부분을 재사용할 수 있는 함수로 만들었다.</p>
<pre><code class="language-js">async function sendAuthRequest(url, options = {}) {
    const accessToken = getAccessToken();
    if (accessToken) {
        options.headers = {
            ...options.headers,
            Authorization: `Bearer ${accessToken}`
        };
    }

    let response = await fetch(url, options);

    // 액세스 토큰이 만료되었을 경우 로직, status 코드에 따라 변경 필요
    if (response.status === 401) {
        try {
            const newAccessToken = await refreshAccessToken();
            options.headers.Authorization = `Bearer ${newAccessToken}`;
            response = await fetch(url, options);
        } catch (error) {
            console.error(&quot;Session expired. User needs to login again.&quot;);
            logout();
        }
    }

    return response;
}</code></pre>
<h3 id="accesstoken-재발급">accessToken 재발급</h3>
<p>accessToken이 만료되었을 때 재발급을 받는 코드</p>
<pre><code class="language-js">async function refreshAccessToken(refreshToken) {
    const response = await fetch(&quot;/api/refresh_token&quot;, {
        method: &quot;POST&quot;,
        headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
        body: JSON.stringify({ refreshToken })
    });

    if (response.ok) {
        const data = await response.json();
        localStorage.setItem(&quot;accessToken&quot;, data.accessToken);
        localStorage.setItem(&quot;refreshToken&quot;, data.refreshToken);
        return data.accessToken;
    } else {
        throw new Error(&quot;Refresh token is invalid or expired.&quot;);
    }
}</code></pre>
<h3 id="실제-요청-예시">실제 요청 예시</h3>
<pre><code class="language-js">async function submitReview(reviewData) {
    try {
        const response = await sendAuthRequest(&quot;/api/reviews&quot;, {
            method: &quot;POST&quot;,
            headers: { &quot;Content-Type&quot;: &quot;application/json&quot; },
            body: JSON.stringify(reviewData)
        });

        if (response.ok) {
            console.log(&quot;Review submitted successfully.&quot;);
        } else {
            console.error(&quot;Failed to submit review.&quot;);
        }
    } catch (error) {
        console.error(&quot;Error submitting review:&quot;, error);
    }
}</code></pre>
<h1 id="회고">회고</h1>
<p>프로젝트 데모에서는 메인 비즈니스 로직을 보여주는 것이 더 중요하다고 생각해서 최대한 로그인을 피해왔었다. 
하지만 이번 프로젝트에서는 유저를 구분하는 것이 중요했기 때문에 로그인을 구현해봤는데 생각보다 고민거리가 더 많았다. 그래도 여러 고민들을 하면서 로그인 구현에 대한 두려움은 확실히 사라졌기 때문에 필요한 시간이었다고 생각한다.</p>
<h1 id="참조">참조</h1>
<p><a href="https://blog.naver.com/h9911120/222310637750">https://blog.naver.com/h9911120/222310637750</a>
<a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies">https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies</a>
<a href="https://medium.com/swlh/whats-the-secure-way-to-store-jwt-dd362f5b7914">https://medium.com/swlh/whats-the-secure-way-to-store-jwt-dd362f5b7914</a>
<a href="https://reactrouter.com/en/main/route/loader">https://reactrouter.com/en/main/route/loader</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Javascript event 다루기(eventListener를 쓰는 이유)]]></title>
            <link>https://velog.io/@sang-mini/Javascript-event-%EB%8B%A4%EB%A3%A8%EA%B8%B0eventListener%EB%A5%BC-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@sang-mini/Javascript-event-%EB%8B%A4%EB%A3%A8%EA%B8%B0eventListener%EB%A5%BC-%EC%93%B0%EB%8A%94-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Tue, 06 Feb 2024 12:57:12 GMT</pubDate>
            <description><![CDATA[<h1 id="🎤-event란">🎤 event란?</h1>
<blockquote>
<p>이벤트(event)란 여러분이 프로그래밍하고 있는 시스템에서 일어나는 사건(action) 혹은 발생(occurrence)인데, 이는 여러분이 원한다면 그것들에 어떠한 방식으로 응답할 수 있도록 시스템이 말해주는 것입니다.
-<strong>mdn</strong></p>
</blockquote>
<p><strong>event</strong>에 대한 mdn의 설명은 위와 같다.
저 설명을 바탕으로 javascript, 특히 웹에서의 event가 무엇인지 생각해보았다. 
<strong>event</strong>는 브라우저 내의 DOM 요소에 발생하는 사건이고, 브라우저는 이것을 감지해서 사용자와의 상호작용을 만들어낸다.</p>
<h2 id="target">target</h2>
<p>위에서 event는 브라우저 내의 특정 DOM요소에 발생한다고 했는데, 이처럼 이벤트가 발생한 대상 객체를 <strong>Event.target</strong>이라고 한다.</p>
<h2 id="이벤트-버블링과-캡처링">이벤트 버블링과 캡처링</h2>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/383137a9-afb6-4950-aa37-765cdf0556ea/image.png" alt=""></p>
<p>우리가 자주 마주하는 DOM 구조를 생각해보면 요소 안에 요소가 위치하는 식으로 계층적인 구조를 이루고 있다. 
그래서 위 그림과 같이 이벤트도 제일 아래에 있는 요소까지 계층적으로 발생하게 된다. 
(책상위에 여러 물건을 쌓아두고 맨 위를 눌렀을 때를 대입해보면 이해가 쉽다.)</p>
<p>이러한 현상을 event propagation이라고 하고, capturing, target, bubbling의 3단계로 이벤트 전파 흐름이 생긴다.</p>
<ul>
<li>capturing: 부모 요소로 부터 자식요소로 이벤트가 전파되는 단계</li>
<li>target: 이벤트가 실제 target 요소에 전달되는 단계</li>
<li>bubbling: 이벤트가 부모 요소로 전파되는 단계</li>
</ul>
<p>기본적으로는 bubbling 순서로 이벤트가 감지 되는데, capturing 단계에서 이벤트를 감지하려면 addEventListener메서드의 3번째 인자에 값을 true로 주면 된다. </p>
<h1 id="🍴-event를-다루는-방법">🍴 event를 다루는 방법</h1>
<p>이제 이벤트를 이용해서 상호작용을 만들어내는 방법을 알아보자.
event의 흐름을 이해했다면 이벤트를 잡아내는 방법이 궁금할 것이다.
여러 방법이 있는데 결론부터 말하자면 <strong>event listener</strong>를 사용하는 것을 권장한다.
다양한 event중 click event를 감지하는 여러가지 방법을 비교해보겠다.</p>
<h2 id="event-handler-property">event handler property</h2>
<pre><code class="language-js">const btn = document.querySelector(&quot;button&quot;);

function click() {
  console.log(&quot;clicked&quot;)
}

btn.onclick = click;</code></pre>
<p>btn이라는 요소를 DOM에서 찾아내서 onclick 프로퍼티에 click이라는 함수를 등록한다.
이렇게 하면 btn이 클릭되었을 때 click함수가 실행된다.</p>
<h2 id="inline-event-handler">inline event handler</h2>
<p>mdn 문서를 보면 아래와 같이 경고를 한다.
<img src="https://velog.velcdn.com/images/sang-mini/post/2a16ca1b-e386-45dc-8222-f1795b1867a7/image.png" alt=""></p>
<p>html과 js를 분리하지 않으면 코드를 분석하기 어렵고, 100개의 버튼에 100개의 어트리뷰트를 더한다면 유지보수가 불가능에 가깝기 때문에 이 방법은 쓰지말라고 한다.</p>
<h2 id="😎-event-listener">😎 event listener</h2>
<pre><code class="language-js">const btn = document.querySelector(&quot;button&quot;);

function click() {
  console.log(&quot;clicked&quot;)
}

btn.addEventListener(&quot;click&quot;, click);</code></pre>
<p>가장 권장되는 방식인데, 말 그대로 event를 기다리고 있다가 event가 발생하면 콜백함수를 실행해준다.</p>
<p>다른 방식과 비교했을 때 몇몇 이점을 가지고 있다.</p>
<h3 id="장점-1-이벤트핸들러를-제거할-수-있다">장점 1. 이벤트핸들러를 제거할 수 있다.</h3>
<pre><code class="language-js">btn.removeEventListener(&quot;click&quot;, click);</code></pre>
<p>결국 이벤트 핸들러도 메모리를 사용하기 때문에 사용하지 않는 이벤트 핸들러를 제거할 수 있다는 것은 큰 장점이다.
주의할 점은 콜백함수의 이름을 찾아서 제거하는 것이기 때문에 콜백함수를 기명함수로 써야한다는 것이다.</p>
<h3 id="장점-2-하나의-타겟에-여러-이벤트-리스너를-등록할-수-있다">장점 2. 하나의 타겟에 여러 이벤트 리스너를 등록할 수 있다.</h3>
<pre><code class="language-js">myElement.onclick = functionA; // functionB로 덮어 씌워짐
myElement.onclick = functionB; 
</code></pre>
<pre><code class="language-js">myElement.addEventListener(&quot;click&quot;, functionA);
myElement.addEventListener(&quot;click&quot;, functionB);
</code></pre>
<h2 id="🤨-target-vs-currenttarget">🤨 target vs currentTarget</h2>
<p>Event.target와 비슷한 Event.currentTarget이 있는데 currentTarget과 target은 다를 수도 있고, 같을 수도 있다.</p>
<p><strong>target</strong>은 이벤트가 발생하는 요소,
<strong>currentTarget</strong>은 이벤트 핸들러가 부착된 요소다.
헷갈린다면 이벤트 캡쳐링과 버블링을 다시보자.</p>
<h1 id="회고">회고</h1>
<p>이벤트 위임에 대해서 정리하기위해 이벤트에 대한 개념들을 쓰다보니 양이 아주 많아졌다... 
이벤트 위임은 다음 글에 써야할 것 같다 😁</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[조건문 다루기 (feat. else if 지옥 벗어나기)]]></title>
            <link>https://velog.io/@sang-mini/%EC%A1%B0%EA%B1%B4%EB%AC%B8-%EB%8B%A4%EB%A3%A8%EA%B8%B0-feat.-else-if-%EC%A7%80%EC%98%A5-%EB%B2%97%EC%96%B4%EB%82%98%EA%B8%B0</link>
            <guid>https://velog.io/@sang-mini/%EC%A1%B0%EA%B1%B4%EB%AC%B8-%EB%8B%A4%EB%A3%A8%EA%B8%B0-feat.-else-if-%EC%A7%80%EC%98%A5-%EB%B2%97%EC%96%B4%EB%82%98%EA%B8%B0</guid>
            <pubDate>Tue, 06 Feb 2024 00:19:35 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>바닐라 자바스크립트로 프로젝트를 할 때 비중이 큰 작업 중 하나가 이벤트 위임을 사용해서 이벤트를 잡아내는 것이었다. 
app이라는 최상위 태그에 click 이벤트 리스너를 달아놓고 event target에 따라 이벤트 핸들러를 실행하는 방식을 사용했다.
이 때문에 이벤트 리스너의 콜백에서 아주 많은 분기처리가 필요했는데 그때 조건문을 다루면서 코드를 개선시킨 경험을 정리해보겠다.</p>
</blockquote>
<h1 id="😵💫-if-else">😵‍💫 if else</h1>
<p>사실 아래의 상황이 되기 전에 코드를 개선하긴 했지만 조건문을 개선하지 않았다면 아래와 같은 코드를 짜게 되었을 것이다. 보기만 해도 어지럽다...</p>
<pre><code class="language-js">import * as handler from &quot;../handler&quot;;

const {
  main: { column },
  history,
  header,
} = handler;

const onClick = ({ target }) =&gt; {
  if (target.classList.contains(&quot;js-openHistory&quot;)) {
    header.showHistory(target);
  } else if (target.classList.contains(&quot;js-closeHistory&quot;)) {
    history.closeHistory(target);
  } else if (target.classList.contains(&quot;js-addCardBtn&quot;)) {
    column.openAddCardForm(target);
  } else if (target.classList.contains(&quot;js-editCardBtn&quot;)) {
    column.card.openEditCardForm(target);
  } else if (target.classList.contains(&quot;js-deleteCardBtn&quot;)) {
    column.card.clickDeleteCard(target);
  } else if (target.classList.contains(&quot;js-addFormCancel&quot;)) {
    column.cardForm.closeAddCardForm(target);
  } else if (target.classList.contains(&quot;js-editFormCancel&quot;)) {
    column.cardForm.closeEditCardForm(target);
  } else if (target.classList.contains(&quot;js-deleteHistory&quot;)) {
    history.deleteHistory(target);
  } else if (target.classList.contains(&quot;js-deleteCancel&quot;)) {
    column.card.cancelDeleteCard(target);
  } else if (target.classList.contains(&quot;js-deleteConfirm&quot;)) {
    column.card.deleteCard(target);
  } 
};</code></pre>
<h1 id="🤨-switch-case">🤨 switch case</h1>
<p>위와 같이 많은 조건들을 처리할 때 조금 더 가독성을 높일 수 있는 것이 <strong>switch case</strong>문인데 이를 이용해서 코드를 개선해보면 아래와 같다.
전보다 조금 더 나아지긴 했지만 여전히 반복이 많고 코드의 흐름을 파악하려면 위에서부터 타고내려오면서 읽어야한다는 단점이 있다.
또한 조건이 추가될 때 또다시 case와 break를 써야한다.</p>
<pre><code class="language-js">import * as handler from &quot;../handler&quot;;

const {
  main: { column },
  history,
  header,
} = handler;

const onClick = ({ target }) =&gt; {
  const className = target.classList[0];

  switch (className) {
    case &quot;js-openHistory&quot;:
      header.showHistory(target);
      break;
    case &quot;js-closeHistory&quot;:
      history.closeHistory(target);
      break;
    case &quot;js-addCardBtn&quot;:
      column.openAddCardForm(target);
      break;
    case &quot;js-editCardBtn&quot;:
      column.card.openEditCardForm(target);
      break;
    case &quot;js-deleteCardBtn&quot;:
      column.card.clickDeleteCard(target);
      break;
    case &quot;js-addFormCancel&quot;:
      column.cardForm.closeAddCardForm(target);
      break;
    case &quot;js-editFormCancel&quot;:
      column.cardForm.closeEditCardForm(target);
      break;
    case &quot;js-deleteHistory&quot;:
      history.deleteHistory(target);
      break;
    case &quot;js-deleteCancel&quot;:
      column.card.cancelDeleteCard(target);
      break;
    case &quot;js-deleteConfirm&quot;:
      column.card.deleteCard(target);
      break;
    default:
      break;
  }
};
</code></pre>
<h1 id="😎-look-up-table">😎 look up table</h1>
<p>가독성을 더 높이고 추가 삭제가 용이한 방법을 찾아본 결과 handlerMap 객체를 만들어서 사용하는 방법을 알게되었다.</p>
<p>핸들러 함수와 target의 선택자를 매핑하는 방식으로 가독성을 높이고 핸들러의 추가 삭제가 쉽도록 했다.</p>
<pre><code class="language-js">
import * as handler from &quot;../handler&quot;;

const {
  main: { column },
  history,
  header,
} = handler;

const clickHandlerMap = {
  &quot;js-openHistory&quot;: header.showHistory,
  &quot;js-closeHistory&quot;: history.closeHistory,
  &quot;js-addCardBtn&quot;: column.openAddCardForm,
  &quot;js-editCardBtn&quot;: column.card.openEditCardForm,
  &quot;js-deleteCardBtn&quot;: column.card.clickDeleteCard,
  &quot;js-addFormCancel&quot;: column.cardForm.closeAddCardForm,
  &quot;js-editFormCancel&quot;: column.cardForm.closeEditCardForm,
  &quot;js-deleteHistory&quot;: history.deleteHistory,
  &quot;js-deleteCancel&quot;: column.card.cancelDeleteCard,
  &quot;js-deleteConfirm&quot;: column.card.deleteCard,
};

const onClick = ({ target }) =&gt; {
  const executeHandler = clickHandlerMap[target.classList[0]];
  if (executeHandler) {
    executeHandler(target);
  }
};
</code></pre>
<h1 id="😁-회고">😁 회고</h1>
<p>else if를 코드에서 줄이는 것이 좋다는 말을 많이 들었는데 다양한 방법으로 조건문을 다뤄보니까 왜 else if를 줄여야하는지 조금 더 와닿았다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JS 단축평가]]></title>
            <link>https://velog.io/@sang-mini/JS-%EB%8B%A8%EC%B6%95%ED%8F%89%EA%B0%80</link>
            <guid>https://velog.io/@sang-mini/JS-%EB%8B%A8%EC%B6%95%ED%8F%89%EA%B0%80</guid>
            <pubDate>Sun, 04 Feb 2024 11:07:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>if else를 코드에서 줄여 나가다 보면 자주 사용하게 되는 단축평가에 대해 정리해보자.</p>
</blockquote>
<h1 id="😵💫-truthy--falsy">😵‍💫 Truthy &amp; falsy</h1>
<p>자바스크립트에는 Truthy 값(참으로 평가되는 값)과 Falsy 값(거짓으로 평가되는 값)이 있다.
이 값들이 조건식에 들어가게 되면 불리언 값으로 암묵적 타입 변환을 한다.</p>
<h2 id="😵-타입변환">😵 타입변환</h2>
<p><strong>🤓 명시적 타입 변환</strong>
개발자의 의도에 따라 값의 타입을 변환하는 것을 말한다.</p>
<pre><code class="language-js">const x = 10;

// 숫자를 문자열로 타입 캐스팅한다.
const str = x.toString();
console.log(typeof str, str); // string 10

// x 변수의 값이 변경된 것은 아니다.
console.log(typeof x, x) // number 10</code></pre>
<p><strong>🤫 암묵적 타입 변환</strong>
개발자의 의도와는 상관없이 표현식을 평가하는 도중에 자바스크립트 엔진에 의해 타입이 변환되는 것을 말한다.</p>
<pre><code class="language-js">const x = 10;

// 문자열 연결 연산자는 숫자 타입 x의 값을 바탕으로 새로운 문자열을 생성한다.
const str = x + &#39;&#39;;
console.log(typeof str, str); // string 10

// x 변수의 값이 변경된 것은 아니다.
console.log(typeof x, x) // number 10
</code></pre>
<h1 id="😎">😎 &amp;&amp;</h1>
<table>
<thead>
<tr>
<th>단축 평가 표현식</th>
<th>평가 결과</th>
</tr>
</thead>
<tbody><tr>
<td>true &amp;&amp; anything</td>
<td>anything</td>
</tr>
<tr>
<td>false &amp;&amp; anything</td>
<td>false</td>
</tr>
</tbody></table>
<p>논리 연산의 결과를 결정하는 피연산자를 타입 변환 없이 그대로 반환한다. 이를 <strong>단축 평가</strong>라 한다.</p>
<p>논리곱(&amp;&amp;) 연산자를 사용한 예시를 보도록 하자.</p>
<pre><code class="language-js">export const getDeviceInfo = () =&gt; {
  const userAgent = navigator.userAgent;

  const isMobile = /Mobi|Android/i.test(userAgent);
  const isIOS = /iPhone|iPad|iPod/i.test(userAgent);

  if (isMobile &amp;&amp; isIOS) {
    return &quot;iOS&quot;;
  }
  if (isMobile &amp;&amp; isAndroid) {
    return &quot;Android&quot;;
  }
  return &quot;Web&quot;;

};</code></pre>
<p>위의 코드는 사용자의 디바이스가 어떤 종류인지 알아내는 코드인데, if문 안의 조건 두개가 모두 참일 경우에만 해당 조건문이 실행된다.
 중첩된 if문을 쓰지 않고 논리곱 연산자를 이용한 early return으로 가독성을 높였다.</p>
<h1 id="🤩-">🤩 ||</h1>
<table>
<thead>
<tr>
<th>단축 평가 표현식</th>
<th>평가 결과</th>
</tr>
</thead>
<tbody><tr>
<td>true || anything</td>
<td>true</td>
</tr>
<tr>
<td>false || anything</td>
<td>anything</td>
</tr>
</tbody></table>
<p>이번에는 논리합 연산자를 이용한 예시를 살펴보자.</p>
<pre><code class="language-js">export const deepCopy = (obj) =&gt; {
  if (obj === null || typeof obj !== &quot;object&quot;) {
    return obj;
  }

  if (Array.isArray(obj)) {
    return obj.map(deepCopy);
  }

  const copiedObj = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copiedObj[key] = deepCopy(obj[key]);
    }
  }

  return copiedObj;
};</code></pre>
<p>위의 코드는 deepCopy를 하는 재귀함수인데 맨 위의 두 조건 중 하나가 참이면 인자를 return하도록 기저조건을 설정했다. </p>
<h1 id="🧐-">🧐 ??</h1>
<p>??는 <code>nullish operator(null 병합 연산자)</code> 인데  왼쪽 피연산자가 <code>null</code> 또는 <code>undefined</code>일 때 오른쪽 피연산자를 반환하고, 그렇지 않으면 왼쪽 피연산자를 반환하는 논리 연산자이다.</p>
<pre><code class="language-js">const foo = null ?? &#39;default string&#39;;
// Expected output: &quot;default string&quot;

const baz = 0 ?? 42;
// Expected output: 0</code></pre>
<p>논리합 연산자(<code>||</code>)와 다른 점은 0을 평가할 때인데 0은 falsy값이라서 논리합 연산자로는 걸러낼 수 없지만 null 병합 연산자로는 걸러낼 수 있다. </p>
<h1 id="😁-회고">😁 회고</h1>
<p>이렇게 여러가지 단축 평가를 &quot;잘&quot;사용한다면 가독성을 높인 코드를 작성할 수 있다. 특히 null 병합 연산자와 논리합 연산자의 차이를 잘 몰랐는데 이번 기회에 제대로 공부할 수 있어서 좋았다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[연관된 State 관리하기(feat. useReducer, custom hook)]]></title>
            <link>https://velog.io/@sang-mini/%EC%97%B0%EA%B4%80%EB%90%9C-State-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0feat.-useReducer-custom-hook</link>
            <guid>https://velog.io/@sang-mini/%EC%97%B0%EA%B4%80%EB%90%9C-State-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0feat.-useReducer-custom-hook</guid>
            <pubDate>Sat, 03 Feb 2024 12:58:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>React로 간단한 todolist 만들기를 하면서 비동기 요청을 할 때 연관된 state가 여러개 필요했다. 이를 클린코드 react 강의에서 배운 여러가지 방법으로 다뤄보자. </p>
</blockquote>
<h1 id="🥲-초기-코드">🥲 초기 코드</h1>
<p>비동기 데이터를 다루는 <code>todolist</code>, 로딩중을 나타내는 <code>loading</code>, 에러 처리를 위한 <code>error</code>, 비동기 요청 성공을 나타내는 <code>success</code> 까지 총 4개의 연관된 상태가 있었다.
그리고 4개의 상태에 따라 다른 결과를 화면에 보여준다.
이전까지 <code>tanstack-query</code>같은 라이브러리를 사용하지 않을 때에는 이런 방식으로 서버 상태를 다루곤 했다.</p>
<pre><code class="language-jsx">function App() {
  const [todoList, setTodoList] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  const fetchTodo = async () =&gt; {
      try {
        setLoading(true);

        const response = await fetch(BASE_URL);

        if (!response.ok) {
          throw new Error(&quot;GET 요청 실패&quot;);
        }

        const data = await response.json();
        setTodoList(data);
        setSuccess(true);

      } catch (error) {
        setError(error.message);

      } finally {
        setLoading(false);
      }
    };


  useEffect(() =&gt; {
    fetchTodo();
  }, []);

  if (loading) return &lt;div&gt;로딩중..&lt;/div&gt;;
  if (error) return &lt;div&gt;에러가 발생했습니다. Error: {error}&lt;/div&gt;;
  if (!success) return null;

  return (
    &lt;div className={styles.container}&gt;
      &lt;header className={styles.header}&gt;
        나&lt;sub&gt;만의&lt;/sub&gt; 작&lt;sub&gt;은&lt;/sub&gt; 스&lt;sub&gt;케줄러&lt;/sub&gt;
      &lt;/header&gt;
      &lt;main className={styles.main}&gt;
        &lt;TodoForm /&gt;
        &lt;TodoList todoList={todoList} /&gt;
      &lt;/main&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<h1 id="😅-열거형-state">😅 열거형 State</h1>
<p>첫번째 개선 방법은 연관된 상태를 <strong>문자열</strong>을 이용해 하나의 상태로 만드는 것이다.
초기 코드를 잘 들여다보면 <code>loading</code>, <code>error</code>, <code>success</code>를 하나의 <code>state</code>로 묶어낼 수 있다는 것을 알 수 있다.</p>
<p><code>promiseState</code> 를 <code>loading</code>, <code>error</code>, <code>success</code> 라는 3가지 문자열로 표현한다.</p>
<p>하지만 이 방법의 경우 연관된 상태가 <code>true</code>나 <code>false</code>일 때만 사용 가능하다는 <strong>단점</strong>이 있다.
그래서 error 메시지를 <code>promiseState</code>로는 표현할 수 없다.</p>
<pre><code class="language-jsx">
const state = {
  loading: &quot;loading&quot;,
  error: &quot;error&quot;,
  success: &quot;success&quot;,
};

function App() {
  const [todoList, setTodoList] = useState([]);
  const [promiseState, setPromiseState] = useState(state.loading);

  const fetchTodo = async () =&gt; {
    try {
      setPromiseState(state.loading);

      const response = await fetch(BASE_URL);

      if (!response.ok) {
        throw new Error(&quot;GET 요청 실패&quot;);
      }

      const data = await response.json();
      setTodoList(data);
      setPromiseState(state.success);
    } catch (error) {
      setPromiseState(state.error);
    }
  };

  useEffect(() =&gt; {
    fetchTodo();
  }, []);

  if (promiseState == &quot;loading&quot;) {
    return &lt;div&gt;로딩중..&lt;/div&gt;;
  }
  if (promiseState == &quot;error&quot;) {
    return &lt;div&gt;에러가 발생했습니다.&lt;/div&gt;;
  }
  if (promiseState == &quot;success&quot;) {
    return (
      &lt;div className={styles.container}&gt;
        &lt;header className={styles.header}&gt;
          나&lt;sub&gt;만의&lt;/sub&gt; 작&lt;sub&gt;은&lt;/sub&gt; 스&lt;sub&gt;케줄러&lt;/sub&gt;
        &lt;/header&gt;
        &lt;main className={styles.main}&gt;
          &lt;TodoForm /&gt;
          &lt;TodoList /&gt;
        &lt;/main&gt;
      &lt;/div&gt;
    );
  }
}

export default App;</code></pre>
<h1 id="🙂-객체형-state">🙂 객체형 State</h1>
<p>다음 개선 방법은 연관된 상태를 객체를 이용해서 표현하는 방법이다. 앞서 문자열로 묶어냈던 3가지 상태들은 하나가 <code>true</code>이면 나머지가 <code>false</code>라는 특징이 있다. 이 점을 이용해서 객체형 <code>state</code> 하나 만을 가지고 연관된 상태들을 표현할 수 있다.</p>
<p>객체를 사용하면 열거형에서와는 다르게 error 메시지를 상태에 담을 수 있다는 장점이 있다.</p>
<pre><code class="language-jsx">
const initialState = {
  loading: false,
  error: false,
  success: false,
};

 const fetchTodo = async () =&gt; {
    try {
      setPromiseState({ ...initialState, loading: true });

      const response = await fetch(BASE_URL);

      if (!response.ok) {
        throw new Error(&quot;GET 요청 실패&quot;);
      }

      const data = await response.json();
      setTodoList(data);
      setPromiseState({ ...initialState, success: true });
    } catch (error) {
      setPromiseState({ ...initialState, error: error });
    }
  };

  useEffect(() =&gt; {
    fetchTodo();
  }, []);

  if (promiseState.loading) {
    return &lt;div&gt;로딩중..&lt;/div&gt;;
  }
  if (promiseState.error) {
    return &lt;div&gt;에러가 발생했습니다. Error: {promiseState.error}&lt;/div&gt;;
  }
  if (promiseState.success) {
    return (
      &lt;div className={styles.container}&gt;
        &lt;header className={styles.header}&gt;
            나&lt;sub&gt;만의&lt;/sub&gt; 작&lt;sub&gt;은&lt;/sub&gt; 스&lt;sub&gt;케줄러&lt;/sub&gt;
        &lt;/header&gt;
        &lt;main className={styles.main}&gt;
          &lt;TodoForm /&gt;
          &lt;TodoList /&gt;
        &lt;/main&gt;
      &lt;/div&gt;
    );
  }
}
export default App;</code></pre>
<h2 id="필요없는-state-없애기">필요없는 state 없애기</h2>
<p><code>useReducer</code>로 코드를 개선하기 전, 짚고 넘어가야하는 부분이 있다.</p>
<p>지금까지 다뤘던 상태중 <code>success</code>는 사실 없어도 되는 상태다.
왜냐하면 비동기 요청이 성공적으로 완료되면 <code>todoList</code> 상태에 데이터가 들어가기 때문이다. </p>
<p>이렇게 다른 상태로 표현 가능한 상태는 없애는 것이 좋다.</p>
<h1 id="😄-usereducer">😄 useReducer</h1>
<p>객체형 <code>state</code>의 경우에는 <code>useState</code> 대신 <code>useReducer</code>라는 hook을 사용할 수 있다.</p>
<p><code>useReducer</code>의 경우 <code>useState</code>와 비슷한데, <strong>state</strong>와 이를 변화시키는 <strong>dispatch</strong> 함수를 반환하고, 인자로는 <strong>reducer</strong> 함수와 초기 상태값을 받는다. </p>
<ul>
<li><code>reducer</code> - 반드시 <strong>순수 함수</strong>여야 하며 <strong>prev state</strong>와 <strong>action</strong>을 인수로 받아서 <strong>next state</strong>를 반환한다. <strong>state</strong>와 <strong>action</strong>에는 모든 데이터 타입이 할당될 수 있다.</li>
<li><code>dispatch</code> - <strong>state</strong>를 새로운 값으로 업데이트하고 리렌더링을 일으킨다. </li>
<li><a href="https://react.dev/reference/react/useReducer">React 공식문서</a></li>
</ul>
<blockquote>
<p>함수형 프로그래밍에서는 어떤 외부 상태에 의존하지도 않고 변경시키지도 않는, 즉 부수 효과(Side Effect)가 없는 함수를 <strong>순수함수(Pure function)</strong>라고 한다. 즉, 동일한 입력이 주어지면 항상 동일한 출력을 반환하는 함수를 말한다.</p>
</blockquote>
<p><code>actionType</code>의 경우, 그냥 문자열로 전달해줘도 되지만 실수를 방지하기 위해서 객체를 만들어서 다뤄보았다.</p>
<p><code>reducer</code>는 아래와 같이 switch case문으로 많이 쓰는데, 사실 if else로 써도 되고, lookup table로 써도 상관없다. 순수함수이기만 하면 됨!</p>
<pre><code class="language-jsx">
const initialState = {
  loading: false,
  error: null,
  data: null,
};

const actionType = {
  loading: &quot;LOADING&quot;,
  error: &quot;ERROR&quot;,
  success: &quot;SUCCESS&quot;,
};
const reducer = (state, action) =&gt; {
  switch (action.type) {
    case &quot;LOADING&quot;:
      return { loading: true, error: null, data: null };
    case &quot;ERROR&quot;:
      return { loading: false, error: action.error, data: null };
    case &quot;SUCCESS&quot;:
      return { loading: false, error: null, data: action.data };
    default:
      throw new Error(`해당하는 action type이 없습니다. ${action.type}`);
  }
};

function App() {
  const [promiseState, dispatch] = useReducer(reducer, initialState);

const fetchTodo = async () =&gt; {
    try {
      dispatch({ type: actionType.loading });

      const response = await fetch(BASE_URL);

      if (!response.ok) {
        throw new Error(&quot;GET 요청 실패&quot;);
      }

      const data = await response.json();
      dispatch({ type: actionType.success, data });
    } catch (error) {
      dispatch({ type: actionType.error, error });
      console.error(error);
    }
  };

  useEffect(() =&gt; {
    fetchTodo();
  }, []);


  const { loading, error, data: todoList } = promiseState;

  if (loading) {
    return &lt;div&gt;로딩중..&lt;/div&gt;;
  }
  if (error) {
    return &lt;div&gt;에러가 발생했습니다. Error: {error}&lt;/div&gt;;
  }
  return (
    &lt;div className={styles.container}&gt;
      &lt;header className={styles.header}&gt;
        나&lt;sub&gt;만의&lt;/sub&gt; 작&lt;sub&gt;은&lt;/sub&gt; 스&lt;sub&gt;케줄러&lt;/sub&gt;
      &lt;/header&gt;
      &lt;main className={styles.main}&gt;
        &lt;TodoForm /&gt;
        &lt;TodoList todoList={todoList} /&gt;
      &lt;/main&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<h1 id="😎-custom-hook">😎 custom hook</h1>
<p>복잡한 부분을 숨기고 <code>useReducer</code>로 개선한 코드를 재사용 할 수 있게 만들기 위해 <strong>custom hook</strong>으로 만들어보겠다.</p>
<p>먼저, 여러 비동기 요청을 할 때 custom hook을 재사용할 수 있도록 비동기 요청만 하는 함수를 따로 분리했다. </p>
<pre><code class="language-jsx">const getTodo = async () =&gt; {
  const response = await fetch(BASE_URL);
  return response;
};</code></pre>
<p>그리고 reducer로 처리하는 부분을 <strong>custom hook</strong>으로 만들었다.
custom hook은 use prefix로 시작하는 것이 규칙이다.</p>
<p>비동기를 처리하는 callback과 useEffect의 dependency를 인자로 받도록 했다.
반환 값으로는 state와 fetchData를 설정했다. fetchData를 부를 때마다 다시 서버에 요청을 하기 때문에 refetch 함수로 쓸 수 있다.
이렇게 하면 여러 비동기 함수에서 재사용할 수 있는 <strong>useAsync</strong> hook이 만들어진다.</p>
<pre><code class="language-js">// useAsync.js
const reducer = (state, action) =&gt; {
  switch (action.type) {
    case &quot;LOADING&quot;:
      return { loading: true, error: null, data: null };
    case &quot;ERROR&quot;:
      return { loading: false, error: action.error, data: null };
    case &quot;SUCCESS&quot;:
      return { loading: false, error: null, data: action.data };
    default:
      throw new Error(`해당하는 action type이 없습니다. ${action.type}`);
  }
};

const actionType = {
  loading: &quot;LOADING&quot;,
  error: &quot;ERROR&quot;,
  success: &quot;SUCCESS&quot;,
};

export const useAsync = (callback, deps = []) =&gt; {
  const [state, dispatch] = useReducer(reducer, {
    loading: false,
    error: null,
    data: null,
  });

  const fetchData = async () =&gt; {
    dispatch({ type: actionType.loading });
    try {
      const response = await callback();

      if (!response.ok) {
        throw new Error(&quot;요청 실패&quot;);
      }

      const data = await response.json();
      dispatch({ type: actionType.success, data });
    } catch (error) {
      dispatch({ type: actionType.error, error });
      console.error(error);
    }
  };

  useEffect(() =&gt; {
    fetchData();
  }, deps);

  return [state, fetchData];
};</code></pre>
<p>만든 useAsync hook은 아래와 같이 사용할 수 있다.</p>
<pre><code class="language-jsx">// App.js
import { useAsync } from &quot;./hooks/useAsync&quot;;

const getTodo = async () =&gt; {
  const response = await fetch(BASE_URL);
  return response;
};

function App() {
  const [promiseState, refetchTodo] = useAsync(getTodo);

  const { loading, error, data: todoList } = promiseState;

  if (loading) {
    return &lt;div&gt;로딩중..&lt;/div&gt;;
  }
  if (error) {
    return &lt;div&gt;에러가 발생했습니다. Error: {error}&lt;/div&gt;;
  }
  return (
    &lt;div className={styles.container}&gt;
      &lt;header className={styles.header}&gt;
        나&lt;sub&gt;만의&lt;/sub&gt; 작&lt;sub&gt;은&lt;/sub&gt; 스&lt;sub&gt;케줄러&lt;/sub&gt;
      &lt;/header&gt;
      &lt;main className={styles.main}&gt;
        &lt;TodoForm /&gt;
        &lt;TodoList todoList={todoList} /&gt;
      &lt;/main&gt;
    &lt;/div&gt;
  );
}
export default App;</code></pre>
<h1 id="🧐-회고">🧐 회고</h1>
<p>이렇게 다양한 방법으로 연관된 상태를 관리할 수 있다는 것을 이번 todolist 만들기를 통해 배웠다. 
이렇게 기본적인 것에서 이를 어떻게 발전시킬 수 있을지 고민하다보면 많은 것을 얻어갈 수 있다.</p>
<h1 id="🤭-참조">🤭 참조</h1>
<p><a href="https://react.vlpt.us/">https://react.vlpt.us/</a>
<a href="https://velog.io/@jini9256/%EC%88%9C%EC%88%98%ED%95%A8%EC%88%98%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94">https://velog.io/@jini9256/순수함수란-무엇인가요</a>
<a href="https://react.dev/reference/react/useReducer">https://react.dev/reference/react/useReducer</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[XSS 방지하기(feat.innerHTML)]]></title>
            <link>https://velog.io/@sang-mini/XSS-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0feat.innerHTML</link>
            <guid>https://velog.io/@sang-mini/XSS-%EB%B0%A9%EC%A7%80%ED%95%98%EA%B8%B0feat.innerHTML</guid>
            <pubDate>Sun, 14 Jan 2024 09:17:34 GMT</pubDate>
            <description><![CDATA[<h1 id="🧩-개요">🧩 개요</h1>
<blockquote>
<p>바닐라 자바스크립트로 팀 프로젝트를 진행하며, 리펙토링 중에 XSS 공격에 대한 취약점을 보완할 필요성을 느끼고 여러 방법을 탐색하고 적용한 과정을 남겨본다.</p>
</blockquote>
<h2 id="⚔️-xss란">⚔️ XSS란?</h2>
<blockquote>
<p><strong>교차 사이트 스크립팅(XSS)</strong>은 공격자가 웹 사이트에 악성 클라이언트 측 코드를 주입할 수 있는 보안 공격입니다. 이 코드는 피해자에 의해 실행되며 공격자가 접근 제어를 우회하고 사용자를 가장할 수 있습니다.
<em>mdn</em></p>
</blockquote>
<p>쉽게 말해서 공격자가 코드 취약점이 있는 부분에 <code>script</code> 태그 등으로 악성 코드를 주입할 수 있다는 것이다. 그래서 우리 프로젝트에도 악성스크립트(?)를 집어넣어 보았다.
<img src="https://velog.velcdn.com/images/sang-mini/post/85b333c4-e71b-4b7c-8f7a-f835e0fa5b24/image.png" width = "370px" align="left"/> 
<img src="https://velog.velcdn.com/images/sang-mini/post/75975d7e-3163-46ae-98ac-96f876fb5ee6/image.png" width = "370px" align="right"/></p>
<hr>  

<p><code>textarea</code>에 <code>html</code> 코드를 적고 제출해보니 작성한 것을 <code>text</code>로 인식하는 것이 아니라 <code>html</code>로 인식해서 해당 <code>html</code>이 화면에 반영되는 것을 볼 수 있었다.</p>
<h2 id="👿-innerhtml의-문제">👿 innerHTML의 문제</h2>
<p>위와 같은 문제가 일어나는 원인은 코드상에 있는 <code>innerHTML</code> 때문이었다. SPA방식으로 개발하다보니 <code>innerHTML</code>로 화면을 갈아끼웠는데 이때 사용자가 입력한 <code>text</code>도 함께 <code>html</code>로 파싱해서 넣기 때문에 이러한 문제가 발생했다.</p>
<pre><code class="language-js">export const renderWagleList = (cardList) =&gt; {
  const wagleList = document.querySelector(&quot;.wagle__list&quot;);
  wagleList.innerHTML = cardList &amp;&amp; cardList.length ? WagleMainView(cardList) : WagleEmptyView();
};</code></pre>
<p>이렇게 <code>card-list</code>안에 innerHTML로 <code>card</code>요소를 넣어주는데,</p>
<pre><code class="language-js">export const OnlyTextCardView = (text) =&gt; {
  return `
&lt;div class=&quot;card__content--only-text&quot;&gt;${text}&lt;/div&gt;
    `;
};</code></pre>
<p><code>card</code> 요소에는 사용자가 입력한 텍스트를 그대로 받아서 <code>string</code>으로 삽입하는 부분이 있다.<br>이 때문에 XSS 공격에 그대로 노출되어있다.</p>
<h1 id="🛡-해결책">🛡 해결책</h1>
<blockquote>
<p>XSS를 방지하는 여러가지 해결책을 탐색해보았다. </p>
</blockquote>
<h2 id="😷-sanitizeapi">😷 sanitizeAPI</h2>
<p>먼저, <a href="https://ui.toast.com/weekly-pick/ko_2021124">toast UI</a>와 <a href="https://web.dev/articles/sanitizer?hl=ko">web dev</a>에서 <strong><a href="https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API">sanitizeAPI</a></strong>에 대해 알게되었다. </p>
<pre><code class="language-js">const $div = document.querySelector(&#39;div&#39;)
const user_input = `&lt;em&gt;hello world&lt;/em&gt;&lt;img src=&quot;&quot; onerror=alert(0)&gt;`
const sanitizer = new Sanitizer()
$div.setHTML(user_input, sanitizer) // &lt;div&gt;&lt;em&gt;hello world&lt;/em&gt;&lt;img src=&quot;&quot;&gt;&lt;/div&gt;</code></pre>
<p>이렇게 sanitizer를 통해 안전한 html로 만든 뒤에 DOM에 삽입하는 방식인데, sanitizeAPI가 아직 모든 브라우저에서 지원을 하지 않아서 사용하기에는 어려움이 있었다.</p>
<h2 id="🧹-dompurify">🧹 DOMPurify</h2>
<p>가장 잘 알려진 sanitize 라이브러리인 DOMPurify에 대해서도 알아보았다.</p>
<pre><code class="language-js">const user_input = `&lt;em&gt;hello world&lt;/em&gt;&lt;img src=&quot;&quot; onerror=alert(0)&gt;`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `&lt;em&gt;hello world&lt;/em&gt;&lt;img src=&quot;&quot;&gt;`</code></pre>
<p><code>sanitizeAPI</code>와의 차이점은 결국 sanitized된 문자열을 <code>innerHTML</code>로 <code>DOM</code>에 삽입해 줘야한다는 점인데, 이는 DOMPurify 및 .innerHTML에 의해 두 번 파싱되는 결과를 낳는다. 이를 개선한 것이 sanitizeAPI라고 한다.
이 정도 기능이면 프로젝트에 적용하는 것에는 무리가 없었지만, 어떤 방식으로 안전한 문자열을 만드는지를 모르고 지나칠 것 같아 또 다른 방법을 찾아보았다.</p>
<h2 id="🔑-문자열-escape">🔑 문자열 escape</h2>
<p><a href="https://ko.legacy.reactjs.org/docs/introducing-jsx.html#jsx-prevents-injection-attacks">React에서는 XSS를 어떤 방식으로 방지</a>하는지를 알아보니 문자열 escape를 통해 JSX에 사용자 입력을 안전하게 삽입한다는 것을 알게 되었다.
즉 &lt;,&gt;,&amp;,&#39;,&quot;등을 &amp;amp와 같은 <a href="https://developer.mozilla.org/en-US/docs/Glossary/Entity#reserved_characters">HTML Entity</a>로 이스케이프해서 렌더링 하기 때문에 입력된 문자를 HTML 마크업으로 인식하지 않고 일반 문자로 인식하도록 할 수 있다. </p>
<p><a href="https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/server/escapeTextForBrowser.js">React.js의 github</a>을 보면 escape처리를 어떻게 했는지 알 수 있었다.</p>
<h2 id="⚖️-escape와-sanitize의-차이">⚖️ escape와 sanitize의 차이</h2>
<p>그렇다면 <strong>escape</strong>와 <strong>sanitize</strong>의 차이는 뭘까? 
<strong>escape</strong>는 특수한 HTML 문자를 HTML Entity로 바꾸는 것을 말한다.</p>
<p><strong>sanitize</strong>는 HTML 문자열에서 스크립트 실행과 같은 유해한 부분을 지우는 것을 말한다.</p>
<pre><code class="language-js">// XSS 🧨
$div.innerHTML = `&lt;em&gt;hello world&lt;/em&gt;&lt;img src=&quot;&quot; onerror=alert(0)&gt;`
// 새니타이징 후 ⛑
$div.innerHTML = `&lt;em&gt;hello world&lt;/em&gt;&lt;img src=&quot;&quot;&gt;`</code></pre>
<p>올바르게 새니타이징하려면 입력 문자열을 HTML로 구문 분석한다. 그리고 유해하다고 판단되는 태그 및 속성은 제거하고 무해한 속성은 유지해야 한다.
앞선 예제에서 <code>&lt;img onerror&gt;</code>는 에러 핸들러를 실행하게 하지만, <code>onerror</code> 핸들러가 제거되면 <code>&lt;em&gt;</code>은 그대로 두고 DOM에서 안전하게 확장할 수 있다.</p>
<h1 id="😁-코드에-적용">😁 코드에 적용</h1>
<p>직접 escape 코드를 적어보는 것이 공부에 더 도움이 될 것 같아서 React의 escape방식을 따라 적용해 보았다. escape의 핵심적인 부분은 다음과 같다.</p>
<pre><code class="language-js">// escapeTextForBrowser.js
 for (index = match.index; index &lt; str.length; index++) {
    switch (str.charCodeAt(index)) {
      case 34: // &quot;
        escape = &quot;&amp;quot;&quot;;
        break;
      case 38: // &amp;
        escape = &quot;&amp;amp;&quot;;
        break;
      case 39: // &#39;
        escape = &quot;&amp;#x27;&quot;; // modified from escape-html; used to be &#39;&amp;#39&#39;
        break;
      case 60: // &lt;
        escape = &quot;&amp;lt;&quot;;
        break;
      case 62: // &gt;
        escape = &quot;&amp;gt;&quot;;
        break;
      default:
        continue;
    }</code></pre>
<pre><code class="language-js">export const OnlyTextCardView = (text) =&gt; {
  const sanitizedText = escapeTextForBrowser(text);
  return `
&lt;div class=&quot;card__content--only-text&quot;&gt;${sanitizedText}&lt;/div&gt;
    `;
};</code></pre>
<h3 id="결과">결과</h3>
<img width="400px" src="https://velog.velcdn.com/images/sang-mini/post/61dbd1d6-2595-4a53-88e0-3433e8b23681/image.png"/>


<h1 id="📌-innerhtml-대안">📌 innerHTML 대안</h1>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Element/insertAdjacentHTML">insertAdjacentHTML</a>
<a href="https://developer.mozilla.org/ko/docs/Web/API/Node/textContent">textContent</a>
<a href="https://developer.mozilla.org/ko/docs/Web/API/HTMLElement/innerText">innerText</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentText">insertAdjacentText</a></p>
<h1 id="📌-참조">📌 참조</h1>
<p><a href="https://developer.mozilla.org/ko/docs/Glossary/Cross-site_scripting">https://developer.mozilla.org/ko/docs/Glossary/Cross-site_scripting</a>
<a href="https://ui.toast.com/weekly-pick/ko_2021124">https://ui.toast.com/weekly-pick/ko_2021124</a>
<a href="https://web.dev/articles/sanitizer?hl=ko">https://web.dev/articles/sanitizer?hl=ko</a>
<a href="https://ko.legacy.reactjs.org/docs/introducing-jsx.html#jsx-prevents-injection-attacks">https://ko.legacy.reactjs.org/docs/introducing-jsx.html#jsx-prevents-injection-attacks</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API">https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전역 공간 사용 최소화 하기]]></title>
            <link>https://velog.io/@sang-mini/%EC%A0%84%EC%97%AD-%EA%B3%B5%EA%B0%84-%EC%82%AC%EC%9A%A9-%EC%B5%9C%EC%86%8C%ED%99%94-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@sang-mini/%EC%A0%84%EC%97%AD-%EA%B3%B5%EA%B0%84-%EC%82%AC%EC%9A%A9-%EC%B5%9C%EC%86%8C%ED%99%94-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 02 Jan 2024 13:51:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>면접에서 Javascript 과제에 즉시 실행 함수를 사용한 것을 보고 전역 공간 오염을 막기 위해서 그렇게 코드를 짰는지 질문을 받았었다.
당시에 전역 공간 사용을 최소화하려는 의도도 아니었고, 전역 공간 사용을 안해야 한다는 것도 몰랐기 때문에 공부를 더 해봐야겠다고 생각했다.</p>
</blockquote>
<h1 id="🏛-전역공간">🏛 전역공간</h1>
<p>전역공간은 최상위 스코프다.</p>
<ul>
<li>browser - window</li>
<li>nodejs - global로도 접근가능</li>
</ul>
<p>왜 전역 공간 사용을 줄여야할까?</p>
<h2 id="전역-변수-선언">전역 변수 선언</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;Document&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;script src=&quot;index.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<pre><code class="language-js">// index.js
var globalVar = &quot;global&quot;;

console.log(window);</code></pre>
<p>이렇게 전역 변수를 선언하고 <code>html</code>을 만들어서 <code>javascript</code>를 브라우저에서 실행시키면 <code>window</code> 객체를 확인할 수 있다.
<img src="https://velog.velcdn.com/images/sang-mini/post/e762c4e6-a446-4ab7-a3bb-09e629c39633/image.png" alt="">
아주 많은 값들 중에 이상한 게 하나 껴있다. 
전역 변수로 선언했던 값이다.
<strong>이처럼 전역 변수를 만들면 <code>window</code> 객체 안에 변수가 만들어지고 <code>window.globalVar</code>로 접근도 가능하다.</strong></p>
<h1 id="🍄-전역-변수의-문제점">🍄 전역 변수의 문제점</h1>
<h2 id="스코프-분리-안됨">스코프 분리 안됨</h2>
<p><code>window</code> 객체에서 전역 변수에 접근할 수 있다는 것이 큰 문제가 될 수 있다.
<code>index2.js</code>라는 다른 파일을 하나 더 만들고 <code>window.globalVar</code>에 접근해보자.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
...
  &lt;body&gt;
    &lt;script src=&quot;index.js&quot;&gt;&lt;/script&gt;
    &lt;script src=&quot;index2.js&quot;&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<pre><code class="language-js">// index2.js
console.log(&quot;index2&quot;, window.globalVar);</code></pre>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/26de0fce-b1f3-4344-af5b-2624ca302502/image.png" alt="">
이처럼 파일을 나눠도 전역공간에 선언한 변수를 참조할 수 있다.
즉, 스코프가 나뉘지 않는다.
이 말은 파일이 여러 개가 되면 변수를 중복 선언하게 될 수도 있다는 뜻이다.</p>
<h2 id="전역-공간-오염">전역 공간 오염</h2>
<p>전역 변수는 window 객체 안에 만들어진다는 것을 위에서 보았다.
그러면 window 객체 안에 있는 global method와 같은 이름으로 선언하면 어떻게 될까?</p>
<pre><code class="language-js">var setTimeout = &quot;global&quot;;

console.log(window);</code></pre>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/b0cfab76-9d0f-4f06-bf1a-69e9a2d15f83/image.png" alt=""></p>
<p><code>setTimeout</code> 변수를 선언해보니 메서드가 없어지고 <code>&quot;global&quot;</code>로 할당되어 있는 것을 볼 수 있다.
이처럼 전역 변수를 잘못 사용하면 메서드를 사용할 수 없게 된다.</p>
<h1 id="😷-전역-공간-사용-최소화">😷 전역 공간 사용 최소화</h1>
<ul>
<li><p>전역에서는 <strong>IIFE</strong>(즉시실행함수) 사용하기</p>
</li>
<li><blockquote>
<p>전역 공간에 함수가 선언되는 것을 방지한다.</p>
</blockquote>
</li>
<li><p><strong>Module</strong> 사용하기</p>
</li>
<li><blockquote>
<p>모듈 파일에서 함수 밖에 선언된 변수는 전역 스코프가 아닌 <strong>모듈 스코프</strong>에 등록된다.</p>
</blockquote>
</li>
<li><p><code>var</code> 대신 <code>const</code>, <code>let</code> 사용하기</p>
</li>
<li><blockquote>
<p><code>var</code>는 block scope가 아니라서 for문 등에 사용하면 전역 공간을 오염시킬 수 있다.</p>
</blockquote>
</li>
<li><p><strong>closure</strong> 사용하기</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[grid 뿌수기]]></title>
            <link>https://velog.io/@sang-mini/grid-%EB%BF%8C%EC%88%98%EA%B8%B0</link>
            <guid>https://velog.io/@sang-mini/grid-%EB%BF%8C%EC%88%98%EA%B8%B0</guid>
            <pubDate>Mon, 01 Jan 2024 12:57:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><code>flexbox</code>에 이어서 <code>grid</code>를 정리해본다. 
기본 개념부터 반응형 웹에서 사용할 수 있는 <code>grid</code> 속성까지 공부해보자.</p>
</blockquote>
<h1 id="🍊-grid-개념">🍊 grid 개념</h1>
<p><code>flex</code>가 <code>main-axis</code>를 기준으로 1차원에서 요소를 정렬했다면 <code>grid</code>는 <code>column</code>과 <code>row</code>로 2차원에서 요소를 정렬한다.</p>
<pre><code class="language-css">.container {
    display: grid;
}</code></pre>
<blockquote>
<p><code>flex</code>와 마찬가지로 부모요소에서 grid를 적용하고 cell의 형태를 정의한다.
하지만 <code>flex</code>와 다르게 자식 요소에 적용하는 속성이 많다</p>
</blockquote>
<h1 id="🛠-속성">🛠 속성</h1>
<h2 id="🔨-부모요소에-적용하는-속성">🔨 부모요소에 적용하는 속성</h2>
<h3 id="grid-template-columns-grid-template-rows">grid-template-columns, grid-template-rows</h3>
<p><code>columns</code>: 열의 너비, 개수
<code>rows</code>: 행의 높이, 개수</p>
<pre><code class="language-css">.father {
    grid-template-columns: 100px 100px;
    grid-template-rows: 200px 100px;

}</code></pre>
<h3 id="gap">gap</h3>
<p><code>flex</code>와 마찬가지로 <code>gap</code>을 사용할 수 있다.
이때 가로와 세로의 <code>gap</code>을 다르게 하고 싶다면 <code>column-gap</code>과 <code>row-gap</code>을 사용한다.</p>
<pre><code class="language-css">.father {
    row-gap: 10px;
      column-gap: 20px;
}</code></pre>
<blockquote>
<p><strong>inspector</strong>
chrome inspector를 활용하면 grid의 형태를 쉽게 알아볼 수 있다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/fb9f1b5b-352e-46b6-8200-c3f0d05cc7e6/image.png" alt=""></p>
<h3 id="line-number">line number</h3>
<p><code>grid</code>에는 <strong>line number</strong>가 있는데 맨 앞 가장자리 부터 1, 맨 뒤 가장자리 부터 -1이 된다. 
line에다가 이름을 지정해 줄 수도 있다.</p>
<pre><code class="language-css">  grid-template-columns: [first] 100px [second] 200px [third] 50px [last];
  grid-template-rows: [korea] 200px [japan] 100px [usa];</code></pre>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/e5965e86-39ab-4d16-b2cf-2b8c591bf307/image.png" alt=""></p>
<h2 id="🔧-자식요소에-적용하는-속성">🔧 자식요소에 적용하는 속성</h2>
<h3 id="grid-column-grid-row">grid-column, grid-row</h3>
<p>자식 요소가 그리드 셀의 몇번째 라인에서 시작해서 몇번째 라인에서 끝날지를 정할 수 있다. 
<strong>line number</strong>또는 <strong>line name</strong>을 사용한다.</p>
<pre><code class="language-css">.child:last-child {
  background-color: tomato;
  /* grid-column-start: 2;
  grid-column-end: -1; */
  grid-column: second / last; /*shortcut*/
}

.child:first-child {
  /* grid-row-start: 1;
  grid-row-end: -1; */
  grid-row: korea / usa; /*shortcut*/
}</code></pre>
<h2 id="🪓-advanced">🪓 Advanced</h2>
<h3 id="fr">fr</h3>
<p>grid-cell의 크기를 정의할 때 사용하는 단위인데 상대적인 크기를 나타낸다</p>
<pre><code class="language-css">.father {
    grid-template-columns: 1fr 1fr 1fr 2fr; /*1 : 1 : 1 : 2*/
}</code></pre>
<h3 id="repeat">repeat</h3>
<p>grid-cell의 크기를 반복적으로 쓸 때 유용하게 사용할 수 있다.</p>
<pre><code class="language-css">.father {
    grid-template-columns: repeat(3, 1fr) 2fr; /*1 : 1 : 1 : 2*/
}</code></pre>
<h3 id="grid-templete-areas-grid-templete">grid-templete-areas, grid-templete</h3>
<p>위의 기본 속성들로 표현하기가 까다로울 경우 <code>grid-templete-areas</code>라는 속성을 사용할 수 있다.
직접 grid-cell의 구역을 지정해준다.
<code>grid-templete</code>으로 줄여 쓸 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/4df97e60-6171-4065-a148-e1f0f1ecdd79/image.png" alt=""></p>
<pre><code class="language-css">body {
  display: grid;
  height: 100vh;

  /* grid-template-columns: 1fr 1fr 1fr 1fr;
    grid-template-rows: 1fr 2fr 1fr;
    grid-template-areas:
      &quot;a a a a&quot;
      &quot;b b b c&quot;
      &quot;d d d d&quot;; */

  /*shortcut*/
  grid-template:
    &quot;a a a a&quot; 1fr
    &quot;b b b c&quot; 2fr
    &quot;d d d d&quot; 1fr / 1fr 1fr 1fr 1fr;
}

header {
  background-color: blue;
  grid-area: a;
}

section {
  background-color: coral;
  grid-area: b;
}

aside {
  background-color: forestgreen;
  grid-area: c;
}

footer {
  background-color: deeppink;
  grid-area: d;
}</code></pre>
<h3 id="span-keyword">span keyword</h3>
<p>자식 요소가 grid-cell을 몇 개 차지할 건지를 나타내는 속성이다.</p>
<pre><code class="language-css">.child {
    grid-column: 1 / span 2; /*1번 라인부터 시작해서 2개의 grid-cell을 차지함*/
}
</code></pre>
<h3 id="grid-auto-column-row-flow">grid-auto-column, row, flow</h3>
<p>지정해둔 grid-cell의 개수보다 많은 자식 요소들이 생길 경우 새로운 자식 요소를 배치하는 속성들이다.</p>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/8f2ca871-0f7e-4303-b729-a6a1df1902d1/image.png" alt=""></p>
<p>4개의 자식요소들을 template으로 조정해주고 나머지는 auto로 정한다</p>
<pre><code class="language-css">.father {
    grid-template-columns: repeat(2, 1fr);
      grid-template-rows: repeat(2, 1fr);
      grid-auto-rows: 1fr; /*새로운 요소의 높이*/
      grid-auto-columns: 1fr; /*새로운 요소의 너비*/
      grid-auto-flow: column; /*grid 요소들을 배치할 때 column을 먼저 채운다*/
}</code></pre>
<h3 id="justify-items-align-items-place-items-place-self">justify-items, align-items, place-items, place-self</h3>
<p>셀 내부 아이템 정렬할 때 사용하는 속성들
px 쓸 때만 유효하다.</p>
<blockquote>
<p><strong>child의 크기는 column의 크기가 아니다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/f9b7aa27-8695-47d0-95fd-732a0ea74781/image.png" alt=""></p>
<pre><code class="language-css">.father {
  ...
  align-items: end;
  justify-items: center;
  place-items: end center; /*shortcut*/
}

.child {
  ...
  width: 50px;
  height: 50px;
}

.child:nth-child(6) {
  ...
  /* align-self: center;
  justify-self: center; */
  place-self: center center; /*shortcut*/
  grid-column: span 2;
}</code></pre>
<h3 id="justify-content-align-content-place-content">justify-content, align-content, place-content</h3>
<p>셀 자체를 그룹 정렬할 때 사용하는 속성들
<img src="https://velog.velcdn.com/images/sang-mini/post/b86ffe9a-a224-4fd0-bfc9-886bb09377e8/image.png" alt=""></p>
<pre><code class="language-css">.father {
  ...
  /*align-content: start;
      justify-content: end;*/
  place-content: start end;
}</code></pre>
<h3 id="min-content-max-content">min-content, max-content</h3>
<p><code>width</code>에 주는 것처럼 <code>grid-template-columns</code>에도 사용할 수 있다.</p>
<pre><code class="language-css">.father {
  grid-template-columns: 1fr max-content, 1fr;
}</code></pre>
<h3 id="minmax">minmax()</h3>
<p>최소값보다 크고 최대값보다 작은 크기 범위를 정의한다.</p>
<pre><code class="language-css">.father {
  grid-template-columns: repeat(3, minmax(300px, 350px));
  /*minmax(min-content,max-content)*/
}</code></pre>
<h2 id="반응형-ui">반응형 ui</h2>
<h3 id="auto-fill">auto-fill</h3>
<p>빈 공간이 있을때 지정한 크기(min)의 열을 최대한 많이 만듬, 그 열이 비어있어도 만듬</p>
<pre><code class="language-css">grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));</code></pre>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/598a4dce-20d7-460a-a0f5-68a06b3646a8/image.png" alt=""></p>
<h3 id="auto-fit">auto-fit</h3>
<p>빈 공간이 있을때 지정한 크기(min)의 열을 최대한 많이 만듬, 자식 요소의 개수만큼이 최대</p>
<pre><code class="language-css">grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));</code></pre>
<p><img src="https://velog.velcdn.com/images/sang-mini/post/4e0a6ead-309b-4033-a66f-90e52e7814dc/image.png" alt=""></p>
<h1 id="🎮-연습하기">🎮 연습하기</h1>
<blockquote>
<p><a href="https://cssgridgarden.com/#ko">grid-garden</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>