<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Lybell</title>
        <link>https://velog.io/</link>
        <description>홍익인간이 되고 싶은 꿈꾸는 방랑자</description>
        <lastBuildDate>Sat, 04 Jan 2025 06:55:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Lybell</title>
            <url>https://velog.velcdn.com/images/lybell_4rt/profile/024cd76e-74db-4702-b9d6-cd6ecf440523/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Lybell. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lybell_4rt" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[자바스크립트는 왜 그 모양일까 - 제네레이터 개선]]></title>
            <link>https://velog.io/@lybell_4rt/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EC%99%9C-%EA%B7%B8-%EB%AA%A8%EC%96%91%EC%9D%BC%EA%B9%8C-%EC%A0%9C%EB%84%A4%EB%A0%88%EC%9D%B4%ED%84%B0-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@lybell_4rt/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EB%8A%94-%EC%99%9C-%EA%B7%B8-%EB%AA%A8%EC%96%91%EC%9D%BC%EA%B9%8C-%EC%A0%9C%EB%84%A4%EB%A0%88%EC%9D%B4%ED%84%B0-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Sat, 04 Jan 2025 06:55:18 GMT</pubDate>
            <description><![CDATA[<p>최근 &lt;자바스크립트는 왜 그 모양일까?&gt;를 읽고 있다. 이 책은 자바스크립트 개발에 참여하고, JSON 포맷을 창시한 더글라스 크락포드가 지었으며, 저자가 생각하는 자바스크립트의 설계 결함을 소개하고, 좀 더 아름답게 자바스크립트를 사용하는 방법을 소개하는 책이다. 그 중, 클로저를 이용하여 자바스크립트의 제네레이터를 개선하는 부분을 소개하고자 한다.</p>
<h2 id="제네레이터란-무엇인가">제네레이터란 무엇인가?</h2>
<pre><code class="language-js">function* sequence(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
    for(let value=start; value&lt;end; value+=step)
    {
        yield value;
    }
}

const iterator = sequence(1, 4, 1);
iterator.next(); // {value:1, done: false}
iterator.next(); // {value:2, done: false}
iterator.next(); // {value:3, done: false}
iterator.next(); // {value:undefined, done: true}

for(let value of sequence(0, 6, 2))
{
    console.log(value); // 0, 2, 4가 순차적으로 출력됨
}
const arr = [...sequence(1,10,1)]; // [1,2,3,4,5,6,7,8,9]</code></pre>
<p>쉽게 말하면, <strong>순열을 생성하는 함수</strong>라고 할 수 있다. 그런데, 그 순열의 형태가 배열인 것이 아니라, 한 번 호출할 때마다 순차적으로 다른 값을 반환하는 객체 같은 형태(제네레이터 객체)로 생성된다.</p>
<p>파이썬이나 자바스크립트에서는 yield라는 문법을 이용하여 구현된다. 이 때, 제네레이터 객체가 한 번 소비되면(next 함수를 호출하거나, 반복문에서 한 번의 반복을 할 때를 의미한다) 제네레이터 함수에 있는 yield가 있는 문까지 실행되고 함수를 종료한다. 그리고 제네레이터 객체가 다음으로 소비될 때는 이전에 멈췄던 yield 다음 문부터 시작한다. 그렇기 때문에, 제네레이터 함수를 <strong>아무 때나 멈출 수 있고 상태를 유지하는 함수</strong>로 부르기도 한다.</p>
<p>자바스크립트에는 ES6부터 도입되었으며, 자바스크립트는 제네레이터 객체를 반복 가능 프로토콜과 반복자 프로토콜을 모두 준수하는 객체로 구현한다. 반복 가능 프로토콜과 반복자 프로토콜을 통틀어 이터레이션 프로토콜이라고 한다.</p>
<ul>
<li>반복 가능 프로토콜(이터러블 프로토콜) : 객체에 <code>Symbol.iterator</code> 메소드가 구현되어 있다. 이 메소드의 반환값은 반복자 프로토콜을 준수하는 객체여야 한다. 이 프로토콜을 만족하면 객체를 <code>for of</code>문으로 순회할 수 있으며, 배열 구조 분해 할당으로 배열화할 수 있다.</li>
<li>반복자 프로토콜(이터레이터 프로토콜) : 객체에 <code>next</code> 메소드가 구현되어 있다. 이 메소드의 반환값은 <code>{value, done}</code> 형태의 객체여야 하다.</li>
</ul>
<h2 id="저자는-왜-제네레이터를-까고-있는가">저자는 왜 제네레이터를 까고 있는가?</h2>
<blockquote>
<p>자바스크립트의 제네레이터는 엉성한 객체 지향 프로그래밍의 결과물입니다.</p>
</blockquote>
<p>자바스크립트 제네레이터 객체를 사용하는 문법이 비직관적이기 때문이다. 저자는 자바스크립트의 제네레이터 문법을 만들 때 엉성한 객체 지향 프로그래밍의 패러다임을 버리지 못했다고 지적한다.</p>
<pre><code class="language-js">const iterator = sequence(1, 4, 1);
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2</code></pre>
<p>보는 바와 같이, 자바스크립트에서 수동으로 제네레이터 객체를 소비해 값을 얻기 위해서는 (이터레이터).next().value로 객체의 내용을 2번 추적해야 한다. 이 얼마나 번거롭고 비직관적인가! 자바스크립트의 제네레이터 문법을 모르는 사람들은 next가 무엇을 하는 것이며 왜 그 값의 value를 참조해야 하는지 모를 것이다.</p>
<pre><code class="language-js">function* sequence(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
    for(let value=start; value&lt;end; value+=step)
    {
        yield value;
    }
}</code></pre>
<p>저자는 제네레이터 함수를 만드는 yield 문의 구조적 문제에 대해서도 지적한다. yield문은 근본적으로 명령을 중단하고 다시 시작한다는 의미를 갖고 있기 때문에, 제네레이터 객체가 한 번 소비될 때에만 집중하는 것이 아니라, 제네레이터 객체의 전체의 절차에 집중하게 되는 명령형 프로그래밍에 더 가깝게 프로그래밍하게 된다. 당연히, 여러 번 소비되는 제네레이터에 대해서는 for문과 같은 반복문을 사용할 수밖에 없고, 이것이 아름다운 프로그래밍을 하지 못한다고 저자는 지적한다. (저자는 for문과 같은 반복문을 좋게 보지 않는다.)
또한, 함수를 중단하고 재시도한다는 yield문의 특성상 실행 흐름의 추적이 어려워져서 예측 불가능한 프로그램이 생성될 수 있다는 문제도 있다고 한다.</p>
<h3 id="대안-클로저-제네레이터">대안-클로저 제네레이터</h3>
<pre><code class="language-js">function closerSequence(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
    let state = start; // initial value
    return function()
    {
        let value = state;
        state += step;
        if(state &gt; end) return; // generator end condition
        return value; // generator yield value
    }
}

const iterator = closerSequence(1, 4, 1);
console.log(iterator()); // 1
console.log(iterator()); // 2</code></pre>
<p>저자는 대안으로 클로저를 이용한 제네레이터 기능을 제안한다. 제네레이터를 클로저로 구현한다면, 자바스크립트의 제네레이터 문법에서 다음의 두 문제가 해결된다.</p>
<ol>
<li>제네레이터 객체를 소비하는 동작이 직관적이게 변한다. 상태가 있는 제네레이터 객체의 메소드를 호출하여 객체의 상태를 변경하고 결과값의 value를 호출하는 것보다, 그냥 제네레이터 객체를 소비한다는 동작으로 간단하게 변한다. 내부의 상태 등은 제네레이터 내부에 숨겨져 있다.</li>
<li>제네레이터 함수를 만들 때, 한 번 소비할 때의 동작에만 집중할 수 있게 된다. 불필요한 for문은 제거되고, 이전의 상태와 현재 동작에만 집중하여 프로그래밍할 수 있게 된다.</li>
</ol>
<p>물론, 클로저를 이용한 제네레이터의 구현은 자바스크립트의 클로저와 재귀함수에 대한 이해가 있어야 하며, 절차적으로 사고하는 초보 개발자들이 이해하기 어렵다는 문제는 있다. 또한, 자바스크립트의 이터레이션 프로토콜을 전혀 준수하지 않기 때문에, 바닐라 자바스크립트의 for of 문이나 배열 구조 분해 할당 등 유용한 기능을 사용하려면 별개의 변환 과정을 거쳐야 한다. 변환 함수는 다음에 소개하겠다.</p>
<h3 id="클로저-제네레이터-변환-함수">클로저 제네레이터 변환 함수</h3>
<pre><code class="language-js">function generatorToCloser(generator)
{
    return function()
    {
        return generator.next().value;
    }
}

const arr = [2025, 1, 3];
const iter = generatorToCloser(arr[Symbol.iterator]());

iter(); // 2025
iter(); // 1
iter(); // 3
iter(); // undefined</code></pre>
<p>간단하게 바닐라 제네레이터 / 클로저 제네레이터 변환 함수를 만들어 봤다.
우선, 바닐라 제네레이터 객체를 클로저 제네레이터 함수로 변환하는 함수다. 인자로 바닐라 제네레이터 객체를 받으며, 각각의 제네레이터 소비 시에는 <code>generator.next().value</code>를 호출해 소비하는 것이 전부다.</p>
<pre><code class="language-js">function* closerToGenerator(generator)
{
    let yielder = generator();
    while(yielder !== undefined)
    {
        yield yielder;
        yielder = generator();
    }
}

function closerSequence(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
    let state = start; // initial value
    return function()
    {
        let value = state;
        state += step;
        if(state &gt; end) return; // generator end condition
        return value; // generator yield value
    }
}
const iter = closerToGenerator(closerSequence(0, 5, 1));
[...iter] // [0, 1, 2, 3, 4]</code></pre>
<p>다음으로, 클로저를 바닐라 제네레이터 객체로 전환하는 제네레이터 함수다. 클로저 제네레이터를 인자로 받아, 바닐라 제네레이터 객체를 반환한다.
클로저 제네레이터를 값이 undefined가 나올 때까지 반복 호출하나, 값이 나왔을 때 yield문으로 제네레이터 외부로 값을 내보낸다. 이를 이용하면 클로저로 우아하게 제네레이터 코드를 작성하면서도 바닐라 자바스크립트의 이터러블 관련 기능도 누릴 수 있게 된다.</p>
<h2 id="클로저-제네레이터의-예제와-해설">클로저 제네레이터의 예제와 해설</h2>
<p>이 파트에서는 &lt;자바스크립트는 왜 그 모양일까?&gt;에 소개된 클로저 제네레이터 팩토리 함수를 각각 소개하고(본인의 스타일로 다시 구현했기에 원본 서적의 코드와 다를 수 있다.), 바닐라 자바스크립트의 제네레이터 문법으로는 어떻게 구현되는지 알아보도록 하겠다.</p>
<h3 id="integer">integer</h3>
<pre><code class="language-js">function integer(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
    let state = start;
    return function()
    {
        let value = state;
        state += step;
        if(step &gt; 0 &amp;&amp; state &gt; end) return;
        if(step &lt; 0 &amp;&amp; state &lt; end) return;
        return value;
    }
}</code></pre>
<p>Python의 range와 비슷하게, 등차순열을 반환하는 제네레이터를 생성한다. from에서 시작하고, step만큼 이동하며, 만약 값이 end 값을 넘어간다면 제네레이터를 종료한다.</p>
<p>해설하자면, integer 팩토리 함수가 호출될 때, state 변수(와 integer 함수의 매개변수들)가 저장되어 있는 객체를 생성한다. 이후, 제네레이터 함수가 반환되는데, 제네레이터 함수는 integer 팩토리 함수가 생성한 state 변수가 있는 객체를 참조하고, state 변수의 값을 바꿀 수 있다. 
이후 제네레이터 함수가 한 번 호출될 때마다, 다음을 실행한다.</p>
<ol>
<li>이전의 상태를 임시 값 value에 저장한다.</li>
<li>상태를 갱신한다. </li>
<li>step과 state, end 값을 참조하여, 상태가 종료값의 범위를 넘어가면 undefined를 반환한다.</li>
<li>그렇지 않다면, value를 반환한다.</li>
</ol>
<p>이러한 동작을 통해, 제네레이터 함수가 호출될 때마다 함수가 참조하는 state 상태(함수가 생성될 떄 같이 생성되었다)를 변경시키고, 함수가 호출될 때마다 상태를 변경시키면서 순열을 반환하게 된다.</p>
<p>똑같은 동작을 바닐라 자바스크립트의 generator 문법으로 구현할 수 있다.</p>
<pre><code class="language-js">function* integer(start=0, end=Number.MAX_SAFE_INTEGER, step=1)
{
    let state = start;
    while(true)
    {
        let value = state;
        state += step;
        if(step &gt; 0 &amp;&amp; state &gt; end) return;
        if(step &lt; 0 &amp;&amp; state &lt; end) return;
        yield value;
    }
}</code></pre>
<p>이 동작은 위의 클로저 제네레이터와 최대한 비슷하게 변환한 것이다. <code>return function()</code> 부분을 <code>white(true)</code>로, <code>return value</code>를 <code>yield value</code>로 변환하면 다를 바 없다. 무한루프가 일어나는 것으로 보이지만 실행 시 무한루프는 일어나지 않는데, 제네레이터 객체가 한 번 소비될 때마다 <code>yield value</code> 또는 <code>return</code>문에서 멈추기 때문이다.</p>
<h3 id="element">element</h3>
<pre><code class="language-js">function element(arr, generator = integer())
{
    return function(...args)
    {
        let index = generator(...args);
        if(index === undefined) return;
        return arr[index];
    }
}</code></pre>
<p>배열을 순회하는 제네레이터를 생성한다. 기본적으로 배열을 인자로 받으며, 배열의 원소를 순차적으로 순회하는 순열을 생성한다.</p>
<p>이 제네레이터는 2번째 인자로 다른 제네레이터를 받을 수 있는데, 다른 제네레이터의 결과를 배열의 해당하는 원소로 매핑시키는 역할을 수행한다. 일종의 함자와 같은 역할이라고 할 수 있겠다. 만약 제네레이터에 배열처럼 map 메소드가 존재한다면, 다음과 같은 느낌일 것이다.</p>
<pre><code class="language-js">generator.map( item=&gt;arr[item] )</code></pre>
<blockquote>
<p><strong>함자란?</strong> : 간단히 말하면, 집합에 함수를 통째로 적용시켜서 다른 집합으로 바꾸는 것이다. 이 때 집합의 각 원소 간 상관관계는 보존된다. 더 간단히 말하면 그냥 <code>map</code> 연산이라고 생각하면 된다. </p>
<p>이 설명은 매우 단순화된 설명으로, 자세히 말하면 다음과 같다.
우리가 말하는 오브젝트가 있다. 오브젝트는 자연수나, 정수나, 문자열이나, 벡터나, 혹은 다른 집합이 될 수도 있다. 오브젝트와 오브젝트를 함수처럼 연관시킬 수도 있는데, 이를 범주론에서는 사상이라고 한다. 범주는 오브젝트와 사상을 모은 것이라고 할 수 있다.
여기서 우리는 <strong>범주를 다른 범주로 바꾸는 관계</strong>를 생각할 수 있을 것이다. 그걸 함자라고 하자. <strong>함자는 대상을 다른 대상으로 바꾸는 것은 사상과 같으나, 사상 역시 다른 사상으로 바꿀 수 있다.</strong> 여기서 사상을 다른 사상으로 바꾼다는 것은 원소에 함자를 먼저 적용시킨 뒤 적용시키는 함수와, 원소에 함수를 먼저 적용한 뒤 함자를 적용하는 것이 같음을 의미한다.
함자는 항등사상(자기 자신을 반환하는 함수와도 같다)을 보존하며, 사상 합성(함수의 합성과도 같다) 역시 보존한다.</p>
<p>자바스크립트의 map 연산을 함자와 매우 유사한 역할을 하는 것로 볼 수 있는데, map 연산은 배열의 원소를 다른 원소로 치환하며, 배열의 원소 간 순서 관계를 사상이라고 생각한다면, 원소 간 순서는 변하지 않으므로 구조를 보존하면서 사상을 치환한 것이라고 할 수 있다. </p>
</blockquote>
<p>똑같은 동작을 바닐라 자바스크립트의 generator 문법으로 구현할 수 있다.</p>
<pre><code class="language-js">function* element(arr, generator = integer())
{
    for( let index of generator )
    {
        yield arr[index];
    }
}</code></pre>
<p>이 함수를 활용하여, 객체의 key를 순회하는 제네레이터 팩토리 함수를 추가로 만들 수도 있다.</p>
<pre><code class="language-js">function keys(obj)
{
    return element(Object.keys(obj));
}</code></pre>
<h3 id="property">property</h3>
<pre><code class="language-js">function property(obj, generator = element(Object.keys(obj)))
{
    return function(...args)
    {
        let key = generator(...args);
        if(key === undefined) return;
        return [key, obj[key]];
    }
}</code></pre>
<p>Object.entries와 비슷하게, key값과 value값의 튜플을 반환하는 제네레이터를 생성한다. 기본적으로 객체를 인자로 받으며, 객체의 key값을 순회하여 key와 value의 엔트리를 만든다. property 함수는 입력한 제네레이터 함수를 <code>[item, obj[item]]</code>과 같은 관계로 매핑한다.</p>
<p>똑같은 동작을 바닐라 자바스크립트의 generator 문법으로 구현할 수 있다.</p>
<pre><code class="language-js">function* property(obj, generator = element(Object.keys(obj)))
{
    for( let index of generator )
    {
        yield [index, arr[index]];
    }
}</code></pre>
<h3 id="collect">collect</h3>
<pre><code class="language-js">function collect(generator, arr = [])
{
    return function(...args)
    {
        let value = generator(...args);
        if(value === undefined) return;
        arr.push(value);
        return value;
    }
}</code></pre>
<p>이 제네레이터 함수는 제네레이터를 그냥 실행하는 것과 동일하나, 사이드 이펙트를 발동시킨다. 제네레이터가 한 번 실행될 때마다, 인자로 받은 배열에 제네레이터의 결과를 추가한다. 함수 외부의 데이터를 변경시키는 사이드 이펙트가 발생하기 때문에 썩 좋은 설계라고 하기는 어렵다.</p>
<p>이 함수의 목적은 배열에 제네레이터의 결과를 저장하기 위함인데, 이렇게 변경한다면 순수성을 유지할 수 있을 것이다.</p>
<pre><code class="language-js">function pureCollect(generator)
{
    let arr = [];
    return function(...args)
    {
        let value = generator(...args);
        if(value === undefined) return;
        arr = [...arr, value];
        return arr;
    }
}</code></pre>
<p>이 제네레이터 함수는 제네레이터를 지금까지 진행했던 제네레이터의 결과값으로 매핑시킨다. 결과적으로 제네레이터를 배열로 변환시키는 역할을 하긴 한다.
제네레이터를 순회할 때마다 새로운 배열 객체가 생성되어서 심각한 가비지 컬렉션을 일으킬 수 있지만, 적어도 순수성은 지킬 수 있다.</p>
<p>다른 방식으로 생각한다면, 배열로 축적한다는 것은 배열의 reduce 메소드와 비슷한 연산이라고 생각할 수도 있지 않을까? reduce와 비슷한 연산을 하도록 제네레이터를 확장해 보았다.</p>
<pre><code class="language-js">function reduce(generator, reducer, initial)
{
    let accumulator = initial;
    return function(...args)
    {
        let value = generator(...args);
        if(value === undefined) return;
        accumulator = reducer(accumulator, value);
        return accumulator;
    }
}

// 제네레이터의 값을 배열로 누적시키기 위해서는...
let harvester = reduce(generator, function(array, cur) {
    array.push(cur);
    return array;
}, []);

// 보다 엄밀한 순수성을 위해서는...
let pureHarvester = reduce(generator, function(array, cur) {
    return [...array, cur];
}, []);
</code></pre>
<p>이 제네레이터 함수는 원본 제네레이터 함수를 리듀서를 적용시킨 누산 결과값으로 변환시킨다. 구체적으로는, 다음과 같은 동작을 한다.</p>
<ol>
<li>accumulator 상태를 선언한 뒤, initial 값으로 초기화한다.</li>
<li>각 제네레이터가 실행될 때마다, 원본 제네레이터를 실행한 뒤, 값이 undefined면 제네레이터를 종료한다.</li>
<li>accumulator 상태와 제네레이터의 결과값을 기반으로 누산 연산을 수행하고, 값을 반환한다.</li>
</ol>
<p>이제 reduce 제네레이터는 보다 순수해졌으며, 비순수성의 도입은 온전히 개발자의 몫이 되었다.</p>
<p>위의 collect 팩토리 함수와 reduce 팩토리 함수의 바닐라 generator 문법 버전은 다음과 같다.</p>
<pre><code class="language-js">function* collect(generator, arr)
{
    for(let item of generator)
    {
        arr.push(item);
        yield item;
    }
}

function* reduce(generator, reducer, initial)
{
    let accumulator = initial;
    for(let item of generator)
    {
        accumulator = reducer(accumulator, item);
        yield accumulator;
    }
}</code></pre>
<h3 id="repeat">repeat</h3>
<pre><code class="language-js">function repeat(generator)
{
    if(generator() === undefined) return;
    return repeat(generator);
}

// 반복문 버전(더글라스 크락포드는 이 버전을 싫어할 것입니다)
function repeat(generator)
{
    while(generator() !== undefined) {};
}</code></pre>
<p>제네레이터는 순열이므로, 순열을 반복해야 한다. 이 함수는 제네레이터도, 제네레이터 팩토리도 아니지만, 제네레이터를 반복해서 완전히 소비시키는 함수다. 반환값은 없다.
저자는 <del>반복문을 싫어하기에</del> 재귀를 이용했는데, 그 중에서도 return문에 함수의 실행값을 집어넣는 꼬리 재귀를 사용하였다. 하지만, 애석하게도 대부분의 자바스크립트 엔진은 <em>꼬리 재귀 최적화를 지원하지 않기에</em>, 많은 양의 반복이 요구되는 제네레이터를 넣으면 스택 오버플로가 뜰 것이다.</p>
<p>바닐라 자바스크립트에서는 그냥 <code>for of</code> 문 써서 반복시키면 된다.</p>
<h3 id="harvest">harvest</h3>
<pre><code class="language-js">function harvest(generator)
{
    const result = [];
    repeat(collect(generator, result));
    return result;
}</code></pre>
<p>드디어 제네레이터를 배열로 만드는 함수가 등장했다. harvest 함수는 collect 함수와 repeat 함수의 조합으로, 제네레이터 함수가 소비되면서 result 배열에 그 결과가 저장되고, 완전히 소비되면 배열을 반환하는 형태다.</p>
<pre><code class="language-js">function harvest(generator)
{
    const result = [];
    const reducer = function(arr, value)
    {
        arr.push(value);
        return arr;
    }
    repeat(reduce(generator, reducer, result));
    return result;
}</code></pre>
<p>이건 reduce 함수를 사용한 형태다. 배열을 선언한 뒤, 제네레이터의 값을 배열로 누적시킨 뒤 이를 반복하여 값을 반환한다.</p>
<p>바닐라 자바스크립트에서는 그냥 배열 구조 분해 할당을 사용하면 된다.</p>
<h3 id="limit">limit</h3>
<pre><code class="language-js">function limit(generator, count = 1)
{
    let num = 0;
    return function(...args)
    {
        if(num &gt;= count) return;
        num += 1;
        return generator(...args);
    }
}</code></pre>
<p>제네레이터의 최대 호출 가능 횟수를 제한시키는 제네레이터를 생성한다. num이라는 내부 상태를 갖고, 제네레이터가 한 번 호출될 때마다 num의 값을 증가시킨다. 만약 미리 설정한 count보다 num 상태가 크거나 같으면 즉시 return하고, 그렇지 않다면 원본 제네레이터를 소비한다.</p>
<p>똑같은 동작을 바닐라 자바스크립트의 generator 문법으로 구현할 수 있다.</p>
<pre><code class="language-js">function* limit(generator, count = 1)
{
    let num = 0;
    for(let item of generator)
    {
        if(num &gt;= count) return;
        num += 1;
        yield item;
    }
}</code></pre>
<h3 id="filter">filter</h3>
<pre><code class="language-js">function filter(generator, predicate)
{
    return function filterGenerator(...args)
    {
        let value = generator(...args);
        if(value === undefined) return;
        if(predicate(value)) return value;
        return filterGenerator(...args);
    }
}

// 반복문 버전
function filter(generator, predicate)
{
    return function(...args)
    {
        let value = generator(...args);
        while(value !== undefined &amp;&amp; !predicate(value))
        {
            value = generator(...args);
        }
        return value;
    }
}</code></pre>
<p>제네레이터와 predicate(술어 함수: boolean을 반환하는 함수)를 인자로 받아, 조건을 만족하는 제네레이터의 결과만 필터링하는 새 제네레이터를 생성한다. 이 제네레이터의 동작은 호출될 때마다 다음과 같이 이루어진다.</p>
<ol>
<li>우선 원본 제네레이터를 한 번 소비한다.</li>
<li>현재 값이 undefined이면(제네레이터가 끝났으면) undefined를 반환한다.</li>
<li>현재 값이 조건을 만족하면 해당 값을 반환한다.</li>
<li>조건을 만족하지 않는다면, <strong>제네레이터가 조건을 만족할 때까지 함수를 반복하여, 원본 제네레이터를 계속 소비</strong>한다.</li>
</ol>
<p>4번의 동작으로 인해, 제네레이터가 반환한 현재 값이 조건에 만족하지 않는 경우 그 단계를 스킵하는 효과가 있으며, 이는 조건에 만족될 때까지 계속 반복된다.</p>
<p>이것은 바닐라 자바스크립트의 제네레이터 문법으로 다음과 같이 구현할 수 있다.</p>
<pre><code class="language-js">function filter(generator, predicate)
{
    for(let item of generator)
    {
        if(predicate(item)) yield item;
    }
}</code></pre>
<p>바닐라 제네레이터 문법으로 구현된 부분은 <code>return filterGenerator(...args)</code>와 같이 재귀하는 부분에 대응하는 부분이 따로 보이진 않는데, for문과 조건문이 제네레이터를 소비하고, 조건에 맞지 않을 시 계속 반복하는 것을 내포하고 있기 때문이다.</p>
<h3 id="concat">concat</h3>
<pre><code class="language-js">function concat(...generators)
{
    const generatorGenerator = element(generators);
    let currentGenerator = generatorGenerator();
    return function concatGenerator(...args)
    {
        if(currentGenerator === undefined) return;

        let value = currentGenerator(...args);
        if(value !== undefined) return value;

        currentGenerator = generatorGenerator();
        return concatGenerator(...args);
    }
}</code></pre>
<p>여러 개의 제네레이터를 인자로 받아, 제네레이터를 순차적으로 실행한다. 도중 제네레이터가 끝나면 다음 순서의 제네레이터를 실행한다.
이 제네레이터의 동작은 다음과 같이 이루어진다.</p>
<ol>
<li>제네레이터 목록을 기반으로, 제네레이터를 순회하는 제네레이터(generatorGenerator)를 생성한다. generatorGenerator는 소비될 때마다 다음 제네레이터를 반환한다.</li>
<li>첫 번째 제네레이터를 currentGenerator 상태에 저장한다.</li>
<li>호출될 때마다, 현재 제네레이터 상태가 없다면, 즉 모든 제네레이터가 끝났다면 제네레이터를 종료한다.</li>
<li>현재 제네레이터를 소비해서 값을 얻는다.</li>
<li>값이 undefined가 아니라면 값을 반환한다.</li>
<li>그렇지 않다면, currentGenerator를 다음으로 넘긴 뒤, currentGenerator가 값이 있는 제네레이터일 때까지 반복한다.</li>
</ol>
<p>6번으로 인해, 제네레이터가 끝났다면 빈 제네레이터를 스킵하고, 비지 않은 제네레이터가 있을 조건에 맞을 때까지 함수를 반복시킨다.</p>
<pre><code class="language-js">function* innerGenerator()
{
    yield 1;
    yield 2;
    yield 3;
}

function* outerGenerator()
{
    yield &quot;Hello!&quot;;
    yield* innerGenerator();
    yield &quot;World!&quot;;
}

[...outerGenerator]; //[&quot;Hello!&quot;, 1, 2, 3, &quot;World!&quot;]</code></pre>
<p>바닐라 자바스크립트의 제네레이터 문법에서는 <code>yield*</code>라는, 다른 제네레이터에 결과를 위임하는 문법이 있다. 이걸 이용하면, 제네레이터 함수 내에서 다른 제네레이터의 결과물을 순차적으로 반환할 수 있다.</p>
<p>이를 활용하여, concat 제네레이터 팩토리 함수를 바닐라 자바스크립트의 제네레이터 문법으로 아주 쉽게 구현할 수 있다.</p>
<pre><code class="language-js">function* concat(...generators)
{
    for(let generator of generators)
    {
        yield* generator;
    }
}</code></pre>
<p>참고로 <code>yield*</code>문을 사용하지 않고는 이중 for of문을 이용하여 다음과 같이 구현할 수 있다.</p>
<pre><code class="language-js">function* concat(...generators)
{
    for(let generator of generators)
    {
        for(let item of generator)
        {
            yield item;
        }
    }
}</code></pre>
<h3 id="join">join</h3>
<pre><code class="language-js">function join(func, ...generator)
{
    return function()
    {
        let consumed = generator.map( gen=&gt;gen() );
        if(consumed.every( value=&gt;value===undefined )) return;
        return func( ...consumed );
    }
}</code></pre>
<p>여러 개의 제네레이터와 한 개의 함수를 받아, 제네레이터를 한 번에 소비하고 소비된 값에 대한 함수의 결과를 반환하는 제네레이터를 생성한다. concat 제네레이터 팩토리가 제네레이터를 순차적으로 실행한다면, join 제네레이터 팩토리는 여러 개의 제네레이터를 병렬적으로 실행시키는 셈이 된다. 추가로, 제네레이터의 결과를 함수에 적용시키는 꼴이 되므로, <strong>인자로 1개의 제네레이터를 넣는다면 마치 map과 같은 효과</strong>를 얻을 수 있다.</p>
<p>이 제네레이터는 실행될 때마다 다음을 수행한다.</p>
<ol>
<li>제네레이터 배열을 한 번에 실행하여, 제네레이터를 실행한 결과로 매핑한다.</li>
<li>만약 모든 제네레이터 배열이 끝났다면, 제네레이터를 종료한다.</li>
<li>제네레이터 실행 결과를 함수에 넣은 값을 반환한다.</li>
</ol>
<p>1번으로 인해 여러 제네레이터를 동시에 소비할 수 있으며, 3번으로 인해 여러 제네레이터 함수가 <code>func(...gen)</code>과 같은 형태로 매핑된다.</p>
<p>앞서 join 제네레이터 팩토리가 매핑의 역할을 한다고 말했으므로, 비슷하게 제네레이터를 매핑시키는 element, property를 join을 이용해 재구성할 수 있다.</p>
<pre><code class="language-js">function element(arr, generator = integer())
{
    return join( (index)=&gt;arr[index], generator );
}
function property(obj, generator=element(Object.keys(arr)))
{
    return join( (key)=&gt;[key, arr[obj]], generator );
}</code></pre>
<p>이것은 바닐라 자바스크립트의 제네레이터 문법으로 다음과 같이 구현할 수 있다.</p>
<pre><code class="language-js">function* join(func, ...generator)
{
    while(true)
    {
        let consumed = generator.map( gen=&gt;gen.next() );
        if(consumed.every( ({done})=&gt;done===true ) ) return;
        yield func( ...consumed.map( ({value})=&gt;value ) );
    }
}</code></pre>
<p>바닐라 자바스크립트의 제네레이터 함수는 undefined가 묵시적인 끝으로 취급되는 클로저형 제네레이터와는 달리, undefined가 값이더라도 제네레이터가 끝이 아닐 수 있다.</p>
<h3 id="번외---재귀적-제네레이터는-어떻게-구현할까">번외 - 재귀적 제네레이터는 어떻게 구현할까?</h3>
<pre><code class="language-js">function* permutation(arr, count, accumulator=[])
{
    if(count &lt;= 0) {
        yield accumulator;
        return;
    }
    for(let i=0; i&lt;arr.length; i++)
    {
        const rest = arr.toSpliced(i, 1);
        yield* permutation(rest, count-1, [...accumulator, arr[i]]);
    }
}</code></pre>
<p>자바스크립트의 제네레이터 문법 중 yield* 문을 적절히 활용하면, 재귀적인 제네레이터를 구현할 수 있다. 위의 코드는 arr 중 count만큼 뽑는 순열을 생성하는 제네레이터 코드인데, 동작은 다음과 같다.</p>
<ol>
<li>count가 0보다 작거나 같으면, 누산된 결과를 그대로 반환한다.</li>
<li>그렇지 않다면, arr의 각 원소에 대해, 다음을 반복한다.</li>
<li>arr에서 원소 하나를 제거한 나머지 배열 rest를 만든다.</li>
<li>rest와 count-1, 그리고 누산된 결과를 추가해서 새로운 순열을 그대로 반환시킨다. 이 때, 반환되는 순열의 꼴은 <code>[기존 accumulator, arr[i], permutation(rest, count-1)]</code>과도 같을 것이다.</li>
</ol>
<pre><code class="language-js">function permutation(arr, count)
{
    if(count &lt;= 0) return ()=&gt;undefined;
    if(count === 1) return join(value=&gt;[value], element(arr));

    const childPermutation = arr.map( (item, i)=&gt;{
        const semiPermutation = permutation(arr.toSpliced(i,1), count-1);
        return join(value=&gt;[item, ...value], semiPermutation);
    } );
    return concat(...childPermutation);
}</code></pre>
<p>이것의 경우, <code>yield*</code>문이 concat으로 변환되며, 조건에 맞는 자기 자신 제네레이터를 연결하는 것으로 해결할 수 있다.
위의 permutation 제네레이터가 아래의 코드로 어떻게 변환되었는지 알아보자.</p>
<ol>
<li><p>위의 코드는 재귀적인 permutation으로 나온 제네레이터와, arr[i]를 합성시킬 수 없어서, 임의로 accumulator라는 매개변수를 넘겨서 작성되었다. 만약 제네레이터를 매핑하는 map 함수가 있다면, 다음과 같이 코드를 변경시킬 수 있다.</p>
<pre><code class="language-js">function* permutation(arr, count)
{
 if(count &lt;= 0) return;
 if(count === 1)
 {
     yield* arr.map( value=&gt;[value] );
     return;
 }
 for(let i=0; i&lt;arr.length; i++)
 {
     const rest = arr.toSpliced(i, 1);
     yield* map(permutation(rest, count-1), (value)=&gt;[arr[i], ...value]);
 }
}</code></pre>
<p>내부적으로만 쓰면서 외부의 결과를 방해할 우려가 있는 accumulator는 제거되었으며, map 함수로 제네레이터를 변화시킴으로써 좀 더 재귀 제네레이터를 명확히 이해하게 되었다. 
종료 조건도 변화되었는데, count가 0 이하일 경우에는 즉시 종료, count가 1일 경우에는 배열의 각 원소에 배열을 씌운 결과를 그대로 제네레이터 형태로 반환하도록 했다.</p>
</li>
<li><p>1번에 의해 변환된 코드를 차례대로 변환해 보자. 우리가 이제부터 만들 제네레이터 팩토리 함수는 제네레이터처럼 행동하는 함수를 반환한다.
우선 <code>if(count &lt;= 0) return;</code>의 경우, 제네레이터가 즉시 종료되므로, <code>()=&gt;undefined</code>를 반환하도록 바꾼다.</p>
</li>
<li><p><code>if(count === 1)</code> 부분의 경우, 순회 가능한 arr을 <code>value=&gt;[value]</code>로 변환한 결과를 반환하고 있다. 이는, element(arr)에 <code>value=&gt;[value]</code>를 매핑한 결과로 변환할 수 있다. <code>if(count === 1) return join(value=&gt;[value], element(arr));</code>로 변환한다.</p>
</li>
<li><p>대망의 나머지 부분이다. <code>yield* map(permutation(rest, count-1), (value)=&gt;[arr[i], ...value]);</code>를 반복해서 반환하는 것에서 볼 수 있듯이, 제네레이터를 순차적으로 실행한다. 우리는 이걸 <code>concat</code> 팩토리 함수를 이용해 바꿀 것이다.
<code>map(permutation(rest, count-1), (value)=&gt;[arr[i], ...value])</code>이라는 결과가 각각의 배열의 원소에 대해 반복되고 있으므로, 원래 배열에 대해 map 함수를 이용하여 제네레이터를 생성한다.
permutation 팩토리 함수가 제네레이터 함수를 반환한다고 하면, <code>permutation(arr.toSpliced(i,1), count-1)</code>은 제네레이터다. 여기에, 제네레이터를 매핑하는 join 함수를 이용해 arr을 <code>join(value=&gt;[item, ...value], semiPermutation)</code>으로 매핑해준다.</p>
</li>
<li><p>이렇게 변환된 제네레이터 배열을 순차적으로 실행해야 하므로, concat 팩토리 함수에 넣는다. concat 팩토리 함수는 제네레이터 함수를 반환하므로 문제가 없다.</p>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Context API에 관한 오해]]></title>
            <link>https://velog.io/@lybell_4rt/Context-API%EC%97%90-%EA%B4%80%ED%95%9C-%EC%98%A4%ED%95%B4</link>
            <guid>https://velog.io/@lybell_4rt/Context-API%EC%97%90-%EA%B4%80%ED%95%9C-%EC%98%A4%ED%95%B4</guid>
            <pubDate>Wed, 31 Jul 2024 01:58:21 GMT</pubDate>
            <description><![CDATA[<h2 id="contextprovider의-상태가-바뀌면-모든-하위-컴포넌트가-리렌더링된다">Context.Provider의 상태가 바뀌면 모든 하위 컴포넌트가 리렌더링된다?</h2>
<pre><code class="language-jsx">const MyContext = createContext();

function Child1()
{
  console.log(&quot;child 1 rerender&quot;);
  return &lt;div&gt;Child 1&lt;/div&gt;
}
function Child2()
{
  console.log(&quot;child 2 rerender&quot;);
  const light = useContext(MyContext);
  return &lt;div&gt;{light ? &quot;light&quot; : &quot;dark&quot;}&lt;/div&gt;
}

function App() {
  console.log(&quot;app rerender&quot;);
  const [light, setLight] = useState(true);
  const [count, setCount] = useState(0);

  return (
    &lt;&gt;
      &lt;MyContext.Provider value={light}&gt;
        &lt;Child1 /&gt;
        &lt;Child2 /&gt;
      &lt;/MyContext.Provider&gt;
      &lt;div onClick={()=&gt;setLight( l=&gt;!l )}&gt;변화&lt;/div&gt;
      &lt;div onClick={()=&gt;setCount( l=&gt;l+1 )}&gt;{count}&lt;/div&gt;
    &lt;/&gt;
  )
}</code></pre>
<p>이런 코드를 짰을 때, light 상태를 변화시키면 자식 컴포넌트가 모두 리렌더링되는 경험을 볼 수 있을 것이다. 그래서 사람들은 &quot;Context Provider의 value가 바뀌면 자식이 모두 리렌더링되는구나! 렌더링에 안 좋겠네?&quot;라고 생각하게 될 것이다.</p>
<p>물론, 이 코드에서 &quot;변화&quot; 버튼을 누르면 Child1과 Child2가 리렌더링되는 건 맞으나, Child1이 리렌더링되는 까닭은 Context Provider가 바뀌기 때문이 아니라, <strong>그냥 App의 state가 바뀌었기 때문</strong>이다. 부모 컴포넌트의 state가 바뀌면 부모가 렌더링되고, 자식도 같이 렌더링되기 때문이다.</p>
<h2 id="contextprovider의-상태와-렌더링은-무관하다">Context.Provider의 상태와 렌더링은 무관하다?</h2>
<pre><code class="language-jsx">const MyContext = createContext();

function Child1()
{
  console.log(&quot;child 1 rerender&quot;);
  return &lt;div&gt;Child 1&lt;/div&gt;
}
function Child2()
{
  console.log(&quot;child 2 rerender&quot;);
  const light = useContext(MyContext);
  return &lt;div&gt;{light ? &quot;light&quot; : &quot;dark&quot;}&lt;/div&gt;
}

const MemoWrapper = memo( ()=&gt;{
  return &lt;&gt;
    &lt;Child1 /&gt;
    &lt;Child2 /&gt;
  &lt;/&gt;
});

function App() {
  console.log(&quot;app rerender&quot;);
  const [light, setLight] = useState(true);
  const [count, setCount] = useState(0);

  return (
    &lt;&gt;
      &lt;MyContext.Provider value={light}&gt;
        &lt;MemoWrapper /&gt;
      &lt;/MyContext.Provider&gt;
      &lt;div onClick={()=&gt;setLight( l=&gt;!l )}&gt;변화&lt;/div&gt;
      &lt;div onClick={()=&gt;setCount( l=&gt;l+1 )}&gt;{count}&lt;/div&gt;
    &lt;/&gt;
  )
}</code></pre>
<p>이 경우에는 변화 버튼을 클릭했을 때, MemoWrapper에 props가 변화하지 않아서 MemoWrapper가 리렌더링되지 않음에도 불구하고, Child2는 리렌더링되고 있다. MyContext.Provider의 value가 바뀌었고, Child2가 MyContext를 소비하고 있기 때문이다.</p>
<p>즉, useContext는 다음의 2개의 역할을 한다.</p>
<ul>
<li>Context 매개변수를 받아서, 가장 가까운 부모의 Context.Provider를 찾아서, 그것의 value를 반환한다.</li>
<li>해당 Context.Provider를 구독하고, value가 바뀔 때 상태 변화와 관계없이 리렌더링을 수행한다.</li>
</ul>
<h2 id="정리">정리</h2>
<p>Context API를 이용해서 상태를 관리할 때, 하위 컴포넌트를 memo로 감쌌을 때와 감싸지 않았을 때의 동작은 다음과 같다.</p>
<ul>
<li>memo로 감싸지 않는 경우<ul>
<li>Provider 자신 : state가 바뀌었기 때문에 리렌더링된다.</li>
<li>컨텍스트를 소비하는 컴포넌트 : 부모 컴포넌트가 렌더링되기 때문에(+context.provider의 value가 바뀌었기 때문에) 리렌더링된다.</li>
<li>컨텍스트를 소비하지 않는 컴포넌트 : <strong>부모 컴포넌트가 렌더링되기 때문에, 리렌더링된다.</strong></li>
</ul>
</li>
<li>memo로 감싸는 경우<ul>
<li>Provider 자신 : state가 바뀌었기 때문에 리렌더링된다.</li>
<li>컨텍스트를 소비하는 컴포넌트 : context.provider의 value가 바뀌었기 때문에 리렌더링된다.</li>
<li>컨텍스트를 소비하지 않는 컴포넌트 : 리렌더링되지 않는다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[functional-lit-element 분석해보기]]></title>
            <link>https://velog.io/@lybell_4rt/functional-lit-element-%EB%B6%84%EC%84%9D%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@lybell_4rt/functional-lit-element-%EB%B6%84%EC%84%9D%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 13 Jul 2024 11:11:52 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>작성자는 웹 컴포넌트에 관심이 많다. 웹 컴포넌트는 브라우저에서 컴포넌트 단위로 프론트엔드 개발을 할 수 있게 하는 웹 API의 집합이라고 할 수 있는데, 웹 컴포넌트를 잘 쓰면 어떠한 라이브러리 없이도 컴포넌트의 마운트, 언마운트 등 생명주기를 관리할 수 있으며, _다양한 웹 프레임워크에서 일관되게 동작할 수 있다_는 이점이 있다.</p>
<p>하지만, 단점은 웹 컴포넌트의 기반이 되는 custom element API가 모던 리액트 생테계가 좋아하는 함수형 방식이 아닌, 클래스 기반으로 이루어져 있다는 것이다. 그래서 <strong>함수형 컴포넌트를 custom element로 바꿔서 등록</strong>시키는 라이브러리를 만들어보고 싶었는데, 감이 잡히지 않았다.</p>
<p>그래서, custom element 기반 웹 라이브러리인 lit을 함수형으로 쓸 수 있게 만드는, <strong>functional-lit-element의 구조를 분석</strong>해보기로 했다. <a href="https://lit.dev/">lit</a>은 HTMLElement를 상속하는 LitElement를 상속하는 클래스형 컴포넌트를 만들어서 컴포넌트 기반 프로그래밍을 할 수 있는 경량화된 라이브러리다. functional-lit-element는 함수형으로 짠 컴포넌트를 LitElement로 변경해주는데, 이 코드를 분석하면 작성자가 원하는 함수형 컴포넌트를 custom element로 바꿔서 등록하는 아이디어를 찾을 수 있을 것이라고 생각했다.</p>
<h3 id="functional-lit-element가-뭔데">functional-lit-element가 뭔데?</h3>
<p><a href="https://github.com/evtaylor/functional-lit-element/tree/master">functional-lit-element: A wrapper for LitElement which provides an API similar to React functional components.</a></p>
<pre><code class="language-jsx">import {LitElement, html} from &quot;lit-html&quot;;
import functionalElementFactory from &quot;functional-lit-element&quot;;

const functionalElement = functionalElementFactory(LitElement);

function MyComponent({className, caption}, {useState})
{
    const [counter, setCounter] = useState(0);
    return html`
        &lt;div class=${className}&gt;
            &lt;div class=&quot;caption&quot;&gt;${caption}&lt;/div&gt;
            &lt;div @onClick=${()=&gt;setCounter(state=&gt;state+1)}&gt;${counter}&lt;/div&gt;
        &lt;/div&gt;
    `;
}

const todoItemComponent = functionalElement(MyComponent);
customElements.define(&quot;todo-item&quot;, todoItemComponent);</code></pre>
<p>간단히 말하면, 위의 코드와 같이 리액트의 함수형 컴포넌트스럽게 짠 함수형 컴포넌트를 LitElement를 상속하는 클래스형 컴포넌트로 변환시키는 라이브러리다.</p>
<p>이 라이브러리는 functionalElement라는 함수를 제공하며, 해당 함수에 인자로 들어가는 함수형 컴포넌트는 다음의 규칙을 갖는 함수여야 한다.</p>
<ul>
<li>매개변수는 2개다.<ul>
<li>첫 번째 매개변수에는 props 객체가 들어간다. 객체 구조 분해 할당으로 각 props를 가져올 수 있다.</li>
<li>두 번째 매개변수에는 hooks 객체가 들어간다. 객체 구조 분해 할당으로 실제로 사용하는 hooks를 가져올 수 있다. 리액트에서는 미리 라이브러리에서 export된 훅을 가져다가 썼지만, 여기서는 함수의 인자로 가져와야 한다는 것이 차이점이다.</li>
</ul>
</li>
<li>반환값은 Lit이 렌더링할 수 있는 값이어야 한다.<ul>
<li>주로 Lit이 제공하는 html 태그드 템플릿을 이용한 값이 들어간다.</li>
</ul>
</li>
</ul>
<p>이렇게 정의한 함수형 컴포넌트를 functionalElement 함수에 집어넣고, 반환된 LitElement를 customElements.define으로 브라우저에 등록시키면 된다.</p>
<h2 id="엔트리포인트">엔트리포인트</h2>
<p>이 라이브러리가 반환하는 메인 함수인 functionalElement는 functionalElementProvider라는 함수의 리턴값으로, 매개변수로 LitElement와 createUseState 등 useState의 생성 함수를 받는다.</p>
<p>이 라이브러리의 브라우저 버전은 functionalElementProvider를 사용하는 것은 동일하나, LitElement를 사용자가 외부에서 주입해야 한다는 점이 차이점이다. 브라우저에서 모듈식 프로그래밍을 하지 않는 경우를 대비하기 위해, FunctionalLitElement 라이브러리를 LitElement를 번들링하는 것을 피하도록 설계된 것으로 보인다.</p>
<p>functionalElementProvider는 dependency를 받아, <strong>render 함수를 받아 클래스형 Lit Component를 반환하는 함수</strong>를 리턴하는 고차함수다. 이것의 리턴값을 즉, 우리는 <strong>render 함수를 받아 클래스형 Lit Component를 반환하는 함수</strong>를 사용하게 되는 것이다.</p>
<h2 id="functionalelementprovider">functionalElementProvider</h2>
<p>render, props, styles 매개변수를 받아서 LitComponent를 extends한 클래스를 반환한다. 이 중 props와 styles는 반환하는 클래스의 static 메소드를 정의하는 데 쓰인다.</p>
<h3 id="getprops">getProps()</h3>
<pre><code class="language-jsx">const getProps = (element) =&gt; Object.keys(props).reduce((renderProps, propName) =&gt; {
    renderProps[propName] = element[propName];
    return renderProps;
}, {});</code></pre>
<p>getProps는 element 객체를 받아서 객체를 반환한다. 이 함수에서는 외부에 있는 props 객체의 key값을 통해 객체를 쌓아올리는 과정을 수행하는데, 새 객체에 기존 element 객체의 value를 그대로 대입하고 있다.</p>
<p>getProps 함수는 기존 엘리먼트에 존재하는 여러 프로퍼티 중 props의 key로 지정된 프로퍼티를 필터링하는 함수라고 보면 될 것 같다. 이 함수는 실제 커스텀 엘리먼트 객체 중 어떤 것이 의미 있는 props인지 정의하기 위해 필요하다.</p>
<h3 id="static-properties">static properties()</h3>
<pre><code class="language-jsx">static get properties() {
    const dynamicState = {
        _dynamicState: {type: Object},
        _dynamicReducerState: {type: Object},
        _context: {type: Object}
    };
    return Object.assign({}, dynamicState, props);
}</code></pre>
<p>어렵지 않다. 내부에서 정의하는 _dynamicState, _dynamicReducerState, _context 상태와 기존에 존재하던 상태 정의 객체인 props 객체를 합쳐서 새 객체로 만들어 준다.</p>
<p>스프레드 연산자로 더 쉽게 표현하면 이렇게 표현할 수 있다.</p>
<pre><code class="language-jsx">static get properties() {
    const dynamicState = {
        _dynamicState: {type: Object},
        _dynamicReducerState: {type: Object},
        _context: {type: Object}
    };
    return {...dynamicState, ...props};
}</code></pre>
<h3 id="constructor">constructor()</h3>
<pre><code class="language-jsx">constructor() {
    super();
    this._dynamicReducerState = new Map();
    this._dynamicState = new Map();
    this._context = new Map();

    this._reducerStateKey = 0;
    this._stateKey = 0;
    this._effectKey = 0;
    this._effects = [];
    this._effectsState = new Map();

    this._contextListeners = new Map();
    this._contextParents = new Map();

    this._createHooks();
}

_createHooks() {
    this._hooks = {};
    this._hooks.useState = createUseState(this);
    this._hooks.useEffect = createUseEffect(this);
    this._hooks.useReducer = createUseReducer(this);
    this._hooks.useContext = createUseContext(this);
    this._hooks.provideContext = createProvideContext(this);
}</code></pre>
<p>hook에서 쓰이는 내부 상태들을 초기화해서 클래스의 인스턴스에 할당한다.</p>
<p>바닐라 자바스크립트에서 리액트의 함수형 컴포넌트를 구현하는 데에 있어서 주안점은 다음과 같다.</p>
<ul>
<li>어떻게 하면 서로 다른 컴포넌트에서 useState를 호출했을 때 일관된 값을 가져올 수 있을까?</li>
<li>어떻게 하면 여러 useState를 호출했을 때 일관된 값을 가져올 수 있을까?</li>
</ul>
<p>functionalLitElement는 근본적으로 클래스인 LitElement를 반환하기 때문에, 클래스의 이점인 메소드의 실행 주체를 기억하는 것을 활용할 수 있다. functionalLitElement는 첫번째 문제를 createUseState에 this를 넘겨주는 방식으로 해결한다. createUseState가 반환하는 함수는 this, 즉 클래스의 인스턴스 자기 자신을 알고 있게 된다.</p>
<p>두 번째 문제는 전통적인 함수형 컴포넌트 구현 방식처럼, hook을 호출한 순서를 따로 기록해두는 방식으로 각각의 hook을 구분하는 방식으로 해결한다. (<code>this._stateKey = 0;</code>)</p>
<h4 id="참고--바닐라-자바스크립트로-리액트-훅-구현-방법">참고 : 바닐라 자바스크립트로 리액트 훅 구현 방법</h4>
<blockquote>
<p>1번의 경우는 여러 가지 방법이 있을 수 있다. 공통적으로는 함수형 컴포넌트를 평가할 때 렌더링하는 함수의 실행 순서와 useState의 호출 순서가 동일하다고 가정하고, 외부 변수에 저장하는 아이디어를 채택하고 있다.</p>
<p>가장 간단하게는, useState의 호출 순서를 기록해두고, 호출된 순서에 맞는 값을 가져오는 것이다. 하지만, 중간에 useState를 호출하는 컴포넌트가 제거되거나 추가될 경우 state가 꼬일 수 있다는 단점이 있다.</p>
<p>좀 더 복잡하게는, 컴포넌트를 렌더링할 때 자식 엘리먼트로 함수의 실행 값이 아닌 함수를 넘겨주고, 함수에 상태값을 바인딩하거나, 함수와 부모 함수를 기반으로 key값을 만들어서 그것을 기반으로 값을 가져오거나, 더 복잡하게는 현재 가상 dom의 렌더링 트리를 모사한 상태 트리를 만들어서 트리 상에서 일치하는 상태를 가져오는 방법도 있다.</p>
<p>2번의 경우 1번과 유사하게, 동일 컴포넌트에서 hook의 호출 순서가 동일하다고 보장하고(실제로 react에서는 조건부로 hook을 생성하는 조건부 hook을 금지하고 있다.), 호출 순서를 기록하는 방식으로 구현할 수 있다.</p>
<pre><code class="language-jsx">    function ReactMaker()
    {
        const stateStore = new ReactStateMap();
        let currentRenderKey = null;
        let currentRenderStatePointer = 0;
        let rootElement = null;
        useState(initialKey)
        {
            const state = stateStore.get(currentRenderKey, currentRenderStatePointer);
            currentRenderStatePointer++
            function setState(newValue)
            {
                stateStore.set(currentRenderKey, currentRenderStatePointer, newValue);
                render();
            }
            return [state, setState];
        }
        render()
        {
            currentRenderKey = makeKey(rootElement.renderFunction);
            currentRenderStatePointer = 0;
            rootElement.renderFunction();
            // 자식 엘리먼트 렌더링
        }
        createReact(el, rootElement_)
        {
            rootElement = rootElement_;
            // 초기화 함수
        }
        return {useState, createReact};
    }</code></pre>
</blockquote>
<pre><code>&gt; 
&gt; 대충 짭액트가 저렇게 구성된다고 보면 될 것 같다.(실제 리액트는 저렇게 안 생겼을 확률이 매우 높다.)


### render()

```jsx
render() {
    super.render();
    this._resetHooks();
    const hooks = {
        useState: this._hooks.useState,
        useEffect: this._hooks.useEffect,
        useReducer: this._hooks.useReducer,
        useContext: this._hooks.useContext,
        provideContext: this._hooks.provideContext,
    };
    const template = render(getProps(this), hooks);
    this._runEffects();
    return template;
}</code></pre><p>render 함수는 state가 변경되면 실행되는 메소드다. 다음의 방식으로 실행된다.</p>
<ol>
<li>hook 상태를 초기화한다. useState, useEffect 등 훅을 구분하기 위한 포인터를 0으로 초기화한다.</li>
<li>this._hooks에 저장된 훅을 별도의 hooks 객체로 복사한다. <code>const hooks = {…this._hooks}</code>와 동일하다.</li>
<li>함수형 컴포넌트를 실행한다. 이 때, getProps 함수로 반환된 props와 hooks를 인자로 넣는다. 이를 통해 내부에서 클래스의 인스턴스로 바인딩된 훅을 함수형 컴포넌트 내에서 사용할 수 있으며, hook이 호출되면 클래스 인스턴스의 상태를 변경시킨다.</li>
<li>함수형 컴포넌트가 호출되면서 useEffect로 등록된 이펙트를 실행한다.</li>
<li>함수형 컴포넌트가 반환한 템플릿을 반환한다. 이를 기반으로 LitElement 내부에서 실제 dom으로 렌더링을 수행한다.</li>
</ol>
<h3 id="_runeffect">_runEffect()</h3>
<pre><code class="language-jsx">_runEffects() {
    return this._effects.map((effect) =&gt; {
        return new Promise((resolve, reject) =&gt; {
            try {
                return resolve(effect());
            } catch (e) {
                reject(e);
            }
        });
    });
}</code></pre>
<p>useEffect의 실행 과정에서 등록된 사이드이펙트 함수들을 실행한다. 따로 effect()의 결과 함수를 어딘가에 저장하는 부분은 없으므로, 실제 리액트의 useEffect에 있는 cleanup code는 딱히 구현하지 않은 걸로 보인다.</p>
<p>실제 useEffect에서는 dependency 배열을 받아서 해당 배열의 값이 달라질 때 콜백 함수를 실행시키는데, 여기에서 이 부분 처리 로직은 useEffect 내부에 있다.</p>
<h2 id="createusestate">createUseState</h2>
<pre><code class="language-jsx">export const createUseState = (element) =&gt; {

    const getState = (key) =&gt; {
        return element._dynamicState.get(key);
    };

    const setState = (key, value)  =&gt; {
        const newState = new Map(Array.from(element._dynamicState.entries()));
        newState.set(key, value);
        element._dynamicState = newState;
    };

    // useState hook
    return (defaultValue = null) =&gt; {
        const currentStateKey = element._stateKey;

        if (getState(currentStateKey) === undefined) {
            setState(currentStateKey, defaultValue);
        }

        const changeValue = (newValue) =&gt; {
            setState(currentStateKey, newValue)
        };

        element._stateKey++;
        return [getState(currentStateKey), changeValue];
    };
};</code></pre>
<p>동적인 상태를 관장하는 useState 훅은 반환하는 createUseState 함수는 3개의 부분으로 구성된다.</p>
<h3 id="getstate">getState</h3>
<p>컴포넌트의 _dynamicState 맵에서 key를 찾아서 value를 반환한다.</p>
<h3 id="setstate">setState</h3>
<p>원래 맵의 엔트리를 복사해서 새 Map 객체를 만든 뒤, 해당 Map에 key - value를 설정해서 새 값을 넣는다. 이후, 컴포넌트의 _dynamicState 프로퍼티를 해당 Map 객체로 변경한다.</p>
<h3 id="usestate">useState</h3>
<p>컴포넌트의 _stateKey를 받아와 별도의 변수에 저장하고, <strong>현재 호출 중인 useState와 실제 데이터를 매칭</strong>시킨다. 참고로 별도의 변수에 저장하지 않으면 changeValue를 호출할 때 changeValue가 가리키는 변수의 실제 주소를 보장하지 못한다는 문제가 생긴다.</p>
<p>만약 <strong>실제 데이터가 undefined이면 defaultValue로 초기화</strong>한다. 단, 어떤 미치광이가 setState(undefined)를 호출해서 undefined를 상태로 넣으면 다시 defaultValue가 된다는 잠재적인 문제가 있다.</p>
<p>changeValue는 내부에서 정의한 setState를 newValue 하나로 호출하도록 래핑해준다. 단 실제 리액트의 useState처럼 이전 함수에 따라 새 값으로 변경하는 건 불가능한 듯하다.</p>
<p><strong>모든 함수의 내부 실행이 끝나면, element의 _stateKey를 1 올린다</strong>. 참고로 _stateKey는 함수형 컴포넌트가 실행되기 직전에 0으로 초기화되므로 <strong>같은 useState는 같은 __stateKey를 갖는 것이 보장</strong>된다.</p>
<h3 id="리팩토링">리팩토링?</h3>
<p><strong>사실 key는 int형 자료형이라서, _dynamicState를 굳이 map으로 선언할 필요는 없어 보인다</strong>. 배열을 사용하고, 실제 useState처럼 함수를 받을 수 있도록 바꿔보면 다음과 같이 바꿀 수 있다.</p>
<pre><code class="language-jsx">export const createUseState = (element) =&gt; {

    const getState = (key) =&gt; {
        return element._dynamicState[key];
    };

    const setState = (key, value) =&gt; {
        const newState = [...element._dynamicState];
        if(typeof value === &quot;function&quot;) newState[key] = value(newState[key]);
        else newState[key] = value;
        element._dynamicState = newState;
    };

    // ... 원래 것과 동일
}</code></pre>
<h2 id="createuseeffect">createUseEffect</h2>
<pre><code class="language-jsx">export const createUseEffect = (element) =&gt; {

    const getEffectState = (key) =&gt; {
        return element._effectsState.get(key);
    };

    const setEffectState = (key, value)  =&gt; {
        const newState = new Map(Array.from(element._effectsState.entries()));
        newState.set(key, value);
        element._effectsState = newState;
    };

    const addEffect = (effect) =&gt; {
        element._effects.push(effect);
    };

    const effectStateHasChanged = (stateToWatch, key) =&gt; {
        const effectState = getEffectState(key);
        if (effectState.length === 0) {
            return false;
        }

        for(let i = 0; i &lt; stateToWatch.length; i++) {
            if (effectState[i] !== stateToWatch[i]) {
                return true;
            }
        }
        return false;
    };

    // useEffect hook
    return (effect, stateToWatch = undefined) =&gt; {
        // If no state to watch, run effect every time
        if (stateToWatch === undefined) {
            addEffect(effect);
            return;
        }

        const currentKey = element._effectKey;

        // If first time useEffect called, set the effect state to watch and run effect
        if (getEffectState(currentKey) === undefined) {
            setEffectState(currentKey, stateToWatch);
            addEffect(effect);
            return;
        }

        // see if state has changed to decide whether effect should run again
        if (effectStateHasChanged(stateToWatch, currentKey)) {
            addEffect(effect);
        }

        setEffectState(currentKey, stateToWatch);
        element._effectKey++;
    }
};</code></pre>
<p>상태 변경에 따른 사이드이펙트를 관장하는 useEffect 훅은 다음의 방식으로 동작한다.</p>
<h3 id="useeffect">useEffect</h3>
<ul>
<li>stateToWatch가 undefined이면 모든 경우에 사이드이펙트가 실행되므로, 사이드이펙트를 컴포넌트에 등록시키고 함수를 종료한다.</li>
<li>그렇지 않을 경우, 이전 의존성 배열과 현재 의존성 배열의 변경을 비교해야 한다. 해당 부분은 컴포넌트의 _effectState 프로퍼티에 저장되어 있으며, 동일한 useEffect마다 동일한 _effectState를 참조해야 하므로, _effectKey라는 참조 변수를 이용한다.<ul>
<li>의존성 배열의 상태가 없으면, useEffect가 가리키는 의존성 배열를 초기화하고 이펙트를 무조건 등록시킨다.</li>
<li>effectStateHasChanged 함수를 호출하여, 직전 의존성 배열과 현재 의존성 배열을 비교한다. 만약 다르다면, 이펙트를 등록시킨다.</li>
<li>의존성 배열을 현재 의존성 배열으로 초기화하고, _effectKey를 1 증가시킨다.</li>
</ul>
</li>
</ul>
<h3 id="geteffectstate">getEffectState</h3>
<p>컴포넌트에 등록된 _effectsState Map에서 key에 해당하는 값을 가져온다. _effectsState 맵은 함수형 컴포넌트가 가지고 있는 현재 의존성 배열들의 상태가 저장되어 있다.</p>
<h3 id="seteffectstate">setEffectState</h3>
<p>컴포넌트에 등록된 _effectsState Map을 기반으로 내용이 동일한 새 맵 객체를 생성하고, key에 해당하는 값을 변경시킨 뒤 _effectState를 변경시킨다.</p>
<p>참고로 _effectState의 변경에 따라서 컴포넌트가 리렌더링되는 건 아니다. LitComponent의 properties에 등록되어 있지 않기 때문이다.</p>
<h3 id="addeffect">addEffect</h3>
<p>컴포넌트의 _effects 배열에 인자로 받은 함수를 추가한다.</p>
<p>참고로 _effects 배열은 컴포넌트의 render 메소드가 호출될 때 매번 빈 배열로 초기화되므로, 직전 렌더링 시간에 등록된 사이드이펙트가 또 실행되지는 않는다.</p>
<h3 id="effectstatehaschanged">effectStateHasChanged</h3>
<p>컴포넌트의 _effectState Map에서 key의 직전 의존성 배열을 가져온다.</p>
<p>현재의 의존성 배열을 기준으로 반복문을 돌리면서 배열이 일치하는지를 파악한다.</p>
<p>만약 달라진 부분이 존재하면 true를 반환하고, 모든 배열의 원소가 일치하면 false를 반환한다.</p>
<p>참고로, 새롭게 생성된 의존성 배열의 원소가 없으면 false를 반환한다.</p>
<p>어떤 미치광이가 의존성 배열의 원소 수를 렌더링할 때마다 다르게 한다면 오류가 날 가능성이 높아지지만, 그 누구도 그렇게 함수형 컴포넌트를 짜지 않으므로 무시해도 좋다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[현대 소프티어 부트캠프 4기 합격 후기]]></title>
            <link>https://velog.io/@lybell_4rt/%ED%98%84%EB%8C%80-%EC%86%8C%ED%94%84%ED%8B%B0%EC%96%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-4%EA%B8%B0-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@lybell_4rt/%ED%98%84%EB%8C%80-%EC%86%8C%ED%94%84%ED%8B%B0%EC%96%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-4%EA%B8%B0-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Thu, 20 Jun 2024 08:26:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/d57e0a33-70e0-430e-b3a9-cf89afeb1b69/image.png" alt="">
오늘 오후 4시, 현대 소프티어 부트캠프 4기 합격 메일이 도착했다! 합격이다! 향후 있을 5기 지원자를 위해 간단히 내가 어떻게 준비했는지를 기록해보고자 한다.</p>
<h2 id="1차-코딩테스트">1차 코딩테스트</h2>
<h3 id="실제로-준비했던-것">실제로 준비했던 것</h3>
<p>여태까지 했던 것처럼 프로그래머스 2단계~3단계 문제를 풀면서 준비했다. 3기 후기를 보았을 때 난이도가 거지같이 나온다고 들었기에, 2단계 문제풀이보다 3단계 문제풀이에 집중했던 것 같다.</p>
<p>모의테스트를 보고, 구름 코딩테스트에 익숙해져야 한다고 느꼈다. 그래서 <a href="https://velog.io/@grap3fruit/%EA%B5%AC%EB%A6%84goorm-%EC%BD%94%ED%85%8C-javascript-%EB%A1%9C-%EC%9E%85%EB%A0%A5%EA%B0%92-%EB%B0%9B%EB%8A%94-%EB%B0%A9%EB%B2%95">자바스크립트 구름 입출력 코드</a>를 참조해서, 실제 <a href="https://devth-preview.goorm.io/exam/53763/devth-preview-kor/quiz/1?_ga=2.22476799.531264434.1598839678-1290480941.1598839678">구름 코딩테스트 환경</a>에서 예시 문제를 몇 개 풀어보면서 입출력에 익숙해지는 시간을 가졌다. </p>
<h3 id="회고하면서-어떻게-준비해야-좋았는가">회고하면서, 어떻게 준비해야 좋았는가</h3>
<p>실제로 코딩테스트 문제를 풀면서, 3기 후기에서 작성했던 것보다는 확실히 쉽게 나온 것 같다고 느껴졌다. 다익스트라 알고리즘 같은 고급 알고리즘을 요구하지는 않았고, 코딩테스트의 기본 유형에 익숙해지고 자유롭게 구사할 수 있을 정도로 준비하면 좋을 것이라고 생각했다. 추가로 구현 문제는 프로그래머스 3단계 수준까지 풀어보는 게 좋다. 특히, 구름 코딩테스트의 특성상 문자열 형태의 데이터가 입출력으로 주어지기에, 문자열을 다루는 유형의 코딩테스트 문제도 풀어보는 게 좋을 듯하다.</p>
<p>요약하자면,</p>
<ul>
<li>구현, 문자열 : 프로그래머스 3단계 수준</li>
<li>나머지 : 프로그래머스 2~2.5단계 수준</li>
</ul>
<p>정도를 목표로 준비하면 될 것 같다.</p>
<h2 id="2차-소프트웨어-지식-테스트">2차 소프트웨어 지식 테스트</h2>
<h3 id="실제로-준비했던-것-1">실제로 준비했던 것</h3>
<p>이번 소프트웨어 지식 테스트에서는 네트워크, 데이터베이스, 운영체제, 프로그래밍, 아키텍처가 출제된다고 예고되었으며, 그것을 위주로 공부했다. 자료구조나 알고리즘은 나오지 않아서, 과감히 포기했다.</p>
<h4 id="공통">공통</h4>
<p><a href="https://velog.io/@sean2337/%ED%98%84%EB%8C%80%EC%9E%90%EB%8F%99%EC%B0%A8%EA%B7%B8%EB%A3%B9-%EC%86%8C%ED%94%84%ED%8B%B0%EC%96%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-3%EA%B8%B0-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0-FE#-%EC%A4%80%EB%B9%84-%EB%B0%A9%EC%8B%9D-1">이 블로그 포스트를 참조</a>해서, 어떻게 공부하면 될지 알아보았다. 책을 사는 건 금전적 문제가 있어서, 인터넷으로 공부할 수 있는 <a href="https://github.com/gyoogle/tech-interview-for-developer">gyoogle 블로그</a>와 <a href="https://github.com/WeareSoft/tech-interview">WeareSoft tech-interview</a>를 보면서 공부했다. 특히 gyoogle 블로그가 많은 도움이 되었다.
이 블로그를 기반으로 공부하면서, <strong>최소한 내가 여기서 다루는 것들을 이해하고 설명할 수 있으며</strong>, 설명된 내용을 보고 어떠한 용어인지 말할 수 있는 정도를 목표로 공부했다. 맨 처음에는 gyoogle 블로그를 훑으며 대략적인 흐름을 살펴보았고, 이후 세부적으로 공부할 때 각 내용을 외우면서 설명할 수 있을 정도로 공부했다.</p>
<p>인터넷에 돌아다니는 정처기 필기 문제도 한 번 풀어봤는데, 84점 나오더라. 와우...</p>
<h4 id="컴퓨터-구조">컴퓨터 구조</h4>
<p>앞서 말한 gyoogle 블로그나 <a href="https://hongong.hanbit.co.kr/%ec%bb%b4%ed%93%a8%ed%84%b0%ec%9d%98-4%ea%b0%80%ec%a7%80-%ed%95%b5%ec%8b%ac-%eb%b6%80%ed%92%88cpu-%eb%a9%94%eb%aa%a8%eb%a6%ac-%eb%b3%b4%ec%a1%b0%ea%b8%b0%ec%96%b5%ec%9e%a5/">혼공에서 준비한 이 포스트</a>를 기반으로 기초적인 흐름을 잡았다.</p>
<p>지엽적인 것들이 나올 수 있으므로, <a href="https://m.blog.naver.com/PostView.naver?blogId=regretduo&amp;logNo=222870126555&amp;fromRecommendationType=category&amp;targetRecommendationDetailCode=1000">이 블로그의 연습문제</a>를 훑어보면서 세부적으로 공부해야 하는 개념들을 익혔다. 지금 되살펴 보면, 이 블로그 연습문제를 봤었기에 내가 한 문제라도 맞힌 게 아닐까 싶다.</p>
<h4 id="운영체제">운영체제</h4>
<p>3기 후기에서 추천한 <strong>혼공컴운 책</strong>으로 공부했다. 이 책이 비전공자가 이해할 수 있게 쉽게 서술되어 있어서, 공부하기에 아주 좋았다. 나는 이 책을 읽으면서 이전 파트에서 나왔던 것들을 떠올리면서 적용하려고 했던 것 같다.
시간 관계상 나는 운영체제만 다루는 8장 이후부터 읽었었는데, 여러분들은 2장부터 읽기를 바란다.</p>
<h4 id="네트워크">네트워크</h4>
<p>단골로 나오는 OSI 7계층, TCP 3-way handshake, 4-way handshake는 네이버 부스트캠프에서 썼었던 학습정리로 복습하고, 처음 본 개념이었던 TCP의 흐름제어, 혼잡제어나 TLS/SSH handshake 위주로 공부했었다.
네트워크에서 지엽적인 것들이 나올 것 같아서, 네트워크 계층의 프로토콜(IP, ARP, NAT)이나 라우팅 프로토콜(RIP, OSPF, BSP)도 막판에 공부했었는데 거기서는 안 나오더라.</p>
<h4 id="데이터베이스">데이터베이스</h4>
<p>gyoggle 블로그와 WeareSoft tech-interview를 활용했다.
추가로, 데이터베이스로 SQL 명령어 시험 문제가 나올 수도 있을 것 같아서, 프로그래머스의 SQL 코딩테스트를 몇 개 풀어보면서 SQL 질의어의 기초를 배웠다. 근데 SQL 질의어가 나오지는 않았다더라.</p>
<h4 id="프로그래밍">프로그래밍</h4>
<p>워낙에 자신 있는 분야라서, 프로그래밍 하면 단골로 나오는 C언어의 증감 연산자와 포인터만 따로 복습했다. 근데 진짜 듣도 보도 못한 게 나오더라.</p>
<h3 id="회고하면서-어떻게-준비해야-좋았는가-1">회고하면서, 어떻게 준비해야 좋았는가</h3>
<p>내가 추천하는 공부법은 다음과 같다.</p>
<ul>
<li><strong>gyoogle 블로그로 기초 익히기</strong></li>
<li>혼공컴운으로 컴퓨터 구조, 운영체제 공부하기</li>
<li><a href="https://m.blog.naver.com/PostView.naver?blogId=regretduo&amp;logNo=222870126555&amp;fromRecommendationType=category&amp;targetRecommendationDetailCode=1000">이 블로그의 연습문제</a>를 풀면서 컴퓨터 구조를 세부적으로 공부하기</li>
<li>적어도 WeareSoft tech-interview에 나오는 질문은 답할 수 있게 준비하기</li>
<li>운영체제 관련해서는 계산 문제가 나올 수 있다. 개념을 이해하고 적용해보기</li>
<li>각 프로그래밍 언어의 특징적인 주요 문법(객체지향 위주로) 개념 간단하게나마 훝어보기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[소프티어 부트캠프 4기 2차 시험 후기]]></title>
            <link>https://velog.io/@lybell_4rt/%EC%86%8C%ED%94%84%ED%8B%B0%EC%96%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-4%EA%B8%B0-2%EC%B0%A8-%EC%8B%9C%ED%97%98-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@lybell_4rt/%EC%86%8C%ED%94%84%ED%8B%B0%EC%96%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-4%EA%B8%B0-2%EC%B0%A8-%EC%8B%9C%ED%97%98-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Mon, 10 Jun 2024 08:16:48 GMT</pubDate>
            <description><![CDATA[<h2 id="난이도">난이도</h2>
<p><strong>전공자라면 그렇게 어렵지는 않은 수준으로 출제</strong>된 것 같으나, 비전공자의 경우는 쉽지 않았을 겁니다. 단, 잘 외우지 않는 지엽적인 부분이 특정 파트에서 대충 3문제 정도 나왔던 걸로 기억합니다.</p>
<h2 id="문제-유형">문제 유형</h2>
<p>객관식, 단답형, 서술형이 나옵니다. <strong>하나의 보기를 주고 여러 개의 문제를 주거나, 이전 문제를 기반으로 꼬리 문제가 출제되는 유형이 굉장히 많이 나왔습니다</strong>. 개념 하나를 모르면 줄줄이 틀릴 수밖에 없습니다.</p>
<h2 id="출제-범위">출제 범위</h2>
<p><strong>네트워크, 데이터베이스, 프로그래밍 언어, 운영체제, 컴퓨터 구조, 이진법</strong>에서 출제되었습니다. 전반적으로 <a href="https://gyoogle.dev/blog/">https://gyoogle.dev/blog/</a> 이분 블로그에서 다루는 부분이 많이 출제되었던 것 같습니다. 단, 프로그래밍 언어 부분과 컴퓨터 구조 부분은 여기서 안 나왔어요. 컴퓨터 구조 부분은 전공책 참조하셔야 합니다.</p>
<p>아키텍처라고 하길래 소프트웨어 공학(설계나 테스트)과 관련된 건가 하고 그것도 공부했었는데, 그건 안 나오더라고요.</p>
<h2 id="후기">후기</h2>
<p>아는 것도 몇 개 있었으나, 헷갈리거나 애매한 문제들이 많아서 불안합니다. 대충 28문제에서 17문제 정도는 확실하게 맞는 것 같은데, 부분문제 포함 11문제나 되는 것에서 맞는지 틀린지 모르는 게 있어서 불안하긴 합니다.
그래도 부트캠프인 만큼 컷은 그렇게 높지는 않겠죠...</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[소프티어 부트캠프 4기 1차 합격 후기]]></title>
            <link>https://velog.io/@lybell_4rt/%EC%86%8C%ED%94%84%ED%8B%B0%EC%96%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-4%EA%B8%B0-1%EC%B0%A8-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@lybell_4rt/%EC%86%8C%ED%94%84%ED%8B%B0%EC%96%B4-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-4%EA%B8%B0-1%EC%B0%A8-%ED%95%A9%EA%B2%A9-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Fri, 31 May 2024 14:06:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/8222168b-b283-4ea1-8889-b01daa39d623/image.png" alt=""></p>
<h3 id="코딩테스트-후기">코딩테스트 후기</h3>
<p>오늘 오후 2시경, 현대자동차그룹에서 운영하는 소프티어 부트캠프 1차 합격 메일이 도착했다. 합격이다! 소프티어 부트캠프는 특이하게도 자소서 등을 보지 않고, 순수한 코딩과 소프트웨어 지식만으로 선발한다.</p>
<p>1차 시험은 코딩테스트였는데, 2시간 내에 5문제를 푸는 것이었다. 인터넷 사용은 허용되나, 직접적인 문제를 검색하거나, 생성형 AI를 사용하는 것은 금지되어 있다. 플랫폼은 구름 플랫폼을 사용했는데, 프로그래머스처럼 함수 기반 코테가 아닌, 백준처럼 표준입출력을 이용한 방식이어서 적응하기 애먹었던 기억이 있었다.</p>
<p>2기와 3기 후기에서 코딩테스트 난이도가 괴랄하다고 들었는데, 4기에는 그렇게 괴랄하지는 않았던 것 같다. 대충 난이도는 프로그래머스 2~2.5단계 수준이었다.
나는 4.2솔 정도 했고(나머지 0.2솔은 테케 포함한 기본 상황은 구현했으나 더 넓은 상황을 구현하지 못한 채로 코테가 종료되었음), 그 결과 1차 합격을 할 수 있었다.</p>
<h3 id="2차-준비">2차 준비?</h3>
<p>2차는 소프트웨어 지식 테스트로, CS지식에 대한 문제가 출제된다. 문제 유형은 객관식, 단답형, 서술형이며, 분야는 운영체제, 네트워크, 데이터베이스, 프로그래밍 언어, 소프트웨어 아키텍처로 구성된다고 한다. 1차와는 다르게 인터넷 검색 행위는 금지되며, 대신 A4용지 필기는 사용할 수 있다.</p>
<p>CS지식은 네이버 부스트캠프에서도 배운 게 있으나, 2년 전의 일이므로 거의 까먹었으므로 남은 1주일 동안 어떻게든 빠르게 학습해야 한다.
특이하게도 소프트웨어 아키텍처가 분야로 나오는데, 학부 시절에 수강했던 소프트웨어공학입문 수업에서 배운 것과 비슷한 분야가 나오지 않을까 싶다.</p>
<p>2기~3기 후기에 따르면 굉장히 지엽적인 것까지 튀어나오기에, 난이도가 매우 괴랄하다고 한다. 대체 어디까지 지엽적인 게 나오는 거지?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[405 비사이드 포텐데이 후기]]></title>
            <link>https://velog.io/@lybell_4rt/405-%EB%B9%84%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%8F%AC%ED%85%90%EB%8D%B0%EC%9D%B4-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@lybell_4rt/405-%EB%B9%84%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%8F%AC%ED%85%90%EB%8D%B0%EC%9D%B4-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Tue, 28 May 2024 07:39:28 GMT</pubDate>
            <description><![CDATA[<p>비사이드라는 서비스에서 진행하는 온라인 해커톤 &#39;포텐데이&#39;에 프론트엔드로 개발자로 참여해서, 5월 26일에 끝을 맺었다. 아래는 비사이드 포텐데이에 참여했던 후기다.</p>
<h2 id="발단-커리어-잘못-쌓았네">발단: 커리어 잘못 쌓았네...</h2>
<blockquote>
<p>작성된 프로젝트 경험들이 대부분 일반적인 웹 서비스를 만들고 유지 보수하는 것과 직접적인 연관이 없습니다. 어떻게 보면 &#39;전형적&#39;이라고 할 수 있는 형태의 웹 서비스를 만들고 운영하는 경험도 해보시는 것을 추천해 드립니다. 어느 기업에 지원하셨는지 모르겠으나, 대부분의 기업이 만드는 서비스와는 다소 다른 경험을 쌓으신 것 같습니다. 이력서를 읽는 사람 입장에서는 가장 기본적인 역량에 대한 검증이 어려운 상황이라고 생각합니다.
-왓에버 서류검토 중</p>
</blockquote>
<p>내 첫 취업 도전기는 혹독했다. 3~4월 동안 대략 17개 정도의 기업에 지원했는데, 서류는 단 하나만 붙었고, 나머지는 전부 서류 광탈이었다. 이력서에 무슨 문제가 있나 싶어서, 이력서 피드백 사이트를 찾아보던 중, 9000원으로 이력서 피드백을 해 준다는 <a href="https://whatever.community/mock-resume">왓에버 서류검토</a> 서비스를 찾아서 이용해 보았다. (+슥삭에서 운영하는 <a href="https://recruit.superpasshr.com/">슈퍼패스</a>의 무료 이력서 피드백도 이용해 보았으나, 무려 1달이 넘게도 이력서 피드백을 못 받고 있다. 사실상 껍데기만 남고 서비스를 종료한 것으로 추정되므로, 웬만해서는 9000원 내고 왓에버 서류검토를 이용하는 걸 추천한다.)</p>
<p>결과는 처참했다. 스스로 기술적인 도전들을 어필하여 기술적인 역량이 드러나는 이력서라고 자부하고 있었는데, 서류검토관은 다르게 보고 있었나 싶었다. 제일 충격적이었던 건 그간 쌓아왔던 경험들이 <strong><em>일반적인</em></strong> 프론트엔드 개발과는 거리가 멀었다는 것이었다.(이건 면접까지 갔었던 단 하나의 기업에서도 지적받은 내용이었다.) <del><strong>내 내다버린 2년이!</strong></del></p>
<p>그래서 일반적인 프론트엔드 프로젝트 경험을 쌓을 수 있는 사이드프로젝트를 알아보던 중, 우연히 비사이드 포텐데이를 알게 되어서 참여하게 되었다.</p>
<h2 id="포텐데이-그-10일-간의-기록">포텐데이, 그 10일 간의 기록</h2>
<h3 id="day--4">Day -4</h3>
<p>오후 5시에 포텐데이 참여자는 슬랙과 노션에 초대받았다. 이번 405 포텐데이에서는 기획자 16명, 디자이너 40명, FE 28명, BE 24명이 모였다.
포텐데이 참여자는 프로젝트 시작 시점 2일 전까지 직접 팀을 빌딩할 수 있고, 직접 팀을 빌딩하지 못한 참여자는 비사이드 운영 측에서 인공지능을 활용해 팀을 배정받는다.</p>
<p>생판 모르는 사람들이 팀을 꾸리는 것인 만큼, 이 시점에서는 팀을 꾸리기 위해서 자기 자신의 역량을 어필하는 게 중요했다. 어필하는 방법은 크게 2가지였다.</p>
<ul>
<li>비사이드 커리어 카드를 정성껏 작성하기</li>
<li>슬랙에 자신을 어필하는 글 올리기</li>
</ul>
<p>나는 비사이드 커리어 카드를 작성하여 이전 사이드프로젝트 경험과 역량을 어필하였고, 감사하게도 이를 좋게 봐 준 한 팀에서 컨택을 받아 합류하게 되었다. 이후 프론트엔드 한 분과 디자이너 한 분이 더 합류하여 기획 1명, 디자이너 2명, FE 2명, BE 1명으로 확정하였다.</p>
<p>해커톤류 프로젝트는 포텐데이 말고도 다른 연합동아리에서 해 본 적이 있으나, 그 때는 끝끝내 아무 팀도 구하지 못하고 끝났던 불쾌한 경험이 있었다. 개인적으로 비사이드 포텐데이가 팀 구성에 있어서 그 시절보다 좋았던 경험은 다음과 같았다.</p>
<ul>
<li>생판 모르는 사람들이 온라인으로 진행되는 만큼, 친목 등의 요소를 배제하고 순수한 역량만으로 팀을 꾸릴 수 있다.</li>
<li>팀을 못 꾸리는 사람을 위해 2가지의 안전장치가 더 마련되어 있다. 팀을 못 구한 사람은 운영 측에서 랜덤으로 잘 맞는 사람들을 배정하며, 구성원의 불균형 등으로 팀을 못 꾸린 경우는 다크호스 시스템을 운영하여 추가 영입을 노려볼 수 있다.</li>
</ul>
<h3 id="day--3-2">Day -3~-2</h3>
<p>미리 팀을 꾸린 팀은 이때부터 아이디어 선정을 시작하며, 나머지는 팀을 찾는 시간이다. 우리 팀의 경우 6명이서 각자 아이디어 브레인스토밍을 진행하여, 최종적으로는 자서전 작성을 돕는 서비스로 구성하기로 결정했다.</p>
<h3 id="day--1">Day -1</h3>
<p>이 시점은 직접 팀 빌딩이 끝나고, 비사이드 측에서 팀을 빌딩해주는 날이다. 오후 3시부터는 다크호스 영입을 시작한다. 디자이너가 타 부서보다 많기 때문에, 팀을 찾지 못하고 하차를 결정한 디자이너분들이 많이 있었다.</p>
<p>디자이너 한 분이 개인적 사정으로 인해 팀을 나갔고, 그 자리에 해당 디자이너분이 컨택했던, 협업 경험이 있는 다른 디자이너를 다크호스로 영입하였다.</p>
<p>따로 회의는 없었고, 기획 파트에서 아이디어를 구체화하는 시간을 가졌다.</p>
<h3 id="day-1">Day 1</h3>
<p>기획자분이 정리한 기획서를 기반으로, 기획안을 피드백하고 실제로 구현할 것들을 결정하는 시간을 가졌다. 백엔드 개발자분이 질문 위주로 초기 구현을 가자고 강력하게 주장하셨다.</p>
<p>FE 측에서는 개발 관련 컨벤션을 결정하고, 초기 개발환경을 세팅하였다. 빠르게 진행해야 한다는 특성을 살려, 개발 세팅이 간편한 vite를 번들러로 채택하였고, 또한 단기 개발의 특성으로 인해 FE 전원이 익숙한 자바스크립트를 이용하였다. 만약 장기 프로젝트라면 TS를 채택했을 것이다.</p>
<h3 id="day-23">Day 2~3</h3>
<p>다음 회의가 Day 4에 이루어지며, 개발 측에서는 할 수 있는 것이 없으므로 구현 자체가 확정된 로그인 기능의 와이어프레임을 구현하였다.</p>
<p>그 외에도 FE 측에서 라이브러리 사용에 대해 회의를 나누었고, 기술 스택을 결정하였다.</p>
<h3 id="day-4">Day 4</h3>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/def5bb1a-875c-498d-bf23-8909aa303a09/image.png" alt="">
기획자분께서 정리하신 IA(정보구조도, 웹∙앱 구축 시 필요한 화면과 메뉴의 정보 구조를 설계 및 정의하는 문서)와 디자이너분의 레퍼런스를 기반으로, 서로 피드백을 나누고 실제 구현 우선순위를 결정하였다. 위의 사진에서 파란색으로 되어 있는 부분이 최우선 구현 내용이다.</p>
<p>이 때, 자서전(폴더) 다수 생성 기능은 최소 기능에 필요하지 않다고 판단되어, 단 하나의 예시 폴더만 보여지도록 구성되었다.</p>
<p>이후 개발 측에서는 결정된 최소 기능 구현체를 바탕으로, 역할을 분담하였다. 나는 서비스 진입 초기 부분인 온보딩, 로그인/회원가입, 홈, 자서전 목록을 담당했다.</p>
<h3 id="day-56">Day 5~6</h3>
<p>와이어프레임이 Day 6에 구현되므로, 레퍼런스 기반으로 빠르게 핵심 기능 구현을 마무리하고, 디자인을 적용하기로 했다.
온보딩 페이지와 메인화면 자서전 목록 페이지에는 swiper.js 라이브러리가 적용되었는데, 직접 스와이핑 기능을 구현하면 그것이 기술적인 도전이겠지만 시간 관계상 스와이퍼 기능 구현에 시간을 다 쓸 것 같으므로, 라이브러리를 이용하기로 했다.
swiper.js는 이전에 stardew dressup에서도 사용한 적이 있었으나, 지금 swiper.js는 web component 기반의 11.1버전으로 업데이트된 상태라서 api가 달라졌다.</p>
<h3 id="day-7">Day 7</h3>
<p>결정된 와이어프레임과 API 명세서에 따라 구현체를 수정하는 시간을 가졌다. 결정된 와이어프레임과 API에 따라 변경된 구현체는 다음과 같다.</p>
<ul>
<li>5초에 1번씩 다른 사람의 질문을 표시하는 방식으로 변경<ul>
<li>이때, 애니메이션 도중에 이전 질문과 현재 질문이 같이 보여지는 부분이 존재해야 한다.</li>
</ul>
</li>
<li>사용자 로그인 확인을 매번 <code>/api/auth/check</code>를 호출하는 방식이 아닌, <code>api/login</code>에서 응답받은 key를 localStorage에 저장하고, 해당 key를 바탕으로 요청을 보낼 때 header의 Authorization 헤더에 Token (토큰)으로 넘기도록 변경</li>
<li><del>왜인지 모르겠으나</del> 비밀번호 확인을 백엔드로 넘기도록 변경</li>
</ul>
<p>추가로 내 파트가 생각보다 일찍 끝났기에 질문 답변 글쓰기에서 질문 목록을 받아오는 것도 구현하였다.</p>
<h3 id="day-8">Day 8</h3>
<p>이날은 내가 <a href="https://softeerbootcamp.hyundaimotorgroup.com/">소프티어 부트캠프</a> 코딩테스트를 보는 날이라서, 비사이드 포텐데이에 많이 참여하지는 못했다.</p>
<p>Day 8 회의에서 현재 구현된 것들을 살펴보고, 추가 구현 가능한 기능들을 추려서 구현체를 결정하였다. 이 때 구현체를 결정하는 기준은 다음과 같다.</p>
<ul>
<li>단기간에 디자인의 수정 없이 구현이 가능한가?</li>
<li>핵심 컨셉을 보여주는 데 충분한가?</li>
</ul>
<p>이를 바탕으로 로그아웃 기능과 AI 타래 생성 기능을 구현하기로 하였다. 다른 프론트엔드 파트가 아직 구현이 끝나지 않은 관계로, 내가 추가 기능을 구현하기로 했다.</p>
<h3 id="day-9">Day 9</h3>
<p>마감일이 얼마 남지 않은 만큼, 우선순위를 정하여 개발을 진행하였다. 내가 Day 9에서 맡은 파트는 추가 기능 구현, 맡은 파트의 그래픽 적용, 그리고 QA에서 나온 버그 수정이었다. 이 때, 추가 기능의 와이어프레임은 오후 12시 이후에, QA 내용은 오후 9시에 받을 수 있으므로, 다음과 같이 우선순위를 정했다.</p>
<ol>
<li>로그아웃 버튼 구현</li>
<li>이미 확정된 그래픽을 컴포넌트에 적용</li>
<li>AI 타래 생성 기능 구현</li>
<li>버그 수정</li>
</ol>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/8f601da9-c94e-460f-b771-0ad642c3f215/image.png" alt="">
오후 9시 이후부터는 배포된 프론트엔드 페이지를 기반으로, 각자 테스트를 돌려 QA를 진행하였다. 그 중 특정 플랫폼에서만 레이아웃이 틀어지거나 의도대로 작동되지 않는 문제도 있어서 까다로웠다. 기억에 남는 것은</p>
<ul>
<li>iOS에서 뒤로가기 버튼이 컴포넌트 중앙이 아닌 상단에 위치하는 버그</li>
<li>iOS에서 글 작성 란을 터치하면 확대되는 버그</li>
</ul>
<p>정도가 있었다.</p>
<h3 id="day-10">Day 10</h3>
<p>1차 버그 수정한 것을 서버에 반영한 뒤, 2차 QA를 진행했다. 간헐적으로 로그아웃 후 로그인이 진행되지 않는 버그나, 타래 수정 버튼 비활성화 요청 등이 있었으며, 이를 해결하였다.</p>
<p>최종적으로 오후 2시 12분에 최종 PR 머지를 진행하여, 마감시간 전에 완전한 서비스를 제출할 수 있었다.</p>
<h2 id="서비스-소개">서비스 소개</h2>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/ec797f16-3717-407d-b569-7219d783adf6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/c610912e-2f19-43ca-a426-50cc1a933bce/image.png" alt=""></p>
<blockquote>
<p>&#39;혼자서 간직하고 싶은 이야기&#39;가 있으신가요?
&#39;누군가에게 들려주고 싶은 이야기&#39;가 있으신가요?
&#39;아직 듣지 못한 부모님의 이야기&#39;가 궁금하신가요?
지금부터 인생을 돌아보며 AI와 함께 자서전을 작성하는 &#39;人타래, 쉽게 엮는 나의 이야기&#39;를 만나보실 수 있습니다!
150개 이상의 재미있는 땀부터 진지한 땀까지 준비되어 있으니 기대해주세요 👏
🤔 타래? 땀? 그게 뭔가요? 🤔
인타래에서는 거창해보이는 자서전 대신 &#39;타래&#39;, 형식적일것만 같은 질문 대신 &#39;땀&#39;을 준비했습니다.
더 쉽고, 친근하게 다가가기 위한 노력을 지켜봐주세요!
💁 인타래 주요 기능 💁</p>
</blockquote>
<ul>
<li>오늘의 한땀: 매일 다양한 땀을 보내드려요! 어떤 내용으로 타래를 채워나가야 할 지 고민하지 않아도 매일 조금씩 땀을 쓰며 타래를 만들어나갈 수 있습니다.</li>
<li>AI 자동 타래 작성: 땀이 충분히 쌓이면 AI가 목차 수립부터 내용 작성까지 도와드립니다. 글을 잘 못써도, 악필이어도 괜찮아요!<blockquote>
<p><a href="https://b.ssrocat.link/">인타래 서비스 바로가기</a> </p>
</blockquote>
</li>
</ul>
<p><strong>人타래, 쉽게 엮는 나의 이야기</strong>는 자서전을 쓰기 어려워하는 사람을 위한 인생 기록 작성, AI 자서전 작성 도우미 서비스이다. 사용자가 날마다 보여지는 질문에 대해 답변을 하여 &quot;땀&quot;을 쓰면, AI가 지금까지 작성한 &quot;땀&quot;들을 모아 &quot;타래&quot;라는 이름의 자서전으로 재구성해주는 서비스이다.</p>
<h4 id="서비스의-목적">서비스의 목적</h4>
<ul>
<li>모바일로 쉽고 꾸준하게 인생 질문들에 답을 하고 기록하며 자서전 소재를 쌓을 수 있도록 한다.</li>
<li>다른 사용자들의 다양한 답변과 자서전을 구경할 수 있도록 한다.</li>
<li>글을 잘 쓰지 못해도 생성형 AI를 통해서 질문 답변 기반으로 자서전을 자동으로 생성할 수 있다.</li>
</ul>
<h4 id="주요-기능">주요 기능</h4>
<ul>
<li>매일 조금씩 질문에 대한 답을 하며 자서전(타래) 소재를 쌓을 수 있는 <strong>오늘의 랜덤 질문(땀) 기능</strong></li>
<li>자유롭게 정해진 질문들을 선택해 자서전 소재를 쌓을 수 있는 <strong>다양한 땀 질문 쓰러가기 기능</strong></li>
<li>질문(땀)들을 엮어 자서전으로 엮는 <strong>AI 타래 작성 기능</strong></li>
<li>다른 사람들의 예시 질문을 메인화면에서 볼 수 있는 기능</li>
</ul>
<h2 id="결과---포텐데이-1pick-2위">결과 - 포텐데이 1pick 2위</h2>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/13f9e40a-7ee0-4f86-ba94-88c51a2d5e55/image.png" alt=""></p>
<p>405 비사이드 포텐데이에서 우리 팀의 서비스가 <strong>2등</strong>을 거머쥐는 성과를 이룰 수 있었다! 아쉽게도 1등은 되지 못했지만, 상위권의 성과를 낼 수 있어서 뿌듯했다.</p>
<h2 id="후기">후기</h2>
<h3 id="좋았던-점">좋았던 점</h3>
<ul>
<li><p><strong>이전 협업보다 FE끼리 원활하게 소통할 수 있었다.</strong> 이전 협업에서는 개발환경, 모킹, 데이터 페칭을 다루는 방식 등 엄밀하게 기술 스택을 결정하지 못하여, 서로 다른 개발 경험을 가진 FE 개발자 측에서 갈등이 있었다. 이번 협업에서는 이전 협업에서의 아쉬웠던 점을 바탕으로, 모킹, 데이터 페칭 등을 명백하게 정의하고, MSW에 대해 잘 모르던 다른 개발자에게 가이드라인을 제시해 좀 더 원활한 개발을 할 수 있었다.</p>
</li>
<li><p><strong>애자일 개발 방식을 체험할 수 있었다.</strong> 기획자나 디자이너가 먼저 기획을 작성하면 개발자가 그것을 따라가는 방식이 아닌, 한정된 시간 내에 주요 핵심 기능을 결정하고, 디자이너와 개발자가 병렬적으로 진행되는 방식으로 개발을 진행하였다. 와이어프레임, 디자인, API명세가 동시에 주어지기 때문에 프론트엔드 측에서는 기능구현 - 디자인 반영 - 백엔드 반영 - 그래픽 반영 - 추가 기능 구현 순으로 할 일이 많아진다는 단점은 있었으나, 빠르게 기능에 대한 피드백을 받을 수 있다는 이점이 있었다.</p>
</li>
</ul>
<h3 id="아쉬웠던-점">아쉬웠던 점</h3>
<ul>
<li><p>여전히 두드러진 기술적인 성장을 하지 못했다는 건 아쉬움이 남는다. 공유 기능이나 사진 입력 기능 등 프로젝트 내에서 시도해볼만 한 기술적인 내용은 있었으나, 한정된 시간 내에 프로토타입을 구현해야 하는 만큼 기본적인 CRUD 위주로 구현된 것은 아쉬웠다.</p>
</li>
<li><p><strong>프론트엔드와 백엔드와의 소통이 덜 이루어졌고, 탑다운 방식으로 전파된 것</strong>이 아쉬웠다. 프론트엔드는 신입 2명이고, 백엔드는 9년차 베테랑이라서 다가가기 어려운 점도 있었으며, FE와 BE끼리 API 명세서나 ERD 등을 협의하고 어떠한 데이터를 보여줄지 확정지어야 하는데, 그 부분에 대한 소통이 없는 게 아쉬었다.
실제로 이 점 때문에 API 명세서 합의가 늦어져, FE가 임의적으로 구현한 mock api와 실제 BE가 구현한 API와의 규격이 맞지 않아서 FE 측에서 일을 두 번 해야 했던 비효율성이 있었다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[리스트에서 보이는 부분만 렌더링하는 두 가지 방법]]></title>
            <link>https://velog.io/@lybell_4rt/%EB%A6%AC%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C-%EB%B3%B4%EC%9D%B4%EB%8A%94-%EB%B6%80%EB%B6%84%EB%A7%8C-%EB%A0%8C%EB%8D%94%EB%A7%81%ED%95%98%EB%8A%94-%EB%91%90-%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@lybell_4rt/%EB%A6%AC%EC%8A%A4%ED%8A%B8%EC%97%90%EC%84%9C-%EB%B3%B4%EC%9D%B4%EB%8A%94-%EB%B6%80%EB%B6%84%EB%A7%8C-%EB%A0%8C%EB%8D%94%EB%A7%81%ED%95%98%EB%8A%94-%EB%91%90-%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sat, 04 May 2024 12:42:13 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>지금까지, 대량의 스크롤이 필요한 리스트를 최적화한 경험이 2번 있었다. 한 번은 prismic에서 미리보기 이미지 리스트를 최적화한 것이었고, 다른 한 번은 xnb.js에서 대량의 텍스트 미리보기 컴포넌트를 최적화한 것이었다. 두 프로젝트의 골자는 보이는 것만 렌더링하는 것이었으나, 그 구현체는 사뭇 달랐다. </p>
<h3 id="왜-보이는-요소만-렌더링해야-하나요">왜 보이는 요소만 렌더링해야 하나요?</h3>
<p>렌더링 성능 최적화에 있다. 브라우저에서 대량의 dom 요소가 렌더링될 때, 각 요소들의 위치를 결정하는 레이아웃 과정을 거친다. 레이아웃 과정은 렌더링되는 엘리먼트가 많을수록, 렌더링할 엘리먼트의 depth가 클수록 소요 시간이 증가한다.
따라서 어떠한 컴포넌트의 렌더링 시간이 길어진다면 엘리먼트가 많아서 그럴 가능성을 의심해야 하며, <strong>렌더링할 엘리먼트의 수를 줄이면 레이아웃 속도를 향상</strong>시킬 수 있다. 이 중, <em><strong>보이지 않는 엘리먼트는 굳이 렌더 트리에 없어도 되지 않는가?</strong></em> 라는 아이디어에서 출발한 것이 보이는 요소 렌더링 기법인 것이다.</p>
<h2 id="prismic에서-사용한-기법---가상-리스트windowing-기법">Prismic에서 사용한 기법 - 가상 리스트(windowing) 기법</h2>
<p>Prismic에서는 windowing 기법을 사용하여 보이는 요소만 렌더링했다. windowing 기법이란 컴포넌트의 스크롤 위치에 따라, 보이는 요소의 범위를 계산하여, 실제 보이는 요소만 dom 트리에 추가하고, 나머지 요소는 렌더링 자체를 하지 않아버리는 기법을 의미한다.</p>
<p>리액트에서 windowing 기법을 구현하는 라이브러리로는 react-window, react-virtualized가 있으나, Prismic의 미리보기 컴포넌트는 이중 폴더 구조 형태의 리스트로 구성되어 있기에, 좀더 높은 자유도를 위해 직접 구현하였다.</p>
<h3 id="windowing-기법의-구현-원리">windowing 기법의 구현 원리</h3>
<p>가상 리스트 기법은 다음의 원리로 구현된다.</p>
<ol>
<li>사용자가 <strong>scroll 이벤트를 발생</strong>시킨다. <strong>컨테이너 컴포넌트의 scrollHeight 요소를 기반으로 렌더링할 요소를 결정</strong>한다. (react에서는 여기서 state를 변경하는 방식으로 리렌더링을 유발한다.)</li>
<li>수학을 이용하여 <strong>렌더링할 요소의 인덱스 범위</strong>를 계산한다.</li>
<li>각 인덱스 범위에 해당하는 <strong>요소의 내용을 렌더링</strong>한다. 요소의 위치는 position:absolute를 활용한 절대 좌표로 결정된다.</li>
</ol>
<h3 id="가상-리스트의-인덱스-공식">가상 리스트의 인덱스 공식</h3>
<p>2번은 현재 스크롤에서 어느 부분이 보여야 할지를 계산하는 방법으로, 가상 리스트를 이해하는 데 가장 중요하니, 부연설명을 해보겠다.</p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/cc83ac06-3114-49c7-b2a0-d8107d92b0bb/image.png" alt="">
우선, 위의 사진은 리스트의 기본 구조이다. 위에서부터 0번, 1번 순으로 이어지며, 0번과 리스트 컨테이너 사이의 간격을 <code>startPadding</code>, 각 리스트 높이를 <code>elemHeight</code>, 리스트 사이의 간격을 <code>elemGap</code>이라고 하겠다.
여기서 0번 엘리먼트의 시작 y값은 <code>startPadding</code>이며, 종료 y값은 <code>startPadding+elemHeight</code>이다. 1번 엘리먼트의 시작 y값은 <code>startPadding+elemHeight+elemGap</code>이고, 종료 y값은 <code>startPadding+elemHeight+elemHap+elemHeight</code>이다. 이 규칙을 적용하면, i번 엘리먼트의 시작 y값과 종료 y값을 구할 수 있다.</p>
<ul>
<li>시작 y값 : <code>startPadding + (elemHeight+elemGap)*i</code></li>
<li>종료 y값 : <code>startPadding + (elemHeight+elemGap)*i + elemHeight</code></li>
</ul>
<p>이제, 저 리스트에 가상의 가로선을 그어보자. 이 가로선이 보이는 엘리먼트의 기준이 되는 선이다. 가로선이 엘리먼트의 시작점에 있을 때, 엘리먼트 중간에 있을 때, 엘리먼트 종료점에 있을 때,엘리먼트와 엘리먼트 사이 공간에 있을 때로 나눠서 생각해보자.</p>
<p>먼저, 가로선 아래에 있는 엘리먼트가 보인다고 가정해보자. 이 가로선은 <strong>스크롤의 시작 가로선</strong>이다.
사진의 각 경우를 살펴보자.</p>
<ul>
<li>시작점에 있을 때 : 2번부터 보임</li>
<li>중간에 있을 때 : 2번부터 보임</li>
<li>끝점에 있을 때 : 3번부터 보임</li>
<li>사이공간에 있을 때 : 3번부터 보임</li>
</ul>
<p>스크롤 시작 가로선의 경우, 끝점을 기준으로 보이는 엘리먼트의 인덱스가 나뉘어진다.</p>
<p>다음으로, 가로선 위에 있는 엘리먼트가 보인다고 가정해보자. 이 가로선은 <strong>스크롤의 종료 가로선</strong>이다.
사진의 각 경우를 살펴보자.</p>
<ul>
<li>시작점에 있을 때 : 1번부터 보임</li>
<li>중간에 있을 때 : 2번부터 보임</li>
<li>끝점에 있을 때 : 2번부터 보임</li>
<li>사이공간에 있을 때 : 2번부터 보임</li>
</ul>
<p>스크롤 종료 가로선의 경우, 시작점을 기준으로 보이는 엘리먼트의 인덱스가 나뉘어진다.</p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/7e19c256-513e-49b6-b90b-d76641cdc958/image.png" alt="">
이를 기반으로, 스크롤 시작 지점과 종료 지점에 보이는 엘리먼트의 인덱스 범위를 시각화해 보았다. 왼쪽 숫자가 스크롤 시작 지점 인덱스 범위, 오른쪽 숫자가 스크롤 종료 지점 인덱스 범위다.</p>
<p>잘 보면, (0번을 제외하고) 각 인덱스 범위의 길이는 <code>elemHeight + elemGap</code>으로 동일하고, 몫 연산을 이용하면 스크롤 시작 지점/종료 지점의 y값을 기준으로 보이기 시작하는/보이기 끝나는 엘리먼트의 인덱스 번호를 알 수 있다.</p>
<ul>
<li>시작점 인덱스 : <code>Math.floor( (scrollStartY - (startGap - elemGap)) / (elemHeight + elemGap) )</code></li>
<li>종료점 인덱스 : <code>Math.floor( (scrollEndY - startGap) / (elemHeight + elemGap) )</code></li>
</ul>
<h3 id="windowing-기법의-장단점">windowing 기법의 장단점</h3>
<h4 id="장점">장점</h4>
<ul>
<li><strong>페이지 내에서 엘리먼트의 수를 줄일 수 있다.</strong><ul>
<li>특히, 가상 DOM으로 컴포넌트의 변경 사항을 순회하며 계산하는 리액트와 같은 웹 프레임워크를 사용하는 데에 효과적이다.</li>
<li>엘리먼트의 수가 대량으로 줄어드므로, 리플로우 계산에 사용되는 연산 횟수 역시 줄어든다. <strong>엘리먼트 수가 많고 리플로우가 잦은 웹 페이지에 효과적</strong>이다.</li>
</ul>
</li>
<li><strong>데이터 기반 렌더링에 효과적</strong>이다. 리스트의 실제 내용은 데이터의 인덱스를 기반으로 렌더링이 이루어지므로, 인덱싱이 된 데이터를 렌더링할 수 있다.<h4 id="단점">단점</h4>
</li>
<li><strong>구현/이해에 수학적 지식을 요구한다.</strong> 좌표 계산을 잘 하지 못하는 사람들은 구현이 힘들 수 있다.</li>
<li><strong>엘리먼트의 추가 및 제거가 빈번하게 일어나므로</strong> 이 부분에서 오버헤드가 일어날 수 있다.</li>
<li><strong>구현에 사용되는 scroll 이벤트의 호출 빈도가 잦다.</strong> 원활한 성능을 위해서는 throttle을 사용하여 렌더링 호출 빈도를 줄일 필요가 있다.</li>
</ul>
<h3 id="왜-prismic에서는-windowing-기법을-사용했는가">왜 Prismic에서는 windowing 기법을 사용했는가?</h3>
<p>Prismic 프로젝트는 대량의 이미지를 분류하기 위해 만들어졌다. 이렇기 때문에, 최소 50개 이상, 최대 3000개의 이미지가 미리보기 컴포넌트에 보여지게 된다.
또한, 유틸리티 웹 어플리케이션이라는 앱의 특성상 싱글 페이지 어플리케이션 구현에 용이한 React를 사용하였다.</p>
<p>이러한 Prismic의 특성으로 인해, 미리보기 리스트의 성능을 향상시키기 위해 windowing 기법을 사용하는 것이 효과적이라고 생각했다. 그 이유는 다음과 같다.</p>
<ul>
<li>엘리먼트의 수가 최대 3000개 이상으로 많다. 즉, 렌더링을 할 때 잦은 스크롤로 발생하는 오버헤드보다 <strong>대량의 엘리먼트로 인해 발생하는 연산 오버헤드가 크다고 판단</strong>했다. windowing 기법은 dom에서 엘리먼트를 제거하므로, 엘리먼트의 수를 효과적으로 줄일 수 있다.</li>
<li>React를 사용한다. React는 가상 DOM을 사용해서 엘리먼트의 변경 사항을 비교하는데, <strong>엘리먼트의 수가 많으면 가상 DOM 비교에 들어가는 연산이 커질 수밖에 없다</strong>. 따라서 엘리먼트의 수를 줄이는 windowing 기법을 사용했다.</li>
</ul>
<h2 id="xnbjs에서-사용한-기법---보이지-않는-요소-hidden처리">xnb.js에서 사용한 기법 - 보이지 않는 요소 hidden처리</h2>
<p>xnb.js에서는 보이지 않는 요소의 css display 속성을 hidden 처리하는 기법을 사용하여 보이는 요소만 렌더링했다. intersection observer api를 이용해, 엘리먼트 영역이 보이면 display 속성을 block으로 처리하고, 보이지 않으면 display 속성을 hidden 처리하여서 렌더 트리에서 해당 엘리먼트를 제거하는 방식으로 구현했다.</p>
<h3 id="보이지-않는-요소-hidden처리-기법의-구현-원리">보이지 않는 요소 hidden처리 기법의 구현 원리</h3>
<p>이 기법은 다음의 원리로 구현된다.</p>
<ol>
<li>모든 요소를 html에 추가한다. 요소는 intersection observer가 감지할 외부 엘리먼트, 실제 내용이 들어갈 내부 엘리먼트로 구성된다.</li>
<li>intersection observer가 각 요소의 외부 엘리먼트를 감지하도록 한다.</li>
<li>intersection observer가 요소의 보임을 감지하면, 요소의 내부 엘리먼트의 display 속성을 block으로 전환한다. 그렇지 않으면, display 속성을 hidden으로 전환한다.</li>
</ol>
<h3 id="보이지-않는-요소-hidden처리-기법의-장단점">보이지 않는 요소 hidden처리 기법의 장단점</h3>
<h4 id="장점-1">장점</h4>
<ul>
<li><strong>구현이 상대적으로 간단하다.</strong> 요소의 배치와 보이는 요소 감지를 전적으로 브라우저에게 맡기기 때문에, 구현이 쉽다.</li>
<li><strong>구현에 사용되는 intersection observer 콜백 호출 빈도가 적다.</strong> scroll 이벤트와는 달리 intersection observer의 콜백 함수는 요소가 보일 때, 숨겨질 때 1번 발생한다. 이 점에서 오버헤드를 줄일 수 있다.<h4 id="단점-1">단점</h4>
</li>
<li><strong>엘리먼트의 수가 줄어들지 않는다.</strong> 이로 인해, 엘리먼트 자체의 연산으로 발생하는 성능 저하를 줄일 수 없다. 리스트 내에 대량의 요소가 존재한다면 이 기법보다는 windowing 기법을 사용하는 것이 효과적이다.<ul>
<li>중간에 요소가 추가될 때 많은 양의 엘리먼트의 리플로우가 그대로 일어나므로, 해당 부분에서 불리할 수 있다.</li>
</ul>
</li>
<li>내부 엘리먼트의 대략적인 크기를 외부 엘리먼트가 알고 있어야 한다.</li>
</ul>
<h3 id="왜-xnbjs에서는-보이지-않는-요소-hidden처리-기법을-사용했는가">왜 xnb.js에서는 보이지 않는 요소 hidden처리 기법을 사용했는가?</h3>
<p>xnb.js의 미리보기 컴포넌트는 최대 1만 줄의 텍스트를 렌더링해야 하지만, 청크 단위로 구분되면 <strong>최대 100개의 청크만 렌더링</strong>하게 된다. 이 점으로 미루어 보아, 보이지 않는 요소 hidden 처리 대신 가상 리스트로 얻을 수 있는 이점이 크게 있지 않을 것이라고 생각했다.</p>
<p>또한, xnb.js 1.3 업데이트는 <strong>빠르게 대중에게 퍼블리싱해야 할 필요</strong>가 있었다. 이미 2만 여 명의 대중에게 배포된 서비스를 업데이트하는 것이기 떄문에, 업데이트 속도가 늦으면 대중이 불편해할 것이기 때문이라고 판단했다. 그렇기 때문에 <strong>빠르게 이해하고 적용할 수 있는 보이지 않는 요소 hidden처리 기법을 사용</strong>했다.</p>
<h2 id="두-방법의-차이점">두 방법의 차이점</h2>
<p>windowing 기법과 보이지 않는 요소 hidden 처리는 보이는 부분만 렌더 트리에 존재한다는 공통점이 있으나, 세부적으로 다음의 차이를 보인다.
<img src="https://velog.velcdn.com/images/lybell_4rt/post/dfda0c22-4d1f-42f8-a04b-bfd665cf3696/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[XSS를 아시나요(feat. innerHTML의 위험성)]]></title>
            <link>https://velog.io/@lybell_4rt/XSS%EB%A5%BC-%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94feat.-innerHTML%EC%9D%98-%EC%9C%84%ED%97%98%EC%84%B1</link>
            <guid>https://velog.io/@lybell_4rt/XSS%EB%A5%BC-%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94feat.-innerHTML%EC%9D%98-%EC%9C%84%ED%97%98%EC%84%B1</guid>
            <pubDate>Thu, 04 Apr 2024 22:13:04 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<pre><code class="language-javascript">const inputForm = document.getElementById(&quot;input&quot;);
const inputButton = document.getElementById(&quot;button&quot;);
inputButton.addEventListener( &quot;click&quot;, ()=&gt;{
  const newElement = document.createElement(&quot;div&quot;);
  newElement.className=&quot;myComponent&quot;;
  newElement.innerHTML = `
    &lt;p&gt;Name : ${inputForm.value}&lt;/p&gt;
    &lt;div class=&quot;delButton&quot;&gt;삭제하기&lt;/div&gt;
  `
  document.body.appendChild(newElement);
})</code></pre>
<p>신입 개발자 A는 버튼을 누르면 input 값의 내용을 가져와 새로운 컴포넌트를 추가하는 코드를 만들고 있었다. 그리고 위와 같은 코드로 작성하였다. 여기서 문제, <strong>이 코드의 문제점은 무엇인가</strong>?
.
.
.
.
바로 <strong>크로스 사이트 스크립팅</strong>(Cross Site Scripting, XSS) 공격에 취약하다는 것이다. 그렇다면, 크로스 사이트 스크립팅이란 무엇이고, 이 코드가 왜 XSS 공격에 취약한 것인가?</p>
<h2 id="크로스-사이트-스크립팅이란">크로스 사이트 스크립팅이란?</h2>
<p>크로스 사이트 스크립팅은 개발자가 아닌 <strong>외부인이 사이트에 스크립트를 주입</strong>할 수 있는 보안상의 이슈로, 주로 <strong>검증되지 않은 사용자 문자열을 충분한 유효성 검사나 필터링 없이 그대로 이용</strong>할 때 발생한다. </p>
<p>사용자의 브라우저는 악의적인 스크립트가 신뢰할 수 없는지를 감지할 수 없으므로 쿠키, 세션 토큰 또는 다른 중요한 사이트별 정보에 대한 접근 권한을 가져가거나 악의적인 스크립트로 HTML 내용을 다시 쓸 수 있다.</p>
<blockquote>
<p>참고로, XSS와 비슷하게 검증되지 않은 사용자 문자열로 보안 이슈가 발생하는 SQL 인젝션이라는 것도 있다. 이것은 사용자 입력을 그대로 SQL 쿼리에 넣으면 생기는 문제.</p>
</blockquote>
<h3 id="innerhtml의-위험성">innerHTML의 위험성</h3>
<p>위의 코드를 보자. inputForm 안의 내용을 아무런 필터링 없이, innerHTML 내에 사용하고 있는데, innerHTML은 문자열을 html 코드로 전환하는 getter인 만큼, <strong>innerHTML에 사용자가 html 코드를 집어넣으면 그 코드가 html 코드로 그대로 전환</strong>된다! </p>
<p>사용자가 <code>&lt;p onclick=&quot;alert(&#39;Its over 9000!&#39;)&quot;&gt;클릭하면 신기한일 일어남&lt;/p&gt;</code> 을 input form에 입력하고, 버튼을 클릭한다고 가정해보자.
inputButton의 클릭 이벤트로 발동된 함수는 newElement.innerHTML을 다음과 같이 해석하게 된다.</p>
<pre><code class="language-js">&lt;p&gt;Name : &lt;p onclick=&quot;alert(&#39;Its over 9000!&#39;)&quot;&gt;클릭하면 신기한일 일어남&lt;/p&gt;&lt;/p&gt;
&lt;div class=&quot;delButton&quot;&gt;삭제하기&lt;/div&gt;</code></pre>
<p>그리고 이게 그대로 html로 해석되면서, &quot;클릭하면 신기한 일 일어남&quot;이라는 문자열을 클릭하면 스크립트가 실행되는 코드가 탄생하게 되는 것이다.
예시는 alert를 이용했지만, alert가 아니라 오만 가지 함수와 코드를 실행할 수 있기에 보안에 취약하다는 잠재적인 문제가 될 수 있다.</p>
<h3 id="스크립트-즉시-실행">스크립트 즉시 실행</h3>
<p>그렇다면 당신은 이렇게 생각할 수도 있다. <code>&lt;script&gt;&lt;/script&gt;</code>를 주입시키면 스크립트가 즉시 실행되는 것이 아닌가? 물론, 이론상으로는 그럴 수 있겠지만 우리의 브라우저는 여러분들 생각보다 똑똑하기 때문에 중간에 인라인 스크립트가 생성되더라도 아무런 행동을 하지 않는다.</p>
<p>그렇다고, 스크립트를 즉시 실행하는 방법이 없는 것은 아니다. 바로 <strong>img 태그의 onerror 이벤트를 이용하는 것</strong>이다. error 이벤트는 이미지가 로드 실패될 때 호출되며, img 태그가 src에 있는 이미지를 바로 로드 시도하는 것을 이용하면, <code>&lt;img src=&quot;&quot; onerror=&quot;alert(153)&quot;&gt;</code>라는 코드로 다음과 같은 일을 일으킬 수 있다.</p>
<ul>
<li>img 태그가 html에 주입되며, src 속성에 저장된 주소로 이미지 로드를 시도한다.</li>
<li>src가 빈 문자열이기 때문에, 이미지 로드는 실패하고, error 이벤트가 생성된다.</li>
<li>onerror에 설정된 스크립트가 실행되며, 결과적으로 사용자가 아무것도 하지 않아도 스크립트가 자동으로 실행되는 공격이 수행된다.</li>
</ul>
<h3 id="저장된-xss-공격">저장된 XSS 공격</h3>
<p>위의 코드는 XSS 공격의 클라이언트 전용 사례를 제시했지만, 서버와 결합하면 더욱 치명적인 결과가 일어날 수 있다. 사용자가 신뢰하지 않는 html 코드를 그대로 서버에 제출하고, 서버가 해당 문자열을 db에 그대로 저장하는 상황을 생각해 보자.</p>
<p>그리고 웹 사이트가 db에 있는 문자열을 서버에 렌더링할 때, <strong>문자열을 그대로 innerHTML을 이용해 렌더링한다면, 모든 사용자에게 오염된 html을 그대로 배포하게 된다!</strong> 이렇게 서버에 스크립트 주입 공격을 db에 저장하고, 신뢰하지 않은 문자열을 그대로 렌더링하는 것을 저장된 XSS 공격이라고 한다.</p>
<p>이 경우에는 프론트엔드 측과 백엔드 측 모두 신뢰하지 않는 문자열을 필터링해서 교차 검증을 하는 것이 중요하다.</p>
<h2 id="대처-방법">대처 방법</h2>
<p><strong>필터링</strong>을 이용하는 방법이 있다. 사용자의 입력값에 포함된 <code>&lt;&gt;</code>과 같은 html 태그를 <code>&amp;lt;</code>, <code>&amp;gt;</code>로 변경시켜서 사용자의 html 주입 시도를 차단하는 방법이 있다.</p>
<p>innerHTML을 사용할 경우, 검증된 문자열만 템플릿 리터럴에 집어넣거나, 아예 집어넣지 않고, 부득이하게 사용자 텍스트를 컴포넌트 내에 넣을 경우 빈 문자열로 태그를 선언한 뒤, <strong>querySelector와 innerText(또는 textContent)를 조합하여 사용자 문자열을 입력</strong>하는 방법이 있다.</p>
<pre><code class="language-js">inputButton.addEventListener( &quot;click&quot;, ()=&gt;{
  const newElement = document.createElement(&quot;div&quot;);
  newElement.className=&quot;myComponent&quot;;
  newElement.innerHTML = `
    &lt;p&gt;&lt;/p&gt;
    &lt;div class=&quot;delButton&quot;&gt;삭제하기&lt;/div&gt;
  `
  newElement.querySelector(&quot;p&quot;).innerText = inputForm.value;
  document.body.appendChild(newElement);
});</code></pre>
<p>innerText로 들어온 문자열은 html 태그에 사용되는 특수 문자도 순수 문자로 판별하여, 사용자 입력을 그대로 출력한다는 특징이 있다.</p>
<blockquote>
<p>참고로, innerText와 textContent의 차이는 setter에서는 별 차이가 없고, getter에서 css나 공백 제거 등으로 숨겨진 문자열을 보여주느냐(textContent), 보여주지 않느냐(innerText)의 차이다. textContent가 css 등을 고려하지 않아 리플로우가 일어나지 않아 대체적으로 성능이 좋은 편이다.</p>
</blockquote>
<h3 id="그럼-innerhtml은-죄악인가요">그럼, innerHTML은 죄악인가요?</h3>
<p>innerHTML이 XSS 공격에 취약한 것은 맞지만, 완전히 사용되어서는 안 되는 것은 아니다. 신뢰할 수 있는 문자열만 innerHTML에 들어간다면, innerHTML은 개발자의 수고를 덜어줄 수 있는 좋은 무기가 된다. 예시를 통해 알아보자.</p>
<pre><code class="language-js">const newElem = document.createElement(&quot;div&quot;);
newElem.innerHTML = `
&lt;h3&gt;타이머&lt;/h3&gt;
&lt;div class=&quot;timerBody&quot;&gt;
&lt;span class=&quot;hour&quot;&gt;59&lt;/span&gt;:&lt;span class=&quot;minute&quot;&gt;59&lt;/span&gt;:&lt;span class=&quot;second&quot;&gt;59&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;timerButton&gt;시작하기&lt;/div&gt;
`</code></pre>
<p>이 코드의 경우, <strong>innerHTML에 개발자가 검증된 문자열만 들어갔기 때문에, XSS 공격이 일어날 여지가 없다</strong>. 바닐라 자바스크립트로 innerHTML을 이용하지 않고 비슷한 코드를 짜려면 createElement를 도배해야 해서 코드의 가독성이 떨어지는 문제가 일어날 수 있다.</p>
<h2 id="참고-자료">참고 자료</h2>
<p><a href="https://developer.mozilla.org/ko/docs/Glossary/Cross-site_scripting">https://developer.mozilla.org/ko/docs/Glossary/Cross-site_scripting</a></p>
<p><a href="https://developer.mozilla.org/ko/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss">https://developer.mozilla.org/ko/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[xnb.js 개발일지(6) - 대량의 텍스트 리플로우 최적화]]></title>
            <link>https://velog.io/@lybell_4rt/xnb.js-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%806-%EB%8C%80%EB%9F%89%EC%9D%98-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%A6%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@lybell_4rt/xnb.js-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%806-%EB%8C%80%EB%9F%89%EC%9D%98-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EB%A6%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Sun, 31 Mar 2024 08:56:25 GMT</pubDate>
            <description><![CDATA[<p>xnb.js는 3월 28일 개발이 완료되어서, 현재 1.3.1버전이 웹에 퍼블리싱된 상태입니다. 본 아티클은 개발 도중 메모한 notion을 재구성한 아티클입니다.</p>
<hr>
<h2 id="개요">개요</h2>
<p>웹 xnb 컨버터를 개발하던 중, 우연히 이런 오류를 발견했다.</p>
<blockquote>
<p>대량의 텍스트 데이터가 미리보기 컴포넌트에 렌더링되었을 때, 창의 너비를 늘리거나 줄이면 앱이 프리징되는 현상</p>
</blockquote>
<p>해당 문제는 웹 xnb 컨버터 1.2 이전에도 있었던 잠재적인 문제였지만, 그 때 다루던 xnb 파일에 저장된 텍스트들은 그렇게 용량이 크지 않았기에, 발견하는 데에 시간이 오래 걸렸다. 스타듀 밸리 1.6 업데이트로 인해 커스텀 자료형 xnb 파일의 데이터를 담은 json 문자열이 5000줄 이상으로 매우 길어지는 경우가 생겼다.</p>
<p>해당 문제는 앱의 사용성과 직결된 프리징 문제였기에, 바로 고치는 작업에 들어갔다.</p>
<h2 id="원인-파악">원인 파악</h2>
<p>웹 xnb 컨버터는 반응형 웹을 채택하고 있다. 즉, 웹 페이지의 너비가 일정 크기 이상이면 다른 종류의 레이아웃을 보여줘야 해서, 웹 페이지의 너비를 특정 너비 이하로 줄이거나 특정 너비 이상으로 늘리면 레이아웃 재계산이 발생하게 된다.
문제가 되는 경우는 대용량의 텍스트가 미리보기되는 것으로, 레이아웃 재계산이 발생할 때, 렌더링된 대용량의 텍스트도 같이 레이아웃을 재계산하게 된다.</p>
<p>잠깐 브라우저의 렌더링 과정에 대해 간략하게 알아보자. 브라우저의 렌더링은 html, css 파싱, 렌더 트리 형성, 레이아웃, 페인팅으로 나뉜다. 여기서 일어난 문제는 <strong>레이아웃 시간의 급격한 증가</strong>인데, <strong>레이아웃을 계산해야 하는 요소의 수가 많을수록</strong> 레이아웃 시간이 증가하게 된다. 텍스트 노드의 경우, 띄어쓰기된 각 텍스트의 단어 혹은 (css 속성 중 white-space가 pre인 경우) 줄바꿈을 기준으로 각 부분의 위치를 계산하게 된다. <em>(이 부분은 웹킷 소스코드를 찾아서 분석하려고 했는데 텍스트를 요소로 만드는 코드를 찾을 수 없었습니다... 제보 부탁드립니다)</em></p>
<p>간단한 실험을 해 봤는데, 매우 긴 문자열(50000자의 한글)을 넣은 뒤 해당 문자열이 속한 태그의 리플로우를 일으켜 보았다.</p>
<ul>
<li>모든 문자가 띄어쓰기가 없을 경우:10ms</li>
<li>모든 문자가 띄어쓰기가 있을 경우:2.42s</li>
<li>모든 문자가 띄어쓰기가 있는 대신, white-space가 pre인 경우:37ms (대신 rasterize paint에서 500ms 이상 소요)</li>
</ul>
<h2 id="해결법-고안">해결법 고안</h2>
<p>레이아웃을 계산해야 하는 텍스트 수가 많다면, 많은 양의 텍스트를 렌더 트리에서 빼 버리면 되지 않을까?라고 생각했다. 렌더 트리에서 엘리먼트를 제거하는 대표적인 방법은 <code>display: none;</code>을 사용하는 것(html 속성의 <code>hidden</code>도 동일한 효과를 낸다.)이다.</p>
<p>첫 번째 고안한 방법은 화면의 크기 변경이 감지되면 일시적으로 미리보기 엘리먼트를 숨기고, 화면 크기 변경이 끝나면 미리보기 엘리먼트를 다시 표시하는 것이었다. 하지만, 그렇게 하면 잦은 화면 변경이 일어나는 모바일 환경에서 사용자 경험을 해치게 되므로, 1안은 포기하였다. (모바일 웹의 특성상, 사용자의 스크롤 행동에 따라 상단 주소표시줄이 나타났다 사라지면서 화면의 크기가 바뀔 수 있다.)</p>
<p>두 번째 방법은 많은 양의 텍스트를 줄 수에 따라 청크로 분리하고, intersection observer api를 사용해 보이는 청크만 렌더링하는 것이다. 화면 너머의 청크는 기본적으로 hidden처리하기로 한다. </p>
<h2 id="첫-번째-시도">첫 번째 시도</h2>
<pre><code class="language-js">class PreviewText extends HTMLElement {
    // data
    #text = &quot;&quot;;
    #chunkedText = [];
    // observer
    #observer;

    get text()
    {
        return this.#text;
    }
    set text(value)
    {
        this.#text = value;
        this.#chunkedText = [];
        const sliced = value.split(&quot;\n&quot;);
        const lines = sliced.length;
        const LINE_CHUNK = 100;
        for(let i=0; i&lt;lines; i+=LINE_CHUNK)
        {
            this.#chunkedText.push(sliced.slice(i, i+LINE_CHUNK).join(&#39;\n&#39;));
        }
        this.render();
    }
    constructor()
    {
        super();
        const shadowRoot = this.attachShadow({mode: &quot;open&quot;});
        const style = new CSSStyleSheet();
        style.insertRule(&quot;pre { margin:0; }&quot;);
        shadowRoot.adoptedStyleSheets = [style];

        this.#observer = new IntersectionObserver((entries, observer)=&gt;{
            for(let entry of entries) {
                entry.target.style.visibility = entry.isIntersecting ? &quot;visible&quot; : &quot;hidden&quot;;
            }
        });
    }
    disconnectedCallback()
    {
        this.#observer.disconnect();
    }
    render()
    {
        const children = this.shadowRoot.children;
        const oldLength = children.length;
        const newLength = this.#chunkedText.length;
        const fragment = new DocumentFragment();

        // update existing element
        for(let i=0; i&lt;newLength; i++)
        {
            if(i &lt; oldLength) {
                children[i].textContent = this.#chunkedText[i];
                children[i].style.visibility = &quot;visible&quot;;
            }
            else {
                let textNode = document.createElement(&quot;pre&quot;);
                textNode.textContent = this.#chunkedText[i];
                this.#observer.observe(textNode);
                fragment.append(textNode);
            }
        }
        // remove overflown element
        for(let i=oldLength-1; i&gt;=newLength; i--)
        {
            this.#observer.unobserve(children[i]);
            children[i].remove();
        }
        this.shadowRoot.append(fragment);
    }
}

window.customElements.define(&quot;preview-text&quot;, PreviewText);

export default PreviewText;</code></pre>
<p>이 코드는 텍스트를 렌더링하는 컴포넌트의 코드로, 바닐라 자바스크립트 + 웹 컴포넌트의 Custom Element, Shadow Dom을 이용해서 제작되었다.</p>
<pre><code class="language-js">constructor()
{
  super();
  const shadowRoot = this.attachShadow({mode: &quot;open&quot;});
  const style = new CSSStyleSheet();
  style.insertRule(&quot;pre { margin:0; }&quot;);
  shadowRoot.adoptedStyleSheets = [style];

  this.#observer = new IntersectionObserver((entries, observer)=&gt;{
  for(let entry of entries) {
    entry.target.style.visibility = entry.isIntersecting ? &quot;visible&quot; : &quot;hidden&quot;;
  }
});
}</code></pre>
<p>이 커스텀 엘리먼트의 생성자 부분이다. shadow dom을 이용하여 constructor 내부에서도 자식의 dom을 초기화하는 작업을 할 수 있게 했으며, 커스텀 엘리먼트 내의 스타일을 외부 스타일과 분리하였다.</p>
<p>intersection observer api를 이용해, 옵저버를 생성하고, 옵저버가 각 청크의 화면상 보임을 감지하면 청크의 visibility를 전환하도록 하였다.</p>
<p>화면 너머의 청크를 숨기는 것은 <code>display: none;</code> 대신 <code>visibility: hidden;</code>을 사용했었는데, 첫 번째 이유로는 display: none;을 이용하면 intersection observer가 영원히 청크의 가시성을 꺼진 상태로 판별하기 때문이었으며, 두 번째 이유로는 각 청크의 높이 계산을 브라우저에 맡기기 위해서였다. 적용하기 전에 비해 성능 향상은 있었으나, 여전히 50만 자 이상의 텍스트를 렌더링하면 100ms 이상의 시간이 소요된다.
지금 생각해보니, <code>visibility: hidden;</code>은 <strong>렌더 트리에서 엘리먼트가 제거되지 않기 때문에 여전히 많은 양의 오브젝트의 바운딩박스를 계산하는 작업이 필요</strong>했다. 그럼에도 불구하고 성능 향상이 있었던 것은 대용량의 텍스트를 페인팅하는 작업이 줄어들었기 때문으로 보여진다.</p>
<pre><code class="language-js">set text(value)
{
  this.#text = value;
  this.#chunkedText = [];
  const sliced = value.split(&quot;\n&quot;);
  const lines = sliced.length;
  const LINE_CHUNK = 100;
  for(let i=0; i&lt;lines; i+=LINE_CHUNK)
  {
    this.#chunkedText.push(sliced.slice(i, i+LINE_CHUNK).join(&#39;\n&#39;));
  }
  this.render();
}</code></pre>
<p>이 커스텀 엘리먼트는 text getter, setter를 가지고 있다. text를 할당하면 원본 텍스트를 <code>\n</code> 기준으로 분리하고, 이 중 100개째의 텍스트를 하나의 청크로 묶어서 chunkedText 프로퍼티에 저장한다. 이후, render 함수를 호출한다.</p>
<pre><code class="language-js">render()
{
  const children = this.shadowRoot.children;
  const oldLength = children.length;
  const newLength = this.#chunkedText.length;
  const fragment = new DocumentFragment();

  // update existing element
  for(let i=0; i&lt;newLength; i++)
  {
    if(i &lt; oldLength) {
      children[i].textContent = this.#chunkedText[i];
      children[i].style.visibility = &quot;visible&quot;;
    }
    else {
      let textNode = document.createElement(&quot;pre&quot;);
      textNode.textContent = this.#chunkedText[i];
      this.#observer.observe(textNode);
      fragment.append(textNode);
    }
  }
  // remove overflown element
  for(let i=oldLength-1; i&gt;=newLength; i--)
  {
    this.#observer.unobserve(children[i]);
    children[i].remove();
  }
  this.shadowRoot.append(fragment);
}</code></pre>
<p>shadow root의 children을 기준으로, 다음의 작업을 수행한다.</p>
<ul>
<li>이미 존재하던 청크는 내용을 변경한다.</li>
<li>새롭게 생긴 청크 수가 원래 청크 수보다 크면, 청크를 생성한 뒤 observer에 해당 청크를 관찰하도록 한다.</li>
<li>새롭게 생긴 청크 수가 원래 청크 수보다 작으면, 넘치는 청크의 observer 관찰을 해제시키고, dom에서 제거한다.</li>
</ul>
<p>이 때 이미 있던 청크의 visibility를 visible로 설정한 이유는 모종의 사유로 인해 새롭게 청크가 생겨날 때 있던 청크가 보여지지 않는 오류가 있기 때문이었으며 (나중에 알았는데, text를 변경할 때 disammountCallback이 호출되어서 모든 청크의 가시성이 해제되기 때문이었다), fragment를 사용하여 dom을 추가한 이유는 불필요하게 실제 dom의 리플로우를 여러 번 일으키는 것을 막기 위해서였다.</p>
<h3 id="첫-번째-시도의-문제점">첫 번째 시도의 문제점</h3>
<p>위에서도 잠깐 언급했었지만, 2가지 문제점이 있었다.</p>
<ol>
<li><p>화면 너머의 청크를 숨길 때 <code>visiblity: hidden</code>을 사용했기 때문에 일부 대용량의 텍스트를 렌더링할 때 여전히 100ms 이상의 시간이 소요된다는 점.</p>
<p><code>visibility: hidden</code>은 요소를 렌더 트리에서 제거하지 않기 때문에, 여전히 리플로우가 일어나면 보이지 않는 요소도 바운딩박스를 재계산하는 과정을 거쳐야 한다.</p>
</li>
<li><p>text를 변경할 때 이미 존재하던 텍스트의 옵저버 관찰이 풀려버리는 문제. 임시로 있던 청크에 <code>visibility: visible</code>을 설정해 보이게 해 놓았으나, 그 점으로 인해 intersection observer를 활용한 청크 단위 렌더링의 효과가 줄어든다는 문제가 있었다.</p>
</li>
</ol>
<p>이 문제를 다음과 같이 해결하기로 하였다.</p>
<ol>
<li><p>이 컴포넌트는 코드를 보여주는 용도이기 때문에, <code>&lt;pre&gt;</code> 태그를 사용해서 텍스트를 렌더링하고 있다. 이 태그는 텍스트의 크기와 텍스트 줄 수에 따라 height가 고정적으로 바뀐다. 따라서, <strong>미리 각 청크의 height값을 계산하여 자리를 차지하도록 전환한 뒤, 실제 텍스트를 렌더링하는 청크의 자식 엘리먼트를 숨기기로 하였다</strong>. 이렇게 하면 observer가 관찰하는 엘리먼트 자체는 보여진 상태로 유지되어 각 엘리먼트가 화면에 보이는 여부를 항상 관측할 수 있으며, 실제로 렌더링에 영향을 많이 미치는 보이지 않는 텍스트 엘리먼트를 렌더 트리에서 제거할 수 있다는 이점이 있다.</p>
</li>
<li><p>connectedCallback에 shadow dom의 모든 자식 엘리먼트를 관찰하게 하는 코드를 추가한다.</p>
</li>
</ol>
<h2 id="두-번째-시도">두 번째 시도</h2>
<pre><code class="language-js">const LINE_CHUNK = 100;

class PreviewText extends HTMLElement {
    // data
    #text = &quot;&quot;;
    #chunkedText = [];
    #lastLines = 0;
    // observer
    #observer;

    get text()
    {
        return this.#text;
    }
    set text(value)
    {
        this.#text = value;
        this.#chunkedText = [];
        const sliced = value.split(&quot;\n&quot;);
        const lines = sliced.length;
        for(let i=0; i&lt;lines; i+=LINE_CHUNK)
        {
            this.#chunkedText.push(sliced.slice(i, i+LINE_CHUNK).join(&#39;\n&#39;));
        }
        this.#lastLines = lines - (this.#chunkedText.length - 1) * LINE_CHUNK;
        this.render();
    }
    constructor()
    {
        super();
        const shadowRoot = this.attachShadow({mode: &quot;open&quot;});
        const style = new CSSStyleSheet();
        style.insertRule(`.wrapper, pre { margin:0; }`);
        style.insertRule(`.wrapper:not(:last-child) { width:100%; height:${LINE_CHUNK*46/3}px; }`);
        style.insertRule(`.wrapper:last-child { width:100%; height:calc(var(--lastLine) * 46px / 3); }`);
        shadowRoot.adoptedStyleSheets = [style];

        this.#observer = new IntersectionObserver((entries, observer)=&gt;{
            for(let entry of entries) {
                const pre = entry.target.firstChild;
                pre.hidden = !entry.isIntersecting;
            }
        });
    }
    connectedCallback()
    {
        for(let child of this.shadowRoot.children) {
            this.#observer.observe(child);
        }
    }
    disconnectedCallback()
    {
        this.#observer.disconnect();
    }
    render()
    {
        const children = this.shadowRoot.children;
        const oldLength = children.length;
        const newLength = this.#chunkedText.length;
        const fragment = new DocumentFragment();
        this.style.setProperty(&quot;--lastLine&quot;, this.#lastLines);

        // update existing element
        for(let i=0; i&lt;newLength; i++)
        {
            if(i &lt; oldLength) {
                const pre = children[i].firstChild;
                pre.textContent = this.#chunkedText[i];
                //pre.style.visibility = &quot;visible&quot;;
            }
            else {
                let wrapperNode = document.createElement(&quot;div&quot;);
                wrapperNode.className = &quot;wrapper&quot;;
                let textNode = document.createElement(&quot;pre&quot;);
                textNode.textContent = this.#chunkedText[i];
                wrapperNode.append(textNode);
                this.#observer.observe(wrapperNode);
                fragment.append(wrapperNode);
            }
        }
        // remove overflown element
        for(let i=oldLength-1; i&gt;=newLength; i--)
        {
            this.#observer.unobserve(children[i]);
            children[i].remove();
        }
        this.shadowRoot.append(fragment);
    }
}

window.customElements.define(&quot;preview-text&quot;, PreviewText);

export default PreviewText;</code></pre>
<p>여러분이 알아보기 쉽게, react 코드로 전환하면 다음과 비슷하다. (실제로 작동하는 코드가 아닐 수 있으니 참고만 하시라.)</p>
<pre><code class="language-jsx">import { useRef, useEffect } from &quot;react&quot;;
import styled from &quot;styled-component&quot;;

const LINE_CHUNK = 100;

const Wrapper = styled.div`
margin: 0;
&amp;:not(:last-child) {
  width: 100%;
  height: {LINE_CHUNK * 46/3}px;
}
&amp;:last-child { 
  width: 100%;
  height: calc(var(--lastLine) * 46px / 3);
}
`;

const Pre = styled.pre`
margin: 0;
`

export function PreviewText({text})
{
  const ref = useRef(null);
  const observer = useRef( new IntersectionObserver( (entries, observer)=&gt;{
    for(let entry of entries) {
      const pre = entry.target.firstChild;
      pre.hidden = !entry.isIntersecting;
    }
  } ) );

  useEffect( ()=&gt;{
    for(let child of ref.children) {
      observer.current?.observe(child);
    }

    return ()=&gt;{
      observer.current?.disconnect();
    }
  }, [text] );


  return &lt;div style={ {&quot;--lastLine&quot;: lastLine} } ref={ref}&gt;
    {chunkedText.map( (content, i)=&gt;{
      return &lt;Wrapper className=&quot;wrapper&quot;&gt;
        &lt;Pre&gt;{content}&lt;/Pre&gt;
      &lt;/Wrapper&gt;
    })}
  &lt;/div&gt;
}</code></pre>
<p>chunkedText를 element로 렌더링하는 부분에서, <code>&lt;pre&gt;</code> 태그를 <code>&lt;div&gt;</code> 태그로 감쌌다.</p>
<pre><code class="language-js">style.insertRule(`.wrapper, pre { margin:0; }`);
style.insertRule(`.wrapper:not(:last-child) { width:100%; height:${LINE_CHUNK*46/3}px; }`);
style.insertRule(`.wrapper:last-child { width:100%; height:calc(var(--lastLine) * 46px / 3); }`);
shadowRoot.adoptedStyleSheets = [style];

this.#observer = new IntersectionObserver((entries, observer)=&gt;{
    for(let entry of entries) {
        const pre = entry.target.firstChild;
        pre.hidden = !entry.isIntersecting;
    }
});</code></pre>
<p>먼저 constructor는 다음과 같이 바뀌었다. pre 엘리먼트는 렌더링되는 문자열의 줄 수와 폰트에 따라 높이가 결정되므로, 래퍼 엘리먼트의 마지막이 아닌 엘리먼트의 높이는 100 * 46/3 px으로 고정된다.</p>
<pre><code class="language-js">this.#lastLines = lines - (this.#chunkedText.length - 1) * LINE_CHUNK;</code></pre>
<p>마지막 엘리먼트일 경우, lastLine css 변수를 따라 계산되는데, 이것은 text를 청크로 나누는 과정에서 계산되고 할당된다.</p>
<p>observer의 경우, 각 엔트리의 첫 번째 자식의 hidden 여부를 결정하도록 변경했다.</p>
<pre><code class="language-js">connectedCallback()
{
  for(let child of this.shadowRoot.children) {
    this.#observer.observe(child);
  }
}</code></pre>
<p>해당 커스텀 엘리먼트가 dom에 추가될 때, shadow root의 모든 자식에 대해 관찰을 시도하는 콜백을 추가했다.</p>
<pre><code class="language-js">let wrapperNode = document.createElement(&quot;div&quot;);
wrapperNode.className = &quot;wrapper&quot;;
let textNode = document.createElement(&quot;pre&quot;);
textNode.textContent = this.#chunkedText[i];
wrapperNode.append(textNode);
this.#observer.observe(wrapperNode);
fragment.append(wrapperNode);</code></pre>
<p>바뀐 dom 설계에 따라 새로운 엘리먼트를 추가하는 코드다.</p>
<pre><code class="language-html">&lt;div class=&quot;wrapper&quot;&gt;
 &lt;pre&gt;(chunkedText[i])&lt;/pre&gt;
&lt;/div&gt;</code></pre>
<p>를 정의하고 프래그먼트에 추가하는 코드라고 보면 된다.</p>
<h2 id="결과">결과</h2>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/651f183f-034a-4ada-b5a8-947caa61aa4e/image.png" alt=""></p>
<p>50만 자의 텍스트 렌더링 기준으로, 이전에는 2s 이상 걸리거나 아예 프리징이 되었던 <strong>텍스트 미리보기 컴포넌트의 레이아웃 계산 속도가 1~2ms로 2000배 이상 단축</strong>된 모습을 볼 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[xnb.js 개발일지(5) - 노가다를 줄이는 길]]></title>
            <link>https://velog.io/@lybell_4rt/xnb.js-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%805-%EB%85%B8%EA%B0%80%EB%8B%A4%EB%A5%BC-%EC%A4%84%EC%9D%B4%EB%8A%94-%EA%B8%B8</link>
            <guid>https://velog.io/@lybell_4rt/xnb.js-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%805-%EB%85%B8%EA%B0%80%EB%8B%A4%EB%A5%BC-%EC%A4%84%EC%9D%B4%EB%8A%94-%EA%B8%B8</guid>
            <pubDate>Fri, 29 Mar 2024 07:42:11 GMT</pubDate>
            <description><![CDATA[<p>xnb.js는 3월 28일 개발이 완료되어서, 현재 1.3.1버전이 웹에 퍼블리싱된 상태입니다. 본 아티클은 개발 도중 메모한 notion을 재구성한 아티클입니다.</p>
<hr>
<h2 id="개요">개요</h2>
<blockquote>
<p>아직 xnb.js 1.2가 처한 문제와 진행상황에 대해 모른다면, <a href="https://velog.io/@lybell_4rt/xnb.js-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%804-%EB%B3%B4%EB%8B%A4-%EC%89%AC%EC%9A%B4-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%A6%AC%EB%8D%94-%EC%A0%9C%EC%9E%91%EA%B8%B0">전편</a>을 읽고 오시라!</p>
</blockquote>
<p>Stardew Valley 1.6 업데이트로 인해, 수많은 커스텀 자료형을 가진 xnb 파일이 생겨버렸고, 커스텀 자료구조의 추가를 수월하게 하기 위해 custom scheme을 받아서 커스텀 자료구조를 다룰 수 있게 하는 Reflective Scheme Reader를 개발하였다. 이 다음은 Reflective Scheme Reader가 해석할 수 있는 custom scheme을 만들면 되는 상황이다.</p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/c92dfb29-8cb3-4190-bd70-b0bdfbc1daee/image.png" alt=""></p>
<pre><code class="language-javascript">export default {
    Name: &quot;String&quot;,
    DisplayName: &quot;String&quot;,
    Description: &quot;String&quot;,
    Price: &quot;Int32&quot;,
    Fragility: &quot;Int32&quot;,
    CanBePlacedOutdoors: &quot;Boolean&quot;,
    CanBePlacedIndoors: &quot;Boolean&quot;,
    IsLamp: &quot;Boolean&quot;,
    $Texture: &quot;String&quot;,
    SpriteIndex: &quot;Int32&quot;,
    $ContextTags: [&quot;String&quot;],
    $CustomFields: {&quot;String&quot;: &quot;String&quot;}
}</code></pre>
<p>(위의 c# 코드를 아래의 형태로 바꿔야 한다.)
하지만, 여전히 문제는 남아 있었다. C# 클래스를 custom scheme으로 변환해야 하는데, 현재로서는 <strong>수동으로 사람이 C# 코드의 필드명과 타입을 파악하여, 일일이 아래와 같이 작성해줘야 하는 상황</strong>이다. 이 부분을 자동화할 수 있다면, 일손이 줄어들어서 개발 속도가 빨라질 것이며, 향후 1.7에서 다른 커스텀 자료형 xnb가 추가되더라도 빠르게 대응할 수 있을 것이라고 생각했다.</p>
<p>그러기 위해서는 C# 코드 문자열에서 타입과 필드명을 추출하는 방안이 필요했다.</p>
<h2 id="마법의-제미니고둥">마법의 제미니고둥</h2>
<blockquote>
<pre><code class="language-c#">publicclassCharacterAppearanceData
 {
/// &lt;summary&gt;An ID for this entry within the appearance list. This only needs to be unique within the current list.&lt;/summary&gt;
publicstringId;
/// &lt;summary&gt;A game state query which indicates whether this entry applies. Default true.&lt;/summary&gt;
    [ContentSerializer(Optional = true)]
publicstringCondition;
/// &lt;summary&gt;The season when this appearance applies, or &lt;c&gt;null&lt;/c&gt; for any season.&lt;/summary&gt;
    [ContentSerializer(Optional = true)]
publicSeason? Season;
/// &lt;summary&gt;Whether the appearance can be used when the NPC is indoors.&lt;/summary&gt;
    [ContentSerializer(Optional = true)]
publicboolIndoors = true;
/// &lt;summary&gt;Whether the appearance can be used when the NPC is outdoors.&lt;/summary&gt;
    [ContentSerializer(Optional = true)]
publicboolOutdoors = true;
/// &lt;summary&gt;The asset name for the portrait texture, or null for the default portrait.&lt;/summary&gt;
    [ContentSerializer(Optional = true)]
publicstringPortrait;
/// &lt;summary&gt;The asset name for the sprite texture, or null for the default sprite.&lt;/summary&gt;
    [ContentSerializer(Optional = true)]
publicstringSprite;
/// &lt;summary&gt;Whether this is island beach attire worn at the resort.&lt;/summary&gt;
/// &lt;remarks&gt;This is mutually exclusive: NPCs will never wear it in other contexts if it&#39;s true, and will never wear it as island attire if it&#39;s false.&lt;/remarks&gt;
    [ContentSerializer(Optional = true)]
publicboolIsIslandAttire;
/// &lt;summary&gt;The order in which this entry should be checked, where 0 is the default value used by most entries. Entries with the same precedence are checked in the order listed.&lt;/summary&gt;
    [ContentSerializer(Optional = true)]
publicintPrecedence;
/// &lt;summary&gt;If multiple entries with the same &lt;see cref=&quot;F:StardewValley.GameData.Characters.CharacterAppearanceData.Precedence&quot; /&gt; apply, the relative weight to use when randomly choosing one.&lt;/summary&gt;
/// &lt;remarks&gt;See remarks on &lt;see cref=&quot;F:StardewValley.GameData.Characters.CharacterData.Appearance&quot; /&gt;.&lt;/remarks&gt;
    [ContentSerializer(Optional = true)]
publicintWeight;
 }</code></pre>
</blockquote>
<pre><code>와 같은 c# 코드 문자열을 입력받아, 해당 클래스의 필드명과 타입을 반환하는 javascript 함수를 작성해 줘.

&gt; I&#39;d be glad to create a JavaScript function that translates the C# class fields and types, incorporating insights from the provided ratings:
&gt; ```js
function convertC SharpeClassToJavaScript(csharpClassString) {
  // Remove comments and leading/trailing whitespace
  const cleanClassString = csharpClassString.replace(/\/\/.*|\/\*.*?\*\/|^\s+|\s+$/gm, &#39;&#39;);
  // Extract field declarations using regular expression
  const fieldMatches = cleanClassString.match(/public (string|int|bool|\?|null) (\w+)(\[[^\]]*\])?( =.+)?;/g);
  if (!fieldMatches) {
    throw new Error(&#39;Invalid C# class format or no fields found.&#39;);
  }
  // Process each field match and build the JavaScript object
  const jsFields = {};
  for (const match of fieldMatches) {
    const parts = match.split(/\s+/);
    const type = parts[1];
    const name = parts[2];
    let isOptional = false;
    // Check for optional types (nullable or array)
    if (type.endsWith(&#39;?&#39;)) {
      isOptional = true;
      type = type.slice(0, -1);
    } else if (parts[3] &amp;&amp; parts[3].startsWith(&#39;[&#39;)) {
      isOptional = true;
      type = &#39;any[]&#39;; // Represent arrays as any[] for simplicity
    }
    // Handle default values
    let defaultValue = null;
    if (parts.length &gt; 5 &amp;&amp; parts[4] === &#39;=&#39;) {
      defaultValue = parts.slice(5).join(&#39; &#39;);
      // Remove quotes for strings if necessary
      if (defaultValue.startsWith(&#39;&quot;&#39;) &amp;&amp; defaultValue.endsWith(&#39;&quot;&#39;)) {
        defaultValue = defaultValue.slice(1, -1);
      }
    }
    jsFields[name] = {
      type: convertCSharpeTypeToJavaScript(type),
      optional: isOptional,
      default: defaultValue,
    };
  }
  return jsFields;
}
function convertCSharpeTypeToJavaScript(csharpType) {
  switch (csharpType) {
    case &#39;string&#39;:
      return &#39;string&#39;;
    case &#39;int&#39;:
      return &#39;number&#39;; // Integers in JavaScript are floating-point numbers
    case &#39;bool&#39;:
      return &#39;boolean&#39;;
    case &#39;null&#39;:
      return &#39;null&#39;;
    default:
      return &#39;any&#39;; // Handle unknown types as any
  }
}</code></pre><p>인공지능 시대에 맞추어, 구글의 인공지능 Gemini를 활용해 기본적인 아이디어를 알아보았다. 역시 인공지능이다. 개발자도 인공지능을 활용하면 능률을 올릴 수 있다.
실제로 저 코드를 돌려본 결과, 나름 어느 정도의 정확성을 띄는 걸 보일 수 있다.</p>
<h3 id="인공지능-코드분석">인공지능 코드분석</h3>
<p>기본적으로, 인공지능이 내린 해답은 정규 표현식을 이용한 방법으로, 특정 패턴이 일치한 모든 문자열을 추출해, 해당 문자열에서 타입과 필드명 정보를 추출하는 방식이다.</p>
<pre><code class="language-js">const cleanClassString = csharpClassString.replace(/\/\/.*|\/\*.*?\*\/|^\s+|\s+$/gm, &#39;&#39;);</code></pre>
<p><strong>필요 없는 주석과 공백을 제거하는 코드</strong>이다. 사용된 정규표현식은 다음의 4개의 정규표현식으로 나눌 수 있는데, 그것은 다음과 같다.
<code>\/\/.*</code> : <code>// (아무 문자)</code>를 감지하는 정규표현식이다.
<code>\/\*.*?\*\/</code> : <code>/* (아무 문자) */</code>를 감지하는 정규표현식이다. 이 때 <code>.*?</code>은 non-greedy한 방식으로 아무 문자를 찾겠다는 의미다. 즉, <code>/*yes*/no*/</code>와 같은 문자열이 있을 때, <code>/*yes*/</code>만 추출하겠다는 의미다.
<code>^\s+</code> : 문자열의 시작이 공백 문자의 연속인 경우를 감지하는 정규표현식이다.
<code>\s+$</code> : 문자열의 끝이 공백 문자의 연속인 경우를 감지하는 정규표현식이다.
정규 표현식 뒤에 붙은 <code>gm</code> 플래그는 전역적으로, 여러 라인에 걸쳐서 문자열을 탐지하라는 의미다.</p>
<pre><code class="language-js">const fieldMatches = cleanClassString.match(/public (string|int|bool|\?|null) (\w+)(\[[^\]]*\])?( =.+)?;/g);</code></pre>
<p><strong>C# 코드에서 프로퍼티 선언 부분을 추출하는 코드</strong>이다. 구체적으로는, 다음과 같다.
<code>public (string|int|bool|\?|null)</code> : <code>public (타입명)</code> 을 추출한다. <em>다만, optional 타입(<code>public string? MyVariable</code>)의 경우 추출하지 못하는 버그가 있다.</em>
<code>(\w+)</code> : 변수명을 추출한다.
<code>(\[[^\]]*\])?</code> : <code>[test123]</code>과 같은, 배열 여부를 추출한다. 구체적으로는, <code>[</code>를 감지하는 <code>\[</code>, 대괄호 내 <code>]</code>이 아닌 문자열을 감지하는 <code>[^\]]*</code>, <code>]</code>를 감지하는 <code>\]</code>로 나뉜다. <em>사실 이 부분은 인공지능의 한계로, 원래 c# 문법이 아니다.</em>
<code>( =.+)?;</code> : 변수의 마지막 부분을 추출한다. = 뒤에 아무거나 있거나 없는 경우를 추출하며, 마지막은 ;로 끝나야 한다.</p>
<pre><code class="language-js">for (const match of fieldMatches) {
    const parts = match.split(/\s+/);
    const type = parts[1];
    const name = parts[2];
    let isOptional = false;
    // Check for optional types (nullable or array)
    if (type.endsWith(&#39;?&#39;)) {
      isOptional = true;
      type = type.slice(0, -1);
    } else if (parts[3] &amp;&amp; parts[3].startsWith(&#39;[&#39;)) {
      isOptional = true;
      type = &#39;any[]&#39;; // Represent arrays as any[] for simplicity
    }</code></pre>
<p>fieldMatches는 정규표현식의 그룹과 상관없이 매치된 모든 문자열 전체를 배열로 저장한다. 이 배열을 순회한다.</p>
<ul>
<li>우선, 공백 문자를 기준으로 파트를 나눈다.</li>
<li>type, name을 추출하고, type이 ?로 끝나는 경우에는 optional 플래그를 true로 변경시킨다.<ul>
<li><em>다만, 인공지능이 생성한 정규표현식에 오류가 있어서 <code>public int? MyProperty;</code>와 같은 실제 optional field는 추출하지 못한다.</em></li>
</ul>
</li>
<li>파트의 4번째가 존재하고, <code>[</code>로 시작할 때, 배열로 간주하고 type을 <code>any[]</code>로 설정한다.<ul>
<li>*단, 실제 C# 코드에서는 배열 프로퍼티를 <code>public int[] test;</code>와 같이 선언하지, <code>public int test []</code>와 같이 선언하지 않는다.<pre><code class="language-js">let defaultValue = null;
if (parts.length &gt; 5 &amp;&amp; parts[4] === &#39;=&#39;) {
  defaultValue = parts.slice(5).join(&#39; &#39;);
  // Remove quotes for strings if necessary
  if (defaultValue.startsWith(&#39;&quot;&#39;) &amp;&amp; defaultValue.endsWith(&#39;&quot;&#39;)) {
    defaultValue = defaultValue.slice(1, -1);
  }
}</code></pre>
</li>
</ul>
</li>
</ul>
<p>추출한 문자열에 =가 있는 경우, parts 뒤에 있는 모든 문자열을 디폴트 값으로 취급하고, 이를 변수에 저장한다. 만약 &quot;&quot;로 시작하는 경우는 앞 뒤의 따옴표를 제거한다.</p>
<pre><code class="language-js">jsFields[name] = {
      type: convertCSharpeTypeToJavaScript(type),
      optional: isOptional,
      default: defaultValue,
    };</code></pre>
<p>result 객체의 이름 필드에, 타입, optional 여부, default 값을 저장한 객체를 할당한다.</p>
<p>자잘한 오류들이 있어서 실제로 적용시킬 수는 없으나, 기본적으로 정규표현식을 이용한다는 아이디어를 얻었으므로, 이 아이디어를 기반으로 코드를 개조할 생각이다.</p>
<h2 id="결과물">결과물</h2>
<pre><code class="language-js">const enums = [
    &quot;Season&quot;,
    &quot;Gender&quot;,
    &quot;MachineOutputTrigger&quot;,
    &quot;MachineTimeBlockers&quot;,
    &quot;QuantityModifier.ModificationType&quot;,
    &quot;QuantityModifier.QuantityModifierMode&quot;,
    &quot;PetAnimationLoopMode&quot;,
    &quot;LimitedStockMode&quot;,
    &quot;ShopOwnerType&quot;,
    &quot;StackSizeVisibility&quot;,
    &quot;QuestDuration&quot;,
    &quot;WildTreeGrowthStage&quot;,
    &quot;PlantableRuleContext&quot;,
    &quot;PlantableResult&quot;
];

function convertCSharpTypeToSchemeData(raw)
{
    if(raw === &quot;int&quot;) return &quot;Int32&quot;;
    if(raw === &quot;string&quot;) return &quot;String&quot;;
    if(raw === &quot;double&quot;) return &quot;Double&quot;;
    if(raw === &quot;bool&quot;) return &quot;Boolean&quot;;
    if(raw === &quot;float&quot;) return &quot;Single&quot;;
    if(raw === &quot;char&quot;) return &quot;Char&quot;;
    if(enums.includes(raw)) return &quot;Int32&quot;;

    let isArray = raw.endsWith(&quot;[]&quot;);
    if(isArray) return `Array&lt;${convertCSharpTypeToSchemeData(raw.slice(0,-2))}&gt;`;

    if(raw.startsWith(&quot;List&quot;)) {
        const [,value]= /List&lt;([\w?\[\]&lt;,.&gt;]+)&gt;/.exec(raw);
        return [ convertCSharpTypeToSchemeData(value) ];
    }
    if(raw.startsWith(&quot;Dictionary&quot;)) {
        const [,key,value] = /Dictionary&lt;([\w?\[\]&lt;.&gt;]+),([\w?\[\]&lt;.&gt;]+)&gt;/.exec(raw);
        return {
            [convertCSharpTypeToSchemeData(key)] : convertCSharpTypeToSchemeData(value)
        }
    }
    return raw;
}

function isPrimitive(type)
{
    const primitiveList = [
        &quot;Int32&quot;,
        &quot;Single&quot;,
        &quot;Double&quot;,
        &quot;Boolean&quot;,
        &quot;Char&quot;,
        &quot;Point&quot;,
        &quot;Vector2&quot;,
        &quot;Vector3&quot;,
        &quot;Vector4&quot;,
        &quot;Rectangle&quot;
    ];

    return primitiveList.includes(type);
}


function convertCSharpClassToSchemeData(csharpClassString)
{
    const cleanClassString = csharpClassString.replace(/\/\/.*|\/\*.*?\*\/|^\s+|\s+$/gm, &#39;&#39;).replace(/,\s*/gm, &#39;,&#39;);

    const regExp = /(\[ContentSerializer\(Optional = true\)\]\n|\[ContentSerializerIgnore\]\n)?public ([\w?\[\]&lt;.,&gt;]+) ([\w_]+)(;| = | { get)/;
    const fieldMatches = cleanClassString.match(new RegExp(regExp, &quot;g&quot;));

    console.log(fieldMatches);

    let result = {};

    for(let rawCode of fieldMatches)
    {
        let [, decorator, type, field] = regExp.exec(rawCode);
        let optional = false;
        if(decorator === &quot;[ContentSerializerIgnore]\n&quot;) continue;
        if(type.endsWith(&quot;?&quot;)) {
            optional = true;
            type = type.slice(0, -1);
        }
        type = convertCSharpTypeToSchemeData(type);

        if(decorator === &quot;[ContentSerializer(Optional = true)]\n&quot; &amp;&amp; !isPrimitive(type)) optional = true;

        if(optional) field = &quot;$&quot;+field;
        result[field] = type;
    }

    console.log(&quot;export default &quot;+JSON.stringify(result, null, 2)+&quot;;&quot;);

    return result;
}</code></pre>
<p>다음과 같은 코드를 작성하였다. 전반적으로, 제미니가 사용한 코드를 기반으로 코드를 깔끔하게 다듬고 실제 C# 코드에 맞게 수정하였다.</p>
<h3 id="코드-분석">코드 분석</h3>
<pre><code class="language-js">const regExp = /(\[ContentSerializer\(Optional = true\)\]\n|\[ContentSerializerIgnore\]\n)?public ([\w?\[\]&lt;.,&gt;]+) ([\w_]+)(;| = | { get)/;</code></pre>
<p>타입을 정의하는 코드를 추출하는 정규표현식을 정의한다. 이 정규표현식이 cleanClassString에서 타입 코드의 목록을 추출하는 역할도 하지만, 각 타입 코드 문자열에서 데코레이터, 타입, 필드를 추출하는 역할을 하기도 한다.
구체적으로 분석하자면 다음과 같다.</p>
<ul>
<li><code>(\[ContentSerializer\(Optional = true\)\]\n|\[ContentSerializerIgnore\]\n)?</code> : <strong>데코레이터 부분을 추출</strong>한다. <ul>
<li><code>[ContentSerializer(Optional = true)]</code>는 해당 c# 필드가 시리얼라이즈되었을 때, 즉 xnb 파일로 변환했을 때 optional한지를 나타내는 필드다. 해당 데코레이터가 존재하면, 해당 필드를 xnb 파일로 다음과 같이 저장한다.<ul>
<li>원시 자료형(int, float, double, boolean, char) : 아무런 영향이 없다. 그대로 저장한다.</li>
<li>참조형 자료형(string, List, Dictionary, 사용자 정의 클래스 등) : nullable 자료형으로 래핑되어서 저장된다. 값이 null이면 첫 번째 바이트에 0을 저장하고 종료하며, 값이 null이 아니면 첫 번째 바이트에 사용되는 리더의 일련번호를 저장한 뒤 두 번째 바이트부터 실제 타입의 리더를 사용해 저장된다.</li>
</ul>
</li>
<li><code>[ContentSerializerIgnore]</code>는 해당 c# 필드가 시리얼라이즈되지 않는다는 의미다. 해당 데코레이터가 존재하면, 저장을 무시한다.</li>
</ul>
</li>
<li><code>public ([\w?\[\]&lt;.,&gt;]+)</code> : <strong>타입 부분을 추출</strong>한다. 타입은 int, string, bool 등만 존재하는 것이 아니라, 사용자 정의 클래스도 존재하며, 제네릭 클래스나 심지어는 .이 붙은 클래스도 존재하기에 다음과 같이 정규표현식을 작성했다.</li>
<li><code>([\w_]+)</code> : <strong>변수명 부분을 추출</strong>한다. 사실 생각해보니 <code>(\w+)</code>로 작성해도 <code>_</code>를 인지할 수 있다.</li>
<li><code>(;| = | { get)</code> : 이 부분이 실제로 변수를 선언하는 것인지를 파악한다. <code>;</code>로 끝나거나, <code>=</code>로 끝나거나, <code>{ get</code>으로 끝나는 경우는 시리얼라이즈되는 변수로 파악한다. 변수가 아닌 함수 등을 거르기 위해 사용되었다.</li>
</ul>
<p>참고로 필드의 디폴트 값은 관심사가 아니기 때문에 추출하지 않는다.</p>
<pre><code class="language-js">const fieldMatches = cleanClassString.match(new RegExp(regExp, &quot;g&quot;));</code></pre>
<p>위의 정규표현식을 기반으로, C# 코드에서 프로퍼티를 선언한 코드의 목록을 추출한다.</p>
<pre><code class="language-js">for(let rawCode of fieldMatches)
{
    let [, decorator, type, field] = regExp.exec(rawCode);
    let optional = false;
    if(decorator === &quot;[ContentSerializerIgnore]\n&quot;) continue;
    if(type.endsWith(&quot;?&quot;)) {
        optional = true;
        type = type.slice(0, -1);
    }

    type = convertCSharpTypeToSchemeData(type);

    if(decorator === &quot;[ContentSerializer(Optional = true)]\n&quot; &amp;&amp; !isPrimitive(type)) optional = true;
    if(optional) field = &quot;$&quot;+field;
    result[field] = type;
}</code></pre>
<p>모든 프로퍼티 선언 코드 목록에 대해, 다음의 코드를 실행한다.</p>
<ul>
<li>위에서 선언한 정규 표현식을 기반으로, 데코레이터, 타입, 필드를 추출해 변수에 저장한다. <code>let [, decorator, type, field]</code>와 같이 생긴 요상한 문법은 ES6에서 추가된 구조 분해 할당이다. 콤마 앞에 아무것도 안 쓴 이유는 일치하는 전체 문자열을 안 쓰겠다는 의미.</li>
<li>데코레이터가 <code>[ContentSerializerIgnore]</code>인 경우 무시하고 다음으로 넘어간다.</li>
<li>타입이 ?로 끝날 경우, nullable한 원시 자료형이다. optional을 true로 하고 type 문자열에서 ?를 제거한다.</li>
<li>데코레이터가 <code>[ContentSerializer(Optional = true)]</code>이고, 타입이 원시 자료형이 아닌 경우, optional을 true로 한다.</li>
<li>C# 타입을 xnb.js의 custom scheme이 사용하는 타입으로 변화시킨다. 해당 함수는 하술.</li>
<li>optional이 true일 때, 필드명 앞에 $를 추가한다.</li>
<li>result 객체에 값을 저장한다.</li>
</ul>
<pre><code class="language-js">function convertCSharpTypeToSchemeData(raw)
{
    if(raw === &quot;int&quot;) return &quot;Int32&quot;;
    if(raw === &quot;string&quot;) return &quot;String&quot;;
    if(raw === &quot;double&quot;) return &quot;Double&quot;;
    if(raw === &quot;bool&quot;) return &quot;Boolean&quot;;
    if(raw === &quot;float&quot;) return &quot;Single&quot;;
    if(raw === &quot;char&quot;) return &quot;Char&quot;;
    if(enums.includes(raw)) return &quot;Int32&quot;;

    let isArray = raw.endsWith(&quot;[]&quot;);
    if(isArray) return `Array&lt;${convertCSharpTypeToSchemeData(raw.slice(0,-2))}&gt;`;

    if(raw.startsWith(&quot;List&quot;)) {
        const [,value]= /List&lt;([\w?\[\]&lt;,.&gt;]+)&gt;/.exec(raw);
        return [ convertCSharpTypeToSchemeData(value) ];
    }
    if(raw.startsWith(&quot;Dictionary&quot;)) {
        const [,key,value] = /Dictionary&lt;([\w?\[\]&lt;.&gt;]+),([\w?\[\]&lt;.&gt;]+)&gt;/.exec(raw);
        return {
            [convertCSharpTypeToSchemeData(key)] : convertCSharpTypeToSchemeData(value)
        }
    }
    return raw;
}</code></pre>
<p>C# 타입을 xnb.js의 custom scheme이 사용하는 타입으로 변화시키는 함수다. 다음의 과정을 거친다.</p>
<ul>
<li>원시 타입이 int, string, double, bool, float, char일 경우, 해당하는 타입으로 변화시킨다. 참고로 저 타입은 xnb 리더의 타입 이름들이다. (Int32Reader의 경우 Int32)</li>
<li>원시 타입이 미리 정의된 enum에 속해 있을 경우, Int32를 리턴한다. enum 자료형은 xnb 파일에 전부 int형으로 저장된다.</li>
<li>원시 타입 문자열이 <code>[]</code>로 끝날 경우, Array 자료형이다. 원시 타입 문자열에서 <code>[]</code>를 제거한 문자열에 대해 다시 해당 변환 과정을 수행하고, <code>Array&lt;타입&gt;</code>을 리턴한다.</li>
<li>원시 타입 문자열이 <code>List</code>로 시작할 경우, List 자료형이다. 원시 타입 문자열에서 <code>&lt;&gt;</code> 내부의 값을 추출한 뒤, 해당 값에 대해 변환 과정을 수행하고, <code>[ 타입 ]</code>을 리턴한다.</li>
<li>원시 타입 문자열이 <code>Dictionary</code>로 시작할 경우, Dictionary 자료형이다. <code>&lt;&gt;</code> 내부 key와 value 타입 값을 추출한 뒤, 해당 값에 대해 변환 과정을 수행하고, <code>{key 타입 : value 타입}</code>을 리턴한다.</li>
</ul>
<pre><code class="language-js">console.log(&quot;export default &quot;+JSON.stringify(result, null, 2)+&quot;;&quot;);</code></pre>
<p>결과값을 콘솔에 추가한다. 사용자는 콘솔에 찍힌 결과값을 복붙해 파일에 저장할 수 있다.</p>
<h2 id="결과">결과</h2>
<p>이렇게 C# 코드를 xnb.js가 해석하는 custom scheme으로 변경하는 코드를 만들었더니, 작업 프로세스가 비약적으로 단축된 모습을 보였다. 기존에는 C# 코드를 직접 불러와 일일이 해석하고 타이핑하는 과정을 거쳤다면, 이제는 C# 코드 복사 -&gt; 함수 실행 -&gt; 실행 결과를 js 파일로 저장하는 과정으로 단축되었기 때문이다.
물론 C# 코드에 적힌 필드의 저장 순서와 실제 xnb 파일에 저장된 필드 저장 순서가 다른 경우도 있어서 그 부분은 직접 수정이 필요했지만, 어쨌거나 대부분의 경우는 잘작동되는 모습을 보였다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[xnb.js 개발일지(4) - 보다 쉬운 커스텀 리더 제작기]]></title>
            <link>https://velog.io/@lybell_4rt/xnb.js-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%804-%EB%B3%B4%EB%8B%A4-%EC%89%AC%EC%9A%B4-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%A6%AC%EB%8D%94-%EC%A0%9C%EC%9E%91%EA%B8%B0</link>
            <guid>https://velog.io/@lybell_4rt/xnb.js-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%804-%EB%B3%B4%EB%8B%A4-%EC%89%AC%EC%9A%B4-%EC%BB%A4%EC%8A%A4%ED%85%80-%EB%A6%AC%EB%8D%94-%EC%A0%9C%EC%9E%91%EA%B8%B0</guid>
            <pubDate>Thu, 21 Mar 2024 00:48:48 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>한국 시간으로 2024년 3월 20일 오전 3시, Stardew Valley 1.6이 업데이트되었다. 이에 따라 Stardew Valley의 유틸리티 웹 앱인 xnb.js와 stardew dressup의 업데이트가 불가피해졌다. Stardew Valley 1.6 업데이트가 xnb.js와 stardew dressup에 미치는 영향은 다음과 같다.</p>
<ul>
<li>xnb.js<ul>
<li><strong>대규모의 xnb 데이터 포맷 변경</strong><ul>
<li>stardew valley가 사용하는 Data를 담은 xnb 파일이 더 이상 {string: string} 형의 딕셔너리 자료형이 아닌, c# 객체를 담은 커스텀 자료구조 포맷으로 변경되었다.</li>
</ul>
</li>
</ul>
</li>
<li>stardew dressup<ul>
<li>신규 모자, 상의, 악세서리 추가</li>
<li>체형 스프라이트 변경</li>
</ul>
</li>
</ul>
<p>따라서, 거의 2년 만에 xnb.js와 stardew dressup을 업데이트하기로 결정했다.</p>
<h2 id="문제-상황">문제 상황</h2>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/640a9549-f613-4936-9456-3c85b0cbb3da/image.png" alt=""></p>
<p style="font-size:12px; text-align:center;">에릭 바론, 이게 맞아요?</p>

<p>Stardew Valley 1.6에서는 수많은 양의 데이터를 저장하는 새로운 클래스들이 생겼다. 1.5까지는 클래스의 양이 적었기 때문에, 다음의 방식으로 코드를 추가했었다.</p>
<pre><code class="language-javascript">import {BaseReader,
    ListReader,
    Int32Reader,
    StringReader,
    DictionaryReader,
    NullableReader
} from &quot;../../readers/readers.js&quot;; //@xnb/readers
import FishPondRewardReader from &quot;./FishPondRewardReader.js&quot;;

/**
 * FishPondData Reader
 * @class
 * @extends BaseReader
 */
export default class FishPondDataReader extends BaseReader {
    static isTypeOf(type) {
        switch (type) {
            case &#39;StardewValley.GameData.FishPond.FishPondData&#39;:
                return true;
            default: return false;
        }
    }
    static parseTypeList() {
        return [&quot;FishPondData&quot;, 
            &quot;List&lt;String&gt;&quot;, &quot;String&quot;, // requiredTags
            null, // spawnTime
            &quot;List&lt;FishPondReward&gt;&quot;, ...FishPondRewardReader.parseTypeList(), //producedItems
            &quot;Nullable&lt;Dictionary&lt;Int32,List&lt;String&gt;&gt;&gt;:4&quot;, 
            &quot;Dictionary&lt;Int32,List&lt;String&gt;&gt;&quot;, &quot;Int32&quot;, &quot;List&lt;String&gt;&quot;, &quot;String&quot; //populationGates
        ];
    }
    static type()
    {
        return &quot;Reflective&lt;FishPondData&gt;&quot;;
    }

    /**
     * Reads FishPondData from buffer.
     * @param {BufferReader} buffer
     * @param {ReaderResolver} resolver
     * @returns {object}
     */
    read(buffer, resolver) {
        const int32Reader = new Int32Reader();
        const stringListDictReader = new NullableReader( new DictionaryReader(
            new Int32Reader(),
            new ListReader( new StringReader() )
        ) );

        const RequiredTags = resolver.read(buffer);
        const SpawnTime = int32Reader.read(buffer);
        const ProducedItems = resolver.read(buffer);
        const PopulationGates = stringListDictReader.read(buffer, resolver);

        return {
            RequiredTags,
            SpawnTime,
            ProducedItems,
            PopulationGates
        };
    }

    write(buffer, content, resolver) {
        const stringListReader = new ListReader( new StringReader() );
        const int32Reader = new Int32Reader();
        const fishPondRewardListReader = new ListReader( new FishPondRewardReader() );
        const stringListDictReader = new NullableReader( new DictionaryReader(
            new Int32Reader(),
            new ListReader( new StringReader() )
        ) );

        this.writeIndex(buffer, resolver);

        stringListReader.write(buffer, content.RequiredTags, resolver);
        int32Reader.write(buffer, content.SpawnTime, null);
        fishPondRewardListReader.write(buffer, content.ProducedItems, resolver);
        stringListDictReader.write(buffer, content.PopulationGates, resolver);
    }

    isValueType() {
        return false;
    }
}</code></pre>
<p>c# 클래스를 기반으로, 읽기 함수, 쓰기 함수, parseTypeList(언팩한 json을 yaml 포맷으로 바꾸는 데 annotation을 붙이는 데 필요하다)를 일일이 작성하고, 이를 클래스로 만들어야 했다. 1.5까지는 커스텀 자료구조를 저장하는 xnb 파일이 별로 없어서 전통적인 방식을 유지해도 상관이 없었으나, 1.6에서 대규모의 커스텀 자료구조 xnb 파일이 추가되면서, 전통적인 방식을 고수하면 <strong>유지보수하기 매우 어려울 것이라는 생각</strong>이 들었다.
따라서, 타입 구조 객체를 받아서 위와 같은 코드로 동작하는 xnb 리더를 만들기로 결정했다.</p>
<h3 id="공통점-파악">공통점 파악</h3>
<pre><code class="language-c#">using Microsoft.Xna.Framework.Content;
using System.Collections.Generic;

namespace StardewValley.GameData.FishPond
{
    public class FishPondData
    {
        public List&lt;string&gt; RequiredTags;

        [ContentSerializer(Optional = true)]
        public int SpawnTime = -1;

        public List&lt;FishPondReward&gt; ProducedItems;

        [ContentSerializer(Optional = true)]
        public Dictionary&lt;int, List&lt;string&gt;&gt; PopulationGates;
    }
}</code></pre>
<pre><code class="language-javascript">read(buffer, resolver) {
    const int32Reader = new Int32Reader();
    const stringListDictReader = new NullableReader( new DictionaryReader(
        new Int32Reader(),
        new ListReader( new StringReader() )
    ) );

    const RequiredTags = resolver.read(buffer);
    const SpawnTime = int32Reader.read(buffer);
    const ProducedItems = resolver.read(buffer);
    const PopulationGates = stringListDictReader.read(buffer, resolver);

    return {
        RequiredTags,
        SpawnTime,
        ProducedItems,
        PopulationGates
    };
}</code></pre>
<p>위는 원본 c# 클래스이며, 아래는 해당 c# 클래스의 인스턴스를 저장하는 xnb 파일을 불러오는 read 함수다. 클래스의 필드명이 언팩 객체의 필드명이 되며, <code>[ContentSerializer(Optional = true)]</code>가 붙은 필드는 nullable로 감싸주고 있다.
resolver(xnb 파일의 헤더를 읽어와 필요한 reader를 선언한다)에 정의되지 않은, 원시 자료형 reader나, nullable reader는 따로 리더 인스턴스를 선언한 뒤 이를 기반으로 읽기 과정을 수행하며, 그렇지 않은 경우 resolver에 읽기 과정을 맡긴다.</p>
<p>잘 생각해 보면, 미리 객체의 key와 reader를 대응시킨 map을 만든 뒤 이를 순회하면 해결할 수 있을 것 같다.</p>
<pre><code class="language-javascript">read(buffer, resolver) {
  const result = {};
  for(let [key, reader] of this.readers.entries())
  {
    if(reader.isValueType()) result[key] = reader.read(buffer);
    else if(reader.constructor.type() === &quot;Nullable&quot;) result[key] = reader.read(buffer, resolver);
    else result[key] = resolver.read(buffer);
  }

  return result;
}</code></pre>
<p>이렇게 변환할 수 있다. write 역시 비슷한 방식으로 바꿀 수 있다.</p>
<p>문제는 타입 데이터를 기반으로 커스텀 reader를 생성하는 것에 있었는데...</p>
<h2 id="접근-과정">접근 과정</h2>
<p>분석 결과, 커스텀 클래스형 자료구조 플러그인은 ReflectiveReader를 사용하고 있었으며, ReflectiveReader의 하위 데이터로 C# 클래스를 갖는 것을 확인할 수 있다. 이를 기반으로, 타입을 정리한 scheme 객체를 이용해 해당 타입 구조를 언팩하고 패킹할 수 있는 ReflectiveReader를 반환하기로 결정한다.</p>
<p>xnb.js가 xnb 파일을 언팩하는 과정은 다음과 같다.</p>
<ol>
<li>lz4 혹은 lzx로 압축된 파일의 압축을 해제한다.</li>
<li>헤더 부분의 유효성을 확인하고, 헤더 데이터를 추출한다.</li>
<li>리더 데이터를 읽어온다.</li>
<li>문자열로 된 리더 데이터를 기반으로, <code>TypeReader</code> static 클래스에서 문자열로 된 리더를 실제 객체 리더로 변환한다.</li>
<li>실제 리더 객체 리스트를 기반으로, 데이터를 언팩한다.</li>
</ol>
<p>xnb.js가 문자열로 된 리더를 실제 객체로 변환하는 과정은 다음과 같다.</p>
<ol>
<li>생 문자열로 된 리더를 간략화한다. 이 과정은 디버깅에도 쓰이지만, 리더 객체를 빠르게 찾거나, yaml로 변환할 때 타입을 첨부하는 데에도 쓰인다.</li>
<li>간략화된 문자열을 기반으로 실제 리더 객체를 반환한다.</li>
</ol>
<p>예시로, <code>Microsoft.Xna.Framework.Content.DictionaryReader｀2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[StardewValley.GameData.Fences.FenceData, StardewValley.GameData, Version=1.6.1.24080, Culture=neutral, PublicKeyToken=null]]</code> 문자열은 <code>Dictionary&lt;String, StardewValley.GameData.Fences.FenceData&gt;</code> 로 간략화되고, 다시 이것이 <code>new DictionaryReader(new StringReader(), new FenceDataReader())</code> 로 바뀌는 형태다.</p>
<p>처음에는 ReflectiveReader를 상속하는 별도의 클래스를 만들거나, ReflectiveReader를 반환하는 함수를 reader 라이브러리에서 만들까 생각했지만, 전자의 경우는 유연성이 떨어질 것으로 예상되며, 후자의 경우는 reader 라이브러리에서 현재 임포트된 reader를 알기 위해 core 라이브러리를 참조하게 되는 경우가 있어서, core 라이브러리의 TypeReader 클래스, 즉 문자열 리더를 기반으로 실제 리더를 반환시키는 것을 담당하는 클래스를 수정하기로 결심했다.</p>
<p>TypeReader 클래스는 다음과 같은 수정과정을 거쳤다.</p>
<ol>
<li><code>simplifyType</code>, <code>getReader</code> 메소드는 ReflectiveReader / ReflectiveScheme 문자열이 감지되면, 하위 타입을 기반으로 <code>ReflectiveScheme&lt;${simple}&gt;</code> / <code>ReflectReader</code> 객체를 반환한다. 이 때, 하위호환성을 위해 이미 ReflectedReader를 커스텀 리더 클래스로 구현한 경우 그것을 우선시한다.</li>
<li><code>simplifyType</code>, <code>getReader</code>  메소드는 알 수 없는 문자열이 감지되면 해당 클래스를 지원하는 schemes가 있는지 감지하고, 있으면 <code>ReflectiveScheme&lt;${simple}&gt;</code> / <code>ReflectReader</code> 객체를 반환한다.</li>
<li>커스텀 타입 구조를 임포트할 수 있는 <code>setSchemes</code>, <code>addSchemes</code> API를 추가했다. 이 때, 임포트를 진행할 때 미리 임포트한 객체를 실제 Reader 객체로 변환시킨다.</li>
<li>scheme을 reader로 변환시키는 코드는 다음과 같다.</li>
</ol>
<pre><code class="language-jsx">function convertSchemeEntryToReader(scheme)
{
    if(typeof scheme === &quot;string&quot;) return TypeReader.getReader(scheme);

    if(Array.isArray(scheme)) {
        const ListReader = TypeReader.getReaderClass(&quot;ListReader&quot;);
        return new ListReader(convertSchemeEntryToReader(scheme[0]));
    }
    if(typeof scheme === &quot;object&quot;) {
        const keyCount = Object.keys(scheme).length;
        if(keyCount === 1) {
            const DictionaryReader = TypeReader.getReaderClass(&quot;DictionaryReader&quot;);
            const [key, value] = Object.entries(scheme)[0];

            return new DictionaryReader(
                convertSchemeEntryToReader(key),
                convertSchemeEntryToReader(value)
            );
        }
        else if(keyCount &gt; 1) {
            return convertSchemeToReader(scheme);
        }
    }
    throw new XnbError(`Invalid Scheme to convert! : ${scheme}`);
}

function convertSchemeToReader(scheme)
{
    const result = new Map();
    for(let [key, type] of Object.entries(scheme))
    {
        let reader = convertSchemeEntryToReader(type);

        if(key.startsWith(&quot;@&quot;)) {
            key = key.slice(1);
            if(!reader.isValueType()) {
                try {
                    reader = new TypeReader.readers.NullableReader(reader);
                }
                catch {
                    throw new XnbError(&quot;There is no NullableReader from reader list!&quot;);
                }
            }
        }
        result.set(key, reader);
    }
    return result;
}</code></pre>
<ul>
<li>scheme 오브젝트의 key, value를 순회한다.<ul>
<li>value를 기반으로 리더를 반환하는데,<ul>
<li>value가 문자열이면 해당 타입과 일치하는 reader 객체를 반환한다.</li>
<li>value가 <code>[&quot;Type&quot;]</code> 형태면 reader 객체를 ListReader로 감싼다.</li>
<li>value가 <code>{&quot;KeyType&quot; : &quot;ValueType&quot;}</code> 형태면 reader 객체를 DictionaryReader로 감싼다.</li>
<li>그 밖의 경우, 재귀적으로 실행한다.</li>
</ul>
</li>
<li>만약 key가 <code>@key</code> 타입인 경우, 해당 key는 optional 필드이다.<ul>
<li>NullableReader로 reader 객체를 감싼다.</li>
<li>만약 reader 객체가 primitive 자료형을 다루는 객체인 경우, 해당 객체는 nullable로 감싼 형태로 패킹되지 않으므로, reader 객체를 감싸지 않는다.</li>
</ul>
</li>
</ul>
</li>
<li>변환된 key와 value를 map 객체에 저장한다. map 객체에 저장하는 까닭은 map 객체는 추가한 순서를 보장하므로, 필드의 순서가 중요한 xnb 언팩에 적합하기 때문이다.</li>
</ul>
<p>이를 기반으로, reflectiveReader는 이름과 readers를 기반으로 다음의 언팩/패킹 과정을 거치게 된다.</p>
<pre><code class="language-jsx">/**
 * Reflective Reader
 * @class
 * @extends BaseReader
 */
export default class ReflectiveSchemeReader {
    static isTypeOf(type) {
        return false;
    }
    static hasSubType() {
        return false;
    }
    static type()
    {
        return &quot;ReflectiveScheme&quot;;
    }
    /**
     * @constructor
     * @param {Object} object scheme
     */
    constructor(name, readers) {
        this.name = name;
        this.readers = readers;
    }

    /**
     * Reads Reflection data from buffer.
     * @param {BufferReader} buffer
     * @returns {Mixed}
     */
    read(buffer, resolver) {
        const result = {};
        for(let [key, reader] of this.readers.entries())
        {
            if(reader.isValueType()) result[key] = reader.read(buffer);
            else if(reader.constructor.type() === &quot;Nullable&quot;) result[key] = reader.read(buffer, resolver);
            else result[key] = resolver.read(buffer);
        }

        return result;
    }

    /**
     * Writes Reflection data and returns buffer
     * @param {BufferWriter} buffer
     * @param {Number} content
     * @param {ReaderResolver} resolver
     */
    write(buffer, content, resolver) {
        this.writeIndex(buffer, resolver);
        for(let [key, reader] of this.readers.entries())
        {
            reader.write(buffer, content[key], (reader.isValueType() ? null : resolver));
        }</code></pre>
<ul>
<li>read 함수는 readers의 entries를 순회해, reader가 value 타입이거나 nullable 타입이면 reader 객체를 이용해 데이터를 가져오고, 그렇지 않으면 미리 정의된 resolver를 이용해 데이터를 가져온다. 불러온 데이터를 기반으로 result 객체에 저장한다.</li>
<li>write 함수는 index를 먼저 저장한 뒤, readers의 entries를 순회해, reader가 원시 타입이면 resolver 매개변수에 null을, 그렇지 않으면 resolver 객체를 인자로 넣어 reader를 이용해 값을 쓴다.<ul>
<li>resolver 매개변수에 원시 타입 여부를 구분하는 까닭은, xnb 포맷이 원시 타입 데이터를 저장하기 전에 사용하는 리더의 인덱스를 저장하지 않기 때문이다.</li>
</ul>
</li>
</ul>
<h2 id="향후-개발사항">향후 개발사항</h2>
<ul>
<li>잠재적으로 특정 c# 클래스의 프로퍼티 타입이 다른 c# 클래스를 참조할 수 있다. 해당 부분은 고려가 필요할 것.</li>
<li>아직 yaml을 대응하기 위한 <code>parseTypeList</code> 함수를 안 만들었다. 곧 만들 예정.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[react swiper.js와 useEffect]]></title>
            <link>https://velog.io/@lybell_4rt/react-swiper.js%EC%99%80-useEffect</link>
            <guid>https://velog.io/@lybell_4rt/react-swiper.js%EC%99%80-useEffect</guid>
            <pubDate>Fri, 15 Mar 2024 08:13:12 GMT</pubDate>
            <description><![CDATA[<p>본 아티클은 2022년 8월에, Stardew Dressup을 개발하다가 생긴 탐구를 개인 Notion에 작성한 것을 재구성한 것입니다.</p>
<hr>
<h2 id="리액트에서-다른-컴포넌트에-스와이퍼-페이지네이션-부착하기">리액트에서 다른 컴포넌트에 스와이퍼 페이지네이션 부착하기</h2>
<pre><code class="language-html">&lt;div class=&quot;swiper&quot;&gt;
    &lt;div class=&quot;swiper-wrapper&quot;&gt;
        &lt;div&gt;1&lt;/div&gt;
        &lt;div&gt;2&lt;/div&gt;
        &lt;div&gt;3&lt;/div&gt;
        &lt;div&gt;4&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;nav class=&quot;pagination&quot;&gt;
    &lt;div class=&quot;pagination-button&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;pagination-button&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;pagination-button&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;pagination-button&quot;&gt;&lt;/div&gt;
&lt;/nav&gt;</code></pre>
<p>위와 같은 구조의 html을 짜고 싶었다. swiper 엘리먼트 내부에서  각 엘리먼트가 스와이프되고, pagination 엘리먼트 내에 있는 각 원소를 클릭하면 스와이퍼가 자동으로 이동하도록 만들고 싶었다. 이걸 react와 swiper.js로 짜야 한다. 어떻게 해야 할까?</p>
<p>Swiper.js는 기본적으로 Swiper 컴포넌트 내에 pagination, navigation 등 엘리먼트를 자동으로 생성한다. 하지만 Swiper 내용과 페이지네이션을 분리하고 싶을 때가 있다.</p>
<p>Swiper 컴포넌트의 <code>pagination</code> 속성에는 <code>el</code>이라는 속성이 있다. null로 지정하면 기본적으로 Swiper 컴포넌트 내에 새로운 페이지네이션 태그를 생성하지만, CSSSelector 문자열이나 html 엘리먼트를 넣을 수 있다. 리액트에서 어떠한 컴포넌트의 실제 html 엘리먼트를 가져오려면? <code>ref</code>를 이용하면 된다.</p>
<h2 id="아니-이게-왜-안-돼요">아니 이게 왜 안 돼요</h2>
<pre><code class="language-jsx">import {useRef} from &quot;react&quot;;
import {Pagination} from &quot;swiper&quot;;
import {Swiper, SwiperSlide} from &quot;swiper/react&quot;;

function mySwiper()
{
    const paginationElement = useRef(null);
    return (
        &lt;&gt;
            &lt;Swiper 
                pagination={
                    el=paginationElement.current,
                    clickable=true
                }
                modules={[Pagination]}
            &gt;
                &lt;SwiperSlide&gt;&lt;div&gt;1&lt;/div&gt;&lt;/SwiperSlide&gt;
                &lt;SwiperSlide&gt;&lt;div&gt;2&lt;/div&gt;&lt;/SwiperSlide&gt;
                &lt;SwiperSlide&gt;&lt;div&gt;3&lt;/div&gt;&lt;/SwiperSlide&gt;
                &lt;SwiperSlide&gt;&lt;div&gt;4&lt;/div&gt;&lt;/SwiperSlide&gt;
            &lt;/Swiper&gt;
            &lt;nav ref={paginationElement}&gt;&lt;/nav&gt;
        &lt;/&gt;
    );
}</code></pre>
<p>그래서 Swiper 컴포넌트 밖에 nav 태그를 추가하고, ref를 준 뒤 <code>pagination.el</code>에 <code>ref.current</code>를 대입하는 것을 시도했지만 실패했다. 왜 실패했을까?</p>
<p>paginationElement ref는 처음에 <code>null</code>로 초기화된다. mySwiper 함수형 컴포넌트가 렌더링되면 Swiper 컴포넌트를 초기화할 때 <code>pagination.el</code>에 null이 대입되고, 이를 기반으로 스와이퍼를 생성한다. 이후 <code>ref</code>가 컴포넌트에 부착되면 <code>ref.current</code>에 컴포넌트의 실제 엘리먼트가 대입된다. 즉 이 경우는 Swiper 컴포넌트가 이미 <code>null</code>로 초기화된 후 <code>paginationElement</code> ref를 nav 태그에 부착했기 때문에 안 되는 것이다.</p>
<pre><code class="language-jsx">function useSwiperRef()
{
    const [wrapper, setWrapper] = useState(null);
    const ref = useRef(null);

    useEffect(() =&gt; {
        setWrapper(ref.current);
    }, []);

    return [wrapper, ref];
};</code></pre>
<p>이 방법을 해결하기 위해 구글링을 하다, 위의 커스텀 hook을 이용하면 가능하다고 한다. 실제로 돌려보니 잘 되었다. 그렇다면 대체 왜 이 커스텀 hook을 이용하면 swiper의 pagination을 잘 부착할 수 있는 것일까?</p>
<h2 id="왜-될까">왜 될까</h2>
<p>useSwiperRef를 이용하면 mySwiper 컴포넌트의 렌더링이 <strong>2번 실행</strong>된다. 1번은 null을 이용하여 Swiper를 초기화하지만, <strong>nav 태그에 ref가 부착되면 mySwiper 컴포넌트가 다시 렌더링되어, nav 태그의 실제 dom을 이용하여 Swiper를 초기화한다.</strong></p>
<p>좀 더 자세하게 살펴보면 다음과 같다.</p>
<ol>
<li>mySwiper 컴포넌트를 처음 렌더링을 시도한다. mySwiper 함수의 useSwiperRef 함수를 실행한다.</li>
<li>useSwiperRef 함수가 실행되고 wrapper state와 ref가 리턴된다.</li>
<li>리턴값을 평가한다. Swiper 컴포넌트를 렌더링 시도하는데, 아직 ref가 바인딩되지 않았으므로 ref는 null으로 취급한다.</li>
<li>ref.current가 실제 nav 태그의 dom으로 대입된다.</li>
<li>컴포넌트가 렌더링 완료되면 커스텀 훅 안에 있는 useEffect를 실행한다. setWrapper로 state를 변경한다.</li>
<li>리액트는 mySwiper의 state가 변경되었다고 판단하고, mySwiper를 리렌더링한다.</li>
<li>useSwiperRef 함수가 실행되고, wrapper state와 ref를 가져온다.</li>
<li>리턴값을 평가한다. Swiper 컴포넌트를 렌더링 시도하면서 nav 태그의 dom으로 대입된 ref.current를 갖고 Swiper를 렌더링한다.</li>
<li>컴포넌트가 렌더링 완료되면 커스텀 훅 안에 있는 useEffect를 실행하려고 하나, 2번째 인자가 빈 배열이므로 실행하지 않는다.</li>
</ol>
<h2 id="useeffect의-2번째-인자">useEffect의 2번째 인자</h2>
<p>useEffect는 2개의 인자를 받으며, 첫 번째 인자로 컴포넌트가 마운팅되고 업데이트될 때 실행하는 콜백 함수를, 두 번째 인자로 배열을 받는다. 2번째 인자의 의미가 무엇일까?</p>
<p>기본적으로 useEffect는 렌더링 함수가 마무리되면 바로 실행된다. 하지만 모든 업데이트 때마다 useEffect의 함수를 실행하면 성능이 저하될 것이다. 그래서 <strong>일부 상태가 변경될 때에만 실행되게 하도록 만들고 싶은데</strong> 이것이 useEffect의 2번째 인자의 존재 의의다.</p>
<p>useEffect가 실행될 때, <strong>직전에 실행했던 배열의 각 원소와 현재 실행되는 배열의 원소를 비교</strong>하여, 그 내용이 변경되면 콜백 함수를 실행한다. 그 예시는 다음과 같다.</p>
<pre><code class="language-jsx">function MyComponent()
{
    const [toggle, setToggle] = useState(false);
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);
    useEffect( ()=&gt;{
        console.log(&quot;yes!&quot;);
    }, [toggle, count1] );
    return &lt;&gt;
        &lt;button onClick={()=&gt;setToggle( prev=&gt;!prev )} &gt;toggle&lt;/button&gt;
        &lt;button onClick={()=&gt;setCount1( prev=&gt;prev+1 )} &gt;count1&lt;/button&gt;
        &lt;button onClick={()=&gt;setCount2( prev=&gt;prev+2 )} &gt;count2&lt;/button&gt;
    &lt;/&gt;
}</code></pre>
<p>MyComponent 컴포넌트가 렌더링되고, <code>[false,0]</code>을 기준으로 useEffect를 실행했다. 만약 toggle이 true가 되었다면, <code>[true,0]</code>으로 배열의 내용이 바뀌었으므로 useEffect가 실행된다. 반면, count2가 2로 변해도 배열의 내용은 <code>[false, 0]</code> 그대로이므로 useEffect는 실행되지 않는다.</p>
<h1 id="레퍼런스">레퍼런스</h1>
<ul>
<li><a href="https://github.com/nolimits4web/swiper/issues/3855">https://github.com/nolimits4web/swiper/issues/3855</a></li>
<li><a href="https://ko.reactjs.org/docs/hooks-effect.html">https://ko.reactjs.org/docs/hooks-effect.html</a></li>
<li><a href="https://yceffort.kr/2022/04/deep-dive-in-react-rendering#%EB%A6%AC%EC%95%A1%ED%8A%B8%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A0%8C%EB%8D%94%EB%A7%81%EC%9D%84-%EB%8B%A4%EB%A3%A8%EB%8A%94%EA%B0%80">https://yceffort.kr/2022/04/deep-dive-in-react-rendering</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[3000개의 이미지, 어떻게 다룰 것인가? (feat. Blob)]]></title>
            <link>https://velog.io/@lybell_4rt/3000%EA%B0%9C%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8B%A4%EB%A3%B0-%EA%B2%83%EC%9D%B8%EA%B0%80-feat.-Blob</link>
            <guid>https://velog.io/@lybell_4rt/3000%EA%B0%9C%EC%9D%98-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8B%A4%EB%A3%B0-%EA%B2%83%EC%9D%B8%EA%B0%80-feat.-Blob</guid>
            <pubDate>Sun, 10 Mar 2024 06:45:28 GMT</pubDate>
            <description><![CDATA[<p>본 아티클은 2023년 9월 <a href="https://lybell-art.github.io/prismic/">Prismic</a>이라는 프로젝트를 개발하면서 생긴 기술적인 논의 메모를 재구성한 메모입니다.</p>
<hr>
<h2 id="개요prismic-프로젝트의-특성">개요(Prismic 프로젝트의 특성)</h2>
<p>Prismic은 <strong>최대 3000개 이상의 대규모 이미지를 쉽게 카테고라이징할 수 있게 돕는 목적으로 만들어졌다.</strong> 그러므로, 3000개 이상의 이미지가 동시에 웹 메모리에 올라와도 애플리케이션이 프리징되지 않아야 할 필요가 있다. 1개의 이미지를 1MB라고 가정하면, 3000개의 이미지는 약 3GB정도 될 것이다. 3GB 정도 되는 파일이 메모리에 올라왔을 때, 브라우저가 프리징되는지 가능성을 알아보기 위해, 간단한 실험을 해 보았다.</p>
<h2 id="실험">실험</h2>
<h3 id="실험-1">실험 1</h3>
<p>간단히 3GB짜리 통짜 파일을 업로드했다. 업로드 자체만으로 브라우저가 크래시되는 일은 없었다. 또한, 1000개의 파일을 업로드하여 blob 객체가 1000개 메모리에 있더라도, 브라우저의 메모리가 고갈날 일은 없었다.
따라서, <strong>단순 업로드 자체는 파일의 용량과 상관없이 자바스크립트가 감당할 수 있음</strong>을 알았다.</p>
<h3 id="실험-2">실험 2</h3>
<pre><code class="language-javascript">function onFileInput(e)
{
  const blob = e.target.files[0];
  if(blob === null) return;
  blob.text().then(e=&gt;console.log(e));
}
document.getElementById(&quot;test&quot;).addEventListener(&quot;change&quot;, onFileInput);</code></pre>
<p>500MB짜리 파일을 업로드해서 1초 후에 전문을 텍스트로 읽어오기를 시도하였다. 브라우저가 파일 처리를 시도하다 콘솔이 멈추더니 out of memory로 메모리 부족 오류가 생겼다.</p>
<h3 id="실험-3">실험 3</h3>
<pre><code class="language-javascript">function addDom(rawText)
{
  const newElem = document.createElement(&quot;p&quot;);
  newElem.innerText = rawText.slice(0, 20);
  document.body.appendChild(newElem);
}
function onFileInput(e)
{
  const blobs = e.target.files;
  for(let blob of blobs) {
    blob.text().then(addDom);
  }
}
document.getElementById(&quot;test&quot;).addEventListener(&quot;change&quot;, onFileInput);</code></pre>
<p>이번에는 1MB짜리 파일을 1000개 업로드한 뒤, 1000개의 파일의 시작 부분을 dom에 추가하였다. 실험 결과, 여유롭게 실행되는 모습을 볼 수 있었다.
참고로 동일한 실험을 별도의 텍스트 배열에 추가하고, 그 배열의 시작부를 dom에 출력하려고 하면 메모리 오류를 반환한다.</p>
<h2 id="가능성-확인">가능성 확인</h2>
<p>Prismic의 경우, 3000개의 이미지의 비트맵 정보가 필요한 것이 아닌, 3000개의 이미지의 주소 참조만 blob 데이터로 저장하고, 필요할 때 단 하나의 이미지의 미리보기를 렌더링할 것으로 구현되므로, 메모리 부족 관련 성능 문제는 걱정하지 않아도 될 듯 하다. 따라서, <strong>구현 가능성이 존재한다</strong>!</p>
<h2 id="blob-데이터는-어디에-저장될까">Blob 데이터는 어디에 저장될까?</h2>
<p>크롬 개발자 도구로 blob이 메모리 상에 점유되는 용량을 분석하면 매우 작은 용량을 점유하는 것으로 드러난다. blob을 콘솔로 출력해 보아도 그 어디에도 내용은 없다.</p>
<pre><code class="language-javascript">let p=&quot;&quot;;
for(let i=0;i&lt;100000;i++){
    p+=Math.random()*89+10;
}
let blob = new Blob([p]);
p = 123;</code></pre>
<p>이 코드를 실행시키면, 대용량의 문자열이 생성되어 메모리에 저장되었다가(이 때 concanated string이 많이 생성되어 메모리 용량이 치솟다 최종 문자열을 제외한 나머지는 가비지 컬렉션된다.), 블롭화되고 원본 대용량 문자열은 가비지 컬렉션된다. 그런데도 <code>let text = await blob.text();</code> 와 같은 방식으로 텍스트를 읽어올 수 있으며, 놀랍게도 해당 텍스트는 외부 텍스트로 취급되어 점유하는 메모리는 고작 20byte밖에 되지 않는다.</p>
<p>과연, blob의 내용은 실제로 어디에 저장되어 있으며, 어떻게 blob의 메타데이터만으로 원본 데이터를 읽어올 수 있는 것일까?</p>
<p>크롬 기준으로 <strong>blob의 실제 내용은 자바스크립트의 힙이 아닌 별개의 공간에 저장된다</strong>. 크롬 자체적으로 <strong>blob을 저장하는 메모리</strong>가 존재하며, blob이 메모리 이상으로 너무 크거나 blob 메모리의 최대 용량이 전부 차면 디스크에 저장한다.</p>
<p>blob 데이터는 자바스크립트 코드 등에서 참조가 있는지 없는지를 계산하여, 참조가 없으면 가비지 컬렉션을 수행한다. 만약 <strong>object url로 blob을 변환했으면, 해당 object url은 blob의 내부 데이터를 “참조”하게 되어 가비지 컬렉션이 일어나지 않는다.</strong> 이는 해당 object url 문자열을 더 이상 참조하지 않더라도 <a href="https://w3c.github.io/FileAPI/#BlobURLStore">blob URL store</a>라는 곳에 내부적으로 저장되므로 가비자 컬렉션이 일어나지 않는다. 브라우저 창이 꺼지면 연결된 blob은 전부 가비지 컬렉션된다.</p>
<p>크롬 기준으로 현재 브라우저 창이 점유하는 실제 blob 오브젝트 데이터의 내역은 <code>chrome://blob-internals/</code>에서 확인할 수 있다.</p>
<p>파일의 경우는 blob 메모리에 실제 데이터 자체를 저장하는 것이 아니라, blob 데이터에 <strong>로컬 파일 시스템을 참조하는 경로</strong>만 저장된다. 실질적으로 File 객체의 실제 데이터는 파일 경로에 그대로 존재하며, 어딘가로 복사되지는 않는다. 파일을 읽어올 때는 File System API가 blob 메모리에 저장된 파일 경로 포인터를 기반으로 직접 로컬 파일에서 데이터를 읽어온다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[그동안의 근황]]></title>
            <link>https://velog.io/@lybell_4rt/%EA%B7%B8%EB%8F%99%EC%95%88%EC%9D%98-%EA%B7%BC%ED%99%A9</link>
            <guid>https://velog.io/@lybell_4rt/%EA%B7%B8%EB%8F%99%EC%95%88%EC%9D%98-%EA%B7%BC%ED%99%A9</guid>
            <pubDate>Wed, 10 Jan 2024 08:37:25 GMT</pubDate>
            <description><![CDATA[<h2 id="20228202212">2022.8~2022.12</h2>
<p>이 때는 네이버 부스트캠프 멤버십이 있었다. 간략하게 말하자면 4번의 스프린트를 진행하면서 도메인 지식을 향상시키고, 그룹 프로젝트로는 Monument Gallery라는 프로젝트를 진행했었다.
<img src="https://velog.velcdn.com/images/lybell_4rt/post/48ba3368-52ec-4daf-82b8-a31e4610115a/image.png" alt="Monument Gallery">
간략하게 말하면, Monument Gallery는 사용자 Notion의 포스트 데이터를 불러와서, 키워드 및 사진을 분석해 3D 공간으로 시각화하는 가상공간 프로젝트다. (현재는 서버비 문제로 인해 서비스 종료 상태) Notion API로 사용자의 기록물들을 분석하고, 키워드와 사진 등 데이터를 추출하여, 이를 맵 데이터로 저장한다. 맵 데이터는 트리 구조로 구성되며, 트리 구조의 자료를 용이하게 렌더링할 수 있는 react-three/fiber를 사용하여 3D 공간에 표현하였다.
생성된 기록물들은 공유할 수 있으며, 메인 로비에서 직접 탐방하거나, 링크 공유를 통해 다른 사람이 생성한 기록물을 조회할 수 있다.</p>
<p>Monument Gallery 관련해서 다양한 기술적인 문제들을 마주했는데, 해당 기술적인 문제는 다른 포스트에 작성하겠다.</p>
<h2 id="2023">2023</h2>
<p>2023년은 내 대학 생활의 마지막 장이었다. 그런데 뭔가 대외활동이라거나 그런 걸 한 건 아니었고, 그냥 과제의 노예로 살았다. 되새겨 보면 이 때 취업 준비를 더 할 걸 그랬다. <del>하지만 사람의 몸은 하나다</del></p>
<p>2023년은 게임 프로젝트 2개와, 사이드 웹 프로젝트 2개, 그리고 해커톤으로 정리할 수 있겠다. 프론트엔드 개발자가 웬 게임을 만드느냐 하겠지만, 사실 원래 나는 게임을 만드는 거에도 관심이 있기도 했고, 졸업 작품으로 그 동안 배운 기술들을 시험해보는 가장 좋은 수단이 게임이라고 생각하기도 했으며, 이 참에 유니티를 더 배워보고자 싶었기 때문이다.</p>
<h3 id="게임-프로젝트">게임 프로젝트</h3>
<p>내가 다니는 학과에는 Creative Capstone Project라고 그 동안 배웠거나 관심이 있는 기술을 적극 활용해서, 혼자서 기획, 디자인, 개발을 모두 진행하는 일종의 졸업창작프로젝트 같은 수업이 있다. 아래의 두 프로젝트는 모두 Creative Capstone Project의 일환으로 제작되었다.</p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/4df5c271-af13-4509-bd98-a403595c1551/image.png" alt="">
<a href="https://lybell-art.github.io/guidance">Guidance 플레이 링크</a>
1학기에는 <strong>Guidance</strong>라는 항아리형 퍼즐 액션 게임을 만들었다. &#39;교육 시스템의 실패&#39;를 주제로 하여, 현 교육의 양상을 게임 시스템으로 대표되는 교육 시스템이, 주인공인 선생에게, 학생을 빠르게 졸업으로 인도하라는 게임의 규칙을 제공하는 형태로 표현하였다. 플레이어인 선생은 화면에 보이는 학생에게 화면을 터치하여 &#39;과제&#39;를 주어 학생을 그 쪽으로 이동할 수 있으며, 교육 시스템이 제공하는 시간 제한 이내에 학생을 졸업까지 보내는 것이 목표다.</p>
<p>이 프로젝트는 작년 7월에 GIGDC에 출품했으며<del>(상은 못 탔다)</del>, 작년 12월 교내 전시회에 출품하여 실제 플레이어의 반응을 보기도 했다. 그 과정에서 게임 디자인적으로 개선이 필요한 부분이 몇몇 보여서, 그 부분을 2월에 수정하여 개선할 계획이다.
Guidance 관련된 이야기는 아마 Guidance를 추가로 개발할 때 더 풀어나갈 예정이다.</p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/b14c0f11-2faf-4d5d-a0d7-d713357305f6/image.png" alt="">
<a href="https://lybell-art.github.io/fluctus-of-deliberation">Fluctus of Deliberation 플레이 링크</a>
2학기에는 <strong>Fluctus of Deliberation</strong>이라는 아케이드 게임을 만들었다. Guidance에서 다뤘던 &#39;교육 시스템의 실패&#39;와 &#39;숙고&#39;의 연장선으로, 인간이 생각하는 과정에서 영감을 얻은 아케이드 게임이다. 주인공은 탐사정 로봇인 &#39;뉴로비&#39;(사진의 노란색 로봇)을 조작하여, 밑에서부터 올라오는 생각 방울을 연속해서 잡으면서 버티면서 맨 밑으로 내려가야 한다. 단순한 게임이지만, 검증된 장르의 특성상 의외로 재미가 있었으며, 특히 방울을 잡으면 대시하는 조작감이 인상적이다.</p>
<p>이 프로젝트는 Creative Capstone Project 2에서 A+이라는 성적을 받았다.</p>
<h3 id="개인-프로젝트">개인 프로젝트</h3>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/f6dc7199-78a2-4d3f-9de8-e9cb018613b2/image.png" alt="">
<a href="https://lybell-art.github.io/prismic">Prismic 링크</a>
마지막 학기에 2개의 프로젝트를 진행했었다. 그 중 하나는 Prismic이라는 이름의 이미지 분류기이다. 사실 이 프로젝트는 본인이 직접 사용하기 위해 만들었는데, 그 당시 인공지능 이미지 학습에 빠져 있던 나는 학습할 데이터를 손수 이미지 뷰어를 보면서 분류하는 것에 지쳐 있었고, 어떻게 하면 더 편하게 이미지를 보면서 분류를 쉽게 할지를 고민하다 만들게 되었다.</p>
<p>이 프로젝트는 여러 이미지 파일들을 업로드하고, 분류할 카테고리를 입력한 뒤 미리보기 이미지를 보면서 이미지를 분류해나간다. 전부 분류가 완료되었으면 완료된 이미지는 폴더로 나뉘어져 압축 파일로 다운로드할 수 있다.</p>
<p>간단해 보이지만, 의외로 생각할 거리들이 정말 많았던 사이드 프로젝트였다. 당장 생각나는 것들만 나열해 보아도 다음과 같다.</p>
<ul>
<li>프로젝트 특성상 대규모의 이미지 파일(최대 1000개 이상)을 처리해야 하는데, 업로드하고 다운로드하는 데 메모리가 바닥나지는 않는가?</li>
<li>모바일에서 스크롤을 내리거나 올릴 때 레이아웃이 변경되는 애니메이션을 처리해야 한다. 어떻게 하면 잦은 리렌더링으로 인한 성능저하 없이 애니메이션을 구현할 수 있는가?</li>
<li>디렉토리 버튼을 클릭하면 카테고리로 분류된 이미지 미리보기 목록이 표시되어야 한다. 프로젝트 특성상 컴포넌트가 너무 많으면 렌더링 속도가 저하되는데, 어떻게 해결할 수 있는가?</li>
<li>가상 리스트로 위의 문제를 해결하였다. 그런데 이미지 캐싱이 안 되어서 스크롤할 때마다 이미지를 다시 불러오는 상황. 어떻게 해결해야 하는가?</li>
</ul>
<p>Prismic을 개발하면서 봉착한 기술적인 문제들이 꽤 많아서, 개발하면서 따로 개인 Notion 페이지에 정리해 두었다. 해당 이야기들은 차후 다른 포스트에서 공개적으로 풀어나갈 예정이다.</p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/9342e86e-19b4-4d6f-a6c6-11cd1c9d398b/image.png" alt="">
<em>(이 프로젝트는 사용성 개선 등 아직 개선이 더 필요한 프로젝트라 공개하지는 않고 있다)</em>
다음 프로젝트는 다른 사람이 만든 <a href="https://randypanopio.github.io/project/cktools/pixel-map.html">코어 키퍼 픽셀 맵 아트 툴</a>을 리메이크하는 프로젝트였다. 기존 맵 아트 툴은 예전의 데이터에서 멈춰 있었으며, 사용성에서 문제가 많아서 개선하기로 결심했다. 처음에는 기존 프로젝트를 약간 손보고 기능을 추가하는 것에서 끝내려고 했으나, 코드에 새로운 기능을 추가하기에는 기존 코드에 문제가 많았기에 아예 기존 코드의 로직만 분석한 뒤 새로 만들기로 했다.</p>
<p>원래 프로젝트가 바닐라 자바스크립트였고, 상당히 간단한 프로젝트였기에 굳이 리액트처럼 무거운 웹 프레임워크<del>(리액트는 라이브러리지만)</del>를 쓸 필요는 없다고 판단하였고, 경량화되고 속도도 빠르며 다른 웹 프레임워크에 부착할 수도 있는 lit을 사용해서 개발하였다. 특이한 기술적인 경험을 한 것은 아니었으나, 원본 프로젝트의 로직을 이해해야 가능한 프로젝트였기에, 코드를 읽는 능력을 많이 요구하는 프로젝트였다. 실제 현업에서는 다른 사람이나 전업자의 코드를 읽는 능력이 필요할 것이라고 예상되기에, 해당 부분을 어필할 수 있을 것 같다.</p>
<p>Core Keeper Pixel Map Art Tool 개발하면서 봉착한 기술적 문제는 향후 다른 포스트에서 풀어나갈 예정이다. 이것도 노션에 초안을 작성해두었다.</p>
<h3 id="해커톤">해커톤</h3>
<p>11월에는 &#39;구름톤 대학&#39;이라는 이름의 연합동아리에 소속하여, &#39;단풍톤&#39; 해커톤에 참여하였다. 개인적으로나, 팀적으로나 결코 좋은 성적을 거두지는 못했던 프로젝트였으나, 본격적으로 기획자, 디자이너, 백엔드 개발자와 역할을 분담하여 진행한 프로젝트라는 점에서 의의가 있었다.(마지막 스프린트에서 디자이너와 호흡이 아주 잘 맞았다)</p>
<p>무슨 아이디어로 나갔는지는 비밀로 하겠다. 기술적인 문제에 도전할 수 있는 아이디어가 아니었으며, 실제로도 기술적인 고민을 하지 못했다는 점은 아쉬웠다.</p>
<h2 id="2024년-무엇을-할-것인가">2024년, 무엇을 할 것인가?</h2>
<p>우선 거의 방치 수준으로 이루어진 개발 블로그를 천천히 채워나갈 계획이며, 추가적으로 새 프로젝트의 아이디어가 생각나기 전까지는 2022년~2023년에 개발했던 프로젝트들의 유지보수 및 업데이트를 진행할 예정이다.</p>
<p>요새 채용시장이 불황이라고 한다. 너무 슬프다. 그래도 취업에 도전해야만 한다. 지금까지는 준비와 준비와 준비의 기간이었으니, 이제는 도전해야 할 시기다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Stardew Dressup 개발일지(4)]]></title>
            <link>https://velog.io/@lybell_4rt/Stardew-Dressup-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%804</link>
            <guid>https://velog.io/@lybell_4rt/Stardew-Dressup-%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%804</guid>
            <pubDate>Sat, 27 Aug 2022 17:16:59 GMT</pubDate>
            <description><![CDATA[<h2 id="개발-상황">개발 상황</h2>
<p>현재 Stardew Dressup 1.1버전을 개발하고 있다. 주된 변경 내역은 디자인 수정으로, 픽셀스럽고 다듬어진 디자인으로 변경하면서, 겸사겸사 성능을 향상시키는 것이 주 목적이다.</p>
<p>개발 예정 목록은 다음과 같다.</p>
<blockquote>
<ul>
<li>랜더마이징 기능 추가</li>
<li>버그 수정<ul>
<li>모바일에서 한글 폰트가 굵게 나오는 버그 수정</li>
<li>모바일에서 하단이 짤려 나오는 버그 수정</li>
</ul>
</li>
<li>사용성 개선<ul>
<li>의상 선택 창 퍼포먼스 개선</li>
<li>리사이징 퍼포먼스 개선</li>
<li>모바일에서 스와이프를 할 때 하단이 스와이프가 안 되는 문제 수정</li>
<li>의상 선택 창 동기화 퍼포먼스 개선</li>
</ul>
</li>
<li>디자인 변경</li>
<li>일부 코드 리팩토링</li>
</ul>
</blockquote>
<h2 id="문제-상황">문제 상황</h2>
<h3 id="swiperjs의-페이지네이션-태그-분리">Swiper.js의 페이지네이션 태그 분리</h3>
<p>Stardew Dressup은 스와이프 기능을 구현하기 위해 Swiper.js 라이브러리를 사용하고 있다.(리액트, 앵귤러, 뷰 등 다른 SPA 라이브러리/프레임워크용 버전을 지원한다) 리액트의 Swiper.js(swiper-react)는 기본적으로 스와이퍼 컴포넌트를 생성할 때 pagination 엘리먼트를 같이 생성하는데, 이것을 다른 태그에 소속되도록 분리할 수 있다.</p>
<pre><code class="language-javascript">import {useState, useRef, useEffect} from &quot;react&quot;;
import {Pagination} from &quot;swiper&quot;;
import {Swiper} from &quot;swiper/react&quot;;

// from https://github.com/nolimits4web/swiper/issues/3855
function UseSwiperRef()
{
  const [el, setEl] = useState(null);
  const elRef = useRef(null);
  useEffect( ()=&gt;{
    selEl(elRef.current);
  }, []);
  return [el, elRef];
}
function MySwiper()
{
  const [el, elRef] = UseSwiperRef();
  return &lt;&gt;
    &lt;Swiper
      pagination={el:el}
      module={[Pagination]}
    &gt;
        &lt;SwiperSlide&gt;123&lt;/SwiperSlide&gt;
    &lt;/Swiper&gt;
      &lt;div className=&quot;mySwiperPagination&quot; ref={elRef} /&gt;
  &lt;/&gt;
}</code></pre>
<blockquote>
<p>참고 : UseSwiperRef를 사용한 이유는 Swiper 컴포넌트 이후에 존재하는 컴포넌트에 ref를 부착하면 Swiper 컴포넌트가 평가될 때 ref는 아직 아무 컴포넌트에도 부착되지 않은 상태이므로 {current:null}로 평가된다. 따라서 useState와 useEffect를 사용하여 컴포넌트가 생성 완료된 후 ref.current를 state로 넣어서 리렌더링을 시도하도록 하였다.</p>
</blockquote>
<h3 id="swiperjs-braekpoints">Swiper.js braekpoints</h3>
<p>Swiper.js에서는 breakpoints라는 기능을 제공한다. breakpoints는 window나 현재 스와이퍼 컨테이너의 가로 길이나 해상도 비율에 맞춰 스와이퍼가 다르게 동작하도록 하는 기능이다. 숫자를 키값으로 적으면 가로 길이 기준으로, @로 시작하는 문자를 키값으로 적으면 해상도 기준으로 동작한다.
리액트에서 Swiper.js의 breakpoints를 사용하는 법은 다음과 같다.</p>
<pre><code class="language-javascript">
function MySwiper()
{
  return &lt;&gt;
    &lt;Swiper
        slidesPerView:{2},
      spaceBetween:{10},
      breakpoints:{
        // when window width is &gt;= 768px
        768: {
          slidesPerView: 3,
          spaceBetween: 20
        },
        // when window width is &gt;= 1366px
        1366: {
          slidesPerView: 5,
          spaceBetween: 30
        }
      }
    &gt;
        &lt;SwiperSlide&gt;123&lt;/SwiperSlide&gt;
    &lt;/Swiper&gt;
  &lt;/&gt;
}</code></pre>
<p>위의 코드는 기본적으로 2개의 보이는 슬라이드와 10px의 갭으로 스와이퍼가 동작하지만, 창 크기가 768px 이상일 때는 3개의 보이는 슬라이드와 20px의 갭으로, 1366px 이상일 때는 5개의 보이는 슬라이드와 30px의 갭으로 동작한다.</p>
<h3 id="breakpoints-버그">breakpoints 버그</h3>
<p>문제는 위와 같이 ref를 이용해서 pagination을 다른 태그로 분리하고, breakpoints를 사용했을 때, 창이 가장 작은 breakpoints 미만으로 줄어들었을 때(위의 예시에서는 768px 이상이었다가 768px 미만으로 줄어드는 상황) <strong>pagination.el이 null로 초기화되어버리는 버그</strong>가 일어난다는 것이다.</p>
<p>나는 1366px 이상일 때는 스와이퍼가 세로로, 1366px 미만일 때는 스와이퍼가 가로로 동작하도록 만들고, 1366px 미만일 때 페이지네이션의 현재 페이지가 하이라이트되도록 만들었는데, 1366px 이상이었다가 창이 1366px 미만으로 줄어들었을 때 페이지네이션의 하이라이트가 동작하지 않았던 것이다. 디버깅을 해 보니 swiper.params.pagination.el이 null로 변경되어 있어서 pagination의 update가 아예 동작하지 않았다.</p>
<pre><code class="language-javascript">function isPaginationDisabled() {
  return !swiper.params.pagination.el || !swiper.pagination.el || !swiper.pagination.$el || swiper.pagination.$el.length === 0;
}

function update() {
  // Render || Update Pagination bullets/items
  const rtl = swiper.rtl;
  const params = swiper.params.pagination;
  if (isPaginationDisabled()) return;
  //...
}</code></pre>
<p>(Swiper.js 라이브러리의 modules/pagination 코드의 일부.)</p>
<h2 id="swiperjs의-breakpoints-동작원리">Swiper.js의 breakpoints 동작원리</h2>
<h3 id="setbreakpoint">setBreakPoint</h3>
<p>Swiper.js는 화면 크기가 변경되면, breakpoints를 param으로 갖고 있는 Swiper 객체에 대해, setBreakPoint라는 함수를 호출한다. setBreakPoint는 현재 스와이퍼 객체에 대해 해당하는 breakpoint를 구하고, 해당 breakpoints에 대해 현재 파라미터와 해당하는 breakpoints의 파라미터를 덮어씌운다.</p>
<pre><code class="language-javascript">function setBreakpoint() {
  const swiper = this;
  const {
    activeIndex,
    initialized,
    loopedSlides = 0,
    params,
    $el
  } = swiper;
  const breakpoints = params.breakpoints;
  if (!breakpoints || breakpoints &amp;&amp; Object.keys(breakpoints).length === 0) return; // Get breakpoint for window width and update parameters

  const breakpoint = swiper.getBreakpoint(breakpoints, swiper.params.breakpointsBase, swiper.el);
  if (!breakpoint || swiper.currentBreakpoint === breakpoint) return;
  const breakpointOnlyParams = breakpoint in breakpoints ? breakpoints[breakpoint] : undefined;
  const breakpointParams = breakpointOnlyParams || swiper.originalParams;
  //중략
  extend(swiper.params, breakpointParams);
  //후략
}</code></pre>
<p>(Swiper.js 라이브러리의 core/breakpoints/setBreakPoint.js 코드의 일부.)
여기서 extend 함수는 <code>{...swiper.params, ...breakpointParams}</code>의 깊은 복사 버전이라고 생각하면 되겠다.</p>
<h3 id="getbreakpoint">getBreakPoint</h3>
<p>현재 스와이퍼 객체의 breakpoint가 무엇인지를 구하는 함수다. getBreakPoint의 알고리즘을 간단하게 말로 풀어서 설명하면 다음과 같다.</p>
<blockquote>
<ol start="0">
<li>아래 내용은 base가 window 기반일 때를 기준으로 한다.</li>
<li>Swiper 객체가 현재 갖고 있는 breakpoints 객체의 키값에 대해 매핑을 하여 points 변수에 저장한다.<ul>
<li>각 breakpoint가 @로 시작하는 문자열일 경우, height * 비율을 breakpoint width로 정한다.</li>
<li>그렇지 않을 경우 자기 자신을 breakpoint width로 정한다.</li>
</ul>
</li>
<li>points 배열을 작은 순으로 정렬한다.</li>
<li>points 배열을 순회하여 각 원소에 대해 현재 window.width가 breakpoint width보다 크면 해당 key를 breakpoint로 정한다. 즉, 현재 window.width보다 작은 breakpoint width들 중 가장 큰 것이 결과값이 된다.</li>
<li>아무 조건도 일치하지 않는 경우 결과값은 &quot;Max&quot;가 된다.</li>
</ol>
</blockquote>
<p>예를 들어 breakpoints의 키값이 <code>[768, 1366]</code>이라고 하자. 창 크기가 1920일 때, 첫 번째 루프에서 768이 조건에 맞고, 두 번째 루프에서 1366이 조건에 맞으므로 결과값은 1366이 된다. 반면 창 크기가 320일 경우는 첫 번째 루프에서 768이 조건에 맞지 않고, 두 번째 루프에서 1366이 조건에 맞지 않으므로 결과값은 &quot;Max&quot;가 된다.</p>
<p><del>Breakpoint가 현재 창 너비보다 작은 지점 중 가장 큰 것, 즉 창 너비가 소속된 범위를 의미하므로 창 너비가 어떤 breakpoint보다도 작으면 결과값이 Min이 되어야 하는데 Max이다. 왜일까</del></p>
<h2 id="버그-원인-해결">버그 원인 해결</h2>
<p>breakpoints가 제일 작은 쪽으로 화면이 줄어들었을 때는 다음의 현상이 일어난다.</p>
<blockquote>
<ol>
<li>화면 크기가 변경되면 setBreakPoint 함수가 호출된다.</li>
<li>setBreakPoint 함수에서 getBreakPoint 함수를 호출한다.</li>
<li>getBreakPoint 함수는 현재 window의 너비가 어떠한 breakpoints보다도 작으므로 &quot;Max&quot;를 반환한다.</li>
<li>breakpoints 객체에서 getBreakPoint의 리턴값이 key값인 value를 찾는다.(<code>this.breakpoints[this.getBreakPoint()]</code>와 같다.)</li>
<li>해당 키값이 breakpoints 파라미터에 존재하면 이를 breakpointParams으로 삼는다. 존재하지 않으면 originalParams 파라미터를 breakpointParams으로 삼는다.</li>
<li>스와이퍼의 현재 params에 breakpointParams를 덮어씌운다.</li>
</ol>
</blockquote>
<p>즉, getBreakPoint가 Max를 반환하는 상황(화면의 너비보다 작은 breakpoints가 없음)에서 breakpoints 객체에 Max라는 키값이 존재하지 않으니, Swiper 객체를 처음 초기화했을 때의 파라미터로 덮어씌우는 상황이라고 할 수 있다.</p>
<p>여기서 문제는 Swiper의 페이지네이션을 분리하기 위해 pagination.el에 state를 할당하는 과정에서 나온다. 맨 처음 el state는 null이기 때문에 originalParams.pagination.el이 null이 되어버린 것이다.</p>
<p>해당 문제는 breakpoints 파라미터 객체에 Max 프로퍼티를 추가하여, breakpoints로 변경되는 부분만 초기화하면 해결된다.</p>
<pre><code class="language-javascript">function MySwiper()
{
  const [el, elRef] = UseSwiperRef();
  return &lt;&gt;
    &lt;Swiper
        slidesPerView:{2}
      spaceBetween:{10}
      breakpoints:{
        // when window width is &gt;= 768px
        768: {
          slidesPerView: 3,
          spaceBetween: 20
        },
        // when window width is &gt;= 1366px
        1366: {
          slidesPerView: 5,
          spaceBetween: 30
        }
        // default(when window width is &lt; 768px)
        Max: {
          slidesPerView: 2,
          spaceBetween: 10
        }
      }
        pagination={el}
      module={[Pagination]}
    &gt;
        &lt;SwiperSlide&gt;123&lt;/SwiperSlide&gt;
    &lt;/Swiper&gt;
      &lt;div className=&quot;mySwiperPagination&quot; ref={elRef} /&gt;
  &lt;/&gt;
}</code></pre>
<p>이렇게 하면 pagination.el 파라미터가 null로 초기화되지 않는다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[심오한 자바스크립트 형변환의 세계]]></title>
            <link>https://velog.io/@lybell_4rt/%EC%8B%AC%EC%98%A4%ED%95%9C-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%98%95%EB%B3%80%ED%99%98%EC%9D%98-%EC%84%B8%EA%B3%84</link>
            <guid>https://velog.io/@lybell_4rt/%EC%8B%AC%EC%98%A4%ED%95%9C-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%98%95%EB%B3%80%ED%99%98%EC%9D%98-%EC%84%B8%EA%B3%84</guid>
            <pubDate>Fri, 26 Aug 2022 10:58:32 GMT</pubDate>
            <description><![CDATA[<h2 id="시작하기-전에">시작하기 전에</h2>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/0e41a842-1c9a-4e7e-a514-008cacb89fd0/image.png" alt=""></p>
<p>(출처 : <a href="https://dev.to/coderslang/javascript-interview-question-34-different-ways-to-get-the-current-date-in-js-4c25">https://dev.to/coderslang/javascript-interview-question-34-different-ways-to-get-the-current-date-in-js-4c25</a>)</p>
<p>우선 시작하기 전에 위 사진의 문제를 풀어보자. 답은 true일까, false일까? 답은 이 글 맨 마지막에 알려주겠다.</p>
<h2 id="명시적-형변환과-암시적-형변환">명시적 형변환과 암시적 형변환</h2>
<p>자바스크립트는 약타입 언어다. 그래서 <code>&quot;123&quot; + 4</code> 같은 것을 처리할 때 다른 강타입 언어는 타입이 달라서 오류를 뱉겠지만, 자바스크립트는 어떻게든 형변환해서 처리한다. 이 때, 프로그래머가 명시적으로 값의 타입을 변환시키면(<code>Number(123)</code> 같은 경우) 명시적 형변환이라고 하고, 그렇지 않고 자바스크립트 엔진이 알아서 형변환하도록 하는 것을 암시적 형변환이라고 한다.</p>
<h2 id="원시-자료형의-형변환">원시 자료형의 형변환</h2>
<p>원시 자료형에는 Number, String, Boolean, null, undefined, Symbol(ES6부터), BigInt(ES2020부터)가 존재한다. 이 중 가장 많이 쓰이고 많이 변환되는 문자열, 숫자, 불리언 자료형으로의 형변환에 대해 알아보자.</p>
<h3 id="tostring문자열-형변환">toString(문자열 형변환)</h3>
<p><code>&quot;Lybell&quot;.concat(123)</code>은 무엇을 반환해야 할까? 문자열에 숫자나 불리언 등을 결합하고 싶을 때 등의 상황에서, 자바스크립트는 암묵적으로 문자열이 아닌 자료형을 문자열로 형변환하는 toString 추상 연산을 호출한다.</p>
<p>ECMAScript 명세서에서 정의하고 있는 <a href="https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tostring">각 자료형의 문자열 형변환 규칙</a>은 다음과 같다.</p>
<table>
<thead>
<tr>
<th align="center">자료형</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td align="center">undefined</td>
<td>&quot;undefined&quot;</td>
</tr>
<tr>
<td align="center">null</td>
<td>&quot;null&quot;</td>
</tr>
<tr>
<td align="center">Boolean</td>
<td>true일 때 &quot;true&quot;, false일 때 &quot;false&quot;</td>
</tr>
<tr>
<td align="center">Number</td>
<td>Number.toString(value, 10)</td>
</tr>
<tr>
<td align="center">String</td>
<td>문자열 그 자체</td>
</tr>
<tr>
<td align="center">Symbol</td>
<td>TypeError를 반환</td>
</tr>
<tr>
<td align="center">BigInt</td>
<td>BigInt.toString(value)</td>
</tr>
<tr>
<td align="center">Object</td>
<td>toPrimitive(value, &quot;string&quot;)으로 원시 자료형으로 만든 뒤 문자열로 형변환</td>
</tr>
</tbody></table>
<p>명시적으로 문자열로 형변환을 하기 위해서는 <strong>템플릿 리터럴</strong>을 사용하거나, <strong>String() 함수</strong>를 이용하면 된다.(<code>new String()</code>을 사용하면 문자열처럼 보이는 객체가 생성되므로 주의!) <code>value+&quot;&quot;</code>와 같은 방법도 존재하나, preferedType이 string이 아니라 default이고, 객체를 형변환할 때 <code>valueOf</code>나 <code>@@Symbol.toPrimitive</code> 메소드가 다른 리터럴을 반환한다면 그것을 기준으로 문자열로 형변환되기 때문에 원하는 값을 얻지 못할 수 있다.</p>
<pre><code class="language-javascript">//String() 함수
console.log(String(123)) // 문자열 123
console.log(String(true)) // 문자열 &quot;true&quot;

//템플릿 리터럴
console.log(`${123}`) // 문자열 &quot;123&quot;
console.log(`${10000000000n}`) // 문자열 &quot;10000000000&quot;
console.log(`${undefined}`) // 문자열 &quot;undefined&quot;
console.log(`${null}`) // 문자열 &quot;null&quot;
console.log(`${Symbol()}`) // TypeError
console.log(`${[1,2,3]}`) // 문자열 &quot;1,2,3&quot;
console.log(`${{a:12}}`) // 문자열 &quot;[Object Object]&quot;

//value+&quot;&quot;로 문자열 이어붙이기
console.log(123+&quot;&quot;); // 문자열 &quot;123&quot;
console.log(null+&quot;&quot;); // 문자열 &quot;null&quot;

//객체의 경우
const test = {valueOf:()=&gt;300, toString:()=&gt;&quot;yes&quot;};
console.log(`${test}`); // 문자열 &quot;yes&quot;
console.log(String(test)); // 문자열 &quot;yes&quot;
console.log(test+&quot;&quot;); // 문자열 &quot;300&quot;?!</code></pre>
<h3 id="tonumber숫자-형변환">toNumber(숫자 형변환)</h3>
<p><code>123 - &quot;45&quot;</code>는 무엇을 반환해야 할까? 숫자와 숫자가 아닌 것을 연산하거나 비교해야 할 때, 자바스크립트는 암묵적으로 숫자가 아닌 것을 숫자로 형변환하는 toNumber 추상 연산을 호출한다. </p>
<p>ECMAScript 명세서에서 정의하고 있는 <a href="https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tonumber">각 자료형의 숫자형 형변환 규칙</a>은 다음과 같다.</p>
<table>
<thead>
<tr>
<th align="center">자료형</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td align="center">undefined</td>
<td>NaN</td>
</tr>
<tr>
<td align="center">null</td>
<td>0</td>
</tr>
<tr>
<td align="center">Boolean</td>
<td>true일 때 1, false일 때 0</td>
</tr>
<tr>
<td align="center">Number</td>
<td>숫자 그 자체</td>
</tr>
<tr>
<td align="center">String</td>
<td>내부 연산 StringToNumber(value)를 호출한다.</td>
</tr>
<tr>
<td align="center">Symbol</td>
<td>TypeError를 반환</td>
</tr>
<tr>
<td align="center">BigInt</td>
<td>TypeError를 반환(toNumeric에서는 숫자 그 자체를 반환)</td>
</tr>
<tr>
<td align="center">Object</td>
<td>toPrimitive(value, &quot;number&quot;)으로 원시 자료형으로 만든 뒤 숫자형으로 형변환</td>
</tr>
</tbody></table>
<p>문자열이 숫자로 형변환될 때 적절하지 않은 포맷(예시:<code>123a</code>)이면 NaN을 반환하며, 공백만 있는 문자열이 0을 반환한다는 것을 기억하도록 하자.</p>
<p>여기에서 빈 배열이 0으로 형변환되는 이유를 알 수 있는데, 빈 배열은 toPrimitive(value, &quot;number&quot;)를 호출하면 <code>[].toString()</code>의 값인 빈 문자열(<code>&quot;&quot;</code>)이 원시 자료형으로 반환되고, 이것을 숫자형으로 파싱하면 0이 되기 때문에 빈 배열이 0으로 초기화되는 것이다.</p>
<p>명시적으로 숫자형으로 형변환을 하기 위해서는 <strong>Number()</strong> 함수를 사용하거나, <strong>unary plus</strong> 연산자를 이용하면 된다. 단, Number() 함수는 BigInt형의 값 역시 잘 변환할 수 있기 때문에, 완벽한 toNumber는 아니다.</p>
<pre><code class="language-javascript">//Number() 함수
console.log(Number(&quot;123&quot;)) // 숫자 123
console.log(Number(&quot;123.45&quot;)) // 숫자 123.45
console.log(Number(&quot;123p&quot;)) // NaN
console.log(Number(&quot; &quot;)) // 숫자 0
console.log(Number(1234n)) // 숫자 1234

//unary plus 연산자
console.log(+&quot;1024&quot;) // 숫자 1024
console.log(+undefined) // NaN
console.log(+null) // 숫자 0
console.log(+Symbol()) // TypeError
console.log(+1024n) // TypeError
console.log(+true) // 숫자 1
console.log(+[]) // 숫자 0
console.log(+[1,2,3]) // NaN
console.log(+{}); // NaN

//객체의 경우
const test = {valueOf:()=&gt;300, toString:()=&gt;&quot;yes&quot;};
console.log(+test); // 숫자 300
const test2 = {toString:()=&gt;&quot;&quot;};
console.log(+test2); // 숫자 0</code></pre>
<h4 id="tonumeric">toNumeric</h4>
<p>내부 연산 toNumber와 비슷하지만, toNumeric은 원시 자료형으로 만든 뒤 그 값이 BigInt형이면 BigInt형 값 그 자체를 반환한다.</p>
<h3 id="toboolean불리언-형변환">toBoolean(불리언 형변환)</h3>
<p>조건문에서 true나 false가 아닌 것이 들어갔을 때 어떻게 처리해야 할까? 조건문 등 불리언 값이 필요한 상황에서 불리언 값이 아닌 값이 들어갔을 때, 자바스크립트는 암묵적으로 불리언 자료형이 아닌 것을 불리언 값으로 형변환하는 toNumber 추상 연산을 호출한다.</p>
<p>ECMAScript 명세서에서 정의하고 있는 <a href="https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toboolean">각 자료형의 불리언 형변환 규칙</a>은 다음과 같다.</p>
<table>
<thead>
<tr>
<th align="center">자료형</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td align="center">undefined</td>
<td>false</td>
</tr>
<tr>
<td align="center">null</td>
<td>false</td>
</tr>
<tr>
<td align="center">Boolean</td>
<td>불리언 값 그 자체</td>
</tr>
<tr>
<td align="center">Number</td>
<td>0, NaN이면 false, 그 외에는 true</td>
</tr>
<tr>
<td align="center">String</td>
<td>length가 0이면(빈 문자열) false, 그 외에는 true</td>
</tr>
<tr>
<td align="center">Symbol</td>
<td>true</td>
</tr>
<tr>
<td align="center">BigInt</td>
<td>0n이면 false, 그 외에는 true</td>
</tr>
<tr>
<td align="center">Object</td>
<td>true</td>
</tr>
</tbody></table>
<p>Object는 빈 배열이든, 빈 객체든, 심지어 new Boolean(false)와 같은 값이 false인 불리언 래퍼 객체든 무조건 true로 형변환된다는 사실에 유의하자.</p>
<p>명시적으로 불리언으로 형변환을 하기 위해서는 <strong>Boolean()</strong> 함수를 사용하거나, <strong>!!</strong> 연산자를 이용하면 된다. !! 연산자는 !으로 값을 형변환한 뒤 불리언 값을 뒤집고, 다시 !을 붙이면 그 값이 뒤집어져 최종적으로 값이 불리언으로 형변환되는 원리다.</p>
<pre><code class="language-javascript">//Boolean() 함수
console.log(Boolean(123)) // true
console.log(Boolean(0)) // false
console.log(Boolean(NaN)) // false
console.log(Boolean(&quot; &quot;)) // true

//!! 연산자
console.log(!!123) // true
console.log(!!&quot;yes&quot;) // true
console.log(!!&quot; &quot;) // true
console.log(!!&quot;&quot;) // false
console.log(!!Symbol()) // true
console.log(!!1024n) // true
console.log(!!null) // false
console.log(!!undefined) // false
console.log(!![]) // true
console.log(!!{}) // true
console.log(!!(new Boolean(false)) )// true</code></pre>
<h2 id="object의-형변환">Object의 형변환</h2>
<p>Object 자료형은 원시 자료형으로 변환하기 위해 특수한 형변환 과정을 거친다. ECMAScript 명세서에서 정의하고 있는 <a href="https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toprimitive">Object의 원시 자료형 형변환</a>은 다음과 같다. 이해를 돕기 위해 자바스크립트로 짠 toPrimitive 함수를 같이 첨부한다.</p>
<pre><code class="language-javascript">function isPrimitive(object)
{
  if(object === null) return true;
  return (typeof object !== &quot;object&quot; &amp;&amp; typeof object !== &quot;function&quot;);
}
function assertHint(preferedType)
{
  if(preferedType === undefined) return &quot;default&quot;;
  if(preferedType === &quot;string&quot;) return &quot;string&quot;;
  return &quot;number&quot;;
}

function toPrimitive(object, preferedType)
{
  if(isPrimitive(object)) return object;
  if(Symbol.toPrimitive in object)
  {
    let hint = assertHint(preferedType);
    let primitive = object[Symbol.toPrimitive](hint);
    if(isPrimitive(primitive)) return primitive;
    throw new TypeError(&quot;Cannot convert object to primitive value&quot;);
  }
  return toOrdinaryPrimitive(object, preferedType);
}

function toOrdinaryPrimitive(object, hint=&quot;number&quot;)
{
  let methodNames;
  if(hint === &quot;string&quot;) methodNames=[&quot;toString&quot;, &quot;valueOf&quot;];
  else methodNames=[&quot;valueOf&quot;, &quot;toString&quot;];
  for(let name of methodNames)
  {
    if(!(name in object)) continue;
    if(typeof object[name] !== &quot;function&quot;) continue;
    let primitive = object[name]();
    if(isPrimitive(primitive)) return primitive;
  }
  throw new TypeError(&quot;Cannot convert object to primitive value&quot;);
}</code></pre>
<blockquote>
<ol start="0">
<li>만약 원시 자료형이면 값을 그대로 반환한다.</li>
</ol>
</blockquote>
<ol>
<li><code>@@Symbol.toPrimitive</code> 메소드의 존재 여부를 파악한다.</li>
<li>만약 <code>@@Symbol.toPrimitive</code> 메소드가 존재하면, preferedType을 통해 hint를 추측한다.<ul>
<li>preferedType이 정해지지 않으면 기본적으로 default</li>
<li>preferedType이 string이면 string</li>
<li>그 외에는 number</li>
</ul>
</li>
<li>만약 <code>@@Symbol.toPrimitive</code> 메소드가 존재하면, <code>[Symbol.toPrimitive](hint)</code> 메소드를 호출한다. 그 값이 원시 자료형이면 값을 반환하고, 그렇지 않으면 오류를 반환한다.</li>
<li>valueOf와 toString을 기반으로 형변환을 진행한다. 이 때 preferedType이 무엇인지에 따라 다르다.<ul>
<li>preferedType이 string이면 <code>toString</code>, <code>valueOf</code> 우선</li>
<li>그 외에는 <code>valueOf</code>, <code>toString</code> 우선</li>
</ul>
</li>
<li>각 methodName에 대해 형변환을 진행한다. 이 때 해당하는 메소드가 존재하지 않거나, 해당하는 식별자가 메소드가 아니면 건너뛴다. 반환값이 원시 자료형이면 값을 반환한다.</li>
<li>만약 <code>@@Symbol.toPrimitive</code>, <code>toString</code>, <code>valueOf</code> 모두 존재하지 않으면 <code>Uncaught TypeError: Cannot convert object to primitive value</code> 에러를 반환한다.</li>
</ol>
<h3 id="symboltoprimitive">@@Symbol.toPrimitive</h3>
<p><code>Symbol.toPrimitive</code> 심볼은 ES6부터 추가된 원시 자료 형변환 메소드로, <code>valueOf</code>와 <code>toString</code>보다 우선된다. <code>valueOf</code>나 <code>toString</code>과는 다르게 Object.prototype에는 <code>@@Symbol.toPrimitive</code> 메소드가 정의되어 있지 않다.</p>
<p><code>@@Symbol.toPrimitive</code> 메소드는 하나의 매개변수를 가지며, 그 인자의 값은 반드시 &quot;string&quot;, &quot;number&quot;, &quot;default&quot; 중 하나가 와야 한다. 리턴값은 반드시 원시 자료형이어야 하고, 위에서 보았듯이 객체 자료형이면 에러가 뜬다. 사용자가 이 메소드를 정의하고자 한다면 hint값에 따라 값이 달라지는 구조로 코드를 작성해야 한다.</p>
<p>다음의 예제를 보자.</p>
<pre><code class="language-javascript">let example = {
  [Symbol.toPrimitive](hint){
    if(hint === &quot;number&quot;) return 123;
    if(hint === &quot;string&quot;) return &#39;456&#39;;
    return 789;
  }
}
console.log(+example); // unary plus는 preferType이 number이므로 123이 반환됨
console.log(example*1); // -, * /, %는 preferType이 number이므로 123이 반환됨

console.log(`${example}`); // 템플릿 문자열은 preferType이 string이므로 &#39;456&#39;이 반환됨
console.log(&quot;&quot;.concat(example)); 
// 문자열의 concat 메소드는 preferType이 string이므로 &#39;456&#39;이 반환됨

console.log(0 + example); // + 연산자는 preferType이 default이므로 789가 반환됨
console.log(789 == example); // == 연산자는 preferType이 default이므로 789가 반환됨</code></pre>
<h3 id="valueof">valueOf</h3>
<p><code>valueOf</code> 메소드는 원시 자료 형변환 메소드로, 원시 자료형을 반환하는 데에 쓰인다. 객체도 반환할 수 있으나, 객체가 반환되면 암시적 형변환에서 무시된다. 많은 사람들이 <code>valueOf</code>는 숫자만 반환할 수 있다고 오해하지만, 그렇지 않다. 객체를 대표할 수 있는 원시 자료형이면 뭐든지 반환할 수 있다.(그래도 숫자를 반환하는 것이 권장된다.)</p>
<p>기본적으로 Object.prototype에는 valueOf 메소드가 정의되어 있으며, 그 값은 자기 자신을 반환한다. 그래서 일반적인 객체는 <code>toString</code>을 기반으로 원시 자료형으로 변경되게 된다. Date 객체의 valueOf 메소드는 1970년 1월 1일 0시 0분 0.0초부터 지금까지 걸린 시간을 숫자형으로 반환한다.</p>
<p>객체를 대표하는 숫자같은 것을 반환한다는 점에서 unary plus 연산자와도 비슷함을 느낄 수 있으나, unary plus 연산자가 평가될 때 valueOf 메소드가 toString보다 우선될 뿐 대체가 불가능하다.</p>
<h3 id="tostring">toString</h3>
<p><code>toString</code> 메소드는 원시 문자열 형변환 메소드로, 객체를 대표하는 문자열을 반환하는 데에 쓰인다. 사실 toString 역시 문자열이 아는 것들이 반환될 수 있으나, 웬만해서는 문자열을 반환해야 한다.</p>
<p>기본적으로 Object.prototype에는 toString 메소드가 정의되어 있으며, 그 값의 결정 방식은 <a href="https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-object.prototype.tostring">다음</a>과 같다.</p>
<blockquote>
<ol>
<li>caller가 undefined이면 <code>[Object Undefined]</code>를 반환한다.</li>
<li>caller가 null이면 <code>[Object Null]</code>을 반환한다.</li>
<li>caller가 원시 자료형이면 이를 래퍼 객체로 변환한다.</li>
<li>builtinTag를 결정한다. 결정 방식은 다음과 같다.<ul>
<li><code>caller.isArray() === true</code>이면 Array</li>
<li>caller가 <code>[[ParameterMap]]</code> 내부 슬롯이 있으면(=argument 객체) Arguments</li>
<li>caller가 <code>[[Call]]</code> 내부 메소드가 있으면(=함수) Function</li>
<li>caller가 <code>[[ErrorData]]</code> 내부 슬롯이 있으면(=Error 객체) Error</li>
<li>caller가 <code>[[BooleanData]]</code> 내부 슬롯이 있으면(=불리언) Boolean</li>
<li>caller가 <code>[[NumberData]]</code> 내부 슬롯이 있으면(=숫자형) Number</li>
<li>caller가 <code>[[StringData]]</code> 내부 슬롯이 있으면(=문자열) String</li>
<li>caller가 <code>[[DateValue]]</code> 내부 슬롯이 있으면(=Date 객체) Date</li>
<li>caller가 <code>[[RegExpMatcher]]</code> 내부 슬롯이 있으면(=정규 표현식) RegExp</li>
<li>그 외의 경우 Object</li>
</ul>
</li>
<li>caller에 <code>@@Symbol.toStringTag</code> 프로퍼티가 존재하고, 그것이 문자열인지 확인한다. 만약 그렇다면 tag는 <code>caller[Symbol.toStringTag]</code>로 정해지고, 그렇지 않으면 builtinTag로 결정된다.</li>
<li>문자열 <code>[Object (tag)]</code>를 반환한다.</li>
</ol>
</blockquote>
<p>ES6부터 추가된 Map, Set, Promise 등의 객체는 각 객체의 프로토타입에  <code>@@Symbol.toStringTag</code>가 설정되어 있으므로, 따로 빌트인 태그 결정 방식을 변경하지 않아도 무방하다.</p>
<p>Function.prototype과 Array.prototype에는 <strong>별도의 toString 메소드가 오버라이딩되어 있으며</strong>, Object.prototype.toString의 로직을 따르지 않는다. 대신, 다음과 같이 call 메소드를 사용함으로써 Object.prototype.toString의 로직을 따르게 변경할 수 있다.</p>
<pre><code class="language-javascript">console.log(String(function(a,b){return a+b})); // &quot;function(a,b){return a+b})&quot;
console.log(String([1,2,3])) // &quot;1,2,3&quot;
console.log(Object.prototype.toString.call([1,2,3])) // &quot;[Object Array]&quot;</code></pre>
<h2 id="각-연산별-형변환-규칙">각 연산별 형변환 규칙</h2>
<h3 id="산술-연산자-비트-연산자-제외">산술 연산자, 비트 연산자(+ 제외)</h3>
<p>산술 연산자(<code>-, *, /, %, **</code>)와 비트 연산자(<code>|, &amp;, ^, &lt;&lt;, &gt;&gt;, &gt;&gt;&gt;</code>)는 추상 연산으로 <code>ApplyStringOrNumericBinaryOperator</code>라는 연산을 이용하여 값을 계산한다. 값을 계산할 때에는 기본적으로 <strong>숫자형/BigInt형</strong>으로 변환되며, 객체를 원시 자료형으로 변환할 때 <strong>preferType은 number</strong>로 정해진다.(@@Symbol.toPrimitive(&quot;number&quot;), valueOf, toString 순으로 우선순위를 가짐)</p>
<p>ECMAScript 명세서에서 정의하고 있는 <a href="https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-evaluatestringornumericbinaryexpression">산술 연산자의 알고리즘</a>은 다음과 같다. 이해를 돕기 위해 자바스크립트로 짠 binaryOperator 함수를 같이 첨부한다.</p>
<pre><code class="language-javascript">function toNumeric(value)
{
  const numeric = toPrimitive(value, &quot;number&quot;);
  if(typeof numeric === &quot;bigint&quot;) return numeric;
  return +numeric;
}
function binaryOperator(lval, operator, rval)
{
  const lnum = toNumeric(lval);
  const rnum = toNumeric(rval);
  if(typeof lnum !== typeof rnum) {
  throw new TypeError(&quot;Cannot mix BigInt and other types, use explicit conversions&quot;);
  }
  if(typeof lnum === &quot;bigint&quot;) return bigIntOperator(lnum, operator, rnum);
  return numberOperator(lnum, operator, rnum);
}</code></pre>
<blockquote>
<ol>
<li>좌항과 우항에 각각 toNumeric 추상 연산을 적용하여 숫자형/bigInt형으로 만든다.</li>
</ol>
</blockquote>
<ul>
<li>이때 각 항이 객체이면 preferType을 number로 설정하여 원시 자료형으로 변환한다.<ol start="2">
<li>만약 좌항과 우항이 다른 타입이면 TypeError를 반환한다.</li>
<li>연산자를 기반으로 좌항과 우항을 연산한다. 연산자에 대한 설명은 생략한다.</li>
</ol>
</li>
</ul>
<h3 id="산술-연산자-">산술 연산자 +</h3>
<p>산술 연산자 중 + 연산자는 문자열 연결 연산자의 역할로도 사용할 수 있기 때문에, 다른 산술 연산자와는 다른 양상을 띈다. 그것은 객체를 원시 자료형으로 변경할 때 <strong>preferType이 default</strong>로 정해진다는 것이다. 즉, 객체에 <code>@@Symbol.toPrimitive</code>가 존재하면 인자로 &quot;default&quot;가 들어간 값이 출력된다.</p>
<p>ECMAScript 명세서에서 정의하고 있는 + 연산자의 알고리즘은 다음과 같다. 이해를 돕기 위해 자바스크립트로 짠 plusOperator 함수를 같이 첨부한다.</p>
<pre><code class="language-javascript">function plusOperator(lval, rval)
{
  const leftPrim = toPrimitive(lval); // preferType default
  const rightPrim = toPrimitive(rval);
  if(typeof leftPrim === &quot;string&quot; || typeof rightPrim === &quot;string&quot;) {
    const leftStr = toString(leftPrim);
    const rightStr = toString(rightPrim);
    return leftStr.concat(rightStr);
  }
  return binaryOperator(leftPrim, &quot;+&quot;, rightPrim);
}</code></pre>
<blockquote>
<ol>
<li>좌항과 우항에 각각 toPrimitive 추상 연산을 적용하여 원시 자료형으로 만든다.<ul>
<li>이때 각 항이 객체이면 preferType을 default로 설정하여 원시 자료형으로 변환한다.</li>
</ul>
</li>
<li>만약 원시 자료형으로 변한 좌항 또는 우항의 타입이 문자열이면, <ol>
<li>변환된 원시 자료형 값들을 문자열로 형변환한다.</li>
<li>좌항에 우항을 연결한 문자열을 반환한다.</li>
</ol>
</li>
<li>그렇지 않으면, 일반적인 산술 연산자의 알고리즘을 따른다. 이때 실질적으로 계산하는 값으로 취급되는 것은 preferType이 default로 설정된 원시 자료형이지, preferType이 number로 취급되는 것이 아님에 주의하자.</li>
</ol>
</blockquote>
<h3 id="비교-연산자부등호">비교 연산자(부등호)</h3>
<p>ECMAScript 명세서에서 비교 연산자는 특이하게 정의된다. 모두 같은 기본 추상 연산인 <code>IsLessThan(left, right, leftFirst)</code>를 사용하기 때문에, 순서와 최종 리턴값이 달라진다. 그 표는 다음과 같다.</p>
<table>
<thead>
<tr>
<th align="center">표현식</th>
<th>isLessThan 호출</th>
<th>리턴값</th>
<th>실제 계산되는 연산식</th>
</tr>
</thead>
<tbody><tr>
<td align="center">left &lt; right</td>
<td>isLessThan(left, right, true)</td>
<td>undefined일 시 false, 그 외 결과값</td>
<td>left &lt; right</td>
</tr>
<tr>
<td align="center">left &lt;= right</td>
<td>isLessThan(right, left, false)</td>
<td>undefined, true일 시 false, false일 시 true</td>
<td>!(right &lt; left)</td>
</tr>
<tr>
<td align="center">left &gt; right</td>
<td>isLessThan(right, left, false)</td>
<td>undefined일 시 false, 그 외 결과값</td>
<td>right &lt; left</td>
</tr>
<tr>
<td align="center">left &gt;= right</td>
<td>isLessThan(left, right, true)</td>
<td>undefined, true일 시 false, false일 시 true</td>
<td>!(left &lt; right)</td>
</tr>
</tbody></table>
<p>isLessThan에 세 번째 연산자로 left가 right보다 실제로 더 왼쪽에 있는가를 표시하는데, 그 이유는 자바스크립트는 왼쪽에서 오른쪽으로 값을 평가하고, 형변환 과정에서 생기는 잠재적인 부수 효과가 왼쪽에서 오른쪽으로 평가되는 것처럼 보이게 하기 위해서이다.(객체의 원시 자료형 변환은 함수를 통해 이루어지기 때문에, 해당 함수에 부수효과가 존재하면 원시 자료형 변환에 부수효과가 존재한다.)</p>
<p>ECMAScript 명세서에서 정의하고 있는 <a href="https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islessthan">비교 연산자의 형변환 과정</a>은 다음과 같다. 이해를 돕기 위해 자바스크립트로 작성한, <code>a&lt;b</code>인 상황을 기준으로 한 isLessThen(left, right)의 코드를 같이 첨부한다.</p>
<pre><code class="language-javascript">function _BigInt(value)
{
  try {
    return BigInt(value);
  }
  catch {
    return undefined;
  }
}
function oneIsBigInt_and_OtherIsString(left, right)
{
  if(typeof left === &quot;string&quot; &amp;&amp; typeof right === &quot;string&quot;) return true;
  if(typeof left === &quot;bigint&quot; &amp;&amp; typeof right === &quot;string&quot;) return true;
  return false;
}
// 문자열 비교 함수
function stringIsLessThen(left, right)
{
  const leftLength = left.length;
  const rightLength = right.length;
  for(let i=0; i&lt;Math.min(leftLength, rightLength); i++)
  {
    const leftChar = left.charCodeAt(i);
    const rightChar = right.charCodeAt(i);
    if(leftChar &lt; rightChar) return true;
    if(leftChar &gt; rightChar) return false;
  }
  return leftLength &lt; rightLength;
}
// 숫자형 비교 함수
function numberIsLessThen(left, right)
{
  if(Number.isNaN(left) || Number.isNaN(right)) return undefined;
  if(left === +0 &amp;&amp; right === -0) return false;
  if(left === -0 &amp;&amp; right === +0) return true;
  if(left === Infinity || right === -Infinity) return false;
  if(left === -Infinity || right === Infinity) return true;
  return left &lt; right;
}

function _isLessThen(left, right)
{
  //좌우항을 원시 자료형으로 변환. 
  /*
  실제 자바스크립트에서는 a&lt;b뿐만 아니라 a&gt;b 같은 것도 IsLessThan 추상 연산을 쓰기 때문에, 
  isLessThen(b,a) 같은 것은 right를 먼저 원시 자료형으로 변경시킴으로써 
  부수효과의 순서를 맞출 필요가 있다.
  */
  const leftPrim = toPrimitive(left, &quot;number&quot;);
  const rightPrim = toPrimitive(right, &quot;number&quot;);

  //원시 좌우항이 모두 문자열이면 문자열 비교 수행
  if(typeof leftPrim === &quot;string&quot; &amp;&amp; typeof rightPrim === &quot;string&quot;) {
     return stringIsLessThen(leftPrim, rightPrim);
  }

  //한쪽이 BigInt형이고 다른 한쪽이 문자열이면 문자열을 BigInt형으로 변환 후 비교
  if(oneIsBigInt_and_OtherIsString(leftPrim, rightPrim)) {
    const leftBigInt = _BigInt(leftPrim);
    const rightBigInt = _BigInt(rightPrim);
    if(leftBigInt === undefined || rightBigInt === undefined) return undefined;
    return leftBigInt &lt; rightBigInt;
  }

  //좌우항을 숫자형/bigInt형으로 변환
  /*
  toNumeric의 대상이 되는 leftPrim과 rightPrim은 원시 자료형이며,
  원시 자료형끼리의 형변환은 자바스크립트 내부에서 취급하기 때문에
  부수효과가 일어나지 않는다. 따라서 순서를 바꿔서 처리할 필요가 없다.
  */
  const leftNum = toNumeric(leftPrim);
  const rightNum = toNumeric(rightPrim);

  //좌우항의 타입이 같을 경우 그대로 각 타입의 비교 조건으로 비교
  if(typeof leftNum === typeof rightNum) {
    if(typeof leftNum === &quot;bigint&quot;) return leftNum &lt; rightNum;
    return numberIsLessThen(leftNum, rightNum);
  }

  //한쪽이 숫자형, 한쪽이 bigInt형일 때 비교
  if(Number.isNaN(leftNum) || Number.isNaN(rightNum)) return undefined;
  if(leftNum === Infinity || rightNum === -Infinity) return false;
  if(leftNum === -Infinity || rightNum === Infinity) return true;
  return leftNum &lt; rightNum;
}

function isLessThen(left, right)
{
  return _isLessThen(left, right) ?? false;
}</code></pre>
<blockquote>
<ol>
<li>좌항과 우항을 원시 자료형으로 변환한다. 이 때 preferType은 <strong>number</strong>로 한다.</li>
<li>좌항과 우항이 모두 문자열이면 문자열의 비교 연산을 수행한다.</li>
</ol>
</blockquote>
<ul>
<li>각각의 문자를 0부터 각 문자열의 길이의 최솟값까지 순회하여, 실질적으로 저장된 숫자값(자바스크립트는 utf-16으로 문자열을 인코딩한다)이 다르면 그것을 결과로 취한다.</li>
<li>모든 문자열이 같을 경우, 길이를 비교한다.<ol start="3">
<li>한쪽이 BigInt형이고, 한쪽이 문자열이면 문자열을 BigInt로 형변환한 뒤 값을 비교한다.</li>
</ol>
</li>
<li>문자열을 BigInt형으로 변환하지 못했을 경우 최종 비교값은 undefined(=false가) 된다.<ol start="4">
<li>원시 자료형으로 변환된 각 항을 toNumeric 추상 연산을 적용하여 숫자형/BigInt형으로 변환한다.</li>
<li>만약 타입이 같을 경우, 그대로 비교한다.</li>
</ol>
</li>
<li>숫자형의 경우<ul>
<li>어느 한쪽이 NaN이면 undefined이다.</li>
<li>어느 한쪽이 +0이고 다른 쪽이 -0이면 같은 것으로 취급한다.</li>
<li>어느 한쪽이 양의 무한대이면 그 쪽을 큰 것으로 취급한다.</li>
<li>어느 한쪽이 음의 무한대이면 그 쪽을 작은 것으로 취급한다.</li>
<li>어느 쪽에도 해당하지 않을 경우 실수 비교 연산을 수행한다.</li>
</ul>
</li>
<li>BigInt형의 경우<ul>
<li>실수 비교 연산을 수행한다.</li>
</ul>
<ol start="6">
<li>어느 한쪽이 NaN이면 undefined이다.</li>
<li>어느 한쪽이 양의 무한대이면 그 쪽을 큰 것으로 취급하고, 음의 무한대이면 작은 것으로 취급한다.</li>
<li>각 항을 실수로 변경한 후 비교 연산을 수행한다.</li>
</ol>
</li>
</ul>
<h3 id="비교-연산자">비교 연산자(==)</h3>
<p>동등 비교 연산자(<code>==</code>)는 추상 연산으로 isLooselyEqual이라는 연산을 이용하여 값을 계산한다. <code>!=</code>의 경우도 동일한 연산을 사용하며, 값만 반대일 뿐이다.</p>
<p>ECMAScript 명세서에서 정의하고 있는 <a href="https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal">==의 형변환 과정</a>은 다음과 같다. 이해를 돕기 위해 자바스크립트로 작성한 isLooselyEqual 코드를 같이 첨부한다.</p>
<pre><code class="language-javascript">function type(value)
{
  if(value === null) return &quot;null&quot;;
  if(typeof value === &quot;function&quot;) return &quot;object&quot;;
  return typeof value;
}
function typeCheck(left, right, [type1, type2=&quot;any&quot;])
{
  const leftType = type(left);
  const rightType = type(right);
  if(type2 === &quot;any&quot;) return (leftType === type1 || rightType === type2);
  if(leftType === type1 &amp;&amp; rightType === type2) return true;
  if(rightType === type1 &amp;&amp; leftType === type2) return true;
  return false;
}
function objectTypeCheck(left, right)
{
  const rightValueType = [&quot;number&quot;, &quot;string&quot;, &quot;symbol&quot;, &quot;bigint&quot;].includes(type(right));
  return type(left) === &quot;object&quot; &amp;&amp; rightValueType;
}

function isLooselyEqual(left, right)
{
  //타입이 같을 시 strict 비교
  if(type(left) === type(right)) return left === right;

  //한쪽이 null이고 다른 쪽이 undefined이면 true
  if(typeCheck(left, right, [&quot;null&quot;, &quot;undefined&quot;])) return true;

  //한쪽이 숫자형이고 다른 쪽이 문자열이면 문자열을 숫자로 변환
  if(typeCheck(left, right, [&quot;number&quot;, &quot;string&quot;])) {
    return isLooselyEqual(+left, +right);
  }
  if(typeCheck(left, right, [&quot;bigint&quot;, &quot;string&quot;])) {
    return isLooselyEqual(BigInt(left), BigInt(right));
  }

  //한쪽이 boolean형이면 불리언을 숫자로 변환
  if(typeCheck(left, right, [&quot;boolean&quot;])) {
    if(type(left) === &quot;boolean&quot;) return isLooselyEqual(+left, right);
    if(type(right) === &quot;boolean&quot;) return isLooselyEqual(left, +right);
  }

  //한쪽이 object형이면 object를 원시 자료형(perferType은 default)으로 변환
  if(objectTypeCheck(left, right)) return isLooselyEqual(toPrimitive(left), right);
  if(objectTypeCheck(right, left)) return isLooselyEqual(left, toPrimitive(right));

  //한쪽이 Number이고 다른 한쪽이 BigInt형일 때
  if(typeCheck(left, right, [&quot;number&quot;, &quot;bigint&quot;])) {
    if(!Number.isFinite(left) || !Number.isFinite(right)) return false;
    return left == right;
  }
  return false;
}</code></pre>
<ol>
<li>좌항과 우항의 타입이 같으면 값을 기준으로 비교한다.</li>
<li>한쪽이 null이고 다른 쪽이 undefined이면 true를 반환한다.</li>
<li>한쪽이 숫자형이고 다른 쪽이 문자열이면 문자열을 숫자로 형변환한 후 비교한다.</li>
<li>한쪽이 BigInt형이고 다른 쪽이 문자열이면 문자열을 BigInt로 형변환한 후 비교한다.</li>
<li>한쪽이 Boolean형이면 불리언을 숫자로 형변환한다.</li>
<li>한쪽이 Object형이고 다른 한쪽이 숫자형(숫자로 형변환된 불리언 포함), 문자열, BigInt, Symbol형이면 Object를 추상 연산 toPrimitive()을 이용하여(<strong>preferType은 default</strong>이다) 원시 자료형으로 변경한 뒤, 이를 기반으로 계산한다.</li>
<li>한쪽이 숫자형이고 다른 쪽이 BigInt형이면<ul>
<li>한쪽이 NaN, Infinity, -Infinity면 false를 반환한다.</li>
<li>각 항을 실수로 변경한 후 비교한 값을 결과로 취한다.</li>
</ul>
</li>
<li>그 외의 경우, false를 반환한다.</li>
</ol>
<h2 id="맨-처음-문제의-답">맨 처음 문제의 답</h2>
<p>답은 <strong>false</strong>다. <code>new Date()</code>와 <code>Date.now()</code>는 자료형이 각각 Object와 Number로 다르기 때문에, 형변환의 과정이 필요하다. 이 때, Object는 해당 오브젝트의 <code>@@Symbol.toPrimitive</code> 메소드를 우선적으로 호출하고(이 때 hint는 default이다) 없으면 <code>valueOf</code>, 그래도 없으면 <code>toString</code> 메소드를 호출한다. <code>new Date()</code>는 <code>[Symbol.toPrimitive]</code> 메소드가 존재하며, 그 값은 문자열이므로 문자열과 숫자를 비교하게 된다. 문자열과 숫자를 비교할 때에는 문자열 쪽이 숫자로 형변환되어서 비교된다. 이 때 NaN과 숫자는 다른 값이므로 false가 나온다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[부스트캠프 7기] 멤버십 합격]]></title>
            <link>https://velog.io/@lybell_4rt/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-7%EA%B8%B0-%EB%A9%A4%EB%B2%84%EC%8B%AD-%ED%95%A9%EA%B2%A9</link>
            <guid>https://velog.io/@lybell_4rt/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-7%EA%B8%B0-%EB%A9%A4%EB%B2%84%EC%8B%AD-%ED%95%A9%EA%B2%A9</guid>
            <pubDate>Wed, 24 Aug 2022 10:42:13 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/5e96d713-ef4f-410b-9328-4d3209248702/image.png" alt="">
오늘 오후 6시 48분, 부스트캠프에서 메일이 왔다. <strong>멤버십 합격</strong>이다!
이렇게 좋은 기회를 제공해 주신 부스트캠프 운영진에게 정말 감사할 따름이다.</p>
<p>+7기 기준, 멤버십에 입과한 캠퍼는 전체의 70% 정도였다.</p>
<h2 id="나는-챌린지-때-어떻게-공부했는가">나는 챌린지 때 어떻게 공부했는가</h2>
<p>혹시 다음 8기생에게 참고삼아서 내 챌린지 때 공부 방법을 잠깐 써 보기로 하겠다. 멤버십 합격 기준은 정말 베일에 쌓여 있고, 자기가 멤버십에 들어올 실력이 아닌 것 같은데 붙었다는 사람도 있다고 하니 진짜 <strong>챌린지 그 순간만 바라보고 공부</strong>하는 것이 능사인 것 같다. 챌린지 수료한 사람의 공통된 말이지만, 챌린지는 멤버십에 가기 위한 수단보다는 지속 가능한 개발자로 성장하기 위한 경험이라고 생각한다.</p>
<ol>
<li><strong>7시 커트라인은 안 중요하다.</strong> 7시는 자신의 페이스를 체크하기 위한 중간 체크포인트일 뿐이다. 사실 예상 문제 해결시간이 8시간인 것도 있는 만큼, 정말 장식인 게 느껴진다.</li>
<li>나의 경우, 문제 해결을 위한 배경지식을 얕게 공부하고 문제를 설계하고 해결한 뒤, 학습정리를 수행했다. 단, 이 경우는 본인의 페이스에 맞게 조절하는 것이 좋다. 다른 분은 학습정리로 먼저 탄탄한 배경지식을 갖고 설계를 한 뒤 문제를 풀었다.</li>
<li>점심시간은 따로 주어지지 않는데, 나는 피어세션이 끝나면 12시에 점심을 먹으면서 문제를 파악했다. 체크아웃을 한 뒤, 저녁을 먹고 1시간 정도 휴식을 가진 뒤 다시 작업에 들어갔다.</li>
<li><strong>학습정리를 좀 빡세게 했다</strong>. 4시간 정도. 부스트캠프를 하면서 기록을 남길 일이 종종 있는데, 기록은 본인이 어떻게 학습했는지, 그 학습으로 어떻게 성장하고 있는지를 남기는 강력한 증거이므로 나는 문제 해결에 집중하기보다는 문제를 어떻게 풀었는지, 그리고 그 과정에서 무엇을 배웠는지 <strong>기록을 열심히 하는 것을 추천</strong>드리고 싶다.<ul>
<li>학습정리의 경우 나는 큰 틀을 기준으로 작은 주제를 dfs식으로 파고드는 방식으로 정리했는데, 작은 주제를 파고들 때 잘 끊는 것 역시 중요한 것 같다.</li>
</ul>
</li>
<li>주말은 하루를 쉬고, 나머지 하루는 리팩토링 등 일주일 활동을 보충하는 시간으로 보냈다.</li>
<li><strong>커뮤니티 활동 역시 중요</strong>하다고 생각한다. 피어세션이든, 슬랙에서의 토론이든 열심히 질문하고 열심히 답변하자. 나는 주로 슬랙에서 질문을 보고 아는 것들은 열심히 답변하는 방향으로 활동했었다.<ul>
<li>부스트캠프 문제를 풀다 보면 일부러 모호하게 지문이 적혀 있어서 다양한 해석의 여지가 있는 부분이 있다. 그것도 많이. 웬만해서는 <strong>해석에 대한 질문보다는 문제에 담긴 원리 등 확장성 있는 질문을 하는 것</strong>이 좋을 것이다.</li>
</ul>
</li>
</ol>
<h3 id="그-외에-중요하다고-생각한-것들">그 외에 중요하다고 생각한 것들</h3>
<ol>
<li>중요한 것은 다른 사람들과의 비교가 아닌, <strong>어제의 자신과의 비교</strong>다. 어제의 자신과 비교하면서 더 나은 방향으로 나아가는 것이 지속 가능한 개발자이기에.<ul>
<li>이 과정에서 다른 사람들을 벤치마킹하려는 의지를 보이는 것이 중요하다. 자신의 팀뿐만 아니라 다른 캠퍼의 코드도 구경해 보자.</li>
</ul>
</li>
<li>비록 자신이 실력이 뛰어나서 문제를 7시간 내에 풀었더라도, 더 나은 개선의 의지를 보이는 것이 중요하기에, 7시간 업적에 연연하지 않고 <strong>리팩토링을 진행</strong>하는 것이 좋을 것이다.</li>
<li><strong>잠은 중요하다</strong>. 무조건 새벽 4시 전에는 자도록 하자. 새벽에는 능률이 떨어질 뿐더러, 무엇보다 늦잠 자다 체크인 시간에 늦을 수 있다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[캔버스 api와 블러 ]]></title>
            <link>https://velog.io/@lybell_4rt/%EC%BA%94%EB%B2%84%EC%8A%A4-api%EC%99%80-%EB%B8%94%EB%9F%AC</link>
            <guid>https://velog.io/@lybell_4rt/%EC%BA%94%EB%B2%84%EC%8A%A4-api%EC%99%80-%EB%B8%94%EB%9F%AC</guid>
            <pubDate>Wed, 24 Aug 2022 10:21:31 GMT</pubDate>
            <description><![CDATA[<p>css 효과에는 <strong>블러</strong>라는 게 있다. 엘리먼트(<code>filter</code>)나 엘리먼트 뒤의 요소(<code>backdrop-filter</code>)를 뿌옇게 해서 다른 요소의 가독성을 높여주는 등 디자인적 요소로 쓰인다. 그런데 css 블러를 캔버스에 적용하면 <strong>프레임 드랍이 일어난다는 사실</strong>, 알고 계셨는가?</p>
<p>css 효과에 블러를 적용하지 않으면 60fps를 유지하던 캔버스가 블러를 적용시키니 30fps 정도로 프레임이 뚝 떨어지는 모습을 볼 수 있다. 아마 css 블러와 web 캔버스 api가 역시너지를 일으키면서 gpu를 잡아먹는 것으로 보이는데, 자세한 것은 더 찾아봐야 할 것 같다.</p>
<p>특이한 점은 backdrop-filter를 적용한 엘리먼트와 캔버스가 겹치면 캔버스가 실제로 다른 엘리먼트에 가려져 보이지 않아도 무조건 프레임 드랍이 일어난다는 것이다. </p>
<p>결론적으로 오늘의 교훈은 다음과 같다.</p>
<ul>
<li>css 블러와 캔버스 api는 절대 겹치지 않도록 할 것. 가급적이면 블러 효과가 들어간 배경을 사용할 때 포토샵 등으로 블러를 먼저 먹인 이미지를 사용할 것.<ul>
<li>헤더에 backdrop-filter는 본문에 캔버스 api를 사용하지 않을 때에나 사용할 것.</li>
</ul>
</li>
<li>캔버스 자체적으로 블러 효과를 먹이고 싶을 경우 css 블러를 사용하는 것이 아니라 캔버스 자체 블러 필터를 사용할 것. (pixi.js의 경우 blurfilter가 존재)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[부스트캠프 7기] 챌린지 수료 후기]]></title>
            <link>https://velog.io/@lybell_4rt/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-7%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@lybell_4rt/%EB%B6%80%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%94%84-7%EA%B8%B0-%EC%B1%8C%EB%A6%B0%EC%A7%80-%EC%88%98%EB%A3%8C-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 21 Aug 2022 16:13:15 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/ce0443b1-451d-4734-9a7b-7ef253bda618/image.png" alt=""></p>
<p>7월 18일부터 8월 12일까지 4주간 진행된 부스트캠프 웹・모바일 7기 챌린지가 드디어 막을 내렸다. </p>
<h2 id="각-일차-회고록">각 일차 회고록</h2>
<h3 id="16일차">16일차</h3>
<blockquote>
<p>이제부터는 배경지식이 없습니다. 미션을 해결하기 위해 얻어야 하는 지식도 스스로 알아가셔야 합니다.</p>
</blockquote>
<p>4주차부터는 기본적으로 주어지는 배경지식이 없다. 그래서 문제를 읽고 배경지식을 유추해서 공부해야 한다. 진정한 야생은 최소한의 길잡이조차 주어지지 않고, 자기가 스스로  부스트캠프가 야생이라는 것이 느껴지는 순간이었다.
사실 16일차 문제와 비슷한 것을 13일차에 이미 시도한 적이 있었다. 그 때에는 거의 야매로 배워서 적용했었는데, 16일차에서 복습하면서 제대로 적용하니 내 지식이 강화된 느낌이었다. 더불어 이전 프로젝트를 하면서 썼던 기술을 적용하는 과정에서 해당 기술의 문제점을 파악할 수 있었다. 이 과정에서 부스트캠프 도중에 배웠던 테크닉이 또다시 빛을 발했다.</p>
<p>4주차에서 유일하게 7시간 내에 클리어한 문제였다. 나머지는 기본적으로 밤 10시에 끝냈고 마지막 날은 새벽에 끝을 보았다.</p>
<h3 id="17일차">17일차</h3>
<blockquote>
<p>설계의 중요성</p>
</blockquote>
<p>설계가 정말 중요한 문제가 나왔다. 바로 전일차에 나왔던 것은 물론 이전에 배웠던 설계방식과 학습 내용을 가져와야 풀 수 있는 문제였다. 추가적으로 학습해야 하는 배경지식이 있다기보다는 이전에 반복적으로 나온 것들을 어디까지 확실히 알고 있냐는 느낌이었다. </p>
<p>코드를 짜다가 특정 모듈에 로직이 너무 집중되어서 비대해지는 것 아니냐는 지적을 받았다. 해당 문제를 해결하기 위해 학습정리를 하다 특정한 디자인 패턴을 알게 되었고 그것을 적용하였다. 나중에 다시 알아보니 이미 코드를 짜면서 쓰고 있었는데 그 명칭과 효과를 몰랐던 패턴이었다더라.</p>
<h3 id="18일차">18일차</h3>
<blockquote>
<p>이게 10시간이 걸릴 게 아닌데?!
-그 당시 나, 학습노트에 느낀 점을 쓰며</p>
</blockquote>
<p>예전에 개인 프로젝트를 하면서 한 번 다뤄본 것이 주제로 나왔다. 그때는 원리를 모르고 썼었는데, 이번에 직접 구현하면서 원리를 알아가니 좋았다더라. 그런데 한 번 다뤄본 거고 설계가 어렵지 않은 것 같았지만 끝나고 나니 10시간이나 걸렸다더라.</p>
<blockquote>
<p>끝없는 예외처리</p>
</blockquote>
<p>4주차 문제들이 다 그렇지만, 예외처리가 정말 중요했었다. 개발자는 자신이 생각한 플로우대로 사용자들이 따라올 것이라고 생각하지만, 실제 환경에서 사용자는 개발자가 상상치도 못한 창발적인 방법으로 버그를 찾기도 한다. 그래서 가능한 한 사용자가 할 수 있는 모든 방법을 틀어막아서(?) 사용자가 개발자가 의도한 방법대로 프로그램을 사용하도록 유도하는 것이 예외처리다. 아마도 사용자 입장에서 어떻게 하면 프로그램에 버그가 날까 하고 생각하면서 가능한 모든 수를 생각하느라 늦어진 게 아닐까 싶다. 그만큼 예외처리도 잘 이루어졌던 것 같다.</p>
<h3 id="19일차">19일차</h3>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/cd1a3e4c-7fc2-4354-b112-64da4addded0/image.png" alt=""></p>
<p>(슬랙에 <a href="https://velog.io/@yangsooplus/%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%B1%8C%EB%A6%B0%EC%A7%80-%EC%88%98%EB%A3%8C-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C#4%EC%A3%BC%EC%B0%A8">어떤 분</a>이 올리신 사진의 원본. 정보유출 문제로 그 사진을 올릴 수 없지만 대충 이번 부스트캠프에서 악명 높은 문제 4천왕을 꼽은 내용이었다. 그리고 끝판왕이 19일차.)</p>
<p>마지막 일차라 그런지, 지금까지 나왔던 대부분의 CS지식과 테크닉을 총동원해서 풀어야 하는 문제가 나왔다. 그 중 화룡점정은 지금까지 나왔던 미션들이 나열되는 부분. 챌린지 4주간의 기억을 되돌아볼 수 있었던 가슴 뭉클한 시간이었다.</p>
<p>놀랍게도 19일차 문제는 내가 한 라이브러리를 써서 해 봤던 문제였다. 하지만 그 때는 라이브러리를 쓸 줄만 알았지 라이브러리가 어떻게 굴러가는지는 몰랐었다. 힘들게 직접 구현해보니 라이브러리의 소중함을 알게 되었다더라.</p>
<h3 id="20일차">20일차</h3>
<blockquote>
<p>훈련소 수료 기다리는 훈련병같지 않아요?
-누군가</p>
</blockquote>
<p>고통스러웠던 19일차 미션이 끝나고, 피어세션 시간이 되었다. 가장 어려운 미션이라 모두 힘들어했지만 그래도 어떻게든 끝낸 모습을 볼 수 있었다. 19일차에는 시간이 빨리 지나가서 어느덧 새벽이 되었는데, 막상 모든 미션이 끝난 20일차가 되고 수료식만을 남겨둔 상황에서는 시간이 정말 가지 않았다. 누군가는 훈련소 수료 기다리는 훈련병이라느니, 전역 기다리는 장병 같다느니 하는데 딱 그 느낌인 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/bd7d3e12-34d1-48f2-b10e-0cf465db7a9b/image.gif" alt=""></p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/d31a4aa1-3ee1-4785-9f27-75ec0088148b/image.png" alt=""></p>
<p>팀 회고 시간에는 3주차처럼 갈틱폰을 진행했다. 이번 주차 갈틱폰도 재미있는 주제들이 나왔다. 참고로 위의 짤을 슬랙 갤러리 채널에 올렸는데, 놀랍게도 챌린지 갤러리 명예의 전당 2위에 빛나는 영광을 얻을 수 있었다!</p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/e7c5a7ba-fea3-4d5d-9eda-997c98f31487/image.png" alt=""></p>
<p>챌린지 수료식은 zep이라는 가상공간 플랫폼에서 진행되었다. 방명록이나 zep 미니게임, 슬랙에 올라왔던 사진들을 모아놓은 갤러리 등 예쁘게 꾸며져 있었다. 특히 가장 압권이었던 건 내준내상이었는데, 온갖 웃긴 상드립을 볼 수 있어서 재미있었다. 마지막으로 zoom에서 소감을 발표하고 7시에 헤어졌는데, 챌린지가 완전히 끝났다는 느낌이 드니 뿌듯하면서도 아쉬운 느낌이 들었다.</p>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/1c3437d0-2085-4b87-baa8-45801c4607ec/image.png" alt=""></p>
<p>(3주차 캠퍼들과 같이 찍은 그 당시 사진.)</p>
<h2 id="챌린지-수료-후기">챌린지 수료 후기</h2>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/5fc88c41-fca7-4542-9277-ef63eeb430e6/image.png" alt="">(미션은 이제 없다!)</p>
<p>당연한 거지만, 챌린지가 끝나니 매우 후련했다. 6개월치 공부를 한 달만에 끝낸 듯한 느낌이었고, 거기에 놀라운 건 그 중 절반이나 소화했다는 것이다. 절반이라고 말하면 별거 아닌 것 같지만, 무려 3개월이다! 챌린지의 1/6밖에 소화하지 못했어도 1달치 공부다. 챌린지를 하지 않고 개인 공부만 했다면 3개월치도 아니고 3일치 공부밖에 소화하지 못했을 것이다.</p>
<p>챌린지 과정은 힘들었지만, 그만큼 얻어간 것들도 많았다. 많은 사람들이 챌린지 힘들다 해서 얼마나 힘들겠냐고 챌린지 시작 전에는 생각했지만, 진짜 자바스크립트 난생 처음 해 본 사람부터 수년간 자바스크립트로만 코드를 짠 사람들, 전공자 졸업생에 회사를 퇴사하고 온 사람들까지 전부 다 가리지 않고 힘들어했다.</p>
<p>대충 어떻게 어려웠냐면, <strong>설계과정이 어려웠다.</strong> 우리가 아는 코딩테스트 같은 알고리즘적인 어려움이 아니라, 설계를 <strong>제대로</strong> 하기 위해서 해당하는 CS지식이 정확이 어떻게 돌아가는지를 알고 있어야 했고, 나는 그 기초 CS지식에 대한 내용이 거의 없는 상태로 들어왔기 때문에, 전공자보다 배는 공부해야 했다. 그나마 코드를 많이 짜 봤기 때문에 코드 짜는 데에 어려운 점은 없었다.</p>
<p>미션 자체의 분량도 엄청났고, 구현해야 할 것들도 상당히 많았다. 한 미션의 경우 무려 <strong>800줄이 넘는 코드</strong>를 작성해야 했다. 분명 2시쯤에 밥을 먹고 설계를 완료하면 그래도 7시 이전에는 끝낼 수 있을 것 같았는데, 코드를 짜다 보면 이렇게 오래 걸릴 게 아닌데...?라고 말하는 자기 자신을 볼 수 있었다. 이 미션이 만약 학교 전공수업 중에 나온 미션이라면 일주일을 줬을 것이다.</p>
<p>무엇보다도 끝없는 밤샘으로 인한 <strong>체력적 소모가 심했다</strong>. 나는 2<del>3주차 기준으로 평균적으로 오후 9</del>10시에 미션을 완료한 편이었지만, 아직 학습정리가 남아 있었기 때문에, 미션 도중 배웠던 점을 정리하고 학습정리를 다 끝내고 나면 새벽에 자는 경우가 부지기수였다. 한번은 고된 피로로 인해 늦잠을 자는 경우도 있었고, 특히 마지막 날에는 미션이 새벽에 끝났기 때문에 학습정리까지 마무리하느라 밤을 새고 말았다.</p>
<p>힘든 것들만 있는 것 같았지만, 얻어간 것들도 상당히 많았다. 챌린지 기간 동안 얻어간 것들은 후술한다.</p>
<h3 id="챌린지에서-얻어간-것들">챌린지에서 얻어간 것들</h3>
<h4 id="다양한-cs지식들">다양한 CS지식들</h4>
<blockquote>
<p>개구리를 해부하지 말고, 직접 만들어라.</p>
</blockquote>
<p>부스트캠프에서 제일 많이 얻어갔던 것이었다. 본래 나는 프로그래밍을 많이 하는 과 출신이었고, 그만큼 코드도 많이 짜 봤지만, 전공자는 아니라서 CS지식에 취약했었다. 부스트캠프에서 제공하는 잘 짜여진 미션을 받고, 코드를 설계하면서, 학교에서는 공부하지 못했던 CS지식을 넓고 깊게 알 수 있었다.</p>
<p>특히 가장 좋았던 것은 CS지식에 대한 <strong>흐름</strong>을 확실히 알 수 있었다는 것이다. 책이나 아티클을 보고 공부하는 것만으로는 <em>좋아, 그런 지식이 있어. 그래서 어쩌라는 거지?</em> 라는 느낌이 들었으며, 정확히 알아도 알지 못하는, 외우기만 하는 느낌이었다면, 부스트캠프를 하면서 해당 CS지식을 모사하며 직접 만들어 보니, CS지식에 대한 개괄적인 지식이 아니라 알고리즘을 위한 흐름 위주로 공부하게 되고, 이러한 원리로 해당 CS지식이 굴러간다는 것을 알게 되었다.</p>
<h4 id="커밋-컨벤션">커밋 컨벤션</h4>
<p><img src="https://velog.velcdn.com/images/lybell_4rt/post/93763cf2-951a-4263-b35c-6cc33e50a040/image.png" alt="">(부스트캠프 직전 프로젝트인 Stardew Dressup 커밋. 부스트캠프 도중 프로젝트는 커밋 내용만으로 주제가 스포되기 때문에 올릴 수 없다.)</p>
<p>본래 나는 커밋 컨벤션을 그렇게 지키는 편이 아니었다. 2일차까지 커밋을 대충 작성했는데, 3일차 피어세션 때 다른 동료들이 커밋 컨벤션을 잘 지키면서 공부한 흔적을 잘 보여준 것을 보고, 그 동안 나의 커밋 스타일이 개판이었다는 것을 자각하게 되었다. 이후 커밋 컨벤션을 스스로 만들어서, 챌린지 20일차까지 커밋 컨벤션을 지키려고 노력하고 있다. 전 세계에 내 서비스를 퍼블리싱할 가능성도 있기 때문에 웬만해서는 영어로 커밋 메시지를 쓰려고 노력하고 있다.</p>
<h4 id="동료학습">동료학습</h4>
<blockquote>
<p>누구도 완벽할 수는 없다. 완벽하지 않기 때문에 서로를 지탱하는 동료가 있는 것이다.</p>
</blockquote>
<p>부스트캠프 이전, 나는 혼자서 모든 것을 해결하려는 사람이었다. 혼자서 공부하고, 개인 프로젝트를 진행하면서 누군가와 같이 공부한다는 생각 자체를 하지 않았다. 하지만 부스트캠프라는 좋은 기회에서 동료들과 같이 코드를 리뷰하면서(내 코드를 남이 볼 것이라는 걸 생각하지도 못했다.) 지식을 공유하고, 때로는 지식을 전수받기도 하면서 같이 성장한다는 기쁨을 누릴 수 있었다.</p>
<p>부스트캠프는 학교 수업 등과 다르게 줄세우기가 없다. 정말로 다른 사람의 객관적인 실력을 아무도 모른다. 그렇기 때문에 저 사람들을 멤버십에 가기 위한 경쟁자로 보는 게 아니라 멤버십은 하늘에 맡기고 서로 성장하는 동료로 쉽게 볼 수 있었던 것 같다. </p>
<p>동시에, <strong>완벽한 사람은 없다</strong>는 것을 알게 되었다. 슬랙에서 날아다니는 사람조차도 부족한 면모는 있으며, 반면 아무리 코드 실력이 부족한 사람이라도 강점이 있기 마련이다.</p>
<h4 id="학습-방법">학습 방법</h4>
<p>부스트캠프가 지향하는 학습 방법은 <strong>만들면서 배우기</strong>다. 특정한 지식을 직접 만들어 보고, 만드는 과정에서 설계적 완성도를 끌어올리거나 실제와 더 흡사하게 만들기 위해 깊게 공부하고, 이 과정에서 새로 배운 것들을 기록하여 훗날의 자신이나 다른 사람들이 쉽게 볼 수 있게 공유하는 것이 부스트캠프의 학습 방식이다. </p>
<p>내가 지향하는 학습 방식도 이와 비슷하다. 실제로 만들면서 배우기를 부스트캠프 이전에도 수행했기도 했다. xnb.js를 제작할 때에는 npm과 번들러, xnb 파일의 구조와 버퍼를 다루는 방법, 워커 스레드에 대해 배웠고, stardew dressup을 제작할 때에는 react, pixi.js에 대해 배웠다. 만약 내가 stardew dressup을 react로 제작하지 않았다면 react를 왜 써야 하는지도 몰랐을 것이다. 부스트캠프에서 새로 알게 된 것은 만들면서 배우기를 <strong>CS지식을 공부하는 데에도 적용</strong>할 수 있다는 것과, 프로젝트를 만들기 위한 수단으로써의 학습이 아닌 <strong>학습의 수단으로써의 프로젝트</strong>라는 패러다임을 전환할 수 있었다는 것이다.</p>
<p>부스트캠프가 끝났는데도 아직도 모르는 것들이 산더미다. 그래도 부스트캠프에서 만든 분야는 흐름은 말할 수 있는 수준으로 끌어올려졌다. 그래서 부스트캠프에서 얕게 넘어갔던, 또는 몰랐던 것들은 부스트캠프 스타일로 스스로에게 미션을 주어서 학습해볼 계획이다.</p>
<h3 id="챌린지에서-기여했던-것들">챌린지에서 기여했던 것들</h3>
<h4 id="코드-스타일">코드 스타일</h4>
<blockquote>
<p>참 챌린지 기간인 &#39;야생&#39; 에 어울리네요.
짧은 시간인데 뭐든지 필요하면 뚝딱뚝딱 만들어 잘 쓰시는군요....ㅋㅋㅋㅋ
구현력이 대단하십니다. 저는 하루만에 저렇게 절대 못 할 것 같아요..
일주일동안 교과서같은 모범 코드 잘 봤습니다
-피어세션 한 캠퍼분</p>
</blockquote>
<p>피어 세션을 하면서 코드 스타일에 대한 칭찬을 많이 받았었다. 사실 코드 스타일에 대해 자신은 없는 편이었고, 본래 목적이 코드 스타일 개선이었던지라, 의외로 코드 스타일에 칭찬을 받아서 코드에 대해 자신감이 조금씩 생기는 것 같았다.</p>
<p>부스트캠프에서 나는 다른 사람을 이끌어주는 포지션이 되었다. 대학에서 하던 게 자바스크립트로 코드 짜는 거라서 그런지, CS지식이나 공대생처럼 사고하기 같은 건 하지 못하지만 내 장기인 자바스크립트 테크닉이나 코드를 예쁘게 보이게 하는 법 같은 부분을 중점적으로 피드백했었다.</p>
<p>나와 함께 했던 18명의 캠퍼분들에게 미숙한 코드를 좋게 봐주셔서 감사할 따름이다.</p>
<h4 id="자바스크립트-테크닉">자바스크립트 테크닉</h4>
<pre><code class="language-javascript">let [first, second, ...rest] = [1,2,3,4,5]; // 구조 분해 할당
let {x, y} = position; // 객체 구조 분해 할당
let copied = [...origin]; // 스프레드 연산자.

// 함수 구조분해 할당
function moveForward({x,y})
{
  return {x:x+1, y};
}

// 배열 고차함수
Array.from({length:100}, (_,i)=&gt;i)
  .map(elem=&gt;elem*2)
  .filter(elem=&gt;elem&gt;10)
  .reduce((sum, cur)=&gt;sum+cur, 0);

// promise 감싸기
function sleep(millis)
{
  return new Promise(resolve=&gt;setTimeout(resolve, millis));
}

// promise 체이닝
sleep(1000)
  .then( ()=&gt;console.log(&quot;1s 경과&quot;) )
  .then( ()=&gt;sleep(2000) )
  .then( ()=&gt;console.log(&quot;2s 추가경과&quot;) )

// 파이프 함수
pipe(
  mult(2),
  filter(elem=&gt;elem&gt;10),
  average
)([1,2,3,4,5,6,7,8,9,10]) 

// 정규표현식 체이닝
regexpChain(
  [/^create\s+(.*)/i, ([,n])=&gt;pipe(parseCreate, createPlayer)(n)],
  [/^move\s+(.*)/i,   ([,n])=&gt;pipe(parseMove,     movePlayer    )(n)],
  [/^delete\s+(.*)/i, ([,n])=&gt;pipe(parseDelete, deletePlayer)(n)]
  [/.*/, ()=&gt;console.log(&quot;다시 입력하세요!&quot;)]
)(command);</code></pre>
<blockquote>
<p><code>function moveForward({x,y}){}</code>
이런식으로 선언하고 파라미터를 따로 파싱할 필요가 없어서 되게 좋은 것 같아요! 이런식으로 편한 문법들이 많은데 저는 아직 활용을 잘 못하고 있었네요. 배워갑니다!
전체적으로 문법 활용을 잘 하셔서 깔끔하고 간결하게 짜신 것 같아요 bb
-피어세션 한 캠퍼분</p>
</blockquote>
<p>올해 초, 코딩테스트를 공부하면서 자바스크립트에 대한 고급 테크닉들을 많이 배웠고, 함수형 프로그래밍에도 어느 정도 관심 있어서 고차함수의 사용이나 배열 메소드 체이닝 등 기초적인 함수형 사고방식을 배웠다. 본격적으로 부스트캠프를 하면서 여러 자바스크립트 테크닉들을 미션에서 많이 사용했고, 피어세션에서 다른 분들에게 피드백을 하면서 지금까지 사용했던 자바스크립트 테크닉들을 알려주기도 했다.</p>
<blockquote>
<p>커맨드의 파싱, 이후 로직을 단계별로 구분해서 함수형 프로그래밍적으로 구현하신 부분도 인상깊습니다.. 많이 배워갑니다.
-피어세션 한 캠퍼분</p>
</blockquote>
<p>올해 초에 배운 것뿐만 아니라, 미션 도중 함수형 프로그래밍을 적용해서 이를 사용하기도 했는데, 대표적으로 위에 적었던 파이프 함수와 정규표현식 체이닝이다. 정규표현식 체이닝은 과도한 정규표현식 파싱 반복에 화가 나서 만든 기법이었는데, 결과적으로 가독성이 향상되는 효과를 얻을 수 있었고, 해당 부분을 배우려고 하는 캠퍼들도 있었다.</p>
<p>1주차와 3주차에 만난 한 캠퍼분이 있다. 지식을 빨아들이는 스펀지같은 분이었는데, 1주차에 내가 알려주었던 자바스크립트 테크닉들을 3주차에 적용해서 사용한 것을 보고 뿌듯함과 존경심을 느꼈다. 나의 지식으로 누군가가 성장하는 것을 통해 동료학습을 통해 모두를 끌어올리는 느낌을 알 수 있었다.</p>
<h3 id="챌린지에서-아쉬웠던-것들">챌린지에서 아쉬웠던 것들</h3>
<h4 id="자신의-코드-스타일-개선">자신의 코드 스타일 개선</h4>
<p>피어세션하셨던 많은 분들이 내 코드를 칭찬해 주셨긴 하지만, 사실 나는 <em>코드 스타일을 개선하려고</em> 부스트캠프에 온 거였다. 물론 챌린지 전에 비해 각 함수 코드가 짧아지고, 가급적이면 순수함수로 짜려고 노력하는 습관이 길들여지긴 했지만, 근본적으로 내 코드가 좋은 코드인가? 라는 질문에 답하려면 그러지는 못할 것 같다. </p>
<p>좋은 코드는 바보가 봐도 이해할 수 있는 코드, 그러니까 별다른 주석 없이도, <em><strong>코드를 처음 본 사람이나 미래의 내가 흐름을 이해할 수 있는 코드</strong></em> 라고 생각한다. 나는 부스트캠프 이후 각 함수가 무슨 역할을 하는지까지는 어느 정도 정리할 수 있는 수준까지는 왔지만, 전체적인 코드의 흐름은 아직까지도 부족한 것 같다.</p>
<h4 id="7시간-제한-업적에-매몰">7시간 제한 업적에 매몰</h4>
<p>챌린지 7시간 제한은 일종의 체크포인트이자 코드를 빨리 완성해야 된다는 일종의 자극제 역할을 하지만, 피어세션 때 <em>마지막으로 gist를 수정한 시간이 전일 19시인가</em> 라는 항목 때문에, 정작 7시간 이내에 마무리한 미션은 코드를 개선하면 <strong>7시간 안에 코드를 다 짰다는 업적이 손실되는 것</strong>처럼 느껴졌기 때문에, 코드를 19시 이후에 개선하는 것에 대해 소극적이었다. 오히려 7시간 내에 끝낸 미션보다 7시간을 넘겨서 끝낸 미션에 대해 리팩토링을 더 부담없이 할 수 있었던 것 같다.</p>
<p>아마 다시 챌린지 1일차로 돌아가게 된다면 7시간 제한은 장식으로 생각하고, 7시간 제한 안에 코드를 짰어도 코드 리팩토링을 더 신경쓸 것 같다.</p>
<h2 id="멤버십">멤버십?</h2>
<p>사실 챌린지 끝나고 1주일이 된 시점에서 멤버십에 대한 욕심이 없다고는 말할 수 없다. 챌린지 도중에는 챌린지 미션과 학습에만 집중하다 보니 멤버십에 관한 생각은 까맣게 잊어버렸지만, 막상 챌린지가 끝나니 이왕이면 멤버십까지 가서 더 성장하고 싶다는 생각이 들었다. 물론 멤버십에 아쉽게 들어가지 못하더라도, 챌린지에서 공부하는 법이나 여러 코드 스타일들, CS지식들을 배웠기 때문에, 이를 기반으로 더 성장할 수 있다고 생각한다.</p>
]]></description>
        </item>
    </channel>
</rss>