<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Tech Blog | Hyewon</title>
        <link>https://velog.io/</link>
        <description>강혜원의 개발일지 입니다.</description>
        <lastBuildDate>Tue, 26 Mar 2024 06:27:05 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Tech Blog | Hyewon</title>
            <url>https://velog.velcdn.com/images/hyewon_kkang/profile/f4aefc92-40f1-4e35-8cb7-9923fc8f5e55/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Tech Blog | Hyewon. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hyewon_kkang" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Next14 + SWR useSWRInfinite 중복 fetch 개선하기, SWR Middleware 적용]]></title>
            <link>https://velog.io/@hyewon_kkang/Next14-SWR-useSWRInfinite-%EC%A4%91%EB%B3%B5-fetch-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-SWR-Middleware-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@hyewon_kkang/Next14-SWR-useSWRInfinite-%EC%A4%91%EB%B3%B5-fetch-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-SWR-Middleware-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Tue, 26 Mar 2024 06:27:05 GMT</pubDate>
            <description><![CDATA[<p>Next.js 14 App Router를 적용한 프로젝트에서 SWR을 통해 서버 데이터 캐싱 동작을 구현했는데, 무한스크롤 구현 시 사용되는 <code>useSWRInfinite</code> 훅에서 개선사항을 발견했다.</p>
<p><strong>발생한 이슈</strong></p>
<p>서버 컴포넌트 → 무한스크롤 리스트에 대해 0과 1페이지에 대한 데이터를 받아와 SWR 캐시에 넣어 해당 페이지들에 대한 api 요청을 막고자 했으나</p>
<p>클라이언트 컴포넌트 → 네트워크 탭에서 해당 페이지들에 대해 API 요청을 보내고 있는 것으로 확인했다.</p>
<p>정리하자면, 서버 컴포넌트에서 0과 1페이지에 대한 데이터를 받아오고, 이를 클라이언트 컴포넌트로 내려서 초기 데이터로 사용한다(fallback). 이때 fallback으로 내려 SWR에 캐시되고 있음에도 0과 1 페이지에 대한 요청을 다시 보낸다. 찾아보니 SWR 구현이 그렇게 되어 있음.. 제어할 수 있는 옵션도 존재하지 않아 개선이 필요했다. <code>useSWRInfinite</code> 훅에서만 발생되는 문제임</p>
<ol>
<li><p><code>useSWRImmutable</code> 훅을 참고해서 <code>useSWRInfinite</code> 호출 시에 <code>config</code> 값으로 <code>revalidate</code>하는 옵션들을 제거했다.</p>
<pre><code class="language-tsx">   const options: SWRInfiniteConfiguration = {
     revalidateFirstPage: false,
     parallel: true,
     initialSize: 2,
     fallbackData: initialData,
     revalidateOnMount: false, // 추가
     revalidateIfStale: false, // 추가
     revalidateOnFocus: false, // 추가
     revalidateOnReconnect: false, // 추가
   };

   const { data, size, setSize, isLoading, mutate, isValidating, ...rest } = useSWRInfinite&lt;
     PageContent&lt;T&gt;,
     FetchError</code></pre>
<p> <code>revalidateOnMount</code> 옵션에 의해서 처음 렌더링될 때 0과 1 페이지에 대한 api 요청을 보내지 않았다. 그래서 해결된 줄 알았는데, 스크롤을 내려 2번 페이지에 대한 요청을 보낼 때 0과 1에 대한 요청도 줄줄이 보낸다..</p>
</li>
<li><p>SWR Middleware 적용</p>
<p> <code>useSWRInfinite</code> 훅이 어떤 식으로 구현되어있는지 확인하기 위해 <a href="https://github.com/vercel/swr/blob/main/src/infinite/index.ts">https://github.com/vercel/swr/blob/main/src/infinite/index.ts</a> 소스코드를 참고하던 중에 <code>middleware</code>를 사용할 수 있음을 알게 되었고, 이를 적용해 훅 호출 전에 서버컴포넌트에서 fetch된 데이터인지 확인하는 로직을 추가해야겠다는 생각을 갖게 되었다.</p>
<p> 미들웨어는 SWR hook을 받고 hook의 실행 전후에 로직을 실행할 수 있다.</p>
<pre><code class="language-tsx"> // src/lib/infiniteMiddleware.ts

 &#39;use client&#39;;

 import { SWRHook, useSWRConfig } from &#39;swr&#39;;

 export const infiniteMiddleware = (useSWRNext: SWRHook) =&gt; {
   const { fallback, cache } = useSWRConfig();

   return (key: any, fetcher: any, config: any): any =&gt; {
     const extendedFetcher = (...args: any[]) =&gt; {
       const path = args[0];
       if (!cache.get(path) &amp;&amp; fallback[path]) {
         return fallback[path];
       } else {
         return fetcher(...args);
       }
     };

     const swr = useSWRNext(key, extendedFetcher, config);

     return swr;
   };
 };</code></pre>
<p> 이제 무한스크롤 리스트에서 서버,클라이언트 간 중복 fetch가 발생하지 않는다.</p>
<p> 추가적으로 다음 페이지를 불러오는 과정 중에 추가 스크롤 동작이 일어나면 동일 요청에 대해 중복 fetch가 발생하고 있어서 아래 조건을 추가해 <code>useSWRInfiniteScroll</code> 을 수정했다.</p>
<pre><code class="language-tsx">   useEffect(() =&gt; {
     if (!inView || !data || isValidating) return;
     setSize((size) =&gt; size + 1);
   }, [inView, isValidating]);
</code></pre>
<p> 참고: <a href="https://swr.vercel.app/ko/docs/middleware">https://swr.vercel.app/ko/docs/middleware</a></p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트 Object를 해시 맵처럼 이용하려면 Map을 사용하자]]></title>
            <link>https://velog.io/@hyewon_kkang/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Object%EB%A5%BC-%ED%95%B4%EC%8B%9C-%EB%A7%B5%EC%B2%98%EB%9F%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EB%A0%A4%EB%A9%B4-Map%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90</link>
            <guid>https://velog.io/@hyewon_kkang/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Object%EB%A5%BC-%ED%95%B4%EC%8B%9C-%EB%A7%B5%EC%B2%98%EB%9F%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EB%A0%A4%EB%A9%B4-Map%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90</guid>
            <pubDate>Mon, 04 Sep 2023 08:35:50 GMT</pubDate>
            <description><![CDATA[<p>커리어리에 올라온 포스트들 가운데 관심이 갔던 주제에 대해 다루어보았다.
<img src="https://velog.velcdn.com/images/hyewon_kkang/post/83e65a84-9f66-4d24-b4f6-d17607c3196d/image.jpeg" width="50%" height="30%"></p>
<h1 id="개요">개요</h1>
<p>사용자의 액션에 대한 결과를 띄워주기 위한 Result 컴포넌트를 구현했다. 이때 Result의 status 상태에 따라 각기 다른 아이콘을 띄워주기 위해 특정 status(key)를 통한 아이콘 정보(value)를 얻고자 하는 상황이 있었다. 이러한 경우가 개발하다보면 정말 많았는데 나는 항상 고민도 하지 않고 Object를 사용해 데이터를 검색하곤 했던 것 같다.</p>
<pre><code class="language-tsx">const ResultIconByStatus: Record&lt;Status, ResultIconInfo&gt; = {
  info: { name: &#39;info-rounded&#39;, color: &#39;#005FFC&#39; },
  success: { name: &#39;check-circle-rounded&#39;, color: &#39;#00C021&#39; },
  warning: { name: &#39;warning&#39;, color: &#39;#FFA825&#39; },
  error: { name: &#39;close-circle&#39;, color: &#39;#FF464A&#39; },
};</code></pre>
<p>위와 같은 케이스를 정리하면 다음과 같겠다. <strong><code>key를 통해 데이터를 검색해 value 값을 얻는 상황</code></strong>.</p>
<p>이러한 자료구조 중 하나로 제목에서 정의된 해시 맵이 있다. 해시 맵은 &lt;key, value&gt; 쌍으로 데이터를 관리하며 많은 양의 데이터 가운데 key를 통한 검색이 가능한 구조다.</p>
<p>모던 자바스크립트 Deep Dive의 정의를 인용하면,
<em>자바스크립트 Object는 프로퍼티와 메서드로 구성된 집합체로, *</em>이처럼 객체는 상태와 동작을 하나의 단위로 구조화할 수 있어 유용*<em>하다</em>로 정의된다.</p>
<p>Object는 위 설명대로 상태와 동작을 하나로 저장할 수 있는 데이터 타입이다. 위의 코드는 여러 상태를 갖고 있지만, 상태들이 하나의 단위로 이용되지는 않아 보인다. 이러한 경우 Object 대신 <code>Map</code>을 이용해볼 수 있다.</p>
<h1 id="object를-map처럼-쓰기">Object를 Map처럼 쓰기</h1>
<pre><code class="language-tsx">const map = {};

// key-value pair 넣기
map[&#39;key1&#39;] = &#39;값1&#39;;
map[&#39;key2&#39;] = &#39;값2&#39;;
map[&#39;key3&#39;] = &#39;값3&#39;;

// 특정 key를 가지고 있는지 확인하기
if (map.hasOwnProperty(&#39;key1&#39;)) {
  console.log(&#39;Map이 key1을 포함하고 있음.&#39;);
}

// 특정 key의 value를 찾기
console.log(map[&#39;key1&#39;]);</code></pre>
<h1 id="map을-사용해야-하는-이유">Map을 사용해야 하는 이유</h1>
<p>자바스크립트에서 Map을 Object 대신 사용하면 여러 가지 장점이 있다.</p>
<h3 id="1-더-다양한-key-types를-제공한다">1. 더 다양한 key types를 제공한다.</h3>
<p>Object는 key로 Symbol과 String 타입만 이용할 수 있다. Map은 key로 어떤 타입이든 사용 가능하다.</p>
<pre><code class="language-tsx">const map = new Map();
const myFunction = () =&gt; console.log(&#39;I am a useful function.&#39;);
const myNumber = 666;
const myObject = {
  name: &#39;plainObjectValue&#39;,
  otherKey: &#39;otherValue&#39;
};
map.set(myFunction, &#39;function as a key&#39;);
map.set(myNumber, &#39;number as a key&#39;);
map.set(myObject, &#39;object as a key&#39;);

console.log(map.get(myFunction)); // function as a key
console.log(map.get(myNumber)); // number as a key
console.log(map.get(myObject)); // object as a key</code></pre>
<h3 id="2-크기를-빠르게-구할-수-있다">2. 크기를 빠르게 구할 수 있다.</h3>
<p>Map은 크기를 O(1)의 시간 복잡도로 구할 수 있으나 Object는 O(n)의 시간이 소요된다. 또한 크기를 구하는 방식도 간단하다.</p>
<pre><code class="language-tsx">const map = new Map();
map.set(&#39;someKey1&#39;, 1);
map.set(&#39;someKey2&#39;, 1);
...
map.set(&#39;someKey100&#39;, 1);

console.log(map.size) // 100, Runtime: O(1)

const plainObjMap = {};
plainObjMap[&#39;someKey1&#39;] = 1;
plainObjMap[&#39;someKey2&#39;] = 1;
...
plainObjMap[&#39;someKey100&#39;] = 1;

console.log(Object.keys(plainObjMap).length) // 100, Runtime: O(n)</code></pre>
<h3 id="3-더-나은-성능">3. 더 나은 성능</h3>
<p>Map은 설계단계부터 데이터의 추가와 제거에 최적화 되어 있기 때문에 성능에 있어서 매우 유리하다. Object는 key-value 쌍의 빈번한 추가 및 제거에 최적화되어 있지 않다.</p>
<p>원문에 따르면 맥북프로에서 천만개의 데이터 셋을 가지고 테스트 했을 때 Object는 1.6초의 처리시간이 필요했고 Map은 1ms 이하의 처리시간을 보였다고 한다.</p>
<h3 id="4-반복문-사용">4. 반복문 사용</h3>
<p>Object는 key들을 먼저 찾아낸 다음 그것들을 토대로 순회한다. 하지만 Map은 그 자체가 iterable 하기 때문에 직접 반복할 수 있다.</p>
<pre><code class="language-tsx">const map = new Map();
map.set(&#39;someKey1&#39;, 1);
map.set(&#39;someKey2&#39;, 2);
map.set(&#39;someKey3&#39;, 3);

for (let [key, value] of map) {
  console.log(`${key} = ${value}`);
}
// someKey1 = 1
// someKey2 = 2
// someKey3 = 3

const plainObjMap = {};
plainObjMap[&#39;someKey1&#39;] = 1;
plainObjMap[&#39;someKey2&#39;] = 2;
plainObjMap[&#39;someKey3&#39;] = 3;

for (let key of Object.keys(plainObjMap)) {
  const value = plainObjMap[key];
  console.log(`${key} = ${value}`);
}
// someKey1 = 1
// someKey2 = 2
// someKey3 = 3</code></pre>
<h3 id="5-순서의-보장">5. 순서의 보장</h3>
<p>ECMAScript 2015 이전 버전에서는 Object에서 키의 순서를 보장하지 않았다. 현재는 지원된다.</p>
<p>Map은 추가한 순서에 따른 순번을 보장한다.</p>
<h3 id="6-key-overriding-금지">6. Key overriding 금지</h3>
<p>Object에는 미리 정의된 prototype이 있기 때문에 key가 충돌할 위험이 있다(toString, constructor, valueOf 등). 하지만 Map을 사용할 때는 그런 걱정을 할 필요가 없다. Map은 기본적으로 키를 포함하지 않는다. 여기에는 명시적으로 입력된 내용만 포함된다.</p>
<pre><code class="language-tsx">const map = new Map();
map.set(&#39;키1&#39;, 1);
map.set(&#39;키2&#39;, 2);
map.set(&#39;toString&#39;, 3); // Map에서는 문제 없습니다.

const plainObjMap = {};
plainObjMap[&#39;키1&#39;] = 1;
plainObjMap[&#39;키2&#39;] = 2;
plainObjMap[&#39;toString&#39;] = 3; // toString은 이미 선점되어 있습니다. toString()을 사용할 수 없게됩니다.</code></pre>
<hr>
<p>그동안 JS로 많은 프로젝트를 진행해보았지만 한 번도 Map을 사용해본 적이 없는 것 같다(😂). 그래서 커리어리에서 관련 포스트를 봤을 때 뜨끔하면서도 관심이 갔다. Object와 Map의 정의, 쓰임은 자바스크립트 면접 대비를 하면서 그 개념을 확실히 정리했었는데, 적용은 하지 못하고 있었다는 사실을 발견했다. 포스트의 내용은 정말 유익했던 것 같다. Map과 Object를 비교해볼 수 있어서 좋았고, 다른 프로젝트를 하면서 바로 적용해볼 수 있겠다는 기대가 들었다. 그리고 데이터 검색에서 너무나 당연하게 Object를 썼던 나를 반성하며 앞으로는 필요에 의해, 근거를 갖고 Object와 Map 중에 선택해서 쓰는 것이 필요할 것 같다.</p>
<h1 id="reference">Reference</h1>
<ul>
<li><a href="https://betterprogramming.pub/stop-using-objects-as-hash-maps-in-javascript-9a272e85f6a8">https://betterprogramming.pub/stop-using-objects-as-hash-maps-in-javascript-9a272e85f6a8</a></li>
<li><a href="https://shanepark.tistory.com/m/220">https://shanepark.tistory.com/m/220</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Map">https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Map</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바스크립트 Closure와 React Hooks 사이의 관계]]></title>
            <link>https://velog.io/@hyewon_kkang/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Closure%EC%99%80-React-Hooks-%EC%82%AC%EC%9D%B4%EC%9D%98-%EA%B4%80%EA%B3%84-aixsw5xf</link>
            <guid>https://velog.io/@hyewon_kkang/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Closure%EC%99%80-React-Hooks-%EC%82%AC%EC%9D%B4%EC%9D%98-%EA%B4%80%EA%B3%84-aixsw5xf</guid>
            <pubDate>Fri, 18 Aug 2023 16:11:22 GMT</pubDate>
            <description><![CDATA[<h1 id="-배경"># 배경</h1>
<p>황준일 님의 개발 블로그에 작성된 포스팅 가운데 <a href="https://junilhwang.github.io/TIL/Javascript/Design/Vanilla-JS-Make-useSate-hook/#_1-%E1%84%8B%E1%85%B4%E1%84%86%E1%85%AE%E1%86%AB%E1%84%8B%E1%85%B3%E1%86%AF-%E1%84%80%E1%85%A1%E1%86%BD%E1%84%80%E1%85%B5">Vanilla Javascript로 React UseState Hook 만들기</a>를 보게 되었다. 그동안 React 함수형 컴포넌트를 이용한 개발은 많이 해봤지만 <code>useState</code>, <code>useEffect</code> 등의 Hook이 어떤 식으로 동작하는지는 알아본 적이 없는 것 같았다. 그래서 React Hook은 실제로 어떻게 동작하는지 궁금해졌고, 찾아보다보니 클로저와의 관계성을 발견하여 클로저 개념을 중심으로 동작 방식을 간단히 구현하고 이해해보고자 한다.</p>
<h1 id="-클로저란"># 클로저란?</h1>
<blockquote>
</blockquote>
<p>💡 클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical Environment)의 조합이다. - MDN</p>
<p>클로저의 정의를 다르게 표현하면,
클로저란 함수가 속한 렉시컬 스코프를 기억하여 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능이다.</p>
<pre><code class="language-tsx">let foo = 1 // global scope

function add() {
  foo += 1
  return foo
}

console.log(add()) // 2
console.log(add()) // 3
console.log(add()) // 4</code></pre>
<p>foo 변수에 1을 더해주는 add 함수가 있다. 위 코드는 정상적으로 동작하지만 아래와 같이 작성할 경우 문제가 발생한다.</p>
<pre><code class="language-jsx">let foo = 1 // global scope

function add() {
  foo += 1
  return foo
}

console.log(add()) // 2
console.log(add()) // 3
foo = 9999
console.log(add()) // 10000</code></pre>
<p>foo를 global에서 접근하여 임의로 수정이 가능하다는 점이다.</p>
<pre><code class="language-jsx">function add() {
  let foo = 1
  foo += 1
  return foo
}

console.log(add()) // 2
console.log(add()) // 2
foo = 9999 // &#39;foo&#39; is not defined.
console.log(add()) // 2</code></pre>
<p>다음은 외부에서 foo에 접근할 수 없지만 add가 정상적으로 동작하지 못한다.</p>
<pre><code class="language-jsx">function getAdd() {
  let foo = 1

  return function () {
    foo += 1

    return foo
  }
}

const add = getAdd()

console.log(add()) // 2
console.log(add()) // 3
foo = 9999 // &#39;foo&#39; is not defined.
console.log(add()) // 4</code></pre>
<p>이때 다음과 같이 클로저를 활용할 수 있다. 이렇게되면 getAdd 함수에서 add 함수를 반환해 함수를 add 함수를 호출할 때 마다 getAdd 함수에 선언되어있는 foo에 접근할 수 있다.</p>
<p>혹은 아래와 같은 모듈 패턴을 통해 클로저를 만들 수도 있다.</p>
<pre><code class="language-jsx">const add = (function getAdd() {
  let foo = 1

  return function () {
    foo += 1

    return foo
  }
})()

console.log(add()) // 2
console.log(add()) // 3
console.log(add()) // 4</code></pre>
<p>이러한 클로저의 성질을 이용해 React Hook은 만들어졌다. useState를 클로저를 활용해 구현해보자.</p>
<h1 id="-클로저를-이용한-hook의-구현"># 클로저를 이용한 Hook의 구현</h1>
<h3 id="usestate">useState</h3>
<p><code>useState</code> 훅을 이용하면 함수형 컴포넌트에서 상태 값을 관리할 수 있다.
<code>useState</code>는 초기값을 넘겨받아 상태값(count)과 상태값을 변경하는 함수(setCount=<em>setState</em>)를 반환한다. 일반적으로 두 요소를 비구조화 할당을 통해 변수에 할당해 사용한다.</p>
<pre><code class="language-tsx">const [state, setState] = useState(initialValue);</code></pre>
<p>함수형 컴포넌트에서 이전과 현재 상태의 변경 여부를 감지하기 위해서는 함수가 실행됐을 때 이전 상태에 대한 정보를 가지고 있어야 한다. React는 이 과정에서 클로저 개념을 이용한다.</p>
<h3 id="usestate-구현하기">useState 구현하기</h3>
<p>이번에는 useState를 간단히 구현해보자.</p>
<pre><code class="language-jsx">function useState(initialState) {
    let value_ = initialState;
    const state = value_;
    const setState = (newState) =&gt; {
        value_ = newState;
    };
    return [state, setState];
}

const [count, setCount] = useState(0);

console.log(count); // 0
setCount(1);
console.log(count); // 0</code></pre>
<p>위 코드는 의도대로 동작하지 않는다. 이는 <code>count</code> 값이 한 번 가져오고 끝난 값이기 때문이다. 이를 의도대로 동작시키려면 <code>const state = value_</code> 부분을 함수 형태로 바꿔, 값을 쓰는게 아니라 호출해주는 식으로 바꾼다면 호출할 때마다 값을 가져오기 때문에 <code>setCount</code>가 반영된 값을 얻을 수 있다.</p>
<pre><code class="language-jsx">function useState(initialState) {
    let value_ = initialState;
    const state = () =&gt; value_; // 호출하는 방식으로 수정
    const setState = (newState) =&gt; {
        value_ = newState;
    };
    return [state, setState];
}

const [count, setCount] = useState(0);

console.log(count()); // 0
setCount(1);
console.log(count()); // 1</code></pre>
<p>위 코드는 정상적으로 동작은 되지만, state가 변수가 아닌 getter 함수로 정의되어 있어 리액트의 useState와는 차이가 있다.</p>
<h3 id="컴포넌트에서-hook-사용하기">컴포넌트에서 Hook 사용하기</h3>
<p>그래서 이번에는 hook을 React 모듈 안으로 넣어 state를 변수로 사용하도록 useState를 변경해보자. 즉, 클로저를 또 다른 클로저의 내부로 이동시켜 해결해보자.</p>
<pre><code class="language-jsx">const React = (function () {
    function useState(initialState) {
        let value_ = initialState;
        const state = () =&gt; value_;
        const setState = (newState) =&gt; {
            value_ = newState;
        };
        return [state, setState];
    }

    return { useState };
})();</code></pre>
<p>그리고 안에 훅을 넣은 함수인 Component를 만든다.</p>
<pre><code class="language-jsx">const React = (function () {
    function useState(initialState) {
        let value_ = initialState;
        const state = () =&gt; value_;
        const setState = (newState) =&gt; {
            value_ = newState;
        };
        return [state, setState];
    }

    function render(Component) {
        const C = Component();
        C.render();
        return C;
    }

    return { render, useState };
})();

function Component() {
    const [count, setCount] = React.useState(0);

    return {
        render: () =&gt; console.log(count),
        increase: () =&gt; setCount(count + 1),
    };
}

var App = React.render(Component); // [Function: state]
App.increase();
var App = React.render(Component); // [Function: state]</code></pre>
<p>현재는 콘솔에 state function이 출력되므로 <code>value_</code>을 함수 외부에 선언하고, 를 수정해보자.</p>
<pre><code class="language-jsx">let value_;

const React = (function () {
    function useState(initialState) {
        const state = value_ || initialState;
        const setState = (newState) =&gt; {
            value_ = newState;
        };
        return [state, setState];
    }

    function render(Component) {
        const C = Component();
        C.render();
        return C;
    }

    return { render, useState };
})();

function Component() {
    const [count, setCount] = React.useState(0);

    return {
        render: () =&gt; console.log(count),
        increase: () =&gt; setCount(count + 1),
    };
}

var App = React.render(Component); // 0
App.increase();
var App = React.render(Component); // 1</code></pre>
<p>이제 정상적으로 동작되도록 구현한 것 같다. 그러나 아직 하나의 문제가 존재한다.</p>
<p>위 구현은 컴포넌트가 단 하나의 state만을 가진다. 그러나 실제 구현에서는 여러 개의 Hook을 가질 수 있다.</p>
<pre><code class="language-jsx">const [count, setCount] = React.useState(0);
const [text, setText] = React.useState(&quot;apple&quot;);</code></pre>
<p>따라서 React는 state를 useState 외부에 <strong>배열</strong> 형식으로 관리한다. useState에 저장된 state들은 배열에 순서대로 저장되고, 이러한 state 배열은 컴포넌트를 구분 짓는 유일한 키를 통해 접근할 수 있다.</p>
<p>배열과 인덱스를 이용해 위 코드를 변경하면 다음과 같다.</p>
<pre><code class="language-jsx">const React = (function () {
    let hooks = [];
    let idx = 0;

    function useState(initialState) {
        const state = hooks[idx] || initialState;
        const setState = (newState) =&gt; {
            hooks[idx] = newState;
        };

        idx++; // 다음 훅을 받을 수 있게 인덱스 증가
        return [state, setState];
    }

    function render(Component) {
        const C = Component();
        C.render();
        return C;
    }

    return { render, useState };
})();

function Component() {
    const [count, setCount] = React.useState(0);
    const [text, setText] = React.useState(&#39;apple&#39;);

    return {
        render: () =&gt; console.log({ count, text }),
        increase: () =&gt; setCount(count + 1),
        type: (word) =&gt; setText(word),
    };
}

var App = React.render(Component); // { count: 0, text: &#39;apple&#39; }
App.increase();
var App = React.render(Component); // { count: 1, text: &#39;apple&#39; }
App.type(&#39;orange&#39;);
var App = React.render(Component); // { count: &#39;orange&#39;, text: &#39;apple&#39; }</code></pre>
<p>increase()는 잘 동작하지만, type()을 실행할 때 count가 ‘orange’로 바뀌게 된다. 이는 App 컴포넌트가 render 되면 useState 함수를 호출하고, 그때마다 계속해서 index가 증가되기 때문이다. 따라서 render 될 때마다 index를 0으로 초기화 해주자.</p>
<pre><code class="language-jsx">const React = (function () {
    let hooks = [];
    let idx = 0;

    function useState(initialState) {
        const state = hooks[idx] || initialState;
        const setState = (newState) =&gt; {
            hooks[idx] = newState;
            console.log(hooks);
        };

        idx++;
        return [state, setState];
    }

    function render(Component) {
        idx = 0;
        const C = Component();
        C.render();
        return C;
    }

    return { render, useState };
})();

function Component() {
    const [count, setCount] = React.useState(0);
    const [text, setText] = React.useState(&#39;apple&#39;);

    return {
        render: () =&gt; console.log({ count, text }),
        increase: () =&gt; setCount(count + 1),
        type: (word) =&gt; setText(word),
    };
}

var App = React.render(Component);
App.increase();
var App = React.render(Component);
App.type(&#39;orange&#39;);
var App = React.render(Component);

// 실행 결과
// { count: 0, text: &#39;apple&#39; }
// { count: 0, text: &#39;apple&#39; }
// { count: 0, text: &#39;apple&#39; }</code></pre>
<p>이번에는 상태가 바뀌지 않는 문제가 발생한다. 이는 render된 후에 useState가 호출되므로 원래 의도했던 배열의 0, 1번 인덱스에 저장되는 것이 아니라 증가된 index(2)에 저장되기 때문이다.</p>
<pre><code class="language-jsx">{ count: 0, text: &#39;apple&#39; }
[ &lt;2 empty items&gt;, 1 ]
{ count: 0, text: &#39;apple&#39; }
[ &lt;2 empty items&gt;, &#39;orange&#39; ]
{ count: 0, text: &#39;apple&#39; }</code></pre>
<p>이를 해결하기 위해 또 클로저 개념을 이용해서 setState 안의 idx 값이 useState에 의해 변하지 않도록 <code>freeze</code> 시켜준다. 이를 통해 정상적으로 동작하는 결과를 얻을 수 있다.</p>
<pre><code class="language-jsx">const React = (function () {
    let hooks = [];
    let idx = 0;

    function useState(initialState) {
        const state = hooks[idx] || initialState;
        const _idx = idx;
        const setState = (newState) =&gt; {
            hooks[_idx] = newState;
        };

        idx++;
        return [state, setState];
    }

    function render(Component) {
        idx = 0;
        const C = Component();
        C.render();
        return C;
    }

    return { render, useState };
})();

function Component() {
    const [count, setCount] = React.useState(0);
    const [text, setText] = React.useState(&#39;apple&#39;);

    return {
        render: () =&gt; console.log({ count, text }),
        increase: () =&gt; setCount(count + 1),
        type: (word) =&gt; setText(word),
    };
}

var App = React.render(Component); // { count: 0, text: &#39;apple&#39; }
App.increase();
var App = React.render(Component); // { count: 1, text: &#39;apple&#39; }
App.type(&#39;orange&#39;);
var App = React.render(Component); // { count: 1, text: &#39;orange&#39; }</code></pre>
<h3 id="usestate의-동작-원리">useState의 동작 원리</h3>
<p>실제 React의 useState는 어떻게 구현되어 있을까? useState 내부 함수를 뜯어보자. (<code>node_modules/react/cjs/react.development.js</code> or <a href="https://github.com/facebook/react/blob/main/packages/react/src/ReactHooks.js">ReactHooks.js</a>)</p>
<img src="https://velog.velcdn.com/images/hyewon_kkang/post/b5885545-30d2-47ca-b6f8-b33e40ce8d16/image.png" width="400"/>

<p><code>useState</code>는 초기값을 넘겨받아 <code>dispatcher</code> 객체의 <code>useState</code> 함수에 넘겨준다.
<code>dispatcher</code>를 반환하는 <code>resolveDispatcher()</code> 함수는 아래와 같다.</p>
<p><img src="https://velog.velcdn.com/images/hyewon_kkang/post/4c765d20-afe1-4065-956a-e09927845f31/image.png" alt=""></p>
<p><code>resolveDispatcher()</code> 함수는 <code>ReactCurrentDispatcher</code>라는 객체의 <code>current</code> 프로퍼티를 반환하고 있다. (중간의 if 조건문은 우리가 Hook을 컴포넌트 외부에서 사용했을 때 에러를 발생시키는 부분이다.)
그렇다면 <code>ReactCurrentDispatcher</code>를 확인해보자.</p>
<img src="https://velog.velcdn.com/images/hyewon_kkang/post/54702d5b-e28c-46fb-872e-4f98e75af3fa/image.png" width="500"/>



<p><code>ReactCurrentDispatcher</code>는 단지 <strong>전역</strong>에 선언된 current라는 속성을 가지는 변수다.</p>
<ul>
<li><p>해당 코드 한 눈에 보기</p>
<pre><code class="language-jsx">  /**
   * Keeps track of the current dispatcher.
   */
  var ReactCurrentDispatcher = {
      /**
       * @internal
       * @type {ReactComponent}
       */
      current: null,
  };

  function resolveDispatcher() {
      var dispatcher = ReactCurrentDispatcher.current;

      {
          if (dispatcher === null) {
              error(
                  &#39;Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for&#39; +
                      &#39; one of the following reasons:\n&#39; +
                      &#39;1. You might have mismatching versions of React and the renderer (such as React DOM)\n&#39; +
                      &#39;2. You might be breaking the Rules of Hooks\n&#39; +
                      &#39;3. You might have more than one copy of React in the same app\n&#39; +
                      &#39;See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.&#39;,
              );
          }
      } // Will result in a null access error if accessed outside render phase. We
      // intentionally don&#39;t throw our own error because this is in a hot path.
      // Also helps ensure this is inlined.

      return dispatcher;
  }

  function useState(initialState) {
      var dispatcher = resolveDispatcher();
      return dispatcher.useState(initialState);
  }</code></pre>
</li>
</ul>
<p>지금까지의 과정을 요약하면 다음과 같다.</p>
<blockquote>
<ol>
<li>useState(를 포함한 모든 Hook)는 React 모듈에 선언되어 있는 함수로,</li>
<li>useState가 실행될 때마다 dispatcher를 선언하고 useState 메서드를 실행해 그 값을 반환받는다.</li>
<li>dispatcher는 전역 변수 ReactCurrentDispatcher로부터 가져온다.</li>
</ol>
</blockquote>
<p>결론적으로, <strong>함수형 컴포넌트가 <code>클로저</code>를 통해 선언되는 시점에 접근 가능했던 외부 상태 값에 계속 접근할 수 있는 것</strong>이다. <strong>함수형 컴포넌트에서 상태값을 변경하면 외부 값이 변경되고, 리렌더링(=함수 재호출)을 통해 새로운 값을 받아오게 된다</strong>.</p>
<p>핵심은 useState 리턴 값의 출처가 전역에서 온다는 점이다.
여기서 우리는 리액트가 실제로 클로저를 활용해 함수 외부의 값에 접근한다는 사실을 알 수 있게 된다.
(useState 내부에 대한 더 자세한 설명은 <a href="https://kyoung-jnn.com/posts/react-useState">여기</a>서 확인할 수 있다.)</p>
<h1 id="-hook의-규칙"># Hook의 규칙</h1>
<p>React 공식 문서에는 Hook을 사용할 때 지켜야 하는 두 가지 규칙을 제시하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/hyewon_kkang/post/78c256f7-30b5-412a-bee8-22ae3cdfa190/image.png" alt=""></p>
<p>그 중 첫 번째 규칙인 <strong>“최상위(at the Top Level)에서만 Hook을 호출해야 합니다”</strong>는 위의 설명대로 <strong>컴포넌트가 렌더링될 때마다 항상 동일한 순서로 Hook이 호출되는 것을 보장하기 위한 규칙</strong>이다. 즉, 컴포넌트에서 여러 Hook을, 여러 번 호출할 때 Hook의 호출 순서를 보장해야 한다는 의미인데 왜 이러한 규칙이 필요한 것일까?</p>
<p><strong>React가 Hook이 호출되는 순서에 의존하기 때문에 모든 렌더링에서 Hook의 호출 순서는 같다</strong>. 이는 다음과 같은 의미를 가진다.</p>
<pre><code class="language-tsx">// 첫 번째 렌더링
useState(1); // const [num, setNum] = useState(&#39;1&#39;);
useEffect(funcA);
useState(false);  // const [bool, setBool] = useState(false);
useEffect(funcB);

// 두 번째 렌더링
useState(1); // 순서: 1
useEffect(funcA); // 순서: 2
useState(false); // 순서: 3
useEffect(funcB); // 순서: 4

...</code></pre>
<p>즉, Hook의 호출 순서가 모든 렌더링에서 동일하다. 만약 이때 조건문 안에서 funcA effect를 호출한다면 어떤 일이 일어날까?</p>
<pre><code class="language-tsx">if (num &gt; 10) {
    useEffect(funcA);
}</code></pre>
<p>결과는 아래와 같아진다. 조건이 false이기 때문에 useEffect(funcA) hook을 건너 뛰어 Hook의 호출 순서는 달라지게 된다.</p>
<pre><code class="language-tsx">useState(1); // 순서: 1
// useEffect(funcA);  // 🔴 Hook을 건너뛴다.
useState(false); // 순서: 2
useEffect(funcB); // 순서: 3</code></pre>
<p>React는 이전 렌더링 때처럼 컴포넌트 내에서 두 번째 Hook 호출이 funcA effect와 일치할 것이라 예상했지만 그렇지 않게 된다. 그 시점부터 건너뛴 Hook 다음에 호출되는 Hook이 순서가 하나씩 밀리면서 버그를 발생시키게 된다.</p>
<p>이것이 공식문서에서 설명하는 <strong>컴포넌트 최상위에서 Hook이 호출되어야만 하는 이유다</strong>.</p>
<h1 id="-요약"># 요약</h1>
<ul>
<li>컴포넌트 함수가 다시 실행(렌더링) 되더라도 상태 값이 초기화되지 않는다. 어떻게 이게 가능할까? → 클로저를 통해 해결</li>
<li>React에서 함수형 컴포넌트의 상태 관리를 위해서는 컴포넌트 외부에 저장된 값을 사용하며, 클로저를 통해 해당 값에 접근해 비교하고 변경한다.</li>
<li>useState는 컴포넌트 내부에서 값을 변경시키는 것이 아니라, 컴포넌트 외부의 값을 변경시키기 때문에 상태가 변경된 직후 컴포넌트가 가진 값은 이전의 값을 그대로 참조한다.</li>
<li>각 컴포넌트의 상태 정보는 순서대로 배열에 저장되기 때문에 Hook을 조건문이나 반복문 안에서 사용하면 잘못된 순서의 값을 참조하게 될 수 있다.</li>
</ul>
<h1 id="-참고"># 참고</h1>
<p><a href="https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/">https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/</a>
<a href="https://medium.com/humanscape-tech/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%81%B4%EB%A1%9C%EC%A0%80%EB%A1%9C-hooks%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-3ba74e11fda7">https://medium.com/humanscape-tech/자바스크립트-클로저로-hooks구현하기-3ba74e11fda7</a>
<a href="https://ko.legacy.reactjs.org/docs/hooks-rules.html">https://ko.legacy.reactjs.org/docs/hooks-rules.html</a>
<a href="https://velog.io/@ggong/useState-Hook%EA%B3%BC-%ED%81%B4%EB%A1%9C%EC%A0%80">https://velog.io/@ggong/useState-Hook과-클로저</a>
<a href="https://seokzin.tistory.com/entry/React-useState%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC%EC%99%80-%ED%81%B4%EB%A1%9C%EC%A0%80">https://seokzin.tistory.com/entry/React-useState의-동작-원리와-클로저</a>
<a href="https://www.rinae.dev/posts/getting-closure-on-react-hooks-summary">https://www.rinae.dev/posts/getting-closure-on-react-hooks-summary</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[벨로그·티스토리 썸네일(배너) 만들기, blog banner generator]]></title>
            <link>https://velog.io/@hyewon_kkang/%EB%B2%A8%EB%A1%9C%EA%B7%B8-%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%8D%B8%EB%84%A4%EC%9D%BC%EB%B0%B0%EB%84%88-%EC%A0%9C%EC%9E%91-%EC%82%AC%EC%9D%B4%ED%8A%B8-blog-banner-generator</link>
            <guid>https://velog.io/@hyewon_kkang/%EB%B2%A8%EB%A1%9C%EA%B7%B8-%ED%8B%B0%EC%8A%A4%ED%86%A0%EB%A6%AC-%EC%8D%B8%EB%84%A4%EC%9D%BC%EB%B0%B0%EB%84%88-%EC%A0%9C%EC%9E%91-%EC%82%AC%EC%9D%B4%ED%8A%B8-blog-banner-generator</guid>
            <pubDate>Tue, 01 Aug 2023 15:25:11 GMT</pubDate>
            <description><![CDATA[<p>기존 github.io에서 벨로그로 개발 블로그를 이전하면서 포스트마다 통일성 있는 썸네일을 주고 싶었다. 사실 재작년에 벨로그를 잠시 이용했었는데 당시에는 포스트를 작성할 때마다 파워포인트로 템플릿을 하나 만들어놓고 텍스트만 고쳐서 사진 파일로 저장하곤 했었다. 그러나 단순히 썸네일 이미지 제작을 위해 ppt 파일을 열었다 닫았다, 텍스트를 지우고 고쳤다 저장했다 하는 것이 비효율적이었고 시간을 쓰는 것이 아까웠다. 그래서 언젠가는 아주아주 간단한 나만의 썸네일 제작 제너레이터를 구현해야겠다는 생각을 가졌었고 블로그 이전을 계기로 빠르게 개발해보았다.</p>
<p>blog-banner-generator는 벨로그 썸네일용 사이즈와 티스토리 배너용 사이즈(1:1)를 모두 지원하도록 했다. 또한, 나의 필요에 의해 만들기는 했지만 나와 같이 예쁘게 썸네일은 주고 싶지만, 직접 만들기는 귀찮은 사람들을 위해 다양한 템플릿을 지원하는 것도 구현에 포함시켰다. 템플릿의 경우는 여러 레퍼런스들을 보면서 베이직한 디자인으로 구현했다.</p>
<p><a href="https://hyewonkkang.github.io/blog-banner-generator/" target="_blank"><img src="https://velog.velcdn.com/images/hyewon_kkang/post/cce70b10-7d9a-4a23-b6ae-774cb5014522/image.png" />
</a></p>
<h2 id="사이트-이용-방법-✨">사이트 이용 방법 ✨</h2>
<h3 id="1-배너-사이즈-고르기">1. 배너 사이즈 고르기</h3>
<p><img src="https://velog.velcdn.com/images/hyewon_kkang/post/9e857f43-aef2-4607-a2a3-346f55a70f45/image.png" alt=""></p>
<p>배너 사이즈는 현재 velog와 tistory 썸네일 용 사이즈만을 제공한다. 따라서 사용자의 개발 블로그 종류에 따라 선택하면 된다.</p>
<h3 id="2-배너-템플릿-고르기">2. 배너 템플릿 고르기</h3>
<p><img src="https://velog.velcdn.com/images/hyewon_kkang/post/842752b7-b05e-4a95-8495-ea2245d49fdf/image.png" alt=""></p>
<p>2023/08/02 현재 사이트에는 8개의 템플릿을 올려두었다. 원하는 템플릿을 선택하면 해당 템플릿의 제목과 소제목의 배치 및 주변 선이나 테두리와 같은 <strong>동일한 레이아웃을 이용</strong>할 수 있다. 이때 예시 이미지의 배경, 폰트, 색상은 유지되지 않고, 이는 다음 작업에서 사용자가 선택할 수 있다.</p>
<h3 id="3-배너-내부-요소-채우기">3. 배너 내부 요소 채우기!</h3>
<p>선택한 배너 템플릿의 배경 및 폰트 스타일을 설정하고 텍스트를 입력하고 배치하는 단계다.</p>
<p><strong>배경 설정</strong>
<img src="https://velog.velcdn.com/images/hyewon_kkang/post/e42d3837-997f-4bc8-812c-090115b71ea5/image.png" alt=""></p>
<p>가장 먼저 배너의 배경은 다음과 같은 세 가지 타입으로 이용 가능하다. </p>
<ul>
<li><strong>그라디언트</strong>: 그라디언트의 경우 <a href="https://github.com/itmeo/webgradients">webgradients</a>에서 제공하는 색상 조합들을 이용했다.</li>
<li><strong>단색</strong>: 단색 배경은 사이트에 제시된 팔레트 색상들을 이용하거나 가장 마지막 컬러 요소를 통해 color picker도 이용 가능하다.</li>
<li><strong>이미지</strong>: 이미지는 랜덤 이미지 생성 버튼(Generate)을 통해 <a href="https://unsplash.com/">unsplash</a> 랜덤 이미지를 받아올 수 있고, 혹은 사용자가 직접 원하는 이미지 주소를 입력 창에 넣어 배경으로 사용도 가능하다.</li>
</ul>
<p><strong>폰트 스타일 설정</strong></p>
<p><img src="https://velog.velcdn.com/images/hyewon_kkang/post/6dd0e93d-7a45-4098-8034-2c3510515906/image.png" alt=""></p>
<ul>
<li>텍스트의 좌측/중앙/우측 정렬</li>
<li>폰트 및 테두리, 선의 색상 지정</li>
<li>텍스트의 크기 large/medium</li>
<li>폰트 선택</li>
</ul>
<p><strong>텍스트 입력</strong></p>
<p><img src="https://velog.velcdn.com/images/hyewon_kkang/post/f3c79d66-8f9e-4cf1-a00e-7e88fdbb1684/image.png" alt=""></p>
<p>현재는 제목과 소제목 두 가지 입력이 가능하다. 소제목은 주로 원하는 태그들을 문자열로 입력하여 사용할 수 있다. 이때 제목과 소제목에 입력한 텍스트가 길어져 사용자 임의로 줄바꿈을 주고 싶다면 input에 <code>&lt;br /&gt;</code> 문자열을 추가하면 줄바꿈이 가능하다. 예: <code># JavaScript&lt;br /&gt;# html2canvas</code></p>
<h2 id="코드-설명-💡">코드 설명 💡</h2>
<h3 id="1-preview">1. Preview</h3>
<p>사용자가 배경을 바꾸고, 폰트를 변경하고, 텍스트를 입력하면 미리보기(preview)에 바로 반영되어야 한다. 이러한 preview에는 배경 타입, 배경색상, 배경이미지, 폰트색상, 제목, 소제목 등 여러 상태들이 저장 및 관리되어야 한다. 그래서 <code>Preview</code> 클래스를 만들어 여러 상태들을 프로퍼티로 갖도록 만들었고, 상태 관리를 위한 메서드들을 추가했다.</p>
<pre><code class="language-jsx">export default class Preview {
    constructor() {
        this.size = &#39;1600x900&#39;; // 1600x900 | 800x800
        this.title = &#39;&#39;;
        this.subtitle = &#39;&#39;;
        this.textAlign = &#39;preview-text-center&#39;;
        this.font = &#39;font-noto-sans&#39;;
        this.fontLarge = true;

        this.gradient = null;
        this.selectedColor = null;
        this.template = &#39;0&#39;;

        this.preview = document.querySelector(&#39;.preview&#39;);
        this.previewTitle = document.querySelector(&#39;.preview-title&#39;);
        this.previewSubtitle = document.querySelector(&#39;.preview-subtitle&#39;).querySelector(&#39;span&#39;);

        this.updateTitle = _.debounce(this.updateTitle.bind(this), 100);
        this.updateSubTitle = _.debounce(this.updateSubTitle.bind(this), 100);
    }
    ...
}</code></pre>
<p>사용자가 배경 이미지를 변경하면 preview에 이를 렌더링 시켜주어야 한다. 그래서 Preview가 갖는 프로토타입 메서드들은 모두 DOM 요소에 접근해 재렌더링을 시켜주는 역할을 한다.</p>
<pre><code class="language-jsx">...
    setColor(color) {
        this.selectedColor = color;
    }

    updateTemplate(val) {
        this.preview.classList.replace(`template-${this.template}`, `template-${val}`);
        this.template = val;
    }

    updateSize(size) {
        this.size = size;
        this.preview.classList.toggle(&#39;preview-size-1600x900&#39;, size === &#39;1600x900&#39;);
        this.preview.classList.toggle(&#39;preview-size-800x800&#39;, size === &#39;800x800&#39;);
    }
...</code></pre>
<h3 id="2-control">2. Control</h3>
<p>미리보기에 사용자의 입력을 바로 적용시키기 위해서는 사용자의 행위에 의해 이벤트를 발생시켜야 하고, 각 이벤트에 이벤트 핸들러를 부착하는 과정이 필요하다. 사이트를 보면 알 수 있듯이 클릭 이벤트가 일어날 요소들이 매우 매우 많다. 이러한 코드들은 모두 <code>control.js</code>에 작성했다. </p>
<p>이벤트 핸들러로는 앞서 구현한 Preview 클래스의 인스턴스를 먼저 생성한 후, 해당 인스턴스가 가지는 프로토타입 메서드들을 전달한다.</p>
<p>아래는 step1의 배너 사이즈를 고르는 로직의 일부다.</p>
<pre><code class="language-jsx">...

/**
 * size
 */

const bannerSizeWrapper = document.querySelector(&#39;.banner-size-select-section&#39;);
const bannerSizeElements = bannerSizeWrapper.querySelectorAll(&#39;.banner-size-template&#39;);
bannerSizeWrapper.addEventListener(&#39;click&#39;, onChangeBannerSize);

function onChangeBannerSize(e) {
    const clickedItem = e.target;
    const selectedClass = &#39;banner-size-selected&#39;;
    const sizeAttributeName = &#39;aria-button-name&#39;;

    if (clickedItem.classList.contains(selectedClass)) return;

    bannerSizeElements.forEach((item) =&gt; {
        const isClickedItem = clickedItem === item;
        item.classList.toggle(selectedClass, isClickedItem);

        if (isClickedItem) {
            const size = item.getAttribute(sizeAttributeName);
            preview.updateSize(size); // preview 메서드 호출
            templateContainer1.classList.toggle(&#39;visible&#39;);
            templateContainer2.classList.toggle(&#39;visible&#39;);
        }
    });
}

...</code></pre>
<h3 id="3-stepper">3. Stepper</h3>
<p>Stepper는 배너 제작을 진행하는 3가지 단계 및 현재 단계를 보여주기 위한 UI로 이 역시도 클래스로 구현했다.</p>
<pre><code class="language-jsx">class Stepper {
    constructor() {
        this.currentIndex = 0;
        this.steps = document.querySelectorAll(&#39;.step&#39;);
        this.nextBtn = document.querySelector(&#39;.next-btn&#39;);
        this.prevBtn = document.querySelector(&#39;.prev-btn&#39;);
        this.submitBtn = document.querySelector(&#39;.submit-btn&#39;);
        this.contents = document.querySelectorAll(&#39;.content&#39;);

        this._listen();
    }

    _listen() {
        this.nextBtn.addEventListener(&#39;click&#39;, this.next.bind(this));
        this.prevBtn.addEventListener(&#39;click&#39;, this.previous.bind(this));
    }

    show(el) {
        el.classList.add(&#39;visible&#39;);
    }

    hide(el) {
        el.classList.remove(&#39;visible&#39;);
    }

    _setFinishButton() {
        this.hide(this.nextBtn);
        this.show(this.submitBtn);
    }

    _setNextButton() {
        this.hide(this.submitBtn);
        this.show(this.nextBtn);
    }

    _setNextItem(next) {
        this.steps[next].className = &#39;step step-item-process&#39;;
    }

    _setNextContent(next) {
        this.hide(this.contents[this.currentIndex]);
        this.show(this.contents[next]);
        this._setNextItem(next);
        this.currentIndex = next;
    }

    next() {
        if (this.currentIndex === this.steps.length - 1) {
            this._setFinishButton();
            return;
        }
        const nextIndex =
            this.currentIndex + 1 &lt; this.steps.length
                ? this.currentIndex + 1
                : this.steps.length - 1;
        this.steps[this.currentIndex].className = &#39;step step-item-finish&#39;;
        this._setNextContent(nextIndex);

        if (this.currentIndex === 1) this.show(this.prevBtn);
        else if (this.currentIndex === this.steps.length - 1) this._setFinishButton();
    }

    previous() {
        const prevIndex = this.currentIndex - 1 &gt;= 0 ? this.currentIndex - 1 : 0;
        this.steps[this.currentIndex].className = &#39;step step-item-wait&#39;;
        this._setNextContent(prevIndex);

        if (this.currentIndex === 0) this.hide(this.prevBtn);
        if (this.currentIndex !== this.steps.length - 1) this._setNextButton();
    }
}

const stepper = new Stepper();</code></pre>
<h3 id="4-html2canvas">4. html2canvas</h3>
<p>사용자가 완성한 배너, 즉 화면에 띄워진 미리보기를 png 형식으로 다운받기 위해 <code>html2canvas</code> 라이브러리를 활용했다. html2canvas 라이브러리는 자바스크립트 브라우저 화면 캡쳐 라이브러리로 캡처 하고 싶은 DOM(<code>.preview</code>)을 <code>html2canvas()</code> 함수의 파라미터로 전달해 호출하면 Promise 객체를 리턴받고, 이를 통해 특정 영역을 포함한 canvas 객체를 받을 수 있다.</p>
<p><code>html2canvas()</code>에 두번째 인수로 전달한 객체는 options로 아래와 같은 이유로 추가했다.</p>
<ul>
<li><code>allowTaint</code>, <code>useCORS</code> → background 이미지를 정상적으로 caputre 하기 위함</li>
<li><code>scale</code> → scale 값을 기본 1에서 4로 설정하여 다운받는 이미지 사이즈를 4배 키워 출력. 이를 통해 이미지 해상도 개선.</li>
</ul>
<pre><code class="language-jsx">const submitBtn = document.querySelector(&#39;.submit-btn&#39;);

submitBtn.addEventListener(&#39;click&#39;, captureExport);

function captureExport() {
    html2canvas(document.querySelector(&#39;.preview&#39;), {
        allowTaint: true,
        useCORS: true,
        scale: 4,
    }).then((canvas) =&gt; {
        saveAs(canvas.toDataURL(), &#39;bloggyBanners&#39;);
    });
}

function saveAs(uri, filename) {
    let link = document.createElement(&#39;a&#39;);
    if (typeof link.download === &#39;string&#39;) {
        link.download = filename;
        link.href = uri;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    } else {
        window.open(uri);
    }
}</code></pre>
<p>(전체 코드는 <a href="https://github.com/HyewonKkang/blog-banner-generator">깃헙</a>에서 확인 가능합니다)</p>
<hr>
<p>해당 사이트는 얼른 벨로그를 시작하고 싶은 마음을 담아 빠르게 개발했다. 현재 리팩토링 할 만한 요소들이 몇 가지 남아있어서 꾸준히 개선해보려고 한다. 앞으로 썸네일 디자인에 애쓰지 않고 간단하게 만들 수 있을 것 같아 완성된 것이 뿌듯하다-! 다음엔 어떤 토이 프로젝트를 해볼까 ..</p>
<hr>
<p><a href="https://hyewonkkang.github.io/blog-banner-generator/">blog-banner-generator 사이트 방문하기</a></p>
<p>사이트를 이용하시다가 에러나 개선했으면 하는 부분을 발견하신다면 <a href="https://github.com/HyewonKkang/blog-banner-generator/issues">이슈</a> 혹은 댓글에 남겨주시면 감사하겠습니다 💪 사이트에 추가되었으면 하는 디자인 또한 이슈에 남겨주시면 최대한 빠르게 추가해드립니다!</p>
<p>많은 피드백 부탁드립니다😻</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JavaScript에서 reduce()를 활용하여 고차 함수 구현하기]]></title>
            <link>https://velog.io/@hyewon_kkang/JavaScript%EC%97%90%EC%84%9C-reduce%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EA%B3%A0%EC%B0%A8-%ED%95%A8%EC%88%98-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hyewon_kkang/JavaScript%EC%97%90%EC%84%9C-reduce%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EA%B3%A0%EC%B0%A8-%ED%95%A8%EC%88%98-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 26 Jul 2023 09:25:37 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p><code>map</code>, <code>filter</code>, <code>some</code>, <code>every</code>, <code>find</code> 같은 모든 배열의 고차 함수는 <code>reduce</code> 메서드로 구현할 수 있다고 한다. 나는 해봐야 알 것 같다.</p>
<p>구현은 고차 함수들의 사용법을 그대로 유지하기 위해 <code>Array.prototype</code>에 구현한 함수들을 프로토타입 메서드로 추가하는 방식을 택했다. (프로토타입 확장)</p>
<hr>
<h1 id="reduce를-이용해-구현하기">reduce를 이용해 구현하기</h1>
<h2 id="map">map</h2>
<p>가장 먼저 map 함수를 구현해보려고 한다. </p>
<p>map 함수의 정의는 다음과 같은 형식을 따른다.</p>
<pre><code class="language-jsx">arr.map(callback(currentValue[, index[, array]])[, thisArg])</code></pre>
<p>map 메서드는 배열 내의 모든 요소 각각에 대해 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.</p>
<p>map 메서드가 생성하여 반환하는 새로운 배열의 length 프로퍼티 값은 map 메서드를 호출한 배열의 length 프로퍼티 값과 반드시 일치한다. 즉, map 메서드를 호출한 배열과 map 메서드가 생성하여 반환한 배열은 1:1 매핑한다. </p>
<pre><code class="language-jsx">if (!Array.prototype.mapUsingReduce) {
    Array.prototype.mapUsingReduce = function (callback, thisArg) {
        return this.reduce((mapped, cur, idx, arr) =&gt; {
            mapped[idx] = callback.call(thisArg, cur, idx, arr);
            return mapped;
        }, []);
    };
}

console.log([1, 2, 3, 4, 5].mapUsingReduce((item) =&gt; item * 2)); // [2, 4, 6, 8, 10]</code></pre>
<p>map 함수의 정의를 보면, 두 번째 인자로 thisArg가 전달되는 경우 해당 값으로 callback을 실행할 때 this 바인딩해주어야 한다. 따라서 mapUsingReduce에 전달되는 콜백함수는 thisArg에 this 바인딩을 해줄 필요가 있다. (<code>call</code>)</p>
<pre><code class="language-jsx">callback.call(thisArg, cur, idx, arr);</code></pre>
<blockquote>
<p>⚠️ <strong>주의</strong></p>
<pre><code class="language-jsx">// ❌
if (!Array.prototype.mapUsingReduce) {
    Array.prototype.mapUsingReduce = (callback, thisArg) =&gt; {
        return this.reduce((mapped, cur, idx, arr) =&gt; { // TypeError: this.reduce is not a function
            mapped[idx] = callback.call(thisArg, cur, idx, arr);
            return mapped;
        }, []);
    };
}
console.log([1, 2, 3, 4, 5].mapUsingReduce((item) =&gt; item * 2)); // [2, 4, 6, 8, 10]</code></pre>
</blockquote>
<pre><code>&gt; 처음 코드를 짤 때 `Array.prototype.mapUsingReduce`의 함수 정의를 화살표 함수를 이용했다. 그 결과 `TypeError: this.reduce is not a function`가 발생했고, this가 GlobalObject를 가리키고 있음을 확인했다. 
이는 **화살표 함수는 함수 자체의 this 바인딩을 갖지 않기 때문에** 발생한 문제다. 즉, 화살표 함수 내부에서 this를 참조하면 상위 스코프인 전역의 this, globalObject를 그대로 참조하게 된다.
따라서 위의 에러를 해결하기 위해서는 화살표 함수 대신 **일반 함수**를 사용해야 했다. 이에 주의하여 나머지 고차 함수들도 구현해보자.


## forEach

```jsx
arr.forEach(callback(currentvalue[, index[, array]])[, thisArg])</code></pre><p><code>forEach</code> 메서드는 주어진 함수를 배열 요소 각각에 대해 실행한다.</p>
<p><code>forEach</code>는 언제나 <code>undefined</code>를 반환한다.</p>
<pre><code class="language-jsx">if (!Array.prototype.forEachUsingReduce) {
    Array.prototype.forEachUsingReduce = function (callback, thisArg) {
        return this.reduce((_, cur, idx, arr) =&gt; {
            callback.call(thisArg, cur, idx, arr);
        }, null);
    };
}

const numbers = [1, 2, 3];
let pows = [];

numbers.forEachUsingReduce((item) =&gt; pows.push(item ** 2));
console.log(pows); // [1, 4, 9]</code></pre>
<p><code>map</code>과 거의 비슷하게 구현이 된다. </p>
<p>처음 구현 시에 <code>Array.prototype.forEachUsingReduce</code> 내부 <code>reduce</code>()의 두번째 인자를 비워두었었다. 그러나 위 실행 결과가 [ 4, 9 ]가 나왔고, 이는 배열의 첫 번째 요소인 1이 누적값으로 사용되면서 콜백 함수가 호출되었기 때문이다. <strong><code>reduce</code> 문의 initialValue를 제공하지 않으면 배열의 첫 번째 요소를 사용한다</strong>. </p>
<p>이를 해결하기 위해 <code>null</code>을 사용하여 <code>reduce</code>의 누적값을 무시하고 각 요소에 대해 콜백 함수를 실행하게 된다.</p>
<p>따라서 <code>reduce</code> 문 사용시 위와 같이 원치 않는 동작이 발생할 수 있기 때문에 <strong>명시적으로 초기값을 설정</strong>해주자.</p>
<h2 id="sort">sort</h2>
<pre><code class="language-jsx">arr.sort([compareFunction])</code></pre>
<p><code>sort</code> 메서드는 배열의 요소를 정렬한다. 원본 배열을 직접 변경하여 정렬된 배열을 반환한다. 기본 정렬 순서는 문자열의 유니코드 코드 포인트를 따른다.</p>
<p><code>sort</code> 메서드는 quicksort 알고리즘을 사용했었다. quicksort 알고리즘은 동일한 값의 요소가 중복되어 있을 때 초기 순서와 변경될 수 있는 불안정한 정렬 알고리즘으로 알려져 있다. ES10에서는 <code>timesort</code> 알고리즘을 사용하도록 바뀌었다.</p>
<p>지금은 reduce를 학습하는 과정이기 때문에 정렬 알고리즘의 효율성은 고려하지 않았다.</p>
<pre><code class="language-jsx">if (!Array.prototype.sortUsingReduce) {
    Array.prototype.sortUsingReduce = function (compareFunction) {
        this.reduce((acc, cur) =&gt; {
            if (!acc.length) return [cur];
            let index = 0;
            while (index &lt; acc.length &amp;&amp; compareFunction(cur, acc[index])) index++;
            acc.splice(index, 0, cur);
            return acc;
        }, []);
    };
}

const fruits = [&#39;Banana&#39;, &#39;Orange&#39;, &#39;Apple&#39;];
fruits.sort();
console.log(fruits); // [&#39;Apple&#39;, &#39;Banana&#39;, &#39;Orange&#39;]

const points = [40, 100, 1, 5, 2, 25, 10];
points.sort((a, b) =&gt; a - b);
console.log(points); // [1, 2, 5, 10, 25, 40, 100]</code></pre>
<p>sort에 전달되는 비교 함수의 반환 값에 따라 정렬되는 순서는 아래와 같다.</p>
<ul>
<li>비교 함수 반환 값 &lt; 0 : 첫 번째 인수를 우선 정렬</li>
<li>비교 함수 반환 값 = 0 : 정렬하지 않음</li>
<li>비교 함수 반환 값 &gt; 0 : 두 번째 인수를 우선 정렬</li>
</ul>
<p>sort() 메서드는 다른 고차 함수와 달리 콜백 함수 내부에서 this로 사용할 객체를 전달하지 않는다.</p>
<h2 id="filter">filter</h2>
<pre><code class="language-jsx">arr.filter(callback(element[, index[, array]])[, thisArg])</code></pre>
<p><code>filter</code> 메서드는 자신을 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출하고, 콜백 함수의 반환 값이 true인 요소로만 구성된 새로운 배열을 반환한다. 이때 원본 배열을 변경되지 않는다.</p>
<pre><code class="language-jsx">if (!Array.prototype.filterUsingReduce) {
    Array.prototype.filterUsingReduce = function (callback, thisArg) {
        return this.reduce((acc, cur, idx, arr) =&gt; {
            if (callback.call(thisArg, cur, idx, arr)) acc = [...acc, cur];
            return acc;
        }, []);
    };
}

const words = [&#39;spray&#39;, &#39;limit&#39;, &#39;elite&#39;, &#39;exuberant&#39;, &#39;destruction&#39;, &#39;present&#39;];
const result = words.filterUsingReduce((word) =&gt; word.length &gt; 6);
console.log(result); // [&quot;exuberant&quot;, &quot;destruction&quot;, &quot;present&quot;]</code></pre>
<h2 id="some">some</h2>
<pre><code class="language-jsx">arr.some(callbackFn(element[, index[, array]])[, thisArg])</code></pre>
<p><code>some</code> 메서드는 자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출한다. 이때 some 메서드는 콜백 함수의 반환값이 단 한번이라도 참이면 true, 모두 거짓이면 false를 반환한다. 즉, 배열의 요소 중에 콜백 함수를 통해 정의한 조건을 만족하는 요소가 1개 이상 존재하는지 확인하여 그 결과를 불리언 타입으로 반환한다.</p>
<pre><code class="language-jsx">if (!Array.prototype.someUsingReduce) {
    Array.prototype.someUsingReduce = function (callbackFn, thisArg) {
        return this.reduce(
            (hasMatch, cur, idx, arr) =&gt; hasMatch || callbackFn.call(thisArg, cur, idx, arr),
            false,
        );
    };
}

const array = [1, 2, 3, 4, 5];
const even = (element) =&gt; element % 2 === 0;
console.log(array.someUsingReduce(even)); // true

const zero = (element) =&gt; !element;
console.log(array.someUsingReduce(zero)); // false</code></pre>
<p>기존 reduce의 accumulator에 boolean 값이 저장된다. 다만 <code>reduce</code>를 이용해 구현하다보니 <code>hasMatch</code>가 <code>true</code>여도 early-return 할 수 없고 모든 배열 내 요소들을 순회해야 some의 반환값을 얻을 수 있어 효율이 떨어진다.</p>
<h2 id="every">every</h2>
<pre><code class="language-jsx">arr.every(callbackFn(element[, index[, array]])[, thisArg])</code></pre>
<p><code>every</code> 메서드는 자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출한다. 이때 every 메서드는 콜백 함수의 반환값이 모두 참이면 ture, 단 한번이라도 거짓이면 false를 반환한다. 즉, 배열의 모든 요소가 콜백 함수를 통해 정의한 조건을 모두 만족하는지 확인하여 그 결과를 불리언 타입으로 반환한다.</p>
<pre><code class="language-jsx">if (!Array.prototype.everyUsingReduce) {
    Array.prototype.everyUsingReduce = function (callbackFn, thisArg) {
        return this.reduce(
            (allMatch, cur, idx, arr) =&gt; allMatch &amp;&amp; callbackFn.call(thisArg, cur, idx, arr),
            true,
        );
    };
}</code></pre>
<h2 id="find">find</h2>
<pre><code class="language-jsx">arr.find(callback(element[, index[, array]])[, thisArg])</code></pre>
<p>ES6에서 도입된 <code>find</code> 메서드는 자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출하여 반환값이 true인 첫 번째 요소를 반환한다. 만약 콜백 함수의 반환값이 true인 요소가 존재하지 않는다면 undefined를 반환한다.</p>
<pre><code class="language-jsx">if (!Array.prototype.findUsingReduce) {
    Array.prototype.findUsingReduce = function (callback, thisArg) {
        return this.reduce((match, cur, idx, arr) =&gt; {
            if (!match &amp;&amp; callback.call(thisArg, cur, idx, arr)) return cur;
            return match;
        }, undefined);
    };
}

const array1 = [5, 12, 8, 130, 44];
const found = array1.findUsingReduce((element) =&gt; element &gt; 10);
console.log(found); // 12</code></pre>
<h2 id="findindex">findIndex</h2>
<pre><code class="language-jsx">arr.findIndex(callbackFn(element[, index[, array]])[, thisArg])</code></pre>
<p>ES6에 도입된 <code>findIndex</code> 메서드는 자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출하여 반환값이 true인 첫 번째 요소의 인덱스를 반환한다.</p>
<pre><code class="language-jsx">if (!Array.prototype.findIndexUsingReduce) {
    Array.prototype.findIndexUsingReduce = function (callbackFn, thisArg) {
        return this.reduce((match, cur, idx, arr) =&gt; {
            if (!match &amp;&amp; callbackFn.call(thisArg, cur, idx, arr)) return idx;
            return match;
        }, undefined);
    };
}

const array1 = [5, 12, 8, 130, 44];
const found = array1.findIndexUsingReduce((element) =&gt; element &gt; 10);
console.log(found); // 1</code></pre>
<h2 id="flatmap">flatMap</h2>
<pre><code class="language-jsx">arr.flatMap(callback(currentValue[, index[, array]])[, thisArg])</code></pre>
<p>ES10에서 도입된 <code>flatMap</code> 메서드는 map 메서드를 통해 생성된 새로운 배열을 평탄화한다. 즉 <code>map</code> 메서드와 <code>flat</code> 메서드를 순차적으로 실행하는 효과가 있다.</p>
<pre><code class="language-jsx">if (!Array.prototype.flatMapUsingReduce) {
    Array.prototype.flatMapUsingReduce = function (callback, thisArg) {
        return this.reduce((acc, cur, idx, arr) =&gt; {
            const mapped = callback.call(thisArg, cur, idx, arr);
            return [...acc, ...mapped];
        }, []);
    };
}

let arr = [1, 2, 3, 4];
console.log(arr.flatMapUsingReduce((x) =&gt; [x * 2])); // [2, 4, 6, 8]
console.log(arr.flatMapUsingReduce((x) =&gt; [[x * 2]])); // [[2], [4], [6], [8]]</code></pre>
<hr>
<p>여기까지 모던 자바스크립트 딥다이브 책에 나오는 배열의 주요 고차 함수들을 reduce로 구현해보았다. 그동안 자바스크립트 공부를 게을리 해왔어서 reduce 함수 사용법도 잘 몰랐었는데 이번 기회에 reduce는 물론 각 함수들의 역할까지 완벽하게 이해하고 넘어가는 것 같다. 특히 reduce로 다양한 구현을 해보면서 매우 강력한 기능을 가지고 있음을 느꼈고, 구현에 오래 걸릴 것이라 예상했는데 생각보다 어렵지 않게 구현해 볼 수 있어서 좋았다. ✨</p>
<p>같이 고민할 내용이나 오류가 있다면 댓글 남겨주시면 감사하겠습니다.</p>
<hr>
<h1 id="reference">Reference</h1>
<p>모던 자바스크립트 DeepDive
<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/filter">MDN JavaScript Array</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Canvas API로 채우기 기능 구현하기]]></title>
            <link>https://velog.io/@hyewon_kkang/Canvas-API%EB%A1%9C-%EC%B1%84%EC%9A%B0%EA%B8%B0-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@hyewon_kkang/Canvas-API%EB%A1%9C-%EC%B1%84%EC%9A%B0%EA%B8%B0-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 26 Jul 2023 00:04:35 GMT</pubDate>
            <description><![CDATA[<p>(Dec 26, 2022 작성글. 블로그 이전)</p>
<p>현재 텔레스트레이션 게임 구현을 위해 리액트에서 canvas를 이용해 그림판을 구현 중에 있다. 그림판의 기능으로는 선 그리기, 지우기, 채우기, 초기화, 색상 변경을 목표로 하고 있는데 그 중에서 <strong>채우기 기능을 어떻게 구현해야 좋을지</strong> 고민이 있었다.</p>
<p>채우기 기능은 moveTo(), lineTo() 등을 이용해 그린 캔버스에서 border가 끊기지 않고 연결된 부분이 있다면 해당 흰색 공간을 선택한 색상으로 채워지는 기능을 의미한다.</p>
<p><code>getImageData()</code>를 통해 캔버스 내 픽셀 정보를 불러올 수 있다는 사실은 알았지만, 단순히 전체를 탐색해야 하는 것인가? 이건 아닐 것 같은데.. 라는 생각이 들었다. 이에 대한 구현 방법을 찾아보다가 <code>flood fill</code> 이라는 알고리즘을 보게 됐고, 해당 내용을 좀 더 공부해보고 캔버스에 적용해보려고 한다. 아이디어는 <a href="https://stackoverflow.com/questions/19073140/fill-a-portion-of-drawn-image-in-canvas">stackoverflow</a> 를 참고했다.</p>
<h1 id="사전지식">사전지식</h1>
<h2 id="getimagedata">getImageData()</h2>
<p>우선 getImageData()가 어떤 값을 반환하는지 확인이 필요했고,</p>
<pre><code class="language-tsx">console.log(getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);</code></pre>
<p><img src="https://user-images.githubusercontent.com/68578916/209682260-01f57dc0-9be1-4b8d-a2c0-e80f37587daf.png" alt="스크린샷 2022-11-21 오전 2 37 44"></p>
<img src='https://user-images.githubusercontent.com/68578916/209682255-3817a02d-64ec-425f-b35c-fec29cdd6188.png' width="300"/>

<p>뭐가 좀 많음을 느껴 찾아봤다.
먼저 해당 함수를 이해하기 위해서 캔버스 내 이미지 개념을 확인할 필요가 있었다.</p>
<blockquote>
<ul>
<li><code>getImageData(x, y, w, h)</code> : ImageData 객체를 리턴하며, data는 <code>래스터 정보</code>이다.</li>
</ul>
</blockquote>
<ul>
<li>래스터(Raster) : 이미지 내 픽셀 정보로, 래스트 정보는 <strong>한 점당 R, G, B, A 요소 각각에 대해 4바이트씩 값을 가지며</strong> 이런 픽셀 정보가 좌→우로, 위→아래로 나열된다. (<a href="http://www.soen.kr/html5/html3/3-2-2.htm">참조 링크</a>)</li>
</ul>
<p><img src="https://user-images.githubusercontent.com/68578916/209682252-2cff12bb-a89f-443e-b3c1-a18faec2e4f0.png" alt="Untitleㅇㄴd"></p>
<p>즉, 우리가 보는 이미지는 2차원 배열이지만 <strong>ImageData의 정보는 1차원 배열로 얻게 되고</strong>, 일차원 배열의 길이는 <code>w * h * 4</code>임을 알 수 있었다.</p>
<p>이 정보를 알고나니 위에서 CANVAS_WIDTH가 742이고, CANVAS_HEIGHT가 468였기 때문에 배열의 길이가 347,256이 되지 않을까 예상했지만 이보다 4배를 더한 1,389,024의 값이 어떻게 나오게 됐는지 알게 되었다.</p>
<h2 id="flood-fill이란">flood fill이란?</h2>
<p>픽셀 데이터를 어떻게 얻는지를 확인했으니 이제 어떤 식으로 구현해야 좋을지를 봐야한다.
플러드 필(flood fill)에 대한 정의는 다음과 같다. (<a href="https://ko.wikipedia.org/wiki/%ED%94%8C%EB%9F%AC%EB%93%9C_%ED%95%84">참조 링크-위키백과</a>)</p>
<h3 id="정의">정의</h3>
<p>플러드 필(flood fill) 혹은 시드 필(seed fill)은 다차원 배열의 어떤 칸과 연결된 영역을 찾는 알고리즘이다.</p>
<p>[사용 영역]</p>
<ul>
<li>그림 프로그램에서 연결된 비슷한 색을 가지는 영역에 “채우기” 도구에 사용</li>
<li>바둑 등의 게임에서 어떤 비어 있는 칸을 표시할지 결정할 때 사용</li>
</ul>
<h3 id="알고리즘">알고리즘</h3>
<p> 플러드 필 알고리즘은 시작 칸, 목표 색, 대체 색의 세 개의 인자를 받는다. 이 알고리즘은 배열에 있는 시작 칸에서 목표 색으로 연결된 모든 칸을 방문해서 대체 색으로 바꾼다. 플러드 필 알고리즘을 구현하는 방법은 여러가지가 있지만, 대부분 큐나 스택과 같은 자료구조를 사용한다. 모서리가 맞닿은 칸이 연결되어 있는 지에 따라서 두 가지 변형이 있다: 각각 4방향과 8방향이다.</p>
<p>이해하고 보니 dfs를 이용해 재귀함수로 구현하거나 bfs로 큐를 이용해 구현하는 방식인 것 같다. 이렇게 생각하니 그냥 백준 단지 채우기였나? 그 문제 풀던 것처럼 생각하면 좋을 것 같다. 오랜만에 그래프 알고리즘을 푼다니.. <del>설렌다</del>. 단, 조금 더 생각해 볼만한 조건들이 필요하다.</p>
<h1 id="고민거리">고민거리</h1>
<h2 id="1-imagedata를-2d-array로-바꿔서-푸는것"><del>1. ImageData를 2D Array로 바꿔서 푸는것?</del></h2>
<p>ImageData의 data가 일차원 배열 형태인데 이를 이차원 배열로 바꿔서 풀면 dfs 알고리즘을 풀듯이 직관적으로 문제 풀이가 가능할 것 같다.</p>
<p>⇒ 처음에 이런 방식을 이용해 구현했는데, 이차원 배열로 바꿨다가 다시 일차원 배열로 만드는 과정에서 오류가 있었고, 결국 1차원 배열을 그대로 사용하는 대신 <code>offset = (y * imageData.width + x) * 4</code>로 계산하여 필요한 픽셀 위치의 인덱스를 찾았다.</p>
<h2 id="2-각-픽셀이-4칸rgba를-갖는-방식이-효율적인가"><del>2. 각 픽셀이 4칸(rgba)를 갖는 방식이 효율적인가?</del></h2>
<p>한 픽셀이 하나의 값을 가지면 좋겠지만, 쭉 나열된 일차원 배열은 <strong>하나의 픽셀이 4칸의 정보를 가지고 있는 형태</strong>다. 그렇다면 1) 4칸씩 다시 하나의 배열로 묶어 이용하거나 2) 4칸의 정보를 rgba 값이 아닌 hex로 바꿔놓고 한 픽셀이 한 칸을 갖도록 하는 방식이 있을 것 같다.</p>
<p>⇒ 마지막에 바꾼 imageData를 캔버스에 다시 얹는 과정(putImageData())이 필요해서 hex로 바꾸지 않는 편이 더 효율적이다.</p>
<h1 id="구현">구현</h1>
<p><a href="https://stackoverflow.com/questions/2106995/how-can-i-perform-flood-fill-with-html-canvas">stackoverflow</a>를 참조했다.</p>
<h2 id="1-캔버스에-클릭한-영역의-좌표와-선택된-색상을-구한다">1. 캔버스에 클릭한 영역의 좌표와 선택된 색상을 구한다.</h2>
<pre><code class="language-tsx">const paintCanvas = useCallback(
    (event: MouseEvent) =&gt; {
        if (drawState.current === CanvasState.PAINT) {
            const curPos = getCoordinates(event);
            if (!curPos) return;
            floodFill(curPos.x, curPos.y, curColor.current);
        }
    },
    [drawState],
);</code></pre>
<p>추가로 코드에서 선택된 색상이 hex로 다뤄지고 있었고, 색상을 imageData에 적용하기 위해서는 [r,g,b,a] 꼴로 변환하는 과정이 필요해 hex-to-rgba 라이브러리를 이용했다.</p>
<h2 id="2-캔버스의-imagedata-객체의-data-값을-floodfill-시켜준다">2. 캔버스의 imageData 객체의 data 값을 floodfill 시켜준다.</h2>
<p>먼저 <code>getImageData()</code>를 이용해 캔버스의 전체 이미지 데이터를 가져오고, 해당 객체의 data 속성을 우리는 floodfill 시켜줘야 한다. 이 과정을 처음에 간단하게 재귀호출을 이용해보았다가 역시 Maximum call stack… 에러를 마주했고, 다시 스택 기반의 iterative 방식으로 구현했다(Iterative stack flood fill).</p>
<pre><code class="language-tsx">const floodFill = (x: number, y: number, fillColor: Uint8ClampedArray) =&gt; {
    const imageData = ctxRef.current.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    const visited = new Uint8Array(imageData.width, imageData.height);
    const targetColor = getPixelColor(imageData, x, y);

    if (!isSameColor(targetColor, fillColor)) {
        // (1)
        const stack = [{ x, y }];
        while (stack.length &gt; 0) {
            const child = stack.pop();
            if (!child) return;
            const currentColor = getPixelColor(imageData, child.x, child.y); // (2)
            if (
                !visited[child.y * imageData.width + child.x] &amp;&amp;
                isSameColor(currentColor, targetColor) //  (3)
            ) {
                setPixel(imageData, child.x, child.y, fillColor); // (4)
                visited[child.y * imageData.width + child.x] = 1;
                stack.push({ x: child.x + 1, y: child.y });
                stack.push({ x: child.x - 1, y: child.y });
                stack.push({ x: child.x, y: child.y + 1 });
                stack.push({ x: child.x, y: child.y - 1 });
            }
        }
        ctxRef.current.putImageData(imageData, 0, 0); // (5)
    }
};</code></pre>
<p>아래는 위 코드 내 주석에 대한 설명이다.</p>
<p><strong><em>(1) targetColor와 fillColor가 달라야 하는 이유</em></strong></p>
<pre><code class="language-tsx">if (!isSameColor(targetColor, fillColor)) { // (1)</code></pre>
<ul>
<li><code>targetColor</code> : 클릭한 위치의 색상 (색칠될 시작점의 현재 색상)</li>
<li><code>fillColor</code> : 팔레트에서 선택된 색상 (색칠하고자 하는 색상)</li>
</ul>
<p>이 부분은 없어도 되지만, 같은 색상으로 같은 위치를 여러번 선택하여 불필요하게 실행되는 부분에 대한 방지다. 이미 선택된 색상으로 칠해진 부분을 클릭한다면, floodfill이 실행되지 않아도 된다.</p>
<p><strong><em>(2) currentColor</em></strong></p>
<pre><code class="language-tsx">const currentColor = getPixelColor(imageData, child.x, child.y); // (2)</code></pre>
<ul>
<li><code>currentColor</code> : 스택에서 pop() 시킨, 탐색하려는 픽셀의 색상</li>
</ul>
<p><strong><em>(3) targetColor와 currentColor가 같은지 비교하는 이유</em></strong></p>
<pre><code class="language-tsx">if (isSameColor(currentColor, targetColor)) { // (3)</code></pre>
<p>현재 클릭한 위치의 색상과 같은 색을 가진 부분만을 fillColor로 채운다. 즉, 테두리에 둘러싸인 곳을 클릭한다면 테두리는 색상이 채워지지 않는다.</p>
<p><strong><em>(4) 색상 칠하기</em></strong></p>
<pre><code class="language-tsx">export const getPixelOffset = (imageData: any, x: number, y: number) =&gt; {
    return (y * imageData.width + x) * 4;
};</code></pre>
<pre><code class="language-tsx">const setPixel = (imageData: any, x: number, y: number, color: Uint8ClampedArray) =&gt; {
    const offset = getPixelOffset(imageData, x, y);
    imageData.data[offset + 0] = color[0];
    imageData.data[offset + 1] = color[1];
    imageData.data[offset + 2] = color[2];
    imageData.data[offset + 3] = color[3];
};

setPixel(imageData, child.x, child.y, fillColor); // (4)</code></pre>
<p>이제 클릭한 위치 주변으로 색을 칠할 조건을 만족하는 픽셀에 색상을 입혀야 한다. imageData의 data 배열에 참조할 위치의 인덱스를 구해(<code>getPixelOffset()</code>) r, g, b, a 값을 선택된 fillColor로 입혀준다.</p>
<p><strong><em>(5) 캔버스에 바뀐 이미지데이터를 띄우기</em></strong></p>
<pre><code class="language-tsx">ctxRef.current.putImageData(imageData, 0, 0); // (5)</code></pre>
<p>getImageData()로 받아온 imageData의 data를 지금까지 수정했다. 이를 캔버스에 적용하기 위해서는 <code>putImageData()</code>를 이용할 수 있다.</p>
<h1 id="issues">Issues</h1>
<h2 id="특정-색이-안채워지는-문제❓">특정 색이 안채워지는 문제❓</h2>
<p>팔레트의 파란색, 노란색, 초록색 등이 안채워지는 문제가 있다. 그외의 팔레트 색상이나 hex 색상 선택기에서 고른 색들은 모두 적용이 잘 된다. 이거는 정말 원인을 모르겠었는데 관련 있을 문제를 찾았다.</p>
<h4 id="같은-색상으로-선을-그리고-색을-칠했는데-칠할-때의-색상이-연하게-출력되는-문제가-있었다">같은 색상으로 선을 그리고 색을 칠했는데 칠할 때의 색상이 연하게 출력되는 문제가 있었다.</h4>
<p><img src="https://user-images.githubusercontent.com/68578916/209683205-9f27a40c-3d7c-4e3d-80b7-52441d903abb.png" alt="스크린샷 2022-11-26 오후 2 03 34"></p>
<p>분명 펜으로 그릴 때와 채우기의 색상을 같이 설정했는데 채우기 색상이 연하게 출력됨을 인지했고, 이것의 원인을 해결하면 모든 색상이 잘 채워질 것이라 생각됐다. 그러고보니 기존에 잘 채워진다고 생각했던 색상들도 연하게 칠해지고 있었다🤯</p>
<p>그래서 rgba 값 중 투명도를 결정하는 알파 값이 문제인 것 같아 색칠하는 함수를 다시 보니 마지막 줄에 color[0]으로 써놨었다 ㅎㅎ 🤯🤯</p>
<p><img src="https://user-images.githubusercontent.com/68578916/209683204-a9e54486-3f3a-4f90-9eac-f59495153ce0.png" alt="스크린샷 2022-11-26 오후 2 08 26"></p>
<p><code>imageData.data[offset + 3] = color[3];</code> 로 수정해서 바로 고쳐질 줄 알았는데..</p>
<p>이상하게 색상이 안채워지는 문제가 있었다. imageData.data에 값이 잘못 들어가는 문제는 아니었다. 이 오류를 해결하는데도 시간이 오래 걸렸는데 이것의 원인은 <strong>canvas의 ImageData의 data 타입이 Uint8ClampedArray</strong> 였기 때문이었다.</p>
<p>📎 <code>ImageData.data</code>는 <code>Uint8ClampedArray</code>형식이며 1차원 배열로 RGBA순서로 정의된 이미지 데이터를 나타낸다. <strong>각 원소는 정수값으로 0에서 255사이의 값을 갖는다.</strong></p>
<p>본래의 rgba 값 중 알파 값은 0<del>1의 범위를 가지며 0에 가까울 수록 투명하고 1에 가까울수록 원색을 띈다. 그런데 Uint8ClampedArray 타입에서는 이 알파값의 범위가 0</del>255로 확장되어 사용이 되고, 나는 hex-to-rgba 라이브러리를 써서 색상의 rgba값을 구했기 때문에 마지막 알파값이 0~1 수준으로 나타나는 것이었다. 그래서 결국 화면 상에는 보이지 않았다..😞  이런 문제가 있었다.</p>
<p>그냥 255로 고정할까 했지만 color picker 안에 투명도를 설정할 수 있는 부분이 있어 0<del>1을 0</del>255 안의 숫자로 매핑해 주는 부분을 추가했다.</p>
<img src="https://user-images.githubusercontent.com/68578916/209683197-2060aaad-0f00-4ccb-a1d7-d02bcc9658c4.png" width="200" />

<pre><code class="language-tsx">export const convertHexToRgba = (color: string) =&gt; {
    ...
    rgba[3] = rgba[3] * 255;
    return new Uint8ClampedArray(rgba);
};</code></pre>
<p><img src="https://user-images.githubusercontent.com/68578916/209683200-55fd27a2-7b7c-427b-9bea-0af78c933e70.png" alt="스크린샷 2022-11-27 오후 3 46 38">
이제 다 잘 채워진다 흑</p>
<h1 id="전체-코드">전체 코드</h1>
<pre><code class="language-tsx">import hexToRgba from &#39;hex-to-rgba&#39;;

const convertHexToRgba = (color: string) =&gt; {
    const rgbaStr = hexToRgba(color);
    const rgba = rgbaStr
        .substring(5, rgbaStr.length - 1)
        .split(&#39;,&#39;)
        .map((str: string) =&gt; Number(str));
    rgba[3] = rgba[3] * 255;
    return new Uint8ClampedArray(rgba);
};

const isValidSquare = (imageData: any, x: number, y: number) =&gt; {
    return x &gt;= 0 &amp;&amp; x &lt; imageData.width &amp;&amp; y &gt;= 0 &amp;&amp; y &lt; imageData.height;
};

const getPixelOffset = (imageData: any, x: number, y: number) =&gt; {
    return (y * imageData.width + x) * 4;
};

const getPixelColor = (imageData: any, x: number, y: number) =&gt; {
    if (isValidSquare(imageData, x, y)) {
        const offset = getPixelOffset(imageData, x, y);
        return imageData.data.slice(offset, offset + 4);
    } else {
        return [-1, -1, -1, -1]; // invalid color
    }
};

const isSameColor = (a: Uint8ClampedArray, b: Uint8ClampedArray) =&gt; {
    return a[0] === b[0] &amp;&amp; a[1] === b[1] &amp;&amp; a[2] === b[2] &amp;&amp; a[3] === b[3];
};

const setPixel = (imageData: any, x: number, y: number, color: Uint8ClampedArray) =&gt; {
    const offset = (y * imageData.width + x) * 4;
    imageData.data[offset + 0] = color[0];
    imageData.data[offset + 1] = color[1];
    imageData.data[offset + 2] = color[2];
    imageData.data[offset + 3] = color[3];
};

const floodFill = (x: number, y: number, fillColor: Uint8ClampedArray) =&gt; {
    const imageData = ctxRef.current.getImageData(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
    const visited = new Uint8Array(imageData.width, imageData.height);
    const targetColor = getPixelColor(imageData, x, y);

    if (!isSameColor(targetColor, fillColor)) {
        const stack = [{ x, y }];
        while (stack.length &gt; 0) {
            const child = stack.pop();
            if (!child) return;
            const currentColor = getPixelColor(imageData, child.x, child.y);
            if (
                !visited[child.y * imageData.width + child.x] &amp;&amp;
                isSameColor(currentColor, targetColor)
            ) {
                setPixel(imageData, child.x, child.y, fillColor);
                visited[child.y * imageData.width + child.x] = 1;
                stack.push({ x: child.x + 1, y: child.y });
                stack.push({ x: child.x - 1, y: child.y });
                stack.push({ x: child.x, y: child.y + 1 });
                stack.push({ x: child.x, y: child.y - 1 });
            }
        }
        ctxRef.current.putImageData(imageData, 0, 0);
    }
};

const paintCanvas = useCallback(
    (event: MouseEvent) =&gt; {
        if (drawState.current === CanvasState.PAINT) {
            const curPos = getCoordinates(event);
            if (!curPos) return;
            floodFill(curPos.x, curPos.y, curColor.current);
        }
    },
    [drawState],
);</code></pre>
<h1 id="optimization"><strong>Optimization</strong></h1>
<p><a href="https://stackoverflow.com/questions/2106995/how-can-i-perform-flood-fill-with-html-canvas">stackoverflow</a> 참조</p>
<h1 id="reference">Reference</h1>
<p><a href="https://learnersbucket.com/examples/algorithms/flood-fill-algorithm-in-javascript/">Flood fill algorithm in javascript - LearnersBucket</a>
<a href="https://codeheir.com/2022/08/21/comparing-flood-fill-algorithms-in-javascript/">Comparing Flood Fill Algorithms in JavaScript - Codeheir</a>
<a href="https://stackoverflow.com/questions/2106995/how-can-i-perform-flood-fill-with-html-canvas">How can I perform flood fill with HTML Canvas?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vite + React + TypeScript 환경에 Storybook v7.0 도입하기 (2)]]></title>
            <link>https://velog.io/@hyewon_kkang/Vite-React-TypeScript-%ED%99%98%EA%B2%BD%EC%97%90-Storybook-v7.0-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0-2</link>
            <guid>https://velog.io/@hyewon_kkang/Vite-React-TypeScript-%ED%99%98%EA%B2%BD%EC%97%90-Storybook-v7.0-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0-2</guid>
            <pubDate>Tue, 25 Jul 2023 19:54:48 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>스토리북의 기본 사용방법에 대해 <a href="https://www.notion.so/Vite-React-TypeScript-Storybook-v7-0-1-a6a9d68398fd46f8955e22daebdd2214?pvs=21">Vite + React + TypeScript 환경에 Storybook v7.0 도입하기 (1)</a> 에서 학습했다. 이번에는 Storybook을 더 잘 사용하기 위한 몇 가지 방법들을 활용해보려고 한다.</p>
<hr>
<h1 id="1-code-panel-추가하기">1. Code panel 추가하기</h1>
<p><img src="https://github.com/HyewonKkang/HyewonKkang/assets/68578916/a8866175-b089-49a6-92e7-524fc589dc4e" alt="스크린샷 2023-04-17 오후 6 03 30"></p>
<p>현재 기본적으로 <code>Controls</code>, <code>Actions</code>, <code>Interaction</code>, <code>Knobs</code> 패널이 존재한다. 여기에 컴포넌트의 코드를 바로 확인할 수 있도록 <code>Code</code> panel을 추가하려고 한다.</p>
<p>간단하게 <code>@storybook/addon-storysource</code>를 설치하고, addons에 규칙을 추가해주면 된다.</p>
<pre><code class="language-tsx">yarn add @storybook/addon-storysource --dev</code></pre>
<pre><code class="language-tsx">// .storybook/main.js

module.exports = {
  addons: [
    &#39;@storybook/addon-links&#39;,
    &#39;@storybook/addon-essentials&#39;,
    &#39;@storybook/addon-mdx-gfm&#39;,
    &#39;@storybook/addon-knobs&#39;,
    **&#39;@storybook/addon-storysource&#39;,** // 추가
  ],
    ...
};</code></pre>
<p>그러면 하단에 Code 패널이 생성된다.</p>
<p><img src="https://github.com/HyewonKkang/HyewonKkang/assets/68578916/4a82d3a2-9be1-4d4c-9ba9-7bf3b5144362" alt="스크린샷 2023-04-17 오후 6 16 51"></p>
<hr>
<h1 id="2-디자인시스템-관련-페이지-작성하기">2. 디자인시스템 관련 페이지 작성하기</h1>
<p>Storybook 자체가 프론트엔드 개발의 문서화를 지원한다. 그래서 컴포넌트 테스트용 목적 외에도 프로젝트를 소개하는 페이지를 추가하고자 한다.</p>
<p>우선 완성된 결과물은 다음과 같다. 좌측에 GETTING STARTED와 COMPONENTS 그룹을 생성했고, 프로젝트에 대한 소개인 <code>Intro</code> 페이지, 프로젝트에서 사용되는 icons를 정의한 <code>Icons</code> 페이지, 컬러토큰을 정의한 <code>Palette</code> 페이지, 폰트 관련 서식을 정의한 <code>Typography</code> 페이지를 추가로 구성했다.</p>
<p><img src="https://github.com/HyewonKkang/HyewonKkang/assets/68578916/0b6bad1b-f8f0-49e2-8099-8ddc83995e35" alt="스크린샷 2023-04-17 오후 6 19 28"></p>
<h3 id="2-1-naming-components-and-hierarchy--grouping--sorting">2-1) Naming components and hierarchy + Grouping + Sorting</h3>
<img src="https://github.com/HyewonKkang/HyewonKkang/assets/68578916/68595fc1-b792-43ee-ac17-27035d631bb6" width="30%" />

<p>다음과 같이 파일을 그룹핑하는 방법부터 알아보자. <code>stories.(tsx|mdx)</code> 파일을 생성한 후, title에서 이를 설정할 수 있다.</p>
<p>Button 컴포넌트를 예로 들면, 아래와 같이 title에 <code>/</code> separator를 이용하면 그룹끼리 묶일 수 있다.</p>
<pre><code class="language-tsx">// src/components/Button/Button.stories.tsx

import Button from &#39;./Button&#39;;

export default {
    title: &#39;Components/Button&#39;,
    component: Button,
};</code></pre>
<p>추가적으로, 정렬하는 방법에 대해 알아보자.</p>
<p>좌측 메뉴의 순서는 기본적으로 추가된 페이지 순으로 정렬되는데, 이러한 규칙을 알파벳 오름차순으로 정렬시키려면, 다음과 같이 parameters options의 <code>storySort</code>를 이용한다.</p>
<p>method에 ‘alphabetical’을 추가하면 알파벳 순서로 스토리가 정렬된다. 이밖에도, 특정 페이지를 맨 위나 맨 아래로 배치하고 싶은 경우가 있다면, <code>order</code> 규칙을 적용할 수 있다. 이는 사용자 임의로 순서를 결정짓는데, 아래와 같은 규칙을 통해 GETTING STARTED 그룹의 순서가 알파벳 규칙을 따르지 않는 것을 확인할 수 있다.</p>
<pre><code class="language-tsx">export default {
    parameters: {
        options: {
            storySort: {
                method: &#39;alphabetical&#39;,
                order: [
                    &#39;Getting Started&#39;,
                    [&#39;Intro&#39;, &#39;Icons&#39;, &#39;Palette&#39;, &#39;Typography&#39;],
                    &#39;Components&#39;,
                ],
            },
        },
    },
};</code></pre>
<h3 id="2-2-introduction-페이지-작성하기">2-2) Introduction 페이지 작성하기</h3>
<p>이제 본격적으로 각 페이지를 작성해보자.</p>
<p><code>Getting Started</code> 하위 페이지 생성을 위해 <code>src/stories</code> 경로에 각 파일들을 작성했고, 컴포넌트를 정의하는 용도가 아닌 markdown 문서를 작성하기 위한 페이지이기 때문에 확장자는 mdx로 작성했다.</p>
<p>다음 그림은 누가봐도 마크다운 문법으로 작성된 페이지다. Storybook에서 제공하는 API 가운데 <code>Meta</code> 를 이용해 title을 정의한다. 그리고 해당 페이지에 대한 내용은 다들 익숙한 md 문법으로 작성하면 된다!</p>
<p><img src="https://github.com/HyewonKkang/HyewonKkang/assets/68578916/b5841efe-1f87-41b8-982a-0091f07a0049" alt="asdfa"></p>
<pre><code class="language-markdown">// src/stories/intro.stories.mdx

import React from &#39;react&#39;;
import { Meta } from &#39;@storybook/blocks&#39;;
import IconButton from &#39;../components/IconButton&#39;;
import { ThemeProvider } from &#39;styled-components&#39;;
import { theme } from &#39;../styles/theme&#39;;

&lt;Meta title=&#39;Getting Started/Intro&#39; /&gt;

&lt;section style={{ display: &#39;flex&#39;, flexDirection: &#39;column&#39;, justifyContent: &#39;center&#39;, alignItems: &#39;center&#39;, padding: &#39;10px&#39; }}&gt;

# Classmate Design System

### Design foundations and components library

&lt;div style={{ padding: &#39;20px&#39; }}&gt;
  &lt;ThemeProvider theme={theme}&gt;
    &lt;IconButton
      name=&#39;github&#39;
      onClick={() =&gt; window.open(&#39;https://github.com/Dev-TeamOne/classmate-frontend&#39;, &#39;_blank&#39;)}
    /&gt;
  &lt;/ThemeProvider&gt;
&lt;/div&gt;

&lt;/section&gt;

## Getting Started

Classmate는 실시간으로 강연자와 청중을 연결해주는 크라우드소싱 플랫폼입니다. 스토리북을 통해 UI에 사용되는 컴포넌트들을 테스트 해볼 수 있습니다.

### Install

git clone https://github.com/Dev-TeamOne/classmate-frontend.git
yarn &amp;&amp; yarn storybook

## References

[Github](https://github.com/Dev-TeamOne)

[UI-Design](https://www.figma.com/file/MMPKJgUWaE5lbRxQzHDycJ/Classmate?node-id=73-1760&amp;t=3xpZO33ms2zuejzm-0)

[Classmate-team-notion](https://shy-cannon-6ec.notion.site/Dev-TeamOne-76767fce51764a35abe0c562753e580c)</code></pre>
<img width="231" alt="스크린샷 2023-04-17 오후 6 24 10" src="https://github.com/HyewonKkang/HyewonKkang/assets/68578916/d023e0fd-8953-4bc8-81b2-09f59192466a">

<h3 id="2-3-icongallery-만들기">2-3) IconGallery 만들기</h3>
<p>이 역시도 Storybook에서 기본 API를 제공한다. <a href="https://storybook.js.org/docs/react/api/doc-block-icongallery">IconGallery</a>를 통해 프로젝트에서 사용되는 아이콘들을 grid 형태로 진열시킨다. IconGallery의 children으로 IconItem들을 넣을 수 있다.</p>
<p><img src="https://github.com/HyewonKkang/HyewonKkang/assets/68578916/2a8758d4-fce8-4de4-bb4b-82465077741f" alt="스크린샷 2023-04-17 오후 7 13 45"></p>
<pre><code class="language-markdown">import React from &#39;react&#39;;
import { Meta, IconGallery, IconItem } from &#39;@storybook/blocks&#39;;
import Icon from &#39;../components/Icon&#39;;
import { icons } from &#39;../components/Icon/utils&#39;;

&lt;Meta title=&#39;Getting Started/Icons&#39; /&gt;

# IconGallery

&lt;IconGallery style={{ marginTop: &#39;50px&#39; }}&gt;
{icons.map((key) =&gt; (
&lt;IconItem name={key}&gt;
&lt;Icon name={key} /&gt;
&lt;/IconItem&gt;
))}
&lt;/IconGallery&gt;</code></pre>
<h3 id="2-4-colorpalette-만들기">2-4) ColorPalette 만들기</h3>
<p><a href="https://storybook.js.org/docs/react/api/doc-block-colorpalette">ColorPalette</a>도 사용법이 간단하다. ColorPalette 컴포넌트의 children으로 나타내는 색상의 ColorItem을 작성하면된다.</p>
<ul>
<li>title : <code>string</code> 표시할 색상의 이름</li>
<li>subtitle : <code>string</code> 색상에 대한 추가설명</li>
<li>colors : <code>string[] | { [key: string]: string }</code> 표시할 색상 목록 (key:value 형태)</li>
</ul>
<p>예시의 grey와 같이 여러 색상으로 분리하려면 color에 순서대로 key:value를 추가해주면 된다.</p>
<pre><code class="language-markdown">&lt;ColorItem
title=&#39;grey&#39;
subtitle=&#39;greyscale&#39;
colors={{ Grey1: &#39;#888888&#39;, Grey2: &#39;#BBBBBB&#39;, Grey3: &#39;#D7D7D7&#39; }}
/&gt;</code></pre>
<p><img src="https://github.com/HyewonKkang/HyewonKkang/assets/68578916/6c729368-4104-4209-b1a5-edfa27a25398" alt="sada"></p>
<pre><code class="language-markdown">// stories/colors.stories.mdx

import { Meta, ColorPalette, ColorItem } from &#39;@storybook/blocks&#39;;
import { colors } from &#39;../styles/theme&#39;;

&lt;Meta title=&#39;Getting Started/Palette&#39; /&gt;

# Colors

&lt;ColorPalette&gt;
  &lt;ColorItem
    title=&#39;primary1&#39;
    subtitle=&#39;서비스의 메인컬러로 사용&#39;
    colors={{ Primary1: &#39;#4B9BFA&#39; }}
  /&gt;
  &lt;ColorItem
    title=&#39;primary2&#39;
    subtitle=&#39;서비스의 보조컬러로 사용&#39;
    colors={{ Primary2: &#39;#BFDBFD&#39; }}
  /&gt;
  &lt;ColorItem
    title=&#39;primary3&#39;
    subtitle=&#39;서비스의 보조컬러로 사용&#39;
    colors={{ Primary3: &#39;#005DCC&#39; }}
  /&gt;
  &lt;ColorItem
    title=&#39;primary4&#39;
    subtitle=&#39;서비스의 보조컬러로 사용&#39;
    colors={{ Primary4: &#39;#F1FAFF&#39; }}
  /&gt;
  &lt;ColorItem title=&#39;red&#39; subtitle=&#39;오류 및 삭제 등에 사용&#39; colors={{ Error: &#39;#F45452&#39; }} /&gt;
  &lt;ColorItem
    title=&#39;titleActive&#39;
    subtitle=&#39;서비스의 보조컬러로 사용&#39;
    colors={{ TitleActive: &#39;#000000&#39; }}
  /&gt;
  &lt;ColorItem
    title=&#39;grey&#39;
    subtitle=&#39;greyscale&#39;
    colors={{ Grey1: &#39;#888888&#39;, Grey2: &#39;#BBBBBB&#39;, Grey3: &#39;#D7D7D7&#39; }}
  /&gt;
  &lt;ColorItem title=&#39;offWhite&#39; subtitle=&#39;서브 배경색으로 사용&#39; colors={{ OffWhite: &#39;#F6F6F6&#39; }} /&gt;
  &lt;ColorItem title=&#39;White&#39; subtitle=&#39;컴포넌트의 배경색으로 사용&#39; colors={{ white: &#39;#FFFFFF&#39; }} /&gt;
  &lt;ColorItem
    title=&#39;background&#39;
    subtitle=&#39;서비스의 배경색으로 사용&#39;
    colors={{ Background: &#39;#F9F9F9&#39; }}
  /&gt;
&lt;/ColorPalette&gt;</code></pre>
<h3 id="2-5-typeset-만들기">2-5) Typeset 만들기</h3>
<p><a href="https://storybook.js.org/docs/react/api/doc-block-typeset">Typeset</a> block은 프로젝트에서 쓰이는 폰트를 나타내는 목적의 문서다. Typeset 컴포넌트의 props로 fontSizes 배열과 fontWeight, fontFamily 를 적용할 수 있으며, 예시 문구는 sampleText로 전달한다.</p>
<ul>
<li>fontSizes</li>
</ul>
<p><img src="https://github.com/HyewonKkang/HyewonKkang/assets/68578916/2046037c-5126-4fba-b1ce-7e6423114154" alt="스크린샷 2023-04-17 오후 6 20 06"></p>
<pre><code class="language-markdown">import React from &#39;react&#39;;
import { Meta, Typeset } from &#39;@storybook/blocks&#39;;
import { theme } from &#39;../styles/theme&#39;;

&lt;Meta title=&#39;Getting Started/Typography&#39; /&gt;

# Base Typography

**Font:** Noto Sans KR

**Weights:** 400(light), 500(text), 700(bold, title)

&lt;Typeset fontSizes={[12, 14, 16, 18, 28, 32]} fontWeight={400} sampleText=&#39;Was he a beast if music could move him so?&#39; /&gt;
&lt;Typeset fontSizes={[12, 14, 16, 18, 28, 32]} fontWeight={500} /&gt;
&lt;Typeset fontSizes={[12, 14, 16, 18, 28, 32]} fontWeight={700} /&gt;</code></pre>
<hr>
<p>이외에도 Storybook은 다양한 API를 지원한다. 조금 더 문서를 잘 작성하고 싶다면 <a href="https://storybook.js.org/docs/react/api/doc-block-icongallery">storybook-docs-api</a> 문서를 보면서 추가해보는 것을 추천한다.</p>
<hr>
<h1 id="reference">Reference</h1>
<p><a href="https://storybook.js.org/addons/@storybook/addon-storysource">https://storybook.js.org/addons/@storybook/addon-storysource</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Vite + React + TypeScript 환경에 Storybook v7.0 도입하기 (1)]]></title>
            <link>https://velog.io/@hyewon_kkang/Vite-React-TypeScript-%ED%99%98%EA%B2%BD%EC%97%90-Storybook-v7.0-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@hyewon_kkang/Vite-React-TypeScript-%ED%99%98%EA%B2%BD%EC%97%90-Storybook-v7.0-%EB%8F%84%EC%9E%85%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Tue, 25 Jul 2023 19:44:49 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>스토리북을 이용하면 리액트로 생성한 <strong>컴포넌트의 테스트 및 문서화를 간소화 시켜주는 효과</strong>를 얻는다.</p>
<p>정해진 스타일 가이드가 있다면, 디자인팀과의 협업에서 스토리북을 이용해 소통하고 수정사항등을 빠르게 체크하고, Mock 데이터를 이용해 실제 보여질 디자인을 빠르게 확인할 수 있다.</p>
<hr>
<h1 id="스토리북-설치하기">스토리북 설치하기</h1>
<blockquote>
<p>개발 환경 기술스택</p>
<p>TypeScript(4.9.3) + Vite(4.1.0) + React(18.2.0) + Styled-components(5.3.8) + Storybook(7.0.5)</p>
</blockquote>
<p>아래 명령어를 이용해 setup을 해주면 사진과 같이 여러 devDependencies가 추가되고, 스토리북 실행 및 빌드를 위한 스크립트 <code>storybook</code>과 <code>build-storybook</code>이 추가된다.</p>
<pre><code class="language-bash">npx sb init --builder @storybook/builder-vite</code></pre>
<img alt="11" src="https://user-images.githubusercontent.com/68578916/236763964-f11b62bd-7d1d-4deb-89af-b090b2cc342e.png" width="30%" />

<p>또한, 다음과 같이 .storybook 폴더와 src/stories 폴더가 생성되는데, src/stories는 샘플을 보여주는 것으로 현 프로젝트에서는 컴포넌트 각각에 stories.tsx를 정의할 것이기 때문에 삭제시켰다.</p>
<img alt="12" src="https://user-images.githubusercontent.com/68578916/236763961-7fe38b4f-a1c5-4653-9faf-e5556b2e4372.png" width="30%" />

<p>추가로, main.cjs 파일을 통해 스토리북 실행 시, 스토리 파일을 찾을 수 있도록 해야한다. 그래서 <code>.storybook/main.cjs</code> 에서 stories의 경로를 아래와 같이 수정했다. (<code>src/components</code> 경로의 모든 <code>*.stories.@(ts|tsx|js|jsx)</code>)</p>
<pre><code class="language-jsx">module.exports = {
  stories: [&#39;../src/components/**/*.stories.@(js|jsx|ts|tsx)&#39;],
  ...
};</code></pre>
<p>이제 이를 실행시키기 위해 <code>yarn storybook</code>을 이용했으나 다음과 같은 에러를 바로 마주했다.</p>
<p><code>start-storybook: command not found</code></p>
<p><img src="https://user-images.githubusercontent.com/68578916/236763959-764fcada-aa9f-49b5-b6ad-c707668dd4d6.png" alt="13"></p>
<p>해결을 위해 <a href="https://github.com/storybookjs/storybook/issues/311">Issue #311</a>를 참고해서 node_modules, yarn.lock 파일을 지우고 설치하고를 반복했으나 .. 해결이 되지 않았다. 여기서 좀 애를 먹었는데 stackoverflow에서 storybook을 automigrate 하는 코드를 찾아 이를 이용해 해결할 수 있었다.</p>
<p>해당 설명에 따르면 <code>npx sb init</code> 이후에는 반드시 sb를 새 버전으로 automigrate 시켜주어야 한다. migration 여부를 묻는 항목들은 전부 Y로 응답했고, 해결할 수 있었다.</p>
<pre><code class="language-bash">npx sb@next automigrate

yarn storybook # 실행</code></pre>
<p><a href="https://stackoverflow.com/questions/69954986/start-storybook-is-not-recognized-as-an-internal-or-external-command">&#39;start-storybook&#39; is not recognized as an internal or external command</a></p>
<p><img src="https://user-images.githubusercontent.com/68578916/236763932-89e45d77-ff08-4f39-adc3-167cfcd8aeaf.png" alt="14"></p>
<hr>
<h1 id="storybook-작성하기">Storybook 작성하기</h1>
<p>기본적으로 스토리 작성 파일명은 <code>컴포넌트명.stories.js</code>로 작성해주어야 한다. 해당 포맷 방식은 <code>./storybook/main.js</code> 에서 수정 가능하다. 스토리 작성은 컴포넌트 코드를 만든 후에 이루어진다.</p>
<p>Button 컴포넌트를 예로 작업해보자. 아래는 프로젝트에서 작성한 버튼 컴포넌트 코드다.</p>
<pre><code class="language-jsx">// src/components/Button/Button

import React, { ButtonHTMLAttributes } from &#39;react&#39;;
import styled from &#39;styled-components&#39;;
import { ButtonSize } from &#39;./types&#39;;

export interface Props extends ButtonHTMLAttributes&lt;HTMLButtonElement&gt; {
    variant?: &#39;text&#39; | &#39;contained&#39; | &#39;outlined&#39;;
    size?: ButtonSize;
    underline?: boolean;
}
function Button({
    variant = &#39;contained&#39;,
    size = &#39;medium&#39;,
    disabled = false,
    underline = true,
    children,
    ...rest
}: Props) {
    switch (variant) {
        case &#39;text&#39;:
            return (
                &lt;TextButton size={size} disabled={disabled} underline={underline} {...rest}&gt;
                    {children}
                &lt;/TextButton&gt;
            );
        case &#39;outlined&#39;:
            return (
                &lt;OutlinedButton size={size} disabled={disabled} {...rest}&gt;
                    {children}
                &lt;/OutlinedButton&gt;
            );
        case &#39;contained&#39;:
        default:
            return (
                &lt;ContainedButton size={size} disabled={disabled} {...rest}&gt;
                    {children}
                &lt;/ContainedButton&gt;
            );
    }
}

export default Button;</code></pre>
<h3 id="0-파일-생성">0) 파일 생성</h3>
<p><code>src/components/Button</code> 경로에 <code>Button.stories.tsx</code>를 추가한다.</p>
<p>story 파일에서도 JSX.Element, 컴포넌트 형태로 내보낼 것이기 때문에 타입 에러 방지를 위해 tsx 확장자를 이용했다.</p>
<h3 id="1-knobs-addon-적용하기">1) knobs addon 적용하기</h3>
<p><code>Knobs</code>는 component의 props를 storybook 화면에서 입력을 통해 값을 바꾸며 바로 반영시켜줄 수 있는 애드온이다. 아래와 같이 설치 후,</p>
<pre><code class="language-bash">yarn add @storybook/addon-knobs --dev</code></pre>
<p><code>.storybook/main.js</code> 파일에 addons에 설치된 라이브러리를 추가해주면 끝이다!</p>
<pre><code class="language-jsx">// .storybook/main.js

module.exports = {
    stories: [&#39;../src/**/*.stories.mdx&#39;, &#39;../src/**/*.stories.@(js|jsx|ts|tsx)&#39;],
    addons: [
        &#39;@storybook/addon-links&#39;,
        &#39;@storybook/addon-essentials&#39;,
        &#39;@storybook/addon-interactions&#39;,
        &#39;@storybook/addon-mdx-gfm&#39;,
        &#39;@storybook/addon-knobs&#39;, // 추가
    ],
    framework: {
        name: &#39;@storybook/react-vite&#39;,
        options: {},
    },
    features: {
        storyStoreV7: true,
    },
    docs: {
        autodocs: true,
    },
    core: {
        builder: &#39;@storybook/builder-vite&#39;,
    },
    typescript: {
        reactDocgen: &#39;react-docgen&#39;,
    },
};</code></pre>
<p>추가로, knobs에서 사용가능한 옵션들은 아래에서 확인 가능하다.</p>
<blockquote>
<p><em>knobs 종류</em></p>
<ul>
<li>text: 텍스트를 입력 할 수 있습니다.</li>
<li>boolean: true/false 값을 체크박스로 설정 할 수 있습니다.</li>
<li>number: 숫자를 입력 할 수 있습니다. 1~10과 같이 간격을 설정 할 수도 있습니다.</li>
<li>color: 컬러 팔레트를 통해 색상을 설정 할 수 있습니다.</li>
<li>object: JSON 형태로 객체 또는 배열을 설정 할 수 있습니다.</li>
<li>array: 쉼표로 구분된 텍스트 형태로 배열을 설정 할 수 있습니다.</li>
<li>select: 셀렉트 박스를 통하여 여러가지 옵션 중에 하나를 선택 할 수 있습니다.</li>
<li>radios: Radio 버튼을 통하여 여러가지 옵션 중에 하나를 선택 할 수 있습니다.</li>
<li>options: 여러가지 옵션을 선택 하는 UI 를 커스터마이징 할 수 있습니다 (radio, inline-radio, check, inline-check, select, multi-select)</li>
<li>files: 파일을 선택 할 수 있습니다.</li>
<li>date: 날짜를 선택 할 수 있습니다.</li>
<li>button: 특정 함수를 실행하게 하는 버튼을 만들 수 있습니다.</li>
</ul>
</blockquote>
<p><a href="https://storybook.js.org/addons/storybook-addon-knobs-color-options">Knobs Addon | Storybook: Frontend workshop for UI development</a></p>
<h3 id="2-기본-설정하기">2) 기본 설정하기</h3>
<p>우선 컴포넌트 스토리에 대한 기본 설정을 작성한다. (title, component, argTypes)</p>
<ul>
<li><code>title</code> : 컴포넌트 명으로, <code>/</code>를 이용해 카테고리 분류가 가능하다.</li>
<li><code>component</code> : 파라미터에 아무것도 지정하지 않은 단순한 컴포넌트 형태를 가져온다.</li>
<li><code>argTypes</code>: 컴포넌트에 필요한 인수와 타입 작성</li>
</ul>
<p>여기서는 knobs를 적용했다.</p>
<pre><code class="language-jsx">// src/components/Button/Button.stories.tsx

import { withKnobs, text, boolean } from &#39;@storybook/addon-knobs&#39;;
import Button from &#39;./Button&#39;;

export default {
  title: &#39;Button&#39;,
  component: Button,
  decorators: [withKnobs],
    argTypes:
};</code></pre>
<p><a href="https://storybook.js.org/docs/react/essentials/controls">argTypes 참조</a></p>
<h3 id="3-스토리-작성하기">3) 스토리 작성하기</h3>
<p>그 다음에는, Basic 이란 이름으로 스토리를 생성해보자.</p>
<blockquote>
<p>💡 기본적으로 스토리를 만들 때 <code>export const</code>를 사용하여 <code>default</code>라는 이름으로 내보낼 수 없다. default란 이름을 사용하기 위해서는 스토리를 만들고, 해당 스토리의 멤버 변수로 story 객체를 설정하면 이름을 변경할 수 있다.
<code>hello.story = { name: ‘Default’ }</code></p>
</blockquote>
<pre><code class="language-tsx">// src/components/Button/Button.stories.tsx

import { withKnobs, text, boolean, select } from &#39;@storybook/addon-knobs&#39;;
import Button from &#39;./Button&#39;;

export default {
    title: &#39;Button&#39;,
    component: Button,
    decorators: [withKnobs],
};

export const Basic = () =&gt; {
    return &lt;Button&gt;hello&lt;/Button&gt;;
};</code></pre>
<p>여기서 또 하나의 문제가 발생했다. 해당 프로젝트에서는 styled-components를 이용하면서 theme을 정의해놓고 색상 등을 가져다 쓰는데, stories에서 theme에 접근할 수 없는 문제가 있었다. <strong><code>Cannot read properties of undefined (reading &#39;primary1&#39;)</code></strong></p>
<img src="https://user-images.githubusercontent.com/68578916/236763927-e22167a7-29e5-4405-ac4b-decc35447962.png" width='450' />

<p><strong>3-1) 스토리북 내부에 styled-components theme 적용시키기 (Decorator)</strong></p>
<p>위를 해결하기 위해 스토리북에서 <code>styled-components</code>를 사용하기 위한 세팅이 추가적으로 필요하다.</p>
<p><code>preview.js</code>는 모든 stories에 global하게 적용될 css 등을 세팅할 수 있는 곳이다. 따라서, <code>ThemeProvider</code>를 통해 theme을 감싸주고, GlobalStyle까지 적용시킬 수 있다. (혹은 특정 컴포넌트만 <code>ThemeProvider</code>로 래핑하는 것도 가능하다.) Storybook 7.0 버전에 맞춰 preview.js→ <code>preview.jsx</code>로 수정한 후 아래와 같이 theme을 적용한다.</p>
<pre><code class="language-jsx">// .storybook/preivew.jsx

import React from &#39;react&#39;;
import { ThemeProvider } from &#39;styled-components&#39;;
import { theme } from &#39;../src/styles/theme&#39;;

export default {
    parameters: {
        actions: { argTypesRegex: &#39;^on[A-Z].*&#39; },
        controls: {
            matchers: {
                color: /(background|color)$/i,
                date: /Date$/,
            },
        },
    },
    decorators: [
        (Story) =&gt; (
            &lt;ThemeProvider theme={theme}&gt;
                &lt;Story /&gt;
            &lt;/ThemeProvider&gt;
        ),
    ],
};</code></pre>
<p><a href="https://storybook.js.org/docs/react/writing-stories/decorators#page-top">https://storybook.js.org/docs/react/writing-stories/decorators</a></p>
<p><strong>3-2) knobs 애드온 추가하기</strong></p>
<p>Button이라는 컴포넌트는 variant, disabled, underline, size 등 여러 props를 가진다. 이러한 props를 스토리북 상에서 사용자가 조절할 수 있도록 하기 위해 knobs를 추가할 수 있다.</p>
<pre><code class="language-tsx">const label = text(&#39;label&#39;, &#39;Button&#39;);</code></pre>
<p>위의 코드를 예로 들면, text 형식으로 사용자의 입력을 받을 것이고, label이라는 필드에 defaultValue로 ‘Button’을 추가하게 된다.</p>
<p><img src="https://user-images.githubusercontent.com/68578916/236763922-40e6d7f5-e561-4799-8ba8-a0d5171dd088.png" alt="16"></p>
<p>설정한 text, boolean, select와 같은 옵션들을 이용해 작성하면 아래와 같이 조절할 수 있는 패널이 등장하고, 해당 옵션 선택의 결과가 UI 상에 바로 보여진다.</p>
<pre><code class="language-tsx">// src/components/Button/Button.stories.tsx

import { withKnobs, text, boolean, select } from &#39;@storybook/addon-knobs&#39;;
import Button from &#39;./Button&#39;;

export default {
    title: &#39;Button&#39;,
    component: Button,
    decorators: [withKnobs],
};

export const Basics = () =&gt; {
    const label = text(&#39;label&#39;, &#39;Button&#39;);
    const props = {
        disabled: boolean(&#39;disabled&#39;, false),
        size: select(&#39;size&#39;, [&#39;large&#39;, &#39;medium&#39;, &#39;small&#39;], &#39;medium&#39;),
    };

    return (
        &lt;div&gt;
            &lt;Button {...props}&gt;{label}&lt;/Button&gt;
            &lt;Button variant={&#39;text&#39;} {...props}&gt;
                {label}
            &lt;/Button&gt;
            &lt;Button variant={&#39;outlined&#39;} {...props}&gt;
                {label}
            &lt;/Button&gt;
        &lt;/div&gt;
    );
};</code></pre>
<p><img src="https://user-images.githubusercontent.com/68578916/236763914-cf32e11a-dda4-4ceb-8499-56a43ba58d17.png" alt="17"></p>
<p><strong>3-3) Actions 애드온 추가하기</strong></p>
<p>Actions 애드온은 컴포넌트를 통하여 특정 함수가 호출됐을 때 어떤 함수가 호출됐는지, 그리고 함수에 어떤 파라미터를 넣어서 호출했는지에 대한 정보를 확인 할 수 있게 해준다. <code>action(’액션 이름’)</code></p>
<pre><code class="language-tsx">import { action } from &#39;@storybook/addon-actions&#39;;

export const Basics = () =&gt; {
  ...

  return (
      &lt;Button onClick={action(&#39;onClick&#39;)} {...props}&gt;
        {label}
      &lt;/Button&gt;
  );
};</code></pre>
<p><img src="https://user-images.githubusercontent.com/68578916/236763902-aa91f17a-e74f-4c43-b224-dc1b2712720e.png" alt="18"></p>
<h3 id="4-컴포넌트-설명-추가하기">4) 컴포넌트 설명 추가하기</h3>
<p>스토리북의 Docs 페이지는 컴포넌트의 Props와 주석에 기반하여 자동으로 문서가 생성된 결과다.</p>
<img src="https://user-images.githubusercontent.com/68578916/236763892-846d533a-c73b-4daa-9c47-3adf3fa0dc7d.png" width="490" />

<p>이제 위 페이지에 컴포넌트에 대한 설명을 추가해보자.</p>
<p>먼저 Subtitle을 설정하는 방법이다. 부제목을 설정할 때에는 기본 설정의 <code>parameters</code>에 <code>componentSubtitle</code> 값을 지정할 수 있다.</p>
<pre><code class="language-tsx">// src/components/Button/Button.stories.tsx

export default {
    title: &#39;Button&#39;,
    component: Button,
    decorators: [withKnobs],
    parameters: {
        componentSubtitle: &#39;버튼 컴포넌트&#39;, // 컴포넌트 부제목
    },
};</code></pre>
<p>부제목 밑에 설명을 넣으려면, 컴포넌트 파일에서 컴포넌트 코드 바로 윗 부분에 주석을 작성하면 된다. 주석 작성 시에는 <code>/**</code>를 이용해 작성한다.</p>
<pre><code class="language-tsx">// src/components/Button/Button.stories.tsx

export default {
  title: &#39;Button&#39;,
  component: Button,
  decorators: [withKnobs],
  parameters: {
    componentSubtitle: &#39;버튼 컴포넌트&#39;,
  },
};

/**
 * 버튼은 동작(또는 일련의 동작)을 의미합니다. 버튼을 클릭하면 해당 비즈니스 로직이 트리거됩니다.
 *
 * 총 세 가지 유형의 버튼을 제공합니다.
 * - `contained button` : 주요 작업을 표시합니다. 하나의 섹션에서 최대 하나를 가집니다.
 * - `text button` : 일반적인 작업에 주로 사용됩니다.
 * - `outlined button` : 클릭 시 다른 페이지로 옮겨지는 등 보조적인 동작에 사용됩니다.
 */

export const Basics = () =&gt; {
    ...
}</code></pre>
<p>이를 통한 Docs 페이지의 결과는 아래와 같다.</p>
<p><img src="https://user-images.githubusercontent.com/68578916/236763836-7297c317-8b22-488c-85f6-5ea2b46e3552.png" alt="20"></p>
<h2 id="결과">결과</h2>
<p>하단 Knobs 패널을 조정하면 해당 상태가 적용된 컴포넌트 UI 결과도 확인할 수 있다.</p>
<img src="https://user-images.githubusercontent.com/68578916/236763812-2096479d-95f0-41c4-bfd9-388e8f73b31f.png" width="500" />
<img src="https://user-images.githubusercontent.com/68578916/236763815-61499b6c-fe80-4aa3-9024-3fc2c054dbb2.png" width="500" />

<hr>
<p>Storybook v7.0이 2023년 3월에 배포되었는데, 그래서 preview 파일에 decorators를 추가하는 부분이라던가 새로운 버전에 대한 error log들을 쉽게 찾을 수 없어서 조금 시간이 걸렸다. 그러나 어떤 것이든 결국엔 공식문서를 꼼꼼히 읽어보면 찾을 수 있다!</p>
<p>소스 코드 : <a href="https://github.com/Dev-TeamOne/classmate-frontend">https://github.com/Dev-TeamOne/classmate-frontend</a></p>
<hr>
<h1 id="reference">Reference</h1>
<p><a href="https://storybook.js.org/tutorials/intro-to-storybook/react/ko/get-started/">https://storybook.js.org/tutorials/intro-to-storybook/react/ko/get-started/</a>
<a href="https://storybook.js.org/addons/storybook-addon-knobs-color-options">https://storybook.js.org/addons/storybook-addon-knobs-color-options</a>
<a href="https://velog.io/@velopert/start-storybook">https://velog.io/@velopert/start-storybook</a>
<a href="https://tech.youha.info/26d0fe3a-c718-4943-8276-233c64a4b023">https://tech.youha.info/26d0fe3a-c718-4943-8276-233c64a4b023</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[네이버 커넥트재단 부스트캠프 웹·모바일 7기 지원부터 수료까지의 후기 🐋]]></title>
            <link>https://velog.io/@hyewon_kkang/%EB%84%A4%EC%9D%B4%EB%B2%84-%EC%BB%A4%EB%84%A5%ED%8A%B8%EC%9E%AC%EB%8B%A8-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%EB%AA%A8%EB%B0%94%EC%9D%BC-7%EA%B8%B0-%EC%A7%80%EC%9B%90%EB%B6%80%ED%84%B0-%EC%88%98%EB%A3%8C%EA%B9%8C%EC%A7%80%EC%9D%98-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@hyewon_kkang/%EB%84%A4%EC%9D%B4%EB%B2%84-%EC%BB%A4%EB%84%A5%ED%8A%B8%EC%9E%AC%EB%8B%A8-%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-%EC%9B%B9%EB%AA%A8%EB%B0%94%EC%9D%BC-7%EA%B8%B0-%EC%A7%80%EC%9B%90%EB%B6%80%ED%84%B0-%EC%88%98%EB%A3%8C%EA%B9%8C%EC%A7%80%EC%9D%98-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 25 Jul 2023 19:32:17 GMT</pubDate>
            <description><![CDATA[<p>(Jan 8, 2023 작성글. 블로그 이전)</p>
<p>2022년에 하반기에 진행된 부스트캠프 웹·모바일 7기 웹풀스택 과정을 무사히 수료했다. 7월 18일부터 12월 16일까지 진행됐던 5개월 간의 긴 여정을 끝내고 이에 대한 회고를 남겨보고 싶었다. 부캠을 진행하면서도 매주 회고를 남기시는 분들을 보면서 대단하다라는 생각을 했고 자극을 많이 받았지만 결국 모든 과정을 마무리하고나서야 남들보다 조금 긴 회고를 작성해보려고 한다.</p>
<p><img src="https://user-images.githubusercontent.com/68578916/211322749-7bc23e88-6e5c-4f5a-80bc-244750a0dc59.png" alt="스크린샷 2022-12-28 오후 5 16 15"></p>
<p>참고: <a href="https://boostcamp.connect.or.kr/program_wm.html">네이버 커넥트재단 부스트캠프 웹·모바일 프로그램 소개 페이지</a></p>
<p>나의 경우 컴퓨터공학 전공생으로 3학년 2학기까지 학교를 다니다가 2022년 1년 휴학 기간을 가지기로 마음을 먹었었다. 휴학을 결심한 이유는 우선 바로 취준을 하기에는 내 실력이 많이 부족하다 생각했기 때문이다. 그래서 학교를 다니면서 빠듯하게 공부하는 것보다는 새로운 환경도 겪어보고 프론트엔드 로드맵을 따라 천천히 학습해보고 싶다는 마음이 있었다. 그러나 단순히 나혼자 알고리즘을 풀고, 인강을 듣고, 사이드 플젝을 진행하는 것만으로는 크게 성장할 수 없겠다라는 생각이 당연히 들었고, 휴학을 결정하면서 2022년에 열리는 카카오 인턴십 혹은 네이버 커넥트재단 부스트캠프 두 가지 과정 중 하나에 반드시 참여해야겠다는 마음을 먹었다.</p>
<p>둘 중 먼저 전형이 시작되었던 <a href="https://careers.kakao.com/2022-intern">[2022 카카오 테크 인턴십]</a>을 지원했었는데 부끄러운 이야기지만 코딩테스트와 서류 전형 과정을 거쳐 나오는 합격자 발표에서 불합격을 통보받았었다. 사실 잘 기억이 나지는 않는데 코테 문제를 생각보다 못 풀었던 것이 이유가 될 것 같았다. 5월에 해당 전형이 진행됐었는데 만약에 부캠도 떨어지면 진짜 어떡하지.. 휴학 괜히 했나..?라는 생각이 계속 머리를 맴돌았다. 물론 카카오 인턴십과 부캠을 모두 못하게 된다해도 휴학동안 공부를 열심히 하면 되겠지만 처음부터 두개를 목표에 두고 휴학을 했기 때문에 아쉬움이 많이 클 것 같았다.</p>
<p>그러던 중 6월 초, 부스트캠프의 접수도 시작되었다. 합격해서가 아니라 정말 부캠을 너무나도 하고 싶었었고, 그래서 처음 모집 기간이 나오지도 않았을 4월 말부터 틈 날때마다 부캠 모집 페이지에 들어가보면서 전형이 언제 열리나 목 빼놓고 기다렸었다. 작년에는 5월에 열렸어서 5월에는 매일 들어가서 페이지 새로고침을 마구 했었다ㅎ. 서류를 정말 빨리 제출해놓고 얼른 코테가 시작되기만을 기다리면서 6월을 보냈다.</p>
<h2 id="지원">지원</h2>
<h3 id="1차-온라인-코딩테스트">1차 온라인 코딩테스트</h3>
<p><img src="https://user-images.githubusercontent.com/68578916/211322746-49d55bb0-df45-4907-84d1-070967a4dba7.png" alt="스크린샷 2022-12-28 오후 5 13 09"></p>
<p>1차 코딩테스트는 전체 지원자에 한해 진행이 되었고, 1차 합불 여부는 기존에 제출했던 서류 검토 + 코딩테스트 결과로 결정되었다. 1차 온라인 코테는 예상했던 난이도보다 훨씬 어려웠다. 나도 7기 지원 시에 6기 수료생들의 후기를 많이 찾아봤었는데 코테 난이도가 쉬운 편이라는 이야기를 정말 많이 들었어서 크게 긴장하지 않고 코테를 보려고 했었다. 그런데 전체적인 문제 난이도가 높았던 것 같고, 코테가 끝나고 오픈채팅방 등을 통해 예상하는 합격 커트라인이 1솔이었던 것 같다. (기억이 정확하지는 않다.) 당시에 0.5문제를 풀었었고 코테가 종료된 이후에는 정말 망연자실이었다.. 그래서 합격에 큰 기대를 하지 않고 앞으로 알고리즘이나 많이 풀어봐야 겠다는 생각을 가지고 지냈다. 그러던 중 예상치 못한 2차 코테 대상자 선정 메일을 받게 되었다.</p>
<h3 id="2차-온라인-코딩테스트">2차 온라인 코딩테스트</h3>
<p><img src="https://user-images.githubusercontent.com/68578916/211322743-42b2b369-7e7f-4433-924d-a39aaedf2f50.png" alt="스크린샷 2022-12-28 오후 5 13 39"></p>
<p>분명 내가 코테를 잘 본 편은 아니었기에, 예상하기로는 제출했던 서류 평가의 비중도 꽤나 높았던 것 같다. 실제로 내 주위의 0솔이었던 분들도 1차 합격을 한 사례도 있었고, 오픈채팅방의 정보는 정확하지는 않지만 1솔 이상이신 분들이 떨어진 경우도 보았다. 일단 모르겠고 붙여주셔서 너무 감사했다. 그래서 더 불타서 2차 코테는 뿌셔야겠다고 생각이 들었다. 다 들어와.</p>
<p>2차 코테는 5개월 전인데도 아직도 기억에 남는 문제가 있다ㅎ. 그만큼 재미있게 푼 기억이 있는데 코테를 푸는 당시에는 시간을 너무 많이 잡아먹는 문제들이 있어서 당황했던 기억이 있다. 3문제가 모두 구현 문제였고, 정말 빡집중해서 빨리빨리 풀지 못하면 제한 시간 안에 모두 풀기에는 어려움이 있었던 것 같다. 문제를 푸면서 이번에는 1.5솔~2솔이상은 해야 무조건 합격이겠다라는 생각이 들어 정말 집중해서 빠르게 해결하려고 노력했다. 그 결과로는 2.5솔이었는데 마지막 문제는 3분만 더 있었으면 완벽하게 풀 수 있었어서 시험이 끝나고 며칠 동안 아른거렸다.. 아 물론 1,2차 모두 문제에서 제공되는 2-3개 정도의 테스트 케이스를 돌린 결과로만 solved인지 아닌지를 판단했다. 그렇게 꽤나 만족스럽게 2차 코테를 마무리하고 떨리는 마음으로 최종 합불 여부를 기다렸다.</p>
<h3 id="최종-합격">최종 합격</h3>
<p><img src="https://user-images.githubusercontent.com/68578916/211322740-114f6bf2-3837-4632-8ebd-a342bfb34756.png" alt="스크린샷 2022-12-28 오후 5 39 21"></p>
<p>사실 어느정도는 예상했지만 그래도 약 9:1의 확률을 뚫고 과연 합격할 수 있을지에 대한 걱정이 커서 긴장하면서 메일을 기다렸고 결국 최종 합격 통보를 받게 되었다.</p>
<p>부캠 챌린지가 시작되기 전까지는 스스로 JS 언어를 다루는게 많이 부족하다고 생각해서 짧은 인강을 찾아 들었다. 단기간에 언어를 모두 이해하려다보니까 아무래도 완벽하게 학습되지는 않았고, 대신에 나처럼 JavaScript 언어를 잘 모르는 사람들은 한 번쯤은 어떤 문법들이 있는지 살펴보고 가면 좋을 것 같다.</p>
<h2 id="부스트캠프-챌린지">부스트캠프 챌린지</h2>
<h3 id="과정">과정</h3>
<p>먼저 챌린지는 4주 동안 과정이 진행된다. 평일 10시에 매일 새로운 과제가 공개되면 그 과제를 코어 타임인 19시까지 진행하고, 진행을 완료하지 못한 경우는 다음날 오전 9시 전까지 마무리하는 식으로 진행되었다. 과제는 웹풀스택 과정의 경우 JavaScript 언어를 사용해 해결해야하고, CS 지식과 웹 개발자라면 기본적으로 알아야 할 개념들에 관한 주제의 문제들이 출제되었다.</p>
<p>처음 부캠에 합격하고 나서는 스스로 멤버십 합격까지 정말 자신이 있었다. 리액트를 이용한 프로젝트 개발도 여러 번 해봤고, CS 지식도 어느정도 잘 알고 있는 상태라고 생각했기 때문이다. 그러나 이러한 근자감은 챌린지 이틀차부터 없어졌다. 내가 예상했던 것보다 과제의 난이도는 매우 높았고, 함께 챌린지를 진행하는 다른 분들의 실력 또한 나보다 월등한 분들이 대다수라고 느꼈다. 그리고 가장 놀라웠던 점은, 나도 어딜가도 열심히 한다, 열정적이다 라는 말을 자주 들었었는데 여기에는 나와 같은, 혹은 나보다 훨씬 더 열심히하고 최선을 다하는 분들이 대다수였다. 처음에는 이런 분위기가 낯설기도 하고, 특히 한주 한주 지나갈수록 과제의 난이도도 점차 어려워져서 완벽하게 해결하지 못한 과제가 늘어나기 시작할 때 챌린지를 합격하지 못할 것 같은 불안함에 뒤처지는 느낌을 자주 받았다. 3주차부터는 이러한 생각들 때문에 집중력도 떨어지고 그래서 매일 매일 밤을 새면서 과제를 풀다보니 컨디션이 많이 저하되었다. 다른 분들의 이야기를 들어보면 이러한 생각을 다들 한번쯤은 가지는 것 같았다. 그래서 아 자연스러운 생각이구나 라고 스스로를 달래면서 4주차부터는 컨디션을 되찾고 조금은 과제 해결을 포기해가면서 꾸역꾸역 4주를 버틴 것 같다. 오히려 합격에 대한 부담감을 내려놓고 나니 과제 풀이에 집중하는 것이 아니라 학습에 집중을 하게 되면서 많이 공부하고 배울 수 있었다.</p>
<p>그래서 4주가 마무리 되었을 때는 몸도 마음도 많이 지쳤지만 그만큼 많은 지식들을 습득할 수 있었고, 개발할 때 가져야 할 좋은 태도를 많이 배울 수 있었다. 그리고 내가 또 언제 이렇게 4주라는 긴 시간동안 몰입해서 공부해 볼 수 있을까를 생각해보면 지나고 보니 매우 뿌듯했던 기간이라고 생각된다.</p>
<h3 id="합격-후기">합격 후기</h3>
<p><img src="https://user-images.githubusercontent.com/68578916/211322729-b77ba7c0-f6be-4fae-80cd-8e6b607564c2.png" alt="스크린샷 2022-12-28 오후 5 15 15"></p>
<p>100 중에 10 정도만 기대했는데 운이 좋게도 다음 멤버십 과정에도 갈 수 있었다. 작년 6기보다 7기의 합격 인원 비중이 더 컸던 것 같다. 그래서 꽤나 많은 인원이 멤버십으로 가게 되었고 나도 운이 좋게 여기에 참여할 수 있었다! 이번 기수는 약 70% 인원이 멤버십에 합격한 것 같다.</p>
<p>처음에는 당연히 합격이 기뻤다. 그러나 한달 과정이 많이 힘들었기 때문에 그것보다 더 긴 14주차를 과연 내가 잘 진행할 수 있을지 걱정이 먼저 앞서긴 했다. 그래서 챌린지의 경험을 토대로 멤버십에서는 조금 더 불안함이나 고민, 걱정을 내려놓고 긴 기간을 함께 갈 실력 있는 캠퍼분들, 마스터님들, 멘토님들을 모두 잘 활용?해서 말 그대로 온전한 ‘학습’에만 집중해보기로 스스로 마음을 잡았었다.</p>
<h2 id="부스트캠프-멤버십">부스트캠프 멤버십</h2>
<p>멤버십은 크게 두 가지 과정으로 나뉜다. 14주의 과정 중 8주를 먼저 학습스프린트 기간으로 보내고 남은 6주를 그룹프로젝트 기간으로 보내게 된다. 간단하게 학습스프린트에서는 웹풀스택 개발을 위해 쌓아가야 할 웹 관련 지식을 익히고 리액트, 노드와 친해지는 시간을 갖는다. 이런 과정은 여러 번의 미션을 통해 진행되고 미션이 진행될수록 점차 요구하는 문제사항들이 쌓이게 되었다. 학습스프린트는 거의 개별 과제 해결로 진행되었고, 그 중 한 미션만 페어 프로그래밍 형식으로 진행되었다. 그리고 이러한 과정을 거쳐 각자가 습득한 지식을 활용해 볼 수 있는 그룹프로젝트 기간을 갖게 된다. 그룹프로젝트는 모두가 자율적으로 팀을 구성하고 자유롭게 주제를 선정해 협업하면서 프로젝트를 완성해나가는 방식이다. 부캠의 챌린지부터 멤버십에서 제시되는 모든 미션들은 외부에 공개할 수 없기 때문에 부캠이 종료되고 실제로 내 포트폴리오에 기록할 수 있는 프로젝트는 마지막 그룹프로젝트만이 존재하게 된다. 그래서 정말 정말 좋은 퀄리티로 완성하고 싶어서 이때만을 기다렸다..! <del>플젝 잼써!!!</del></p>
<h3 id="학습스프린트-8주">학습스프린트 (8주)</h3>
<p>챌린지의 여파로 큰 겁을 먹고 들어온 학습스프린트 기간은 사실 나에게는 너무 재밌었다. 모든 미션이 중요한 웹 관련 지식을 습득할 수 있도록 이끌어주었고 정말 많은 내용을 공부했던 것 같다. 특히 기초가 없던 나에게는. 그리고 단순히 지식 공부가 아니라 각 미션에서 제공되는 UI 및 기능 설명서를 따라 직접 구현하는 것이 좋았던 것 같다. 부캠 전까지는 직장에서 일을 해본 경험도 없고, 그래서 와이어프레임이나 스토리보드와 같은 UI 설계 문서를 받아 개발해 본 경험이 없었다. 보통 프로젝트를 진행하면 알아서 디자인까지 도맡아 했기 때문에.. 마음에 드는 UI를 만들어본 적이 없었고, 디자인하느라 시간을 많이 썼던 것 같다. 그래서 이 기간동안 주어지는 프로토타입을 따라 개발하는 것이 재밌었고, 사실 부캠의 모든 과제가 그렇듯 완벽한 구현이 목표인 과제들이 절대 아니지만, 구현이 너무 재미있다 보니.. 정말 자주 밤을 새면서 개발했던 것 같다. 억지로 한 것은 아니고 정말 즐거워서 참여했고 그래서 8주의 시간이 좀 빨리 지나가지 않았나 싶다. 하나 아쉬운 점은 앞서도 언급했지만, 이렇게 열심히 만든 사이드 프로젝트들을 어딘가 공개할 수 없는게 개인적으로 많이 아쉬웠다. 그치만 더 이를 갈고 그룹 프로젝트 때 지금까지 배운 모든 것들을 다 써먹어야겠다고 생각했다 ㅎ. 아무튼 학습스프린트 기간은 개인적으로 정말 만족도가 높았다. 다만 이건 내가 이전에 웹 개발을 해왔기 때문에 즐겁게 느끼는 것이고, 오히려 챌린지보다 멤버십 기간이 힘들었다는 캠퍼 분들도 있었다. 아무래도 처음 개발을 하는 사람에게 갑자기 프+백 웹을 구현하라고 하면.. 많이 힘들 것 같다. 그렇지만 확실한 건 무엇이든 처음만 힘들다. 다들 금방 적응해나가며 8주가 지나고 있을 그룹프로젝트에서 충분히 1인분을 할 수 있다.</p>
<h3 id="그룹프로젝트-6주">그룹프로젝트 (6주)</h3>
<p><img src="https://user-images.githubusercontent.com/82160479/206986782-0e602f98-bf09-4ece-ab7a-891e27f59e91.png" alt="젤로드로우"></p>
<p><a href="https://github.com/boostcampwm-2022/web37-ZelloDraw">깃허브 바로가기</a> &nbsp;| &nbsp;<a href="https://www.notion.so/9e414fde5019400f9ecf6371bfca3775">팀노션 바로가기</a> &nbsp;| &nbsp;<a href="https://zellodraw.com">ZELLO DRAW 지금 바로 플레이해보기</a></p>
<p>➡ 플레이 해보시고 재미있으셨다면 <a href="https://github.com/boostcampwm-2022/web37-ZelloDraw">깃허브</a> 스타 한번 부탁드립니다:)
<img src="https://user-images.githubusercontent.com/68578916/211180119-9c4d95de-5cb6-4847-9412-1a00735a5d7b.png" alt="image"></p>
<p>그룹프로젝트 1주차에는 프로젝트 기획 및 설계 기간을 갖고 2~5주차에 개발을 진행하게 된다. 그리고 마지막 6주차에는 테스트 및 리팩토링 기간을 가지는 순서로 진행된다. 처음에는 개발기간 4주가 널널하지 않을까 생각했는데 마무리에 도달하게 될 때는 시간이 부족해서 리팩토링 기간에도 계속 개발을 이어갔었다. 6주 안에 기능 개발은 어찌저찌 마무리가 되었지만 아직 해결해야 할 버그들이 많았고 부캠을 수료한 뒤에도 팀원들과 추가하거나 수정할 기능들을 이슈에 등록해놓고 구현하는 중이다.</p>
<img src="https://user-images.githubusercontent.com/68578916/211180016-c3967f0e-7d40-476b-8710-c2ff8e926d4d.png" width="400" />

<p>팀에서 만든 서비스가 여러 명이 즐기는 드로잉 게임 서비스였는데 다른 캠퍼 분들이 젤로드로우를 즐겁게 플레이하시는걸 보고 정말 기분이 좋았고 감사했던 경험이 있다. 내가 왜 웹 개발을 하려고 마음을 먹었는지 다시금 기억나게 된 순간이었다 ㅎ. 앞으로도 1년 정도는 서버를 유지할 생각인데 그동안 이용자들이 많이 생기면 좋겠다. 얼른 완성하고 여기저기 홍보해야지.</p>
<p>팀에서 또한 시간을 많이 들였던 부분은 <code>개발일지 작성</code>, <code>회의록 관리</code>, <code>팀 내 기술 공유 컨퍼런스</code>, <code>코드 리뷰</code> 등이 있다. 단순히 프로젝트 개발만을 목표로 했기 보다는 협업을 어떻게 하면 더 잘 할 수 있을지와 이 프로젝트 개발 후 결국 우리에게 남는 것은 무엇일지를 고민한 결과라고 생각된다. 특히 팀 내 개발일지 자것와 같이 좋은 문화들은 부캠 커뮤니티 내에서도 적극적으로 공유하여 커뮤니티 내에서 선순환을 만들도록 노력했고 나에게도 기록이라는 정말 좋은 습관을 만들게 해준 계기가 되었다.</p>
<p><img src="https://user-images.githubusercontent.com/68578916/211180207-115293fd-0b4a-4aa8-9b11-82be1f96f5f5.png" alt="스크린샷 2023-01-08 오후 1 16 02"></p>
<p>사실 나는 그룹프로젝트 운이 좋았다고 생각된다. 부캠 기간동안 친해진 캠퍼 분들도 없고 해서 처음 팀 구성할 때 랜덤 팀으로 신청하려고 했었는데 너무 좋은 분들께서 같이 하자고 제안을 주셨고, 덥석 물었다 ㅎ. 우리 팀의 결과물이 만족스럽게 나올 수 있었던 가장 큰 이유가 팀원들의 시너지 효과 덕분이라고 생각한다. ‘젤로’라는 팀명을 널리 널리 홍보하고 우리 팀이 가진 팀 내 문서화, 회의록 관리, 기술 공유 등의 팀 문화를 다른 캠퍼 분들에게 긍정적인 영향을 끼치기 위해 모두가 많이 노력했던 것 같다. 팀에서 매주 회고를 남기면서 서로에게 짧은 편지를 적었었는데 모아보니 뿌듯하고 벌써 뭉클하고 그렇다. 항상 좋은 일에 앞장 서는 팀원 분들 덕분에 내가 부족한 점들도 찾을 수 있었고, 배운 점들도 정말 많다. 특히나 앞으로는 어떤 커뮤니티를 가든 조금 더 능동적으로 참여하고, 이끌어 보기도 하고, 좋은 것들은 널리 공유하는 적극적인 태도를 가져야 겠다고 마음을 먹었다. <del>팀 젤로 사랑해욥</del> THX!</p>
<p><img src="https://user-images.githubusercontent.com/68578916/213858433-00508ca1-a796-4769-b613-ed410b94acc7.png" alt="Untitled"></p>
<p>(나도 동재 님 블로그를 따라 팀원 분들이 써주신 내용들을 모아봤다 ㅎ.)</p>
<h2 id="후기">후기</h2>
<p>부캠이 끝나고 한 주는 놀고 먹고 늦잠도 자면서 나만의 방학 기간을 가졌다. 사실 나는 아직 학교 졸업까지 1년이 남았고, 그래서 돌아갈 곳이 있기 때문에 다른 분들처럼 취업 준비를 급하게 하지는 않았다. 부캠 채용연계와 관련해서 나는 부캠 파트너 기업 중에 한 곳에서 인터뷰 제안이 왔었다. 그래서 너무 감사하기도 하고 좋은 기회가 될 것 같아 인터뷰를 하기로 했었으나 다른 곳에서 어시스턴트 직무를 하게 되어 인터뷰를 진행해보지는 못했다. (부캠 채용연계 x)</p>
<p>부캠의 또 하나의 장점 중에는 현직자와의 소통의 기회가 많은 것이 있다. 기업 부스에서 만나는 개발자 분들이나 강연을 해주시는 개발자 분들, 현업에서 근무하시는 리뷰어 분들, 멘토 분들과의 컨텍 기회가 정말 많았기 때문에 여기서 많은 인사이트를 얻을 수 있지 않나 생각된다. 실제로 그룹 프로젝트를 진행할 때도 우리 팀 멘토 님께 도움을 받은 부분이 많다. 이런 관계가 오래 유지되면 정말 좋을 것 같다ㅎ.</p>
<hr>
<p>드디어 미뤄놨던 부캠 후기를 작성했다. 이 글은 다음 기수인 8기를 준비하는 분들에게 도움이 되면 좋겠지만, 사실 정보 공유의 목적보다는 내가 보낸 5개월을 어딘가에 기록해놓고 종종 다시 읽어내려가면서 이때 느낀 열정과 행했던 노력을 앞으로도 놓치지 않기 위한 목적이다. 부캠을 하면서 만든 새로운 습관은 바로 <code>기록</code>이다. 블로그를 시작한 것도 학습한 모든 것들을 잊어버리지 않기 위함이고, 또한 공유하는 개발자 문화에 나도 이바지하기 위함이 될 것 같다. 부캠을 통해 얻은 것 중 앞으로 내가 취업 전까지 어떤 식으로 준비하고 공부하면 될지 그 <code>방향성</code>을 스스로 알게 해준 것이 가장 좋은 성과라고 생각된다. 앞으로도 이러한 열정을 잃지 않고 계속해서 성장해나가며 더 좋은 개발자가 되기 위해 노력할 것이다.</p>
<p><strong>힘들었지만 재밌었던 5개월 간의 부캠 여정은 오래오래 기억될 것 같다. 함께한 모든 7기 캠퍼 분들 다시 한 번 수료를 축하드립니다!</strong></p>
]]></description>
        </item>
    </channel>
</rss>