<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>bae-sh.log</title>
        <link>https://velog.io/</link>
        <description>FE 개발자</description>
        <lastBuildDate>Sun, 26 May 2024 10:10:46 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>bae-sh.log</title>
            <url>https://velog.velcdn.com/images/bae-sh/profile/895880bc-dfa6-4f12-8125-fcdad722373c/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. bae-sh.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/bae-sh" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[DebounceClick 컴포넌트를 알아보자 (feat. toss/slash)]]></title>
            <link>https://velog.io/@bae-sh/Debounce%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C-feat.-tossslash</link>
            <guid>https://velog.io/@bae-sh/Debounce%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C-feat.-tossslash</guid>
            <pubDate>Sun, 26 May 2024 10:10:46 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Toss의 오픈소스 라이브러리인 slash에 DebounceClick 이라는 유틸 컴포넌트가 있습니다. 어떤 방식으로 Slash에서는 구현을 했을까 궁금하여 코드를 분석해보며 로직이나 타입에 대해 배울것이 많아 정리해보려고 합니다.</p>
</blockquote>
<h1 id="debounce-란">Debounce 란?</h1>
<p>분석에 앞서 Debounce에 대해서 간단하게 알아 봅시다.</p>
<p>Debounce는 자바스크립트에서 특정 이벤트가 일정 시간 동안 반복해서 발생하는 것을 제어하는 기법입니다. 주로 사용자가 입력 필드에 타이핑을 할 때마다 발생하는 이벤트나 스크롤 이벤트와 같이 자주 발생하는 이벤트를 처리할 때 유용합니다. debounce를 사용하면 특정 시간 동안 이벤트가 발생하지 않을 때까지 이벤트 핸들러가 호출되지 않도록 할 수 있습니다.</p>
<p>대표적인 예시로 velog의 검색 필터링이 있습니다. <img src="https://velog.velcdn.com/images/bae-sh/post/1ce00b65-bd45-43fa-8c89-503ad18b4b91/image.gif" alt=""></p>
<p>velog에서는 검색어를 기반으로 포스트를 graphql로 요청하는데요. 검색어를 치고 몇초뒤에 api가 호출되는것을 확인할 수 있습니다. 또한 검색어를 계속 치고있을경우 api 호출이 발생하지 않는것 또한 볼 수 있습니다.</p>
<p>이렇게 Debounce를 사용하여 리소스를 서버에 무리하게 요청하는 문제를 막을 수 있습니다.</p>
<h1 id="debounceclick-component">DebounceClick Component</h1>
<p>그럼 본격적으로 slash에서는 어떤식으로 <a href="https://github.com/toss/slash/blob/main/packages/react/react/src/components/DebounceClick/DebounceClick.tsx">DebounceClick</a> 컴포넌트를 구현하였는지 알아봅시다.</p>
<p>DebounceClick 컴포넌트는</p>
<blockquote>
<p>click event에 debounce를 적용할 수 있는 유틸 컴포넌트입니다.</p>
</blockquote>
<p>라고 설명을 하고 있습니다.</p>
<p>내부 코드는 다음과 같습니다.</p>
<pre><code class="language-tsx">import { Children, cloneElement, ReactElement } from &#39;react&#39;;
import { useDebounce } from &#39;../../hooks/useDebounce&#39;;

/** @tossdocs-ignore */
interface Props {
  /**
   * @description 이벤트를 묶어서 한번에 보낼 시간으로 ms 단위
   * e.g.) 200ms 일 때, 200ms 안에 발생한 이벤트를 무시하고 마지막에 한번만 방출합니다.
   */
  wait: Parameters&lt;typeof useDebounce&gt;[1];
  options?: Parameters&lt;typeof useDebounce&gt;[2];
  children: ReactElement;
  /**
   * @default &#39;onClick&#39;
   * @description 이벤트 Prop 이름으로 &#39;onClick&#39; 이름 외로 받을 때 사용합니다.
   * e.g. &quot;onCTAClick&quot;, &quot;onItemClick&quot; ...
   */
  capture?: string;
}

export function DebounceClick({ capture = &#39;onClick&#39;, options, children, wait }: Props) {
  const child = Children.only(children);
  const debouncedCallback = useDebounce(
    (...args: any[]) =&gt; {
      if (child.props &amp;&amp; typeof child.props[capture] === &#39;function&#39;) {
        return child.props[capture](...args);
      }
    },
    wait,
    options
  );

  return cloneElement(child, {
    [capture]: debouncedCallback,
  });
}</code></pre>
<p>코드는 많이 복잡하지는 않네요.
DebounceClick 컴포넌트에 delay시간, 옵션 등을 props로 받고 child의 capture(onClick)에 해당하는 함수를 useDebounce에 할당하는 모습입니다. 그 뒤 child의 컴포넌트를 cloneElement 함수를 통해서 return 해주고 있네요. 여기서 React의 몇몇 함수와 타입이 낯선것들이 있었는데요. 하나하나 분석해 봅시다.</p>
<h2 id="type">Type</h2>
<h3 id="parameters">Parameters</h3>
<blockquote>
<p>함수 타입의 매개변수 타입을 추출하는 TS 내장 유틸리티 타입</p>
</blockquote>
<p>개발을 진행하며 특정 함수의 매개변수와 동일한 타입을 가져오고 싶을 경우가 있잖아요? 이럴때 사용하면 유용할 것 같습니다. 여기서는 wait과 options 에서 사용되었는데요. <code>useDebounce</code> 함수의 파라미터의 1번째 인자와 2번째 인자의 타입을 받아옴을 알 수 있습니다.</p>
<h3 id="reactelement">ReactElement</h3>
<blockquote>
<p>React 라이브러리에서 사용되는 기본적인 타입 중 하나로, React 컴포넌트가 반환하는 요소를 표현하는 타입</p>
</blockquote>
<h4 id="ts에서-정의하는-reactelement">Ts에서 정의하는 ReactElement</h4>
<pre><code class="language-ts">interface ReactElement&lt;P = any, T extends string | JSXElementConstructor&lt;any&gt; = string | JSXElementConstructor&lt;any&gt;&gt; {
  type: T;
  props: P;
  key: Key | null;
}</code></pre>
<p>React에서 렌더링 하기위해서 사용되는 element를 표현하는 타입입니다. 위 예시에서는 children을 필수적으로 받으며 ReactElement 하나만 받도록 되어있네요.</p>
<p>유사하게 <strong>ReactNode</strong> 라는 타입도 존재하는데요. ReactNode는 React가 렌더링할 수 있는 모든 종류의 값을 나타내며 좀 더 포괄적인 타입입니다.</p>
<p>ReactNode에는 다음과 같은 타입을 허용합니다</p>
<ul>
<li>문자열, 숫자, boolean (true/false는 렌더링되지 않지만 타입으로 허용됨)</li>
<li>ReactElement</li>
<li>ReactFragment</li>
<li>배열 (중첩된 ReactNode 포함)</li>
<li>null 또는 undefined</li>
</ul>
<p>여기까지 DebounceClick 의 Props 타입에 대해서 정리해 보았습니다. 다음으로는 사용한 React 유틸리티를 알아봅시다.</p>
<h2 id="react-utility">React Utility</h2>
<pre><code class="language-tsx">export function DebounceClick({ capture = &#39;onClick&#39;, options, children, wait }: Props) {
  const child = Children.only(children);
  const debouncedCallback = useDebounce(
    (...args: any[]) =&gt; {
      if (child.props &amp;&amp; typeof child.props[capture] === &#39;function&#39;) {
        return child.props[capture](...args);
      }
    },
    wait,
    options
  );

  return cloneElement(child, {
    [capture]: debouncedCallback,
  });
}</code></pre>
<h3 id="children">Children</h3>
<p>React의 Children는 React 컴포넌트의 자식 요소를 다루기 위한 유틸리티입니다.이를 통해 부모 컴포넌트에서 자식 컴포넌트를 효율적으로 다루고 관리할 수 있습니다. Children이 사용할 수 있는 메서드는 여러가지 존재하지만 이곳에서 사용된 only에 대해서만 알아봅시다.</p>
<h4 id="only">only</h4>
<p>자식 요소가 정확히 하나인지 확인하고, 그렇지 않으면 오류를 발생시킵니다. DebounceClick 에서는 <code>Children.only(children)</code> 를 통해 children이 항상 1개임을 명시하기 위해 사용한 것 같습니다. Props에서 <code>children: ReactElement;</code> 를 통해 2개 이상의 child가 있을 경우 타입에러가 발생하지만 js를 위해서 한번 더 사용한 것으로 보입니다.</p>
<h3 id="cloneelement">cloneElement</h3>
<p>cloneElement는 React에서 기존의 React 요소를 복제하고, 추가적으로 props를 변경하거나 덮어쓰는 기능을 제공하는 유틸리티 함수입니다. 이를 사용하면 기존 요소의 구조를 유지하면서 새로운 속성을 추가하거나 기존 속성을 수정할 수 있습니다.</p>
<p>기본적인 문법은 다음과 같습니다.</p>
<pre><code class="language-jsx">cloneElement(
  element,   // 복제할 요소
  [props],   // 새로 추가하거나 덮어쓸 props
  [...children] // element의 새로운 자식 요소 (선택 사항)
)</code></pre>
<p><code>DebounceClick</code> 에서는 기존의 child를 복사하되, capture(onClick)에 debounce 함수만 변경하는 방식으로 구현을 하였습니다. 결국 DebounceClick 컴포넌트는 Wrapper의 껍대기 역할만 하고 실제 child인 button에 debounce된 함수를 넣어주는 역할이 되겠네요.</p>
<h2 id="정리">정리</h2>
<blockquote>
<p>이렇게 Debounce에 대해서 간단하게 알아보며 toss/slash에서 사용하고 있는 DebounceClick가 어떤식으로 구현되어 있는지 보았습니다. 다양한 타입과 React 유틸리티 함수를 보며 적용해보고 싶은 생각이 드는데요. 읽어주셔서 감사합니다! 🙇🏻‍♂️</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[터미널에 npm start 를 치면 무슨일이 발생할까?]]></title>
            <link>https://velog.io/@bae-sh/%ED%84%B0%EB%AF%B8%EB%84%90%EC%97%90-npm-start-%EB%A5%BC-%EC%B9%98%EB%A9%B4-%EB%AC%B4%EC%8A%A8%EC%9D%BC%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@bae-sh/%ED%84%B0%EB%AF%B8%EB%84%90%EC%97%90-npm-start-%EB%A5%BC-%EC%B9%98%EB%A9%B4-%EB%AC%B4%EC%8A%A8%EC%9D%BC%EC%9D%B4-%EB%B0%9C%EC%83%9D%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Sun, 28 Apr 2024 06:37:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>매번 리액트를 실행시키기 위해 <code>npm start</code> 혹은 <code>npm run dev</code>와 같은 명령어를 작성하면 리액트 서버가 켜지면서 우리가 작성한 웹페이지를 브라우저에서 확인할 수 있습니다. 수도없이 이 명령어를 실행했지만 막상 어떠한 과정을 통해서 이 웹서버가 켜지는지에 대해 생각해본적이 없어서 NPM 내부 코드를 학습해보며 정리하려 합니다.</p>
</blockquote>
<h1 id="npmnode-packaged-manager-이란">NPM(Node Packaged Manager) 이란?</h1>
<p>들어가기에 앞서 NPM에 대해서 간단하게 알아봅시다. 위키백과에서는 NPM을 다음과 같이 정의합니다</p>
<blockquote>
<p>npm은 자바스크립트 프로그래밍 언어를 위한 패키지 관리자이다. 자바스크립트 런타임 환경 Node.js의 기본 패키지 관리자이다. 명령 줄 클라이언트, 그리고 공개 패키지와 지불 방식의 개인 패키지의 온라인 데이터베이스로 이루어져 있다. <a href="https://ko.wikipedia.org/wiki/Npm_(%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4)">위키백과</a></p>
</blockquote>
<p>즉, Node.js로 만들어진 패키지들을 설치하고 관리해주는 프로그램입니다. 리액트 또한 하나의 패키지로 구성되어 있으며 <a href="https://www.npmjs.com/package/react">NPM 사이트</a>에서 확인할 수 있습니다.</p>
<p>패키지 내부에는 <code>package.json</code> 이라는 파일을 볼 수 있습니다. 이 파일은 패키지에 대한 명세서가 들어있다고 생각하시면 됩니다. 예를들면, 패키지 이름, 버전, 의존성, 스크립트 등등 말이죠... 여기서 스크립트는 해당 패키지의 명령어 들이 적혀있는데요. start,build,test 등등 다양한 명령어를 통해서 스크립트를 실행시킬 수 있습니다.</p>
<h1 id="npm-start">NPM Start</h1>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/c612e2ab-1a97-461e-94ab-a7b07ef2ba56/image.png" alt=""></p>
<p>리액트 환경은 CRA로 설치하여 진행하였습니다. CRA를 설치하고 <code>npm start</code>를 입력하면 첫 CRA 페이지를 확인할 수 있습니다. 그전에 <code>Package.json</code> 파일을 봅시다.
<img src="https://velog.velcdn.com/images/bae-sh/post/69ac3e7e-94e2-4c56-9458-899015706615/image.png" alt=""></p>
<p>CRA에서 기본적으로 제공하는 스크립트는 start, build, test, eject 정도가 있네요. 사실 우리가 위에서 실행한 start의 경우 <strong>npm이 pakage.json의 start에 해당하는 react-scripts의 start 명령어를 실행</strong>했다고 추측할 수 있습니다.</p>
<h3 id="실행-코드">실행 코드</h3>
<pre><code class="language-js">//lib/cli/entry.js
 try {
    const { exec, command, args } = await npm.load()

    ...
    const execPromise = npm.exec(command, args)

       ...
    updateNotifier(npm).then((msg) =&gt; (npm.updateNotification = msg))

    await execPromise
    return exitHandler()
  } catch (err) {
    ...
}</code></pre>
<p>해당 코드는 npm 실행 시 진입점인 <a href="https://github.com/npm/cli/blob/latest/lib/cli/entry.js">entry 파일</a>의 일부 입니다. npm 을 실행하면 load 함수를 통해 command를 받아올 수 있고 해당 command를 기반으로 npm을 exec(실행) 한다고 추측할 수 있습니다.</p>
<p>그럼 npm의 load 함수를 확인해 봅시다.</p>
<pre><code class="language-js">//lib/npm.js

async load () {
  return time.start(&#39;npm:load&#39;, () =&gt; this.#load())
}

async #load () {

    ...

    // Remove first argv since that is our command as typed
    // Note that this might not be the actual name of the command
    // due to aliases, etc. But we use the raw form of it later
    // in user output so it must be preserved as is.
    const commandArg = this.argv.shift()

    // This is the actual name of the command that will be run or
    // undefined if deref could not find a match
    const command = deref(commandArg)

    ...

    return { exec: true, command: commandArg, args: this.argv }
}



</code></pre>
<p> 이 코드는 <a href="lib/npm.js">npm.js</a> 의 일부입니다. load 함수는 npm 객체의 #load를 호출하고 #load 함수는 <code>npm start</code> 명령어 중 npm을 제외한 start값이 commandArg에 들어갑니다. 이후 별칭임을 deref로 확인하고 반환합니다.</p>
<p> 그럼 어떻게 start 커맨드가 exec함수에서 실행되는 지 따라가 봅시다.</p>
<pre><code class="language-js">
 static cmd (c) {
    const command = deref(c)
    if (!command) {
      throw Object.assign(new Error(`Unknown command ${c}`), {
        code: &#39;EUNKNOWNCOMMAND&#39;,
        command: c,
      })
    }
    return require(`./commands/${command}.js`)
  }


 // Call an npm command
  async exec (cmd, args = this.argv) {
    const Command = Npm.cmd(cmd)
    const command = new Command(this)

    // since &#39;test&#39;, &#39;start&#39;, &#39;stop&#39;, etc. commands re-enter this function
    // to call the run-script command, we need to only set it one time.
    if (!this.#command) {
      this.#command = command
      process.env.npm_command = this.command
    }

    return time.start(`command:${cmd}`, () =&gt; command.cmdExec(args))
  }
</code></pre>
<p>이 함수에서는 cmd으로 &#39;start&#39; 값을 받을것이고 cmd를 통해 start.js의 객체를 받아옵니다. 그 객체 <code>cmdExec</code> 함수를 실행하는 군요.</p>
<pre><code class="language-js">class Start extends LifecycleCmd {
  static description = &#39;Start a package&#39;
  static name = &#39;start&#39;
  static params = [
    &#39;ignore-scripts&#39;,
    &#39;script-shell&#39;,
  ]
}

class LifecycleCmd extends BaseCommand {
  static usage = [&#39;[-- &lt;args&gt;]&#39;]
  static isShellout = true
  static workspaces = true
  static ignoreImplicitWorkspace = false

  async exec (args) {
    return this.npm.exec(&#39;run-script&#39;, [this.constructor.name, ...args])
  }

  async execWorkspaces (args) {
    return this.npm.exec(&#39;run-script&#39;, [this.constructor.name, ...args])
  }
}

class BaseCommand {
    ...
  async cmdExec (args) {
    return this.exec(args)
  }
}</code></pre>
<p>해당 부분이 조금 특이합니다. 집중해서 보셔야 하는데요. <code>Start</code>클래스는 <code>LifecycleCmd</code>를 상속받고 있고 <code>LifecycleCmd</code>는 <code>BaseCommand</code>를 상속 받고 있습니다. 이 <code>cmdExec</code> 함수는 <code>BaseCommand</code>의 <code>cmdExec</code>를 실행하고 있습니다. 또한 이 <code>cmdExec</code> 함수는 <code>LifecycleCmd</code>의 <code>exec</code> 함수를 실행하고 있고요.</p>
<p>이 <code>LifecycleCmd</code>의 <code>exec</code> 함수는 다시 NPM 객체의 <code>run-script</code> 커맨드를 실행합니다. </p>
<pre><code class="language-js">class RunScript extends BaseCommand {


  async exec (args) {
    if (args.length) {
      return this.run(args)
    } else {
      return this.list(args)
    }
  }


  async run ([event, ...args], { path = this.npm.localPrefix, pkg } = {}) {
    const runScript = require(&#39;@npmcli/run-script&#39;)

    const { scripts = {} } = pkg

    pkg.scripts = scripts

    // positional args only added to the main event, not pre/post
    const events = [[event, args]]

    for (const [ev, evArgs] of events) {
      await runScript({
        path,
        // this || undefined is because runScript will be unhappy with the
        // default null value
        scriptShell: this.npm.config.get(&#39;script-shell&#39;) || undefined,
        stdio: &#39;inherit&#39;,
        pkg,
        event: ev,
        args: evArgs,
      })
    }
  }
}</code></pre>
<p>실행하면 아까와 같은 과정으로 <code>run-script.js</code> 파일의 Runscript 객체의 <code>exec</code> 이 실행됩니다. 이 exec은 run을 실행하고 pkg(package.json 객체)의 sciprt(<strong>react-scripts start</strong>)를 실행함을 할 수 있습니다. </p>
<h1 id="react-scripts-start">react-scripts start</h1>
<p><code>react-scrips start</code>는 무슨의미 일까요? 이는 react-scripts 모듈을 start 하겠다는 의미로 해석 할 수 있습니다. node_modules에 있는 react-scripts는 다음과 같습니다.</p>
<img src="https://velog.velcdn.com/images/bae-sh/post/16435f3b-b230-4846-8899-7c8cdc15667c/image.png" width="20%"/>

<p>여기서 bin 파일의 <code>react-scripts.js</code>가 실행이 되는데요. 왜 bin폴더의 파일이 실행되는지 코드로는 찾지 못하여서 관련 게시글을 통해 알 수 있었습니다. <a href="https://www.freecodecamp.org/news/create-react-app-npm-scripts-explained/">참고</a></p>
<p>그럼 bin 폴더가 무엇일까요??</p>
<h2 id="bin">bin</h2>
<blockquote>
<p>Binary File 바이너리 파일이라고 부르며, 컴퓨터가 사용하는 이진 텍스트 파일입니다. <a href="https://blog.naver.com/gmlehd2071/221985819399">참고</a></p>
</blockquote>
<p>npm에서는 package.json파일의 bin 프로퍼티에 bash script를 설정하여 권한 부여 없이 자동으로 실행할 수 있도록 도와줍니다.<a href="https://medium.com/nerd-for-tech/what-bin-does-in-package-json-931d691b1e33">참고</a> 또한, npm에서는 이부분부터 시작이 되어 스크립트의 엔트리 포인트로 추측이 되긴합니다.</p>
<h2 id="react-scripts-내부">react-scripts 내부</h2>
<p>다시 돌아와서 이 스크립트 내부를 살펴보면 이전에 존재했던 4가지의 커맨드가 존재합니다.</p>
<pre><code class="language-js">const args = process.argv.slice(2);

const scriptIndex = args.findIndex(
  x =&gt; x === &#39;build&#39; || x === &#39;eject&#39; || x === &#39;start&#39; || x === &#39;test&#39;
);
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
const nodeArgs = scriptIndex &gt; 0 ? args.slice(0, scriptIndex) : [];

if ([&#39;build&#39;, &#39;eject&#39;, &#39;start&#39;, &#39;test&#39;].includes(script)) {
  const result = spawn.sync(
    process.execPath,
    nodeArgs
      .concat(require.resolve(&#39;../scripts/&#39; + script))
      .concat(args.slice(scriptIndex + 1)),
    { stdio: &#39;inherit&#39; }
  );
}</code></pre>
<p>커맨드에 따라 스크립트 폴더 내부에 있는 커맨드 파일에 접근합니다. 저희 같은 경우에는 <code>start.js</code> 파일이 실행되겠죠?? 최종적으로 start.js 파일이 실행되고 내부에는  프로젝트를 번들링후 웹서버를 키는 동작과정으로 React 파일이 실행되게 됩니다.</p>
<h1 id="결론">결론</h1>
<p>이렇게 npm start를 react 프로젝트에서 입력했을 경우에 어떤과정으로 통해 react 서버가 켜지는지 알아보았습니다. 마지막으로 전체적인 과정을 요약하며 마무리 하겠습니다. 읽어주셔서 감사합니다!</p>
<h2 id="과정">과정</h2>
<blockquote>
<ol>
<li>CRA 프로젝트 환경에서 npm start 입력</li>
<li>npm은 <code>package.json</code> script의 start 값을 확인</li>
<li>scirpt에 적혀있는 <code>react-scripts start</code>를 확인</li>
<li>node_modules에 react-scripts를 확인하고, package.json의 bin에 명시되어있는 <code>react-scripts.js</code> 파일 접근</li>
<li>여러 커맨드 중 입력 받은 start.js의 파일에 접근</li>
<li>프로젝트 번들링과 웹서버 실행</li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Tailwindcss 오픈소스 기여 과정기]]></title>
            <link>https://velog.io/@bae-sh/Tailwindcss-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-%EA%B3%BC%EC%A0%95%EA%B8%B0</link>
            <guid>https://velog.io/@bae-sh/Tailwindcss-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC-%EA%B3%BC%EC%A0%95%EA%B8%B0</guid>
            <pubDate>Tue, 16 Apr 2024 10:54:16 GMT</pubDate>
            <description><![CDATA[<h2 id="계기">계기</h2>
<p>tailwindcss는 현재 프론트엔드 분야에서 스타일링을 위해 많이 쓰이는 라이브러리 중 하나입니다. 제가 맡은 프로젝트에서도 tailwindcss를 사용하고 있는데요. <strong>arbitrary의 calc 사용 중 공백을 이용한 가독성 증가</strong>를 원했습니다.</p>
<pre><code class="language-html">&lt;div className=&quot;w-[calc(100px + 200px)]&quot;&gt;&lt;/div&gt;
</code></pre>
<p>위와 같이 <code>+</code> 양옆에 공백을 주어 가독성을 높인 class를 작성하였습니다.
(css에서도 operator 사용 시 문법적으로 공백이 필요하다고 합니다. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/calc#notes">참고</a>)</p>
<p>하지만 tailwind에서는 className의 공백을 기준으로 파싱을 하기 때문에 <code>w-[calc(100px</code> , <code>+</code>, <code>200px)]</code> 와 같이 독립적으로 분리가 되었고 결국 적용 되지 않았습니다.</p>
<p><code>w-[calc(100px+200px)]</code> 와 같이 공백을 제거해 문법에 맞게 작성하면 가장 단순한 해결 방식이지만 분명 저와 비슷한 불편함을 겪는 개발자가 있을 것이고 tailwindcss에 기여해보면 좋지 않을까 생각하여서 도전해보았습니다.</p>
<h2 id="과정">과정</h2>
<p>먼저 tailwindcss의 git을 클론 받았습니다. 많은 파일이 존재하였는데요. 먼저 테스트 파일을 보면 <em>독립적으로 실행되는 과정을 따라갈 수 있지 않을까?</em> 생각하여 arbitrary에 해당하는 <a href="https://github.com/tailwindlabs/tailwindcss/blob/f1f419a9ecfcd00a2001ee96ab252739fca47564/tests/arbitrary-values.test.js#L600">테스트 파일</a>을 확인하였습니다.</p>
<h3 id="testsarbitrary-valuestestjs">tests/arbitrary-values.test.js</h3>
<pre><code class="language-js">
// tests/arbitrary-values.test.js
it(&#39;should support slashes in arbitrary modifiers&#39;, () =&gt; {
  let config = {
    content: [{ raw: html`&lt;div class=&quot;text-lg/[calc(50px/1rem)]&quot;&gt;&lt;/div&gt;` }],
  }

  return run(&#39;@tailwind utilities&#39;, config).then((result) =&gt; {
    return expect(result.css).toMatchFormattedCss(css`
      .text-lg\/\[calc\(50px\/1rem\)\] {
        font-size: 1.125rem;
        line-height: calc(50px / 1rem);
      }
    `)
  })
})</code></pre>
<p>config 객체 안 content에 우리가 원하는 값들이 배열형태로 들어가는 것을 확인할 수 있습니다. 그럼 이 raw 안에 있는 class가 어떻게 파싱이 되어서 원하는 css로 변하게 될 수 있을까요? 아마 run을 실행하면 어떠한 함수가 실행이 되고 Promise를 리턴하는 것 같습니다. 이 run이 어떤 함수인지에 대해서 좀 더 살펴봅시다.</p>
<h3 id="testsutilrunjs">tests/util/run.js</h3>
<pre><code class="language-js">//tests/util/run.js

export function run(input, config, plugin = tailwind) {
  let { currentTestName } = expect.getState()

  return postcss(plugin(config)).process(input, {
    from: `${path.resolve(__filename)}?test=${currentTestName}`,
  })
}</code></pre>
<p>plugin 이라는 함수에 config가 인자로 들어가고 그 반환값을 postcss로 받아서 처리하는 군요. 일반적으로 postcss는 js의 스타일을 css로 변경해 주는 툴 입니다. 저희는 변환하기 전 <code>calc 내부의 공백을 제거하는 것이 목적</code>이므로 plugin 내부에서 해결책을 찾을 수 있을거라 추측할 수 있습니다.</p>
<h3 id="srcpluginjs">src/plugin.js</h3>
<pre><code class="language-js">// src/plugin.js

module.exports = function tailwindcss(configOrPath) {
  return {
    postcssPlugin: &#39;tailwindcss&#39;,
    plugins: [
      env.DEBUG &amp;&amp;
        function (root) {
          console.log(&#39;\n&#39;)
          console.time(&#39;JIT TOTAL&#39;)
          return root
        },
      async function (root, result) {
        // Use the path for the `@config` directive if it exists, otherwise use the
        // path for the file being processed
        configOrPath = findAtConfigPath(root, result) ?? configOrPath

        let context = setupTrackingContext(configOrPath)

        if (root.type === &#39;document&#39;) {
          let roots = root.nodes.filter((node) =&gt; node.type === &#39;root&#39;)

          for (const root of roots) {
            if (root.type === &#39;root&#39;) {
              await processTailwindFeatures(context)(root, result)
            }
          }

          return
        }

        await processTailwindFeatures(context)(root, result)
      },
      env.DEBUG &amp;&amp;
        function (root) {
          console.timeEnd(&#39;JIT TOTAL&#39;)
          console.log(&#39;\n&#39;)
          return root
        },
    ].filter(Boolean),
  }
}</code></pre>
<p>이 부분을 해석하는데 조금 어려움이 있었는데요. 여기서는 plugins 배열을 반환하는데 그 안에는 여러 함수들이 존재합니다. 그 중 디버깅과는 관련없으니 제거를 하면 하나의 함수가 남겠군요. 저희는 Path와 관련된(url과 같은) config가 아니기에 <code>configOrPath</code>는 config와 같습니다. </p>
<p>이제 새로운 개념인 context가 나오는데요. 해당 부분을 hover해보면 다음과 같음을 알 수 있습니다.
<img src = "https://velog.velcdn.com/images/bae-sh/post/78bb5fa6-ae1d-4c0d-a350-70ae08f48bdb/image.png" width="50%"/></p>
<p>context를 생성하는 함수를 반환하는 함수? 쯤으로 생각해도 좋을 것 같네요. 그럼 이 부분을 사용하는 <code>processTailwindFeatures</code> 을 찾아봅시다.</p>
<h3 id="srcprocesstailwindfeaturesjs">src/processTailwindFeatures.js</h3>
<pre><code class="language-js">// src/processTailwindFeatures.js

export default function processTailwindFeatures(setupContext) {
  return async function (root, result) {
    let { tailwindDirectives, applyDirectives } = normalizeTailwindDirectives(root)

    // Partition apply rules that are found in the css
    // itself.
    partitionApplyAtRules()(root, result)

    let context = setupContext({
      tailwindDirectives,
      applyDirectives,
      registerDependency(dependency) {
        result.messages.push({
          plugin: &#39;tailwindcss&#39;,
          parent: result.opts.from,
          ...dependency,
        })
      },
      createContext(tailwindConfig, changedContent) {
        return createContext(tailwindConfig, changedContent, root)
      },
    })(root, result)
    if (context.tailwindConfig.separator === &#39;-&#39;) {
      throw new Error(
        &quot;The &#39;-&#39; character cannot be used as a custom separator in JIT mode due to parsing ambiguity. Please use another character like &#39;_&#39; instead.&quot;
      )
    }

    issueFlagNotices(context.tailwindConfig)

    await expandTailwindAtRules(context)(root, result)
    ...많은 함수
  }
}</code></pre>
<p>꽤나 복잡한 함수인데요. 함수명으로 유추해보면 테일윈드 기능들을 처리하는 함수쯤으로 해석할 수 있습니다. 이곳에서 아까 의문점인 context을 사용하는데요. <code>setupContext</code> 로 props를 받아서 사용을 합니다. 그럼 이 <code>context에는 어떠한 것이 있을까?</code> 궁금하실텐데요. 해당 부분을 콘솔로 찍어보면 tailwind의 Config들, corePlugins, 유저로 부터 받은 커스텀 plugins, changedContent 등등 너무 많은 정보들이 context에 담겨있습니다. 여기서 가장 중요한 부분은 <code>changedContent</code> 인데요. 이 부분에는 저희가 처음에 넣었던 class관련 정보들이 들어있습니다.</p>
<pre><code class="language-js">changedContent: [
        {
          content: &#39;&lt;div class=&quot;text-lg/[calc(50px/1rem)]&quot;&gt;&lt;/div&gt;&#39;,
          extension: &#39;html&#39;
        }
      ],</code></pre>
<p>그리고 <code>expandTailwindAtRules</code> 이 함수 실행 이후에는 <code>changedContent</code> 내부는 빈 배열이 되며, <code>text-lg/[calc(50px/1rem)]</code> 해당 클래스는 candidateRuleCache, classCache 등 context의 여러 프로퍼티에 들어가는 것을 알 수 있는데요. 이를 통해 expandTailwindAtRules 함수에서 어떠한 변환이 이루어 짐을 추측할 수 있습니다.</p>
<p><a href="https://github.com/tailwindlabs/tailwindcss/blob/f1f419a9ecfcd00a2001ee96ab252739fca47564/src/lib/expandTailwindAtRules.js#L100">expandTailwindAtRules</a> 부분도 꽤나 복잡한것 같지만 변수명이 직관적이기에 어떠한 역할을 하는 부분인지 짐작이 가능합니다. 전처리 부분을 제외한 핵심 부분을 보겠습니다.</p>
<h3 id="srclibexpandtailwindatrulesjs">src/lib/expandTailwindAtRules.js</h3>
<pre><code class="language-js">
// src/lib/expandTailwindAtRules.js
    let regexParserContent = []
    for (let item of context.changedContent) {
      let transformer = getTransformer(context.tailwindConfig, item.extension)
      let extractor = getExtractor(context, item.extension)
      regexParserContent.push([item, { transformer, extractor }])
    }

    const BATCH_SIZE = 500

    for (let i = 0; i &lt; regexParserContent.length; i += BATCH_SIZE) {
      let batch = regexParserContent.slice(i, i + BATCH_SIZE)
      await Promise.all(
        batch.map(async ([{ file, content }, { transformer, extractor }]) =&gt; {
          content = file ? await fs.promises.readFile(file, &#39;utf8&#39;) : content
          getClassCandidates(transformer(content), extractor, candidates, seen)
        })
      )
    }</code></pre>
<p>첫번째 for문에서는 아까 저희가 봤던 <code>changedContent</code> 프로퍼티에 접근하여 extension에 따른 변환 함수와 추출함수를 <code>regexParserContent</code> 배열에 넣어주고 있네요.</p>
<p>이후 두번째 for문에서 <code>getClassCandidates</code> 함수를 사용하고 있고 content를 감싸는 transformer 의 경우 <code>builtInTransformers</code>를 보면 svelt 일 경우 변환이 되는 구조라 저희는 content가 그대로 사용됩니다.</p>
<pre><code class="language-js">// Scans template contents for possible classes. This is a hot path on initial build but
// not too important for subsequent builds. The faster the better though — if we can speed
// up these regexes by 50% that could cut initial build time by like 20%.
function getClassCandidates(content, extractor, candidates, seen) {
  if (!extractorCache.has(extractor)) {
    extractorCache.set(extractor, new LRU({ maxSize: 25000 }))
  }
  for (let line of content.split(&#39;\n&#39;)) {
    line = line.trim()

    if (seen.has(line)) {
      continue
    }
    seen.add(line)

    if (extractorCache.get(extractor).has(line)) {
      for (let match of extractorCache.get(extractor).get(line)) {
        candidates.add(match)
      }
    } else {
      let extractorMatches = extractor(line).filter((s) =&gt; s !== &#39;!*&#39;)
      let lineMatchesSet = new Set(extractorMatches)

      for (let match of lineMatchesSet) {
        candidates.add(match)
      }

      extractorCache.get(extractor).set(line, lineMatchesSet)
    }
  }
}</code></pre>
<p>지금까지 잘 따라와 주셔서 감사합니다. 마지막 분석할 함수입니다. <code>getClassCandidates</code> 함수는 class가 될수있는 후보들을 뽑아내는 함수입니다. for문 내부에서는 content의 line별 split을 진행하고 해당 공백을 제거합니다. seen을 통해 이전에 추출을 했던 부분은 캐싱하는 작업까지 알 수 있습니다. 그 뒤 아래부분에서는 해당 Line을 추출하고 class 후보로 지정하는 함수임을 확인하였습니다.</p>
<p>저는 추출하기 전 calc 내부의 공백을 제거해주면 될것 같다고 판단하여 다음과 같은 함수를 작성하여 수정하였습니다.</p>
<pre><code class="language-js">
function removeSpacesInsideCalc(content) {
  return content.replace(/calc\(([^)]*)\)/g, (_, p1) =&gt; `calc(${p1.replace(/\s+/g, &#39;&#39;)})`)
}

function getClassCandidates(content, extractor, candidates, seen) {
  if (!extractorCache.has(extractor)) {
    extractorCache.set(extractor, new LRU({ maxSize: 25000 }))
  }
  for (let line of content.split(&#39;\n&#39;)) {
    line = line.trim()
    line = removeSpacesInsideCalc(line) // 해당 부분 추가

    ...
  }
}</code></pre>
<p>이를 통해 class 후보를 확인하기 전 calc의 공백을 제거하여 하나의 클래스로 인식할 수 있게 하였고 테스트 코드를 작성하여 정상적으로 작동함을 확인하였습니다.</p>
<pre><code class="language-js">it(&#39;should support arbitrary calc values with spaces`&#39;, () =&gt; {
  let config = {
    content: [
      {
        raw: html`&lt;div
          class=&quot;w-[calc(100px + 200px)] h-[calc(3rem - 1rem)] text-lg/[calc(50px / 1rem)]&quot;
        &gt;&lt;/div&gt;`,
      },
    ],
  }

  return run(&#39;@tailwind utilities&#39;, config).then((result) =&gt; {
    expect(result.css).toMatchFormattedCss(css`
      .h-\[calc\(3rem-1rem\)\] {
        height: 2rem;
      }
      .w-\[calc\(100px\+200px\)\] {
        width: 300px;
      }
      .text-lg\/\[calc\(50px\/1rem\)\] {
        font-size: 1.125rem;
        line-height: calc(50px / 1rem);
      }
    `)
  })
})</code></pre>
<img src= "https://velog.velcdn.com/images/bae-sh/post/100fe36f-a8d5-413b-963e-83487cf2401f/image.png" width="50%"/>

<h2 id="후기">후기</h2>
<img src="https://velog.velcdn.com/images/bae-sh/post/d4976a30-c5e8-4c36-8d28-464a55bd0d6f/image.png" width="70%"/>
<img src="https://velog.velcdn.com/images/bae-sh/post/aebf33b8-b02a-40ca-b746-5c00ce6c0db7/image.png" width="70%"/>


<p>결론적으로는 reject 당했습니다 ㅎㅎ... tailwind 컨트리뷰터분이 친절하게 피드백을 주셨는데요! 결론적으로는 공백으로 클래스를 구분하는 것은 html 고유의 특성이므로 수정해서는 안되는것 같아요. </p>
<p>한편으로는 아쉬움이 많이 남지만 해당 과정에서 tailwind의 동작과정을 짧게나마 알아볼 수 있었고 유명한 개발자와 소통을 해볼 수 있었다는 뿌듯함이 있었던것 같습니다. 또한 해당 부분의 이슈들을 찾아보다 보니 자연스럽게 영어도 늘 수 있어서 재밌었습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Link의 prefetch를 까보자]]></title>
            <link>https://velog.io/@bae-sh/Link%EC%9D%98-prefetch%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@bae-sh/Link%EC%9D%98-prefetch%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Sun, 31 Mar 2024 04:54:41 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>NextJS 환경에서 개발하다 보면 <code>Link</code> 컴포넌트를 사용하지 않을 수 없는데요. 프로젝트를 진행하다가 Link 태그에 hover시 무한렌더링이 발생하는 이슈를 겪었습니다. 그런데 참 이상하게 <strong>개발환경에서는 발생하지 않지만 배포환경에서만 발생</strong>을 하였습니다. 해당 이슈 해결과정을 적어보려고 합니다.</p>
</blockquote>
<h1 id="문제-상황">문제 상황</h1>
<p>문제 상황은 아주 간단합니다. App router 개발 환경에서 Link 버튼을 클릭하면 정상적으로 라우팅이 되었지만, 막상 배포환경에서는 Link 컴포넌트 hover시에 페이지 전체가 계속해서 무한 렌더링이 발생하였습니다. 처음에는 <code>어떻게  프론트 단에서 개발환경과 배포환경이 다를수가 있지?</code> 라는 충격과 함께 디버깅을 위해 매번 배포하는 멍청한 짓을 했습니다.</p>
<h3 id="next-build---next-start">next build -&gt; next start</h3>
<p>문제 해결을 하기 전 prodction 환경을 테스팅 하기에 매번 배포를 하는것은 시간비용이 엄청나게 낭비된다고 생각했습니다. 분명 배포 환경을 테스팅 하는 법이 있을것이라고 생각하여 찾아보니 <a href="https://nextjs.org/docs/getting-started/installation#manual-installation">공식문서</a>에서 다음과 같이 친절하게 적혀있었습니다.
<img src = 'https://velog.velcdn.com/images/bae-sh/post/ee006564-601d-44cc-b765-c84e5b8b0980/image.png' width="80%"/></p>
<p>매번 무의식적으로 NextJS를 개발을 위해 <code>npm run dev</code> 명령어를 통해 해당 뜻도 모르고 개발했던것을 반성하며 <code>한 도구를 사용하는 것에 대해 정확히 알고 있는가?</code> 에 대해 다시 생각해 보았습니다.</p>
<h1 id="문제-원인">문제 원인</h1>
<p>프로젝트에 전역상태(jotai), react-query, tanstack table 등등 너무 많은 라이브러리들이 의존되어 있어 문제의 원인을 찾기가 어려웠습니다. 계속해서 검색을 해보며 Link 와 production 환경의 키워드로 Link 태그의 <code>prefetch</code> 속성이 있다는 것을 알았습니다.</p>
<h2 id="prefetch">Prefetch</h2>
<p>Link 태그에는 <a href="https://nextjs.org/docs/app/api-reference/components/link#prefetch">prefetch</a> 라는 속성이 있습니다. 이 속성은 말 그대로 Link태그의 href에 해당 사이트의 데이터를 미리 받아오는것입니다. 왜 미리 받아올까요? </p>
<p>NextJs는 Client side render 방식처럼 한번에 모든 데이터를 받아오는 방식과 다르게 <strong>Page 별 데이터를 요청할 때 마다 받아오는 방식</strong>입니다. 그러다 보니 유저가 페이지 라우팅을 위해 Link를 클릭 한 후 데이터를 받아오게 되면, 데이터가 클 경우 유저는 오래 기다리게 되어 좋지않은 UX를 제공합니다.</p>
<p>이를 개선하기 위해 NextJS는 유저의 Viewport에 해당하는 Link들은 이동할 가능성이 있다고 판단하여 미리 데이터를 받아놓습니다. 기본적으로 App router에서 prefetch는 null로 설정되어 있고 이는 static routes의 경우 모든데이터를, dynamic routes의 경우 loading.js boundary의 가장 가까운 세그먼트를 다운받게 된다고 합니다.</p>
<h1 id="해결">해결</h1>
<p>정확한 원인은 모르겠지만 환경에 따라 영향을 받는것은 prefetch 기능이니 기능을 false로 변경하여 정상적으로 무한렌더링이 해결되었습니다. 하지만 매우 찝찝합니다. prefetch기능은 분명 viewport와 관련이 있는것이였고 그럼 <code>hover가 아닌 Link가 viewport에 들어온 시점부터 무한 렌더링이 발생해야 하는것이 아닌가?</code> 또한, <code>왜 무한렌더링이 발생하는것일까?</code> 에 대해서 궁금증이 남아있었습니다.</p>
<h2 id="그럼-hover와-무슨-상관">그럼 hover와 무슨 상관?</h2>
<p>Link hover와 관련된 게시글을 몇몇 찾아보니 다음과 같이 false일경우에도 hover 경우에는 작동한다고 하였습니다.</p>
<blockquote>
<p>공식 문서에 있는 설명인데, 기본값(true )일 경우 브라우저의 Viewport 내에 있으면 Link의 경로에 해당하는 페이지를 백그라운드에서 미리 가져오는 역할을 한다고 한다. 근데 이 기능은 false 일 때도 완전히 꺼지는 것은 아니고 링크를 hover하면 로딩한다고 한다. - 출처 : <a href="https://medium.com/hcleedev/web-next-js-link%EC%99%80-prefetch-%EA%B3%BC%EC%A0%95-%ED%8C%8C%ED%97%A4%EC%B3%90%EB%B3%B4%EA%B8%B0-44e22ace13e7">Web: Next.js Link와 Prefetch 과정 파헤쳐보기</a> , <a href="https://nextjs.org/docs/pages/api-reference/components/link#prefetch">공식문서</a></p>
</blockquote>
<p>매우 이상합니다. 이 논리대로면 prefetch 기능을 false로 변경하더라도 hover시에는 완전히 꺼지지 않으므로 계속해서 무한렌더링이 발생해야하는데요. 여기서 위 게시글을 작성자 처럼 NextJS 의 Link 태그를 까보기로 결정합니다.</p>
<pre><code class="language-tsx">// tag v14.1.2
// https://github.com/vercel/next.js/blob/f564deef86be32a6b25125ddb8172c7c27d3f19a/packages/next/src/client/link.tsx#L638

     onMouseEnter(e) {
        if (!legacyBehavior &amp;&amp; typeof onMouseEnterProp === &#39;function&#39;) {
          onMouseEnterProp(e)
        }

        if (
          legacyBehavior &amp;&amp;
          child.props &amp;&amp;
          typeof child.props.onMouseEnter === &#39;function&#39;
        ) {
          child.props.onMouseEnter(e)
        }

        if (!router) {
          return
        }

       // 이곳을 주목!!!
        if (
          (!prefetchEnabled || process.env.NODE_ENV === &#39;development&#39;) &amp;&amp;
          isAppRouter
        ) {
          return
        }

        prefetch(
          router,
          href,
          as,
          {
            locale,
            priority: true,
            // @see {https://github.com/vercel/next.js/discussions/40268?sort=top#discussioncomment-3572642}
            bypassPrefetchedCheck: true,
          },
          {
            kind: appPrefetchKind,
          },
          isAppRouter
        )
      },</code></pre>
<p>위 코드는 NextJs의 Link 컴포넌트 구현 중 onMouseEnter(hover) 이벤트 발생 시 작동하는 로직입니다. 그중 제가 주석을 달아 놓은 부분의 조건문을 확인해보면 prefetch 조건을 끄고 App router이면 return을 발생시킵니다. 결국, prefetch기능이 false이더라도 hover시 작동하는 것은 Page Rotuer일 경우였고 <code>App router</code>의 경우는 발생하지 않는다고 합니다.</p>
<img src= 'https://velog.velcdn.com/images/bae-sh/post/1ebe5437-bfac-4ec2-b722-cd94c8e37814/image.png' width="70%"/>

<p>몇몇 스택오버플로우에서는 Prefetch를 껐는데도 hover할때 자꾸 발생한다며 불만이 있어서 그런것 같습니다. </p>
<h3 id="hover가-아닌-link가-viewport에-들어온-시점부터-무한-렌더링이-발생해야-하는것이-아닌가">hover가 아닌 Link가 viewport에 들어온 시점부터 무한 렌더링이 발생해야 하는것이 아닌가?</h3>
<p>그래도 우리는 이것에 대한 해답을 찾지 못했습니다. 문득 이런생각이 들었습니다.</p>
<blockquote>
<p>viewport에 들어왔을때 발생하는 prefetch와 hover시에 발생하는 prefetch의 종류가 다른가?</p>
</blockquote>
<p>그래서 다시 오픈소스를 뜯어보기로 하였습니다.</p>
<pre><code class="language-tsx">// https://github.com/vercel/next.js/blob/f564deef86be32a6b25125ddb8172c7c27d3f19a/packages/next/src/client/link.tsx#L551

    React.useEffect(() =&gt; {
      // in dev, we only prefetch on hover to avoid wasting resources as the prefetch will trigger compiling the page.
      if (process.env.NODE_ENV !== &#39;production&#39;) {
        return
      }

      if (!router) {
        return
      }

      // If we don&#39;t need to prefetch the URL, don&#39;t do prefetch.
      if (!isVisible || !prefetchEnabled) {
        return
      }

      // Prefetch the URL.
      prefetch(
        router,
        href,
        as,
        { locale },
        {
          kind: appPrefetchKind,
        },
        isAppRouter
      )
    }, [
      as,
      href,
      isVisible,
      locale,
      prefetchEnabled,
      pagesRouter?.locale,
      router,
      isAppRouter,
      appPrefetchKind,
    ])
</code></pre>
<p>이 코드는 viewport에 들어왔을때 prefetch가 실행되는 코드 입니다. 그럼 위의 <code>onMouseEnter</code>과 어떤 차이가 있는지 비교해보면 한가지 차이가 있습니다.</p>
<pre><code class="language-tsx">// viewport의 prefetch
prefetch(
  router,
  href,
  as,
  { locale },
  {
    kind: appPrefetchKind,
  },
  isAppRouter,
);


//onMouseEnter의 prefetch
prefetch(
  router,
  href,
  as,
  {
    locale,
    priority: true,
    // @see {https://github.com/vercel/next.js/discussions/40268?sort=top#discussioncomment-3572642}
    bypassPrefetchedCheck: true,
  },
  {
    kind: appPrefetchKind,
  },
  isAppRouter,
);</code></pre>
<p>여기서 차이점은 옵션으로 priority와 bypassPrefetchedCheck 인데요. 우선 bypassPrefetchedCheck의 경우 위 주석을 들어가보면 hover할떄마다 prefetch를 해서 캐시를 하기위해 준 옵션 같습니다.</p>
<p>그럼 priority가 하는 역할을 따라가 봅시다. <del>(제발 이것이 해답이길....)</del>
<strong>prefetch의 정의된 함수</strong>를 보면 다음과 같습니다.</p>
<pre><code class="language-tsx">// https://github.com/vercel/next.js/blob/f564deef86be32a6b25125ddb8172c7c27d3f19a/packages/next/src/client/link.tsx#L124

import type {
  NextRouter,
  PrefetchOptions as RouterPrefetchOptions,
} from &#39;../shared/lib/router/router&#39;

type PrefetchOptions = RouterPrefetchOptions &amp; {
  /**
   * bypassPrefetchedCheck will bypass the check to see if the `href` has
   * already been fetched.
   */
  bypassPrefetchedCheck?: boolean
}

function prefetch(
  router: NextRouter | AppRouterInstance,
  href: string,
  as: string,
  options: PrefetchOptions,
  appOptions: AppRouterPrefetchOptions,
  isAppRouter: boolean
): void {
// ... 불필요한 것 생략
  const prefetchPromise = isAppRouter
    ? (router as AppRouterInstance).prefetch(href, appOptions)
    : (router as NextRouter).prefetch(href, as, options)

}</code></pre>
<blockquote>
<p>엥? AppRouter의 경우에는 options를 사용하지 않네? 그럼 priority는 상관없는 것 이였구나...</p>
</blockquote>
<p>이때부터 방향을 잃어서 아쉽게도 원인을 찾지 못했습니다... 허무하셨다면 죄송합니다.. 😭
하지만 여기서 끝내기에는 아쉬우니 <code>priority</code>가 하는 역할이 무엇인지 찾아봅시다!</p>
<h3 id="prefetch의-priority-option-page-router">prefetch의 priority option (Page router)</h3>
<p>먼저 router에서 prefetch의 프로퍼티에 접근하여 호출하는것 같으므로 router를 찾아봅시다.</p>
<pre><code class="language-tsx">//https://github.com/vercel/next.js/blob/f564deef86be32a6b25125ddb8172c7c27d3f19a/packages/next/src/client/link.tsx#L260C1-L292C44
import { RouterContext } from &#39;../shared/lib/router-context.shared-runtime&#39;
const Link = React.forwardRef&lt;HTMLAnchorElement, LinkPropsReal&gt;(
  function LinkComponent(props, forwardedRef) {
    // ... 생략

    const pagesRouter = React.useContext(RouterContext)
    const appRouter = React.useContext(AppRouterContext)
    const router = pagesRouter ?? appRouter</code></pre>
<p>router는 LinkComponent에서 선언이 되어 있었고 <code>RouterContext</code>를 통해 받아옴을 알 수 있었습니다.</p>
<pre><code class="language-tsx">// &#39;../shared/lib/router-context.shared-runtime&#39;
export const RouterContext = React.createContext&lt;NextRouter | null&gt;(null)

// https://github.com/vercel/next.js/blob/1c5aa7fa09cc5503c621c534fc40065cbd2aefcb/packages/next/src/client/index.tsx#L321C14-L332C40

&lt;RouterContext.Provider value={makePublicRouterInstance(router)}&gt;
  &lt;HeadManagerContext.Provider value={headManager}&gt;
    &lt;ImageConfigContext.Provider
      value={process.env.__NEXT_IMAGE_OPTS as any as ImageConfigComplete}
    &gt;
      {children}
    &lt;/ImageConfigContext.Provider&gt;
  &lt;/HeadManagerContext.Provider&gt;
&lt;/RouterContext.Provider&gt;;
</code></pre>
<p><code>../shared/lib/router-context.shared-runtime</code> 경로에는 단순 RouterContext 를 선언한 곳이였고 해당 Context의 value는 Provider에 <code>makePublicRouterInstance(router)</code>를 통해 주입하고있었습니다.</p>
<pre><code class="language-tsx">//https://github.com/vercel/next.js/blob/1c5aa7fa09cc5503c621c534fc40065cbd2aefcb/packages/next/src/client/router.ts#L169
export function makePublicRouterInstance(router: Router): NextRouter {
 // ...생략

  return instance
}</code></pre>
<p>정확하게 <code>makePublicRouterInstance</code> 함수의 내부 로직은 이해하지 못했지만 React의 Router를 props로 받아 NextRouter로 변환해서 반환하는 함수로 추론했습니다.</p>
<pre><code class="language-tsx">//https://github.com/vercel/next.js/blob/4efe14238b5ab11935e73aa09631ef5ec8045b13/packages/next/src/shared/lib/router/router.ts#L355

export type NextRouter = BaseRouter &amp;
  Pick&lt;
    Router,
    | &#39;push&#39;
    | &#39;replace&#39;
    | &#39;reload&#39;
    | &#39;back&#39;
    | &#39;forward&#39;
    | &#39;prefetch&#39;
    | &#39;beforePopState&#39;
    | &#39;events&#39;
    | &#39;isFallback&#39;
    | &#39;isReady&#39;
    | &#39;isPreview&#39;
  &gt;

  //https://github.com/vercel/next.js/blob/4efe14238b5ab11935e73aa09631ef5ec8045b13/packages/next/src/shared/lib/router/router.ts#L2283

 async prefetch(
    url: string,
    asPath: string = url,
    options: PrefetchOptions = {}
  ): Promise&lt;void&gt; {
    // Prefetch is not supported in development mode because it would trigger on-demand-entries
    if (process.env.NODE_ENV !== &#39;production&#39;) {
      return

     //생략

        await Promise.all([
      this.pageLoader._isSsg(route).then((isSsg) =&gt; {
        return isSsg
          ? fetchNextData({
              dataHref: data?.json
                ? data?.dataHref
                : this.pageLoader.getDataHref({
                    href: url,
                    asPath: resolvedAs,
                    locale: locale,
                  }),
              isServerRender: false,
              parseJSON: true,
              inflightCache: this.sdc,
              persistCache: !this.isPreview,
              isPrefetch: true,
              unstable_skipClientCache:
                options.unstable_skipClientCache ||
                (options.priority &amp;&amp;
                  !!process.env.__NEXT_OPTIMISTIC_CLIENT_CACHE),
            })
              .then(() =&gt; false)
              .catch(() =&gt; false)
          : false
      }),
      this.pageLoader[options.priority ? &#39;loadPage&#39; : &#39;prefetch&#39;](route),
    ])
}}</code></pre>
<p>NextRouter 타입이 선언된 곳으로 찾아보니 prefetch라는 속성을 가지고 있었고, 해당 함수를 선언한 곳을 보니 마지막 줄에 option.priority의 여부에 따라 loadPage와 prefetch를 나눠서 적용하는 것을 확인할 수 있었습니다.</p>
<p>이후, pageLoader가 어떤역할을 하는지 분석해야 할 양이 방대하여 여기까지 마무리 하였습니다.</p>
<h2 id="정리">정리</h2>
<ol>
<li>App router 환경에서 Link 컴포넌트에 hover시 무한렌더링이 발생하는 이슈 발생</li>
<li>prefetch 기능 작동을 off하여 문제 해결</li>
<li>prefetch는 page router와 app router의 작동 방식이 조금 다름</li>
<li>prefetch 작동 방식을 Next 코드를 까보며 동작 방식을 이해<blockquote>
<p>해당 이슈를 정확하게 재연하기가 힘들어 원인 분석이 명확하게 못해서 아쉬움이 많이 남습니다. 
해결책은 알지만 왜 해당 이슈의 해결책인지 정확하게 알지 못해 찝찝함이 남지만 NextJS의 prefetch의 작동방식을 공식문서에 담지못한 부분까지 찾아보았다는 점은 흥미로웠습니다!
혹시 잘못된 부분이나 원인을 알고계시면 댓글로 알려주시면 감사하겠습니다. 읽어주셔서 감사합니다🙇🏻‍♂️</p>
</blockquote>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[useState 의 state 는 어떤 코드로 쓰여있나?! - 정리]]></title>
            <link>https://velog.io/@bae-sh/useState-%EC%9D%98-state-%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%BD%94%EB%93%9C%EB%A1%9C-%EC%93%B0%EC%97%AC%EC%9E%88%EB%82%98-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@bae-sh/useState-%EC%9D%98-state-%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%BD%94%EB%93%9C%EB%A1%9C-%EC%93%B0%EC%97%AC%EC%9E%88%EB%82%98-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 07 Mar 2024 15:32:07 GMT</pubDate>
            <description><![CDATA[<h2 id="usestate-완전분석feat-linked-list">useState 완전분석(feat. Linked list)</h2>
<pre><code class="language-jsx">// react/packages/react-reconciler/src/ReactFiberHooks.js
// 926번 줄

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

    // 만약 Hook이 없으면 첫번째 값을 할당
  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // workInProgress를 한칸 옆으로 이동시킴
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}</code></pre>
<ul>
<li>memoizedState 는 상태가 변한 후 최종적으로 기억되는 값</li>
</ul>
<ul>
<li>next 는 링크드리스트 처럼 연결되어 있다. 이 값은 다음 훅을 할당될 키값 이다.</li>
<li>fiber는 hook이 링크드리스트로 연결되어 있다.</li>
</ul>
<h3 id="mountstateimpl">mountStateImpl</h3>
<pre><code class="language-jsx">//1750 줄
function mountStateImpl&lt;S&gt;(initialState: (() =&gt; S) | S): Hook {
  const hook = mountWorkInProgressHook(); // 여기서 hook을 넘겨줌
  if (typeof initialState === &#39;function&#39;) {
    const initialStateInitializer = initialState;
    // $FlowFixMe[incompatible-use]: Flow doesn&#39;t like mixed types
    initialState = initialStateInitializer();
    if (shouldDoubleInvokeUserFnsInHooksDEV) {
      setIsStrictModeForDevtools(true);
      // $FlowFixMe[incompatible-use]: Flow doesn&#39;t like mixed types
      initialStateInitializer();
      setIsStrictModeForDevtools(false);
    }
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue&lt;S, BasicStateAction&lt;S&gt;&gt; = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}</code></pre>
<ul>
<li>이곳에서 initialState가 함수이면 함수를 실행시킨 값을 전달함을 알 수 있다.</li>
</ul>
<h2 id="hook에서-왜-queue-객체를-사용할까">Hook에서 왜 queue 객체를 사용할까?</h2>
<pre><code class="language-jsx">const queue: UpdateQueue&lt;S, BasicStateAction&lt;S&gt;&gt; = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };</code></pre>
<ul>
<li>pending은 업데이트 될 다음 상태를 나타낸다.</li>
<li>dispatch는 queue에서 추가로 들어갈 수 있도록 도와주는 것</li>
</ul>
<pre><code class="language-jsx">function mountState&lt;S&gt;(
  initialState: (() =&gt; S) | S,
): [S, Dispatch&lt;BasicStateAction&lt;S&gt;&gt;] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  const dispatch: Dispatch&lt;BasicStateAction&lt;S&gt;&gt; = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}</code></pre>
<ul>
<li>dispatch는 이전에 선언했던 queue가 바인딩 되고 있다.</li>
<li>이 dispatch는 외부로 노출되고 있다. → 이것이 결론적으로 setState</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[renderWithHooks 함수? useState 를 할당하는 과정 코드 까보기?! - 정리]]></title>
            <link>https://velog.io/@bae-sh/renderWithHooks-%ED%95%A8%EC%88%98-useState-%EB%A5%BC-%ED%95%A0%EB%8B%B9%ED%95%98%EB%8A%94-%EA%B3%BC%EC%A0%95-%EC%BD%94%EB%93%9C-%EA%B9%8C%EB%B3%B4%EA%B8%B0-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@bae-sh/renderWithHooks-%ED%95%A8%EC%88%98-useState-%EB%A5%BC-%ED%95%A0%EB%8B%B9%ED%95%98%EB%8A%94-%EA%B3%BC%EC%A0%95-%EC%BD%94%EB%93%9C-%EA%B9%8C%EB%B3%B4%EA%B8%B0-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 05 Mar 2024 12:07:21 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>유튜브에 <a href="https://youtu.be/7mU7ARgrpfI?si=I_TFl0WyfFlfZmbk">React 까보기 시리즈</a> 라는 영상으로 스터디를 진행하며 의미있는 강의는 자주 정리해 보려고 합니다. 
강의를 찍으신 시점과 제가 학습하는 시점에 차이가 발생해 React 라이브러리의 코드가 다소 변화되었습니다. 이 글을 읽는 시점에도 코드가 다를 수 있음을 알려 드립니다.</p>
</blockquote>
<h2 id="어떻게-usestate를-export-하는가">어떻게 useState를 export 하는가</h2>
<pre><code class="language-jsx">//react/packages/react-reconciler/src/ReactFiberHooks.js

//159번줄
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;</code></pre>
<ul>
<li>먼저 ReactCurrentDispatcher.current에 할당을 해야함</li>
<li>이 할당은 <code>renderWithHooks</code> 함수에서 조건에 따라 <code>HooksDispatcherOnMount</code> 와 <code>HooksDispatcherOnUpdate</code> 이 결정된다.</li>
</ul>
<pre><code class="language-jsx">//react/packages/react-reconciler/src/ReactFiberHooks.js

//476번줄

export function renderWithHooks&lt;Props, SecondArg&gt;(
...
if (__DEV__) {
    if (current !== null &amp;&amp; current.memoizedState !== null) {
      ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
    } else if (hookTypesDev !== null) {
      // This dispatcher handles an edge case where a component is updating,
      // but no stateful hooks have been used.
      // We want to match the production code behavior (which will use HooksDispatcherOnMount),
      // but with the extra DEV validation to ensure hooks ordering hasn&#39;t changed.
      // This dispatcher does that.
      ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
    } else {
      ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
    }
  } else {
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  }</code></pre>
<ul>
<li><code>current === null || current.memoizedState === null</code> 이 된다면 Mount 해야하며, 아닐경우 Update 일경우로 생각</li>
<li>강의랑 다름 강의에선 nextCurrentHook으로 나옴 → 결국 Mount 하냐 Update 하냐의 결정은 current가 Dom에 반영 여부로 확인</li>
</ul>
<h2 id="그럼-hooksdispatcheronmount-안에-뭐가-있나">그럼 HooksDispatcherOnMount 안에 뭐가 있나?</h2>
<ul>
<li>useState 가 안에 존재</li>
</ul>
<pre><code class="language-jsx">//react/packages/react-reconciler/src/ReactFiberHooks.js

//3470번줄
const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  use,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
}; 

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  use,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
};</code></pre>
<ul>
<li>Update 함수에는 updateState가 들어간다.</li>
</ul>
<h2 id="renderwithhooks">renderWithHooks</h2>
<ul>
<li>renderWithHooks() → hook과 함께 render 즉, hook을 주입하는 역할을 한다</li>
</ul>
<pre><code class="language-jsx">//react/packages/react-reconciler/src/ReactFiberHooks.js

//476번줄
export function renderWithHooks&lt;Props, SecondArg&gt;(
  current: Fiber | null,
  workInProgress: Fiber ... ): any {
  renderLanes = nextRenderLanes;
  **currentlyRenderingFiber = workInProgress; &lt;- 이 코드가 핵심**</code></pre>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/6d5c0bbf-cdab-46d9-a575-706d268a6c00/image.png" alt=""></p>
<ul>
<li>이 사진에서 WorkInProgress 작업물을 Current에 주입하는 역할</li>
</ul>
<h3 id="nextcurrenthook">NextcurrentHook</h3>
<ul>
<li>강의의 코드와 다른점 존재. nextCurrentHook은 더이상 renderWithHooks에서 사용하지 않고 <code>updateWorkInProgressHook</code> 함수에서 사용</li>
<li>이에 따라 돔에 반영되어있는지 아닌지 확인하는 조건은   <code>=== null || current.memoizedState === null</code> 로 대체된다.</li>
<li>이 memoizedState는 Hook이 들어있음을 추측</li>
<li>renderWithHooks의 다른 역할은 컴포넌트를 호출한다.</li>
</ul>
<pre><code class="language-jsx">// 572번
let children = Component(props, secondArg);

    // **업데이트 정보를 스케쥴러와 패키지에게 전달 했음?을 확인 -&gt; Mount 일경우는 false**
  if (didScheduleRenderPhaseUpdateDuringThisPass) {
    // Keep rendering until the component stabilizes (there are no more render
    // phase updates).
    children = renderWithHooksAgain(
      workInProgress,
      Component,
      props,
      secondArg,
    );
  }</code></pre>
<pre><code class="language-jsx">// 607번
function finishRenderingHooks&lt;Props, SecondArg&gt;(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) =&gt; any,
): void {

  // **이것을 왜?**
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const didRenderTooFewHooks =
    currentHook !== null &amp;&amp; currentHook.next !== null;

  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  currentHook = null;
  workInProgressHook = null;
</code></pre>
<ul>
<li><code>ReactCurrentDispatcher.current = ContextOnlyDispatcher;</code>  이 코드가 의미하는 것은 ReactCurrentDispatcher.current를 재할당 하는 것이 아니라 위에서 컴포넌트를 호출한 뒤 더 이상 Hook을 요청해서는 안될때 Error를 알려주기 위함을 의미</li>
</ul>
<pre><code class="language-jsx">//3432번줄

export const ContextOnlyDispatcher: Dispatcher = {
  readContext,

  use,
  useCallback: throwInvalidHookError,
  useContext: throwInvalidHookError,
  useEffect: throwInvalidHookError,
  useImperativeHandle: throwInvalidHookError,
  useInsertionEffect: throwInvalidHookError,
  useLayoutEffect: throwInvalidHookError,
  useMemo: throwInvalidHookError,
  useReducer: throwInvalidHookError,
  useRef: throwInvalidHookError,
  useState: throwInvalidHookError,
  useDebugValue: throwInvalidHookError,
  useDeferredValue: throwInvalidHookError,
  useTransition: throwInvalidHookError,
  useSyncExternalStore: throwInvalidHookError,
  useId: throwInvalidHookError,
};

function throwInvalidHookError() {
  throw new Error(
    &#39;Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for&#39; +
      &#39; one of the following reasons:\n&#39; +
      &#39;1. You might have mismatching versions of React and the renderer (such as React DOM)\n&#39; +
      &#39;2. You might be breaking the Rules of Hooks\n&#39; +
      &#39;3. You might have more than one copy of React in the same app\n&#39; +
      &#39;See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.&#39;,
  );
}</code></pre>
<ul>
<li>이 코드를 보면 useState에 Error를 던지는 것을 할 수 있음</li>
</ul>
<pre><code class="language-jsx">// 926번줄

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
</code></pre>
<ul>
<li>이 코드는 memoizedState에  hook이 대입되는 것을 알 수 있음</li>
<li>이 코드는 Mount State 에서 불린다.</li>
</ul>
<pre><code class="language-jsx">// 1750번

function mountStateImpl&lt;S&gt;(initialState: (() =&gt; S) | S): Hook {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === &#39;function&#39;) {
    const initialStateInitializer = initialState;
    // $FlowFixMe[incompatible-use]: Flow doesn&#39;t like mixed types
    initialState = initialStateInitializer();
    if (shouldDoubleInvokeUserFnsInHooksDEV) {
      setIsStrictModeForDevtools(true);
      // $FlowFixMe[incompatible-use]: Flow doesn&#39;t like mixed types
      initialStateInitializer();
      setIsStrictModeForDevtools(false);
    }
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue&lt;S, BasicStateAction&lt;S&gt;&gt; = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}

function mountState&lt;S&gt;(
  initialState: (() =&gt; S) | S,
): [S, Dispatch&lt;BasicStateAction&lt;S&gt;&gt;] {
  const hook = mountStateImpl(initialState);
  const queue = hook.queue;
  const dispatch: Dispatch&lt;BasicStateAction&lt;S&gt;&gt; = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}</code></pre>
<ul>
<li>Reconciler는 Fiber에 Hook 정보를 담아주는 역할을 한다.</li>
</ul>
<pre><code class="language-jsx">///607 번줄 
function finishRenderingHooks&lt;Props, SecondArg&gt;(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) =&gt; any,
): void {
  if (__DEV__) {
    workInProgress._debugHookTypes = hookTypesDev;
  }

  // We can assume the previous dispatcher is always this one, since we set it
  // at the beginning of the render phase and there&#39;s no re-entrance.
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  // This check uses currentHook so that it works the same in DEV and prod bundles.
  // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.
  const didRenderTooFewHooks =
    currentHook !== null &amp;&amp; currentHook.next !== null;

  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  currentHook = null;
  workInProgressHook = null;</code></pre>
<ul>
<li>이곳에서 null로 초기화를 하는 이유는 이 Hook들은 전역으로 사용하고 있기 때문에 다른 컴포넌트들도 사용될 수 있어 초기화를 진행    </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSS 라이브러리들의 특징]]></title>
            <link>https://velog.io/@bae-sh/CSS-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%93%A4%EC%9D%98-%EB%8F%99%EC%9E%91%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@bae-sh/CSS-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%93%A4%EC%9D%98-%EB%8F%99%EC%9E%91%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Sat, 20 Jan 2024 04:15:37 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>요즘에는 정말 다양한 스타일을 위한 라이브러리가 많습니다. 저 또한 개발을 하면서 그 당시 인기있는 라이브러리를 따라 사용해 왔습니다.. Styled Component -&gt; Sass -&gt; ChakraUI -&gt; tailwindcss (현재 가장 많이 쓰고있음)
하지만 이 라이브러리 별 동작 과정을 정확하게 이해하고 있나 의구심이 들어 이 기회에 각 특징을 정리해보려고 합니다. 특히 빌드, 런타임 시점 위주로 정리를 해보겠습니다.</p>
</blockquote>
<h1 id="css-파일">.CSS 파일</h1>
<p>가장 근본이며 순정인 css 파일부터 정리를 해봅시다. 우리가 프론트엔드를 가장 먼저 시작할때 html 다음으로 배우는 스타일을 위한 언어입니다. 사용방법으로는 3가지가 있습니다.</p>
<blockquote>
<ol>
<li>내부 스타일 시트</li>
<li>외부 스타일 시트</li>
<li>인라인 스타일</li>
</ol>
</blockquote>
<h2 id="동작-방식">동작 방식</h2>
<p>그럼 이 CSS 파일은 어떻게 적용이 될까요? 먼저 로컬에 html, css 파일이 있고 html을 실행시키면 다음과 같은 방식으로 <code>painting</code>이 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/b8db95d8-3b3a-4357-b1b4-a0e048889c1a/image.png" alt=""></p>
<p>결국 우리는 .CSS 파일을 사용할 떄는 정적으로 파싱이 되어 DOM에 적용 된다고 생각하면 됩니다. 만약 동적으로 스타일을 바꾸고 싶을경우에는 JS를 이용하여 바꾸어 주어야 합니다.</p>
<h1 id="sass">Sass</h1>
<blockquote>
<p>Sass is a stylesheet language that’s compiled to CSS. It allows you to use variables, nested rules, mixins, functions, and more, all with a fully CSS-compatible syntax. Sass helps keep large stylesheets well-organized and makes it easy to share design within and across projects.
출처 : <a href="https://sass-lang.com/documentation/">Sass</a></p>
</blockquote>
<p>Sass에서 말했듯 전처리기를 통해 다양한 기능 Ex) 변수, 중첩규칙, mixins, 함수 등등 을 통해 좀 더 빠르고 효율적으로 개발을 할 수 있습니다. 밑에서 다룰 CSS in JS 보다 훨씬 빠를 수 밖에 없죠. </p>
<p>참고로 Sass와 Scss의 차이는 문법이 살짝 다른것 같습니다. 두 문법 다 Sass 라이브러리에서 사용할 수 있지만 대체적으로 Scss 문법이 좀 더 css에 가까워 익숙합니다.</p>
<h4 id="sass-1">Sass</h4>
<img src = 'https://velog.velcdn.com/images/bae-sh/post/eb1e1c0a-9f93-426a-adaa-6853f963cf4b/image.png' width='50%'/>

<h4 id="scss">Scss</h4>
<img src = 'https://velog.velcdn.com/images/bae-sh/post/a654fb82-bcf5-4881-a8d2-3dcd523f1689/image.png' width='50%'/>

<hr>
<h1 id="css-module">CSS Module</h1>
<p>웹이 점점 더 복잡해지면서 프론트엔드 세계에는 리액트 프레임워크가 등장하게 되었습니다. 그럼으로써 고유한 클래스 명을 관리하기가 비용이 너무 많이 들어 그에 따라 .CSS의 단점을 보완하고자 CSS Module이 등장하였습니다.</p>
<p>CSS Module의 사용방법은 간단합니다. [파일이름].module.css 로 작성을 한뒤 원하는 곳에서 import를 하면 되기 때문입니다. 그럼 어떻게 .CSS 의 단점을 보완할 수 있었을까요?</p>
<h2 id="css-module-컴파일러">CSS Module 컴파일러</h2>
<blockquote>
<p>React 컴포넌트에서 해당 CSS 파일을 불러올 때 선언된 CSS 클래스명은 모두 고유한 이름으로 자동 변환됩니다. 고유한 클래스명은 파일 경로, 파일 이름, 원래 작성한 클래스명, 해쉬값 등을 사용하여 자동 생성됩니다. 따라서 CSS Module을 사용하면 CSS 파일마다 고유한 네임스페이스를 자동으로 부여해 주기 때문에 각각의 React 컴포넌트는 완전히 분리된 스타일을 보장받게 됩니다. 
출처 : <a href="https://www.tcpschool.com/react/react_styling_cssmodule">TCP School</a></p>
</blockquote>
<img src = https://velog.velcdn.com/images/bae-sh/post/83d6f44a-52ab-4609-96ff-345ce8e977bc/image.png width="50%"/>

<p>리액트는 정말 똑똑하다고 생각합니다... 해당 컴포넌트에 필요한 CSS Module이 있으면 그 모듈은 컴파일러를 통해 고유의 className으로 변경되고 css파일과 js파일로 변경이 됩니다. 이를 통해 Css 의 단점을 보완할 수 있습니다.</p>
<hr>
<h1 id="css-in-js">CSS in JS</h1>
<h4 id="최신-스타일-라이브러리-추세">최신 스타일 라이브러리 추세</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/27ce814d-2828-49a2-9d5d-b0d01cfc0c25/image.png" alt=""></p>
<p>점점 더 프로젝트의 규모가 커지면서 css를 모듈로 관리하기에는 힘들어졌습니다. 그래서 CSS in JS라는 방법이 생겼는데요. 이는 <strong>css 구문을 js파일에서 관리하자!</strong> 입니다. 이로 인해 스타일이 지역스코프, 스타일을 해당 컴포넌트 파일에서 작성, JS 변수 사용 가능 등등 정말 많은 장점이 있습니다. 하지만 그에따른 단점도 존재하겠죠? </p>
<h2 id="styled-component">Styled-component</h2>
<p>먼저 <code>Styled-component</code> 의 경우 CSR이 유행할 당시에 많이 사용하였습니다. Styled-component는 빌드타임이 아닌 런타임에 스타일을 해석하기에 렌더링이 느려질 수 있습니다. 특히 요즘 SSR이 유행하기에 런타임에 스타일을 붙이는 방식은 추천하지 않습니다.</p>
<h2 id="tailwindcss">tailwindcss</h2>
<p>tailwindcss는 CSS in JS 방식이지만 런타임에 스타일을 해석하는 방식과 조금 다릅니다. 라이브러리 내부에는 미리 css 파일이 정리되어 있으며 개발자는 컴포넌트의 <code>className</code>에 css를 입력하여 적용하는 방식입니다. 이를 통해 빌드 타임에 css 파일로 변환될 수 있습니다. </p>
<p>간혹가다 <code>bg-red-${prop}</code> 과 같이 작성하였을때 적용이 안되는 경우가 있는데 이는 tailwind가 빌드 타임시에 잘못된 css 작성이나 적용되지 않는 부분은 알아서 삭제되기에 올바른 방식이 아니라고 할 수 있습니다.</p>
<h1 id="zero-runtime-css-in-js">zero runtime css in JS</h1>
<p>css in js 중 styled component는 런타임에 빌드가 될 경우 painting이 느려질 수 있다고 말씀 드렸는데요. 이를 해결하기 위해 <code>zero runtime css in js</code>가 나왔습니다. 이는 스타일을 js에서 동적으로 받을 수 있지만 이를 미리 빌드타임때 css 파일로 컴파일 되어 zero time에 사용할 수 있습니다.</p>
<p>대표적으로는 vanilla extract가 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[useQuery을 넘어 useMutation 사용]]></title>
            <link>https://velog.io/@bae-sh/useQuery%EC%9D%84-%EB%84%98%EC%96%B4-useMutation-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@bae-sh/useQuery%EC%9D%84-%EB%84%98%EC%96%B4-useMutation-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Mon, 01 Jan 2024 07:50:13 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/8d541b00-9d58-45cd-83a6-834598ece4f1/image.gif" alt=""></p>
<blockquote>
<p>게시글의 댓글 작성 기능을 구현하고 있었습니다. 사용자가 현재 게시글에 <strong>댓글 등록 버튼을 누른 후, 새로고침</strong>을 해야만 새로운 댓글 정보를 서버로 부터 받아오는 문제가 발생하였습니다. 댓글 정보는 <code>react query</code>를 통해서 관리하고 있었기에 어떠한 방식으로 문제를 해결하고 개선하였는지 작성해보려 합니다.</p>
</blockquote>
<h2 id="react-query-란">react query 란?</h2>
<p><a href="https://tanstack.com/query/v3/docs/react/overview">TanStack Query</a> 에 따르면</p>
<blockquote>
<p>React Query는 종종 React에 없는 data-fetching 라이브러리로 되지만, 좀 더 기술적인 용어로 표현하자면 React 애플리케이션에서 서버 상태를 불러오고, 캐싱하고, 동기화하고, 업데이트하는 작업을 쉽게 해줍니다.</p>
</blockquote>
<p>v4 부터 TanStack팀은 react 보다 좀 더 다양한 프레임워크에게 기능을 제공하여 react query대신 TanStack Query라는 표현을 사용하는것 같습니다.</p>
<h3 id="이-라이브러리를-사용하는-이유">이 라이브러리를 사용하는 이유</h3>
<p>reDuck팀은 react query를 통해 <strong>로컬 데이터와 서버 데이터(게시글, 댓글) 로직을 분리</strong>하여 관리하기 위해 사용합니다. 또한, 서버 데이터를 캐싱하면서 서버에 데이터 요청 횟수를 최적화 할 수 있다는 장점이 있습니다.</p>
<h2 id="문제의-원인">문제의 원인</h2>
<p>현재 댓글 상태 관리 flow는 다음과 같습니다.</p>
<blockquote>
<ol>
<li><code>get</code> 요청을 통해 현재 게시글의 댓글 list 들을 불러온다.</li>
<li><code>post</code> 요청을 통해 현재 게시글에 댓글을 추가한다.</li>
<li>새 댓글을 렌더링 하기 위해 1번을 다시 시작한다.</li>
</ol>
</blockquote>
<p>여기서 문제의 원인은 3번입니다. react-query에서는 서버의 상태(댓글 리스트)가 변했는지 알지 못하기에 개발자가 직접 알려주어야 합니다. 그러기 위해서 useQuery의 refetch 기능을 이용하여 post 요청 시 댓글 리스트 get 요청을 다시 하기로 하였습니다.</p>
<h3 id="refetch를-이용한-코드">refetch를 이용한 코드</h3>
<pre><code class="language-tsx">//ComentList.tsx
function ComentList() {
  const { data, refetch } = useQuery({
    queryKey: [&#39;commentList&#39;],
    queryFn: async () =&gt; await postManager.getPost(),
  });
  const comments = data?.comments

  return (
    &lt;&gt;
      &lt;CommentUploadButton refetch={refetch}/&gt;

        {comments?.map((comment: IComment) =&gt; (
          &lt;Comment/&gt;
        ))}
    &lt;/&gt;
  );
}

//CommentUploadButton.tsx

function CommentUploadButton({refetch}) {
  const handleComment = async (content: string) =&gt; {
    await commentManager.createComment(content);
    refetch();
  };

  return &lt;button onClick={handleComment}&gt;등록&lt;/button&gt;;
}
</code></pre>
<p>댓글 리스트가 필요한 컴포넌트에서 <code>useQuery</code> 훅을 사용하여 서버로 부터 데이터를 받고, 댓글 업로드 버튼에 Props 로 <code>refetch</code>를 전달하여 댓글 작성 완료 후 <code>refetch</code>를 실행하여 새로운 데이터를 렌더링 하였습니다. </p>
<h3 id="refetch의-아쉬운-점과-개선">refetch의 아쉬운 점과 개선</h3>
<p>이러한 방식으로 해결은 하였지만 깔끔하게 해결된 느낌을 받지 못했습니다. 컴포넌트가 좀 더 복잡해지면 props drilling이 발생할 수 있고 refech를 해야하는 모든 경우에 메소드를 한번씩 적어야 하기 때문입니다.</p>
<h2 id="usemutation을-사용하자">useMutation을 사용하자.</h2>
<p><a href="https://tanstack.com/query/latest/docs/react/guides/mutations">공식문서</a>에 따르면 데이터를 Get 요청할때에는 useQuery를 그 외(post,put,delete)의 경우는 <code>useMutation</code>을 권장합니다. <code>useMutation</code> 훅에서 제공되는 다양한 기능을 통해 좀 더 현명하게 데이터를 다룰 수 있습니다.</p>
<p>저는 여기서 댓글 작성 성공 시 기존 댓글 작성의 <strong>cache가 더이상 유효하지 않다고 알리고</strong> 데이터를 다시 받아오려고 합니다.</p>
<h3 id="usemutation를-이용한-코드">useMutation를 이용한 코드</h3>
<pre><code class="language-tsx">
//ComentList.tsx
function ComentList() {
  const { data } = useQuery({
    queryKey: [&#39;commentList&#39;],
    queryFn: async () =&gt; await postManager.getPost(),
  });
  const comments = data?.comments

  return (
    &lt;&gt;
      &lt;CommentUploadButton /&gt;

        {comments?.map((comment: IComment) =&gt; (
          &lt;Comment/&gt;
        ))}
    &lt;/&gt;
  );
}

//CommentUploadButton.tsx

function CommentUploadButton() {
  const queryClient = useQueryClient();
  const { mutate } = useMutation({
    mutationFn: (content: string) =&gt;
      commentManager.createComment({ content}),
    onSuccess: () =&gt;
      queryClient.invalidateQueries({ queryKey: [&#39;commentList&#39;] }),
  });

  const handleComment = () =&gt; {
    mutate(content);
  };

  return &lt;button onClick={handleComment}&gt;등록&lt;/button&gt;;
}</code></pre>
<p>코드를 요약하면 다음과 같습니다.</p>
<ol>
<li><code>useMutation</code> 훅에 댓글 작성 함수를 mutationFn에 등록</li>
<li>성공시에 <code>commentList</code> 라는 쿼리키를 가진 쿼리가 더이상 유효하지 않다고 <code>onSuccess</code>에 등록</li>
<li>댓글 작성 시 <code>mutation</code>을 실행시킴.</li>
<li>mutation이 성공한 뒤 <code>queryClient.invalidateQueries</code>를 실행 시켜 <code>commentList</code>에 해당하는 쿼리를 다시 받아옴</li>
</ol>
<p>이를 통해 refetch 함수를 강제로 실행시키지 않아 <code>CommentUploadButton</code> 컴포넌트와 <code>ComentList</code>의 결합도를 낮추었습니다. </p>
<p>이러한 개선 과정을 통해 TanStack이 get이아닌 요청에 대해 <code>useMutation</code>을 쓰라고 권장하는 이유가 <strong>post이 요청되는 타이밍이 렌더링 시점이 아닌, 특정 이벤트를 받아 body값을 전달한다는 이유</strong>이지 않을까 조심스럽게 추론해 봅니다.</p>
<blockquote>
<p>혹시 잘못된 부분은 댓글로 알려주시면 감사하겠습니다.🙇🏻‍♂️</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[rAF를 이용하여 성능과 UX를 잡아보자!!!]]></title>
            <link>https://velog.io/@bae-sh/rAF%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%84%B1%EB%8A%A5%EA%B3%BC-UX%EB%A5%BC-%EC%9E%A1%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@bae-sh/rAF%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%EC%84%B1%EB%8A%A5%EA%B3%BC-UX%EB%A5%BC-%EC%9E%A1%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 22 Nov 2023 15:51:48 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/bae-sh/post/ebc135d5-990a-4b29-99d8-d339796e1c63/image.gif" alt=""></p>
<blockquote>
<p>약 1년전 쯤 위 영상 처럼 순서를 기억하는 게임 프로젝트를 구현한 적이 있습니다. 혹시 위 영상에서 UX쪽으로 불편한 점이 느껴지시나요? 개발 당시 남은 시간을 알려주는 타이머 부분이 뚝뚝 끊기는 것이 불량품처럼 마음에 들지 않았습니다. 그 당시에 임시방편으로 해결한 방식과 <code>rAF(requestAnimationFrame)</code> 기법을 이용하여 학습했던 과정을 작성해보려고 합니다.</p>
</blockquote>
<h1 id="왜-불량품일까">왜 불량품일까?</h1>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/b7f30b9b-5cff-4637-a5fa-901b34cd3648/image.gif" alt=""></p>
<p>문제를 개선하기 전에는 원인을 분석해야 합니다. 이를 위해 작성했던 코드를 보며 원인을 분석해 봅시다. 문제인 부분만 직관적으로 이해하기 위해 불필요한 로직은 생략했습니다!</p>
<pre><code class="language-tsx">// MemoryGameTimer.tsx (타이머 UI 컴포넌트)
import { Progress } from &#39;@chakra-ui/react&#39;;

function MemoryGameTimer() {
  const { time, startTimer } = useTimer();

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

  return &lt;Progress hasStripe value={time / 600} /&gt;;
}

// useTimer.ts (Timer 로직을 담아 둔 hook)
function useTimer() {
  const INITIAL_TIME = 60000; // 60s
  const INTERVAL_MILLISECOND = 1000; // 1s
  const [time, setTime] = useState(INITIAL_TIME);
  const intervalRef: { current: NodeJS.Timeout | null } = useRef(null);

  const startTimer = useCallback(() =&gt; {
    if (intervalRef.current !== null) return;

    intervalRef.current = setInterval(
      () =&gt; setTime(time =&gt; time - INTERVAL_MILLISECOND),
      INTERVAL_MILLISECOND,
    );
  }, []);

  return { time, startTimer };
}</code></pre>
<p>간단하게 코드 흐름을 설명하면 다음과 같습니다.</p>
<blockquote>
<ol>
<li><code>MemoryGameTimer</code> 컴포넌트는 타이머의 UI 담당을 하고 있습니다. 이 프로젝트에서는 스타일링을 위해 chakraUI 라이브러리를 사용하고 있고 제공되는 타이머UI 컴포넌트를 사용하였습니다. 남은시간인 time 상태에 따라 Progress Bar의 size가 변화하게 됩니다.</li>
<li><code>useTimer hook</code>은 Timer 로직을 담고 있습니다. 게임의 제한시간은 <strong>60s</strong>로 설정하였고, 남은시간을 time 상태에 담아 MemoryGameTimer 컴포넌트에 제공합니다.</li>
<li>time 컴포넌트는 설정한 시간 간격 <strong>1s</strong> 마다 setInterval에 의해 <code>setTime</code>이 반복적으로 호출되며 time 상태가 새롭게 변경됩니다.</li>
</ol>
</blockquote>
<p>여기서 문제가 무엇인지 확인 하셨나요??</p>
<p>네 맞습니다. 60초의 타이머를 1초마다 갱신을 하다보니 60번의 렌더링이 발생하였습니다. 이는 타이머를 60등분으로 나누어서 이동하기에 뚝뚝 끊어진 고장난 타이머가 되어 버린것입니다....</p>
<h1 id="간격을-줄여보자">간격을 줄여보자!</h1>
<p>그럼 <em>1초의 간격을 더 촘촘하게 만들면 되지 않나요?</em> 라고 생각을 하실 수 있는데요. 작년의 저도 같은 생각으로 1s를 <code>10ms</code> 간격으로 <code>INTERVAL_MILLISECOND</code> 변수를 변경하여 해결을 했었습니다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/84f82338-ab2c-4ed9-b758-a61acdd232c2/image.gif" alt=""></p>
<p>으음... 확실히 이전보다 부드러워 진것 같죠?? 타이머를 60등분을 했던 것과 다르게 6000(60s / 0.01s)등분을 하다 보니 확실히 연속된것 처럼 보이네요! </p>
<blockquote>
<p><em>10ms 마다 상태를 변경하여 렌더링 시키는 것이 성능적으로 안좋지 않을까?</em> 라는 생각을 했었습니다. </p>
</blockquote>
<p>하지만 일단 해결은 됐으니 급한 다른 부분을 해결하고 돌아오자 라며 방치를 해두었죠... (그렇게 1년이 흐르고 잊혀질때 쯤에...)</p>
<h1 id="requestanimationframe">requestAnimationFrame</h1>
<p>부트캠프 교육을 받던 중 rAF(requestAnimationFrame) 함수를 알게 되었고 애니메이션을 좀 더 부드럽게 하기 위해서 적용을 할 수 있다고 마스터님께서 말씀하셨습니다. 그 때 1년 전 불량품 타이머를 만들었던 기억 갑자기 떠오르면서 <em>이걸 적용해 보면 해결 할 수 있을거 같다!</em> 생각하였습니다. </p>
<p>적용하는 법은 간단하지만 어떠한 원리로 애니메이션이 부드러워 지는지 알 필요가 있기에 브라우저 렌더링 과정부터 시작하여 좀 더 학습해 봅시다.</p>
<h2 id="렌더링-동작-과정">렌더링 동작 과정</h2>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/c4abac3b-0ead-41a4-b852-70e3d756b374/image.png" alt=""></p>
<p>이 그림을 보며 어떻게 브라우저 화면에 그려지는지 알아봅시다.</p>
<blockquote>
<ol>
<li>HTML 파일을 분석하여 DOM 트리를 구성</li>
<li>CSS 파일을 분석하여 CSSOM 이라는 스타일 트리를 구성</li>
<li>두 트리를 합쳐서 실제로 브라우저에 렌더링할 렌더 트리를 구성하여 화면에 보여줌
3-1. 만약 JS로 DOM을 조작하면 (예를들어 버튼을 클릭하여 리스트 추가) Reflow 단계에서 새롭게 구성
3-2. 만약 CSS를 조작하면 (예를들면 색상을 검정에서 파랑으로 변경) Repaint단계에서 새롭게 구성</li>
</ol>
</blockquote>
<p>우리는 타이머의 스타일이 계속해서 변경되는 것이니 <code>Repaint</code>가 계속해서 일어난다고 생각해도 좋을 것 같습니다.</p>
<h2 id="frame">Frame</h2>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/7025fc64-32aa-4c23-abd2-374e4cf7437b/image.png" alt=""></p>
<p>우리가 모니터를 살때 60Hz, 144Hz 라는 주사율을 확인할 수가 있습니다. 특히 144Hz는 게이밍 모니터라고도 불리는데 이러한 차이가 무엇일까요?</p>
<p>60Hz란 1초에 60번 렌더링이 될 수 있다고 생각하면 쉽습니다. 예를들어 다음과 같은 타이머의 제한시간이 1초라고 하면</p>
<h4 id="1초전">1초전</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/1493d312-78ee-4738-8478-63258ae0eb50/image.png" alt=""></p>
<h4 id="1초후">1초후</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/65de1920-95f6-49d6-8ff1-aa67e7d07286/image.png" alt=""></p>
<p>모니터가 60Hz 주사율을 가지고 있다면 1초동안 저 타이머 막대기는 60등분되어 60번 줄어들면서 1초후에 없어질 것입니다. 1초 / 60 = 16.66 ms 이므로 <code>16ms</code> 마다 새롭게 렌더링이 될 수 있습니다.</p>
<p>그럼 144HZ 주사율을 가지고 있으면 어떨까? 1초동안 144등분되어 줄어들면서 144번 줄어들고 1초후 사라질것입니다. 1초 / 144 = 6.94 ms 이므로 <code>7ms</code> 마다 새롭게 렌더링이 될 수 있습니다.</p>
<p>그럼 조금 더 잘게 쪼개서 보여준 144HZ가 좀 더 부드럽고 자연스럽게 보이지 않을까요?</p>
<p>이렇게 하나의 새롭게 랜더링 된 하나의 장면을 <code>frame</code> 이라고 합니다.</p>
<h2 id="raf-동작-이해">rAF 동작 이해</h2>
<p>그럼 <code>rAF</code>는 어떻게 작동하는 것일까? 먼저 많은 사람들이 60Hz를 사용하므로 주사율이 60Hz라고 가정해봅시다.
<img src="https://velog.velcdn.com/images/bae-sh/post/3ee53369-727c-45ee-b1be-0f8f99d52de6/image.png" alt=""></p>
<p><code>rAF</code>를 적용하여 timer의 스타일을 갱신하는 timerAnimation 함수는 <strong>주사율에 맞게 브라우저가 적절하게 실행합니다</strong>. 그러면 우리는 timerAnimation이 한번 실행될 때 마다 화면에 변경된다고 보장할 수 있습니다. 만약 144Hz 라면? 간격이 <code>7ms</code> 마다 실행되는것 외에는 동일합니다.</p>
<h2 id="setinverval-비교">setInverval 비교</h2>
<p>그럼 이전에 작성한 rAF가 아닌 setInterval로 <code>10ms</code> 마다 실행하면 어떻게 될까요?? 아마 다음과 같이 예상하실 수 있을 겁니다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/89b5fba2-7c4a-4d71-a9ac-777aa1f2381a/image.png" alt=""></p>
<p>이렇게 실행이 되면 무엇이 문제일까요? 우리 화면은 16ms 단위로 화면이 렌더링 되기에  16ms 사이에 함수가 여러번 실행되어도 1번만 적용이 됩니다. 이러면 CPU 입장에서는 의미 없는 일을 하는 격이기에 낭비라고 볼 수 있겠네요. 하지만 더 큰 문제가 있습니다. </p>
<blockquote>
<p><em>과연 setInterval이 우리가 원하는 대로 시간에 맞춰 실행이 될까?</em></p>
</blockquote>
<h2 id="event-loop">Event Loop</h2>
<blockquote>
<p>*<em>JS에서 작동하는 Timer는 우리가 생각하는대로 딱 정확하게 실행되지는 않습니다. *</em></p>
</blockquote>
<p>그러한 이유는 JS는 싱글 쓰레드 언어이기 때문인데요. 싱글 쓰레드 언어는 단순하게 한번에 하나의 작업만을 할 수 있다는 것을 의미합니다. 하지만 구현을 하다보면 I/O나 비동기 이벤트(이 경우에는 타이머)를 처리해야 하는 경우가 종종 생기는데 이는 <code>Event Loop</code>를 통해서 JS엔진이 똑똑하게 처리합니다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/ee9622d5-7da4-46a8-9a3a-24d506b35609/image.png" alt=""></p>
<p>이 사진은 유명한 이벤트 루프 동작을 나타내는 사진 중 하나인데요. 이벤트 루프에 대해서는 다음 기회에 자세하게 다뤄보도록 하고 지금은 간단하게 과정만 설명하겠습니다.</p>
<blockquote>
<ol>
<li>JS에서 실행되는 함수는 <code>CallStack</code>에 들어갑니다.</li>
<li>만약 즉시 실행 가능한(동기) 함수 일 경우 <code>CallStack</code>에 바로 빠져 나와 실행됩니다.</li>
<li>타이머나 그 외 비동기적인 작업은 <code>WEB APIs</code>에 넘겨지고 해당 비동기가 끝나면 <code>Callback Queue</code>로 실행 되어야 할 함수가 넘겨집니다.</li>
<li>Event Loop는 <code>CallStack</code>이 비어있을 때 까지 기다린 후 <code>Callback Queue</code> 순서대로 함수를 <code>CallStack</code>으로 옮깁니다.</li>
</ol>
</blockquote>
<p><strong>여기서 위의 10ms Interval을 생각해 봅시다</strong>. 만약 10ms가 끝나서 <code>Callback Queue</code>에 timerAnimation 함수가 들어갔는데 <code>CallStack</code>에 아직 작업들이 남아있다면?? EventLoop는 이 작업이 끝날때 까지 기다리게 되고 그럼 우리가 원하는 10ms 마다 실행할 수 없겠죠?</p>
<h2 id="그래서-어떻게-구현하는데">그래서 어떻게 구현하는데?</h2>
<p>브라우저에서 정말 고맙게도 frame을 새로 그릴때 실행되어야 할 함수를 등록할 수 있는 <code>window.requestAnimationFrame</code> 이라는 함수를 제공하고 있습니다. 이는 setInteval 처럼 등록하는 것이 아니고 다음 1번의 frame을 그릴때 실행 되는 1회성 함수입니다. 그릴때 마다 실행 되기 위해선 재귀를 사용해서 계속해서 불러야 한다고 <a href="https://developer.mozilla.org/ko/docs/Web/API/window/requestAnimationFrame">MDN</a> 에 나와있네요.</p>
<h3 id="코드">코드</h3>
<pre><code class="language-tsx">// MemoryGameTimer.tsx (타이머 UI 컴포넌트)
import { Progress } from &#39;@chakra-ui/react&#39;;

function MemoryGameTimer() {
  const { time, startTimer } = useTimer();

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

  return &lt;Progress hasStripe value={time / 600} /&gt;;
}

// useTimer.ts (Timer 로직을 담아 둔 hook)
function useTimer() {
  const INITIAL_TIME = 60000; // 60s
  const [time, setTime] = useState(INITIAL_TIME);
  const startTime = useRef&lt;number | null&gt;(null);

  const startTimer = useCallback((timeStamp: number) =&gt; {
    if (!startTime.current) {
      startTime.current = timeStamp;
    }
    const elapsedTime = timeStamp - startTime.current;
    setTime(INITIAL_TIME - elapsedTime);
    if (elapsedTime &lt; INITIAL_TIME) {
      requestAnimationFrame(startTimer);
    }
  }, []);


  return { time, startTimer };
}
</code></pre>
<h2 id="결과는-">결과는 ??</h2>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/64065b6d-2802-42c7-b456-a3ed16477245/image.gif" alt=""></p>
<p>아주 부드러운 UX를 제공하고 있습니다 👍 유저는 만족하면서 게임을 즐길 수 있겠군요 ㅎㅎ 유저에게 좀 더 부드럽고 만족스러운 UX를 제공하고 싶으시면 rAF를 이용해 보시는 것도 좋은 방법이라고 생각이 됩니다!</p>
<h2 id="미해결">미해결</h2>
<p>하지만 아쉬운점이 몇가지 있었습니다.</p>
<h3 id="1-setstate-사용">1. setState 사용</h3>
<p>위 코드는 <a href="https://react.dev/reference/react/Component#setstate-caveats">비동기 함수인 setTime을 통해 적절한 시점에 time의 상태를 변경</a>하고 이를 기반으로 타이머 Progress Bar Animation이 작동합니다. 그런데 브라우저가 Frame을 새로 그리기 전 <code>time</code> 이라는 State가 변경이 반영되었다고 확신할 수 있을까요?? 이 부분에 대해선 좀 더 고민해봐야 할것 같습니다.</p>
<h3 id="2-성능-관점에서의-비교">2. 성능 관점에서의 비교</h3>
<p>블로그를 작성하며 사실 <code>10ms간격</code>과 <code>rAF</code> 썼을때의 UX측면에서 결과 비교가 잘 안나기 때문에 조금 아쉬운 마음을 가지고 CPU나 Memory에서 어떠한 차이가 있는지 측정을 해보고 싶었습니다. 하지만 어떠한 방식으로 해야할지 잘 알지 못해서 숙제로 남겨두었습니다...😭</p>
<blockquote>
<p>혹시 잘못된 부분이나 아쉬운 점을 해결할 방법을 댓글로 알려주시면 감사하겠습니다.🙇🏻‍♂️</p>
</blockquote>
<blockquote>
<p><a href="https://inpa.tistory.com/entry/%F0%9F%8C%90-requestAnimationFrame-%EA%B0%80%EC%9D%B4%EB%93%9C">웹 애니메이션 최적화</a>
<a href="https://dev.to/gopal1996/understanding-reflow-and-repaint-in-the-browser-1jbg">reflow &amp; repaint</a>
<a href="https://medium.com/sessionstack-blog/how-does-javascript-actually-work-part-1-b0bacc073cf">How JavaScript works: an overview of the engine, the runtime, and the call stack</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Lighthouse CI 적용]]></title>
            <link>https://velog.io/@bae-sh/Lighthouse-CI-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@bae-sh/Lighthouse-CI-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Thu, 16 Nov 2023 09:29:26 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><em>새로운 기능을 추가하고 PR 전 lighthouse를 수동으로 체크하는 것이 너무 귀찮은데 자동화 할 수 없을까?</em></p>
</blockquote>
<p>와 같은 생각으로 lighthouse CI를 적용해 보았습니다. 기존에 너무나 자세하게 작성된 <a href="https://fe-developers.kakaoent.com/2022/220602-lighthouse-with-github-actions/">카카오 기술 블로그</a> 를 많이 보면서 활용하였습니다.</p>
<h2 id="lighthouse">Lighthouse</h2>
<p><a href="https://developer.chrome.com/docs/lighthouse/overview/">Lighthouse</a> 는 구글에서 개발한 웹사이트의 성능 측정 도구입니다. 
사용 방법은 간단합니다.  먼저 개발자 도구에서 <strong>lighthouse</strong> 탭을 들어간 뒤 <strong>Analyze page load</strong> 버튼을 클릭합니다.</p>
<p>다음 사진은 네이버 메인 페이지의 성능 검사 결과 입니다.
<img src="https://velog.velcdn.com/images/bae-sh/post/dcafd3ac-aa35-481d-b3de-e7e58a82524c/image.png" alt=""></p>
<p><strong>성능은 평가 지표에 따라 점수가 달라질 수 있기에 lighthouse가 무조껀 정답이라고는 할 수 없을 것입니다.</strong> 하지만 보편적으로 편리하게 많이 사용하고 있기에 lighthouse의 평가 지표를 기준으로 프로젝트 성능 개선을 시도해보는 것 또한 좋을 것 같습니다.</p>
<h2 id="lighthouse-ci">Lighthouse CI</h2>
<p><a href="https://github.com/GoogleChrome/lighthouse-ci">lighthouse CI github</a> 를 보면 어떠한 방식으로 무엇을 설정할 것 인지 자세하게 설명이 되어있습니다. 저는 먼저 설정 파일을 작성해보겠습니다.</p>
<h3 id="lighthousercjs">.lighthouserc.js</h3>
<pre><code class="language-js">module.exports = {
  ci: {
    collect: {
      startServerCommand: &#39;npm run start&#39;,
      startServerReadyPattern: &#39;ready on&#39;,
      url: [&#39;http://localhost:3000&#39;],
      numberOfRuns: 3,
      settings: {
        preset: &#39;desktop&#39;,
      },
    },
    upload: {
      target: &#39;filesystem&#39;,
      outputDir: &#39;./lhci_reports&#39;,
      reportFilenamePattern: &#39;%%PATHNAME%%-%%DATETIME%%-report.%%EXTENSION%%&#39;,
    },
    assert: {
      assertions: {
        &#39;categories:performance&#39;: [&#39;warn&#39;, { minScore: 0.9 }],

        &#39;categories:performance&#39;: [&#39;error&#39;, { minScore: 0.8 }],
        &#39;categories:accessibility&#39;: [&#39;error&#39;, { minScore: 0.9 }],
        &#39;categories:best-practices&#39;: [&#39;error&#39;, { minScore: 0.9 }],
        &#39;categories:seo&#39;: [&#39;error&#39;, { minScore: 0.9 }],
      },
    },
  },
};</code></pre>
<p>먼저 collect의 경우 어떠한 환경에서 실행되는지 설정값 입니다. 여기서 numberOfRuns의 의미는 <em>lighthouse를 몇번 실행할 것인가?</em> 로 저는 3번 실행 하여 점수를 확인하는 것이 좋다고 판단했습니다. </p>
<p><strong>upload</strong>의 경우 실행 결과 파일을 어디에 저장할 지 설정하는 부분 입니다.</p>
<p><strong>assert</strong>의 경우 warn의 기준 점수, error의 기준 점수에 대해서 설정을 하고 이 값보다 낮은 경우 결과값은 fail을 보여줍니다.</p>
<p>물론 100점이먼 너무 좋겠지만 현실적인 부분을 고려하여 perfomance는 90점 이하는 경고 80점 이하는 에러를 발생 시켰습니다.</p>
<h3 id="lighthouseyml">lighthouse.yml</h3>
<p>그 다음 github에서 trigger을 시키기 위해 root파일에서 .github/workflows 폴더를 생성하고 <strong>lighthouse.yml</strong> 파일을 생성하였습니다.</p>
<pre><code class="language-yml">name: Run lighthouse CI When Push
on: [push]
jobs:
  lhci:
    name: Lighthouse CI
    runs-on: ubuntu-latest
    env:
      working-directory: ./client

    steps:
      - uses: actions/checkout@v2

      - name: Use Node.js
        uses: actions/setup-node@v1
        with:
          node-version: &#39;16&#39;
      - name: Install packages
        run: yarn install &amp;&amp; yarn global add @lhci/cli@0.8.x
        working-directory: ${{ env.working-directory }}
      - name: Build
        run: yarn build
        working-directory: ${{ env.working-directory }}
      - name: Run Lighthouse CI
        run: lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
        working-directory: ${{ env.working-directory }}</code></pre>
<p>그 뒤 origin으로 Push를 하게 성공시 다음과 같은 lhci/url 창에 점수가 뜨게 됩니다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/8f12cb0b-66e7-4e37-85d0-b6cf5ef4f457/image.png" alt=""></p>
<p>하지만 이 브랜치가 merge가 되고 나면 해당 점수를 다시 확인할 수 없다는 불편함을 겪었습니다. 그래서 githuh bot이 comment를 달아놓으면 merge 후에도 확인할 수 있어 좋을것 같다고 판단하고 위의 yml에 추가적으로 구현을 하였습니다.</p>
<pre><code class="language-yml">      - name: Format lighthouse score
        id: format_lighthouse_score
        uses: actions/github-script@v3
        with:
          github-token: ${{secrets.GITHUB_TOKEN}}
          script: |

            const fs = require(&#39;fs&#39;);
            const results = JSON.parse(fs.readFileSync(&quot;${{ env.working-directory }}/lhci_reports/manifest.json&quot;));
            let comments = &quot;&quot;;

            results.forEach((result,index) =&gt; {
              const { summary } = result;

              const formatResult = (res) =&gt; Math.round(res * 100);

              Object.keys(summary).forEach(
                (key) =&gt; (summary[key] = formatResult(summary[key]))
              );

              const score = (res) =&gt; (res &gt;= 90 ? &quot;🟢&quot; : res &gt;= 70 ? &quot;🟠&quot; : &quot;🔴&quot;);

              const comment = [
                `⚡️ Lighthouse report ${index}`,
                `| Category | Score |`,
                `| --- | --- |`,
                `| ${score(summary.performance)} Performance | ${summary.performance} |`,
                `| ${score(summary.accessibility)} Accessibility | ${summary.accessibility} |`,
                `| ${score(summary[&#39;best-practices&#39;])} Best practices | ${summary[&#39;best-practices&#39;]} |`,
                `| ${score(summary.seo)} SEO | ${summary.seo} |`,
                `| ${score(summary.pwa)} PWA | ${summary.pwa} |`,
                `\n`,
              ].join(&quot;\n&quot;);

              comments += comment + &quot;\n&quot;;
            });

            core.setOutput(&#39;comments&#39;, comments)

      - name: comment PR
        uses: unsplash/comment-on-pr@v1.3.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          msg: ${{ steps.format_lighthouse_score.outputs.comments}}</code></pre>
<p>카카오 블로그에서는 <code>FCB</code>, <code>LCP</code>등 좀 더 상세한 정보도 활용하였지만 이 프로젝트에서는 불필요하다고 생각이 되어서 핵심적인 <code>Perfomance, Accessibility, Best practices, SEO, PWA</code> 5가지만 테스팅 하였습니다.</p>
<p>여기서 처음 알았던 사실은 <code>secrets.GITHUB_TOKEN</code> 토큰값은 개발자가 직접 작성하는 것이 아닌 기본적으로 github에 설정되어 있다는 것 입니다. </p>
<h2 id="결과">결과</h2>
<p>이후 PR을 날려보면 다음과 같이 git-bot이 lighthouse 성능 지표에 대해 코멘트를 달아 주었습니다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/11df2971-2db4-47e7-9f13-0ae61b43399d/image.png" alt=""></p>
<p><code>lighthouse CI</code>를 통해서 좀 더 효율적으로 프로젝트를 테스팅 할 수 있었습니다. 이와 같은 테스팅 자동화로 좀 더 유저에게 최적화된 사이트를 제공하는 것이 중요하다고 생각합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[React-quill에서 tiptap 으로 - 2부]]></title>
            <link>https://velog.io/@bae-sh/React-quill%EC%97%90%EC%84%9C-tiptap-%EC%9C%BC%EB%A1%9C-2%EB%B6%80</link>
            <guid>https://velog.io/@bae-sh/React-quill%EC%97%90%EC%84%9C-tiptap-%EC%9C%BC%EB%A1%9C-2%EB%B6%80</guid>
            <pubDate>Sun, 05 Nov 2023 12:20:45 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/bae-sh/post/d115218a-9d53-4fd3-abbf-1d1d565eefc3/image.png" alt=""></p>
<blockquote>
<p><a href="https://velog.io/@bae-sh/React-quill%EC%97%90%EC%84%9C-tiptap-%EC%9C%BC%EB%A1%9C">1부</a> 에서 작성한 글에 이어 2부를 작성해보려고 한다. 2부에서는 어떻게 <code>lowlight</code>로 게시글의 코드블럭에 색상을 넣었는지 많은 삽질과정을 확인해보자.</p>
</blockquote>
<h1 id="codeblock">codeblock</h1>
<blockquote>
<p><em>왜 코드블럭에 색상을 넣어야할까?</em> </p>
</blockquote>
<p>아래의 두 코드블럭만 비교해봐도 쉽게 알 수 있다. 얼마나 가독성에 있어서 차이가 나는지...</p>
<pre><code>const a = 1;
const b = 2;
console.log(a+b);</code></pre><pre><code class="language-js">const a = 1;
const b = 2;
console.log(a+b);</code></pre>
<p>우리 프로젝트는 개발자를 위한 커뮤니티이기에 <strong>문법 색상을 넣는것은 필수</strong>라고 생각했고 사용할 수 있는 2가지 라이브러리를 찾아보았다.</p>
<ol>
<li>Prism.js</li>
<li>lowlight</li>
</ol>
<h2 id="prismjs">Prism.js</h2>
<p><a href="https://prismjs.com/">Prism.js</a> 는 syntax highliter 중 가장 유명하고 많이 쓰이는 라이브러리 이다. 웹 사이트에서 볼 수 있듯이 React 공식문서를 포함한 많은 곳에서 사용되고 있다. 많이 사용된다는 것은 업데이트도 자주 된다는 의미일 것이고 정보도 많기에 적용 해보려고 시도했었다.</p>
<p>적용하는 방식은 inline으로 head 파일에 다운받은 css(스타일링 색상),js(파싱을 위한 함수)을 다음과 같이 적용해 볼 수도 있다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/f7030f00-a4bc-41fc-a1f7-84177b1d4ec3/image.png" alt=""></p>
<p>바닐라 스크립트 환경이였으면 충분히 적용해볼만 했지만 npm으로 관리하는 것이 편할것이라 판단 되어 <strong>Prismjs 모듈을 설치</strong>하여 진행하였다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/6c283cb2-fc00-4fa4-966c-1178dd10fc68/image.png" alt=""></p>
<p>NEXTJS 환경에서는 <a href="https://dev.to/amitchauhan/syntax-highlighting-with-prismjs-and-react-1lep">게시글</a> 을 참고하여 CSR 시에 <code>Prism.highlightAll()</code> 함수를 실행하여 해당 컴포넌트의 code 블럭에 색상을 넣고싶었다. 다음과 같이 코드를 작성해보았다.</p>
<pre><code class="language-tsx">import React, { useEffect } from &#39;react&#39;;
import { useEditor, EditorContent } from &#39;@tiptap/react&#39;;
import ToolBar from &#39;../Toolbar&#39;;

//tiptap
import StarterKit from &#39;@tiptap/starter-kit&#39;;
import Highlight from &#39;@tiptap/extension-highlight&#39;;

import Prism from &#39;prismjs&#39;;
import &#39;prismjs/themes/prism-tomorrow.css&#39;;
interface TiptapProps {
  content: string;
}

const Tiptap = ({ content }: TiptapProps) =&gt; {
  const editor = useEditor({
    extensions: [StarterKit, Highlight],
  });
  useEffect(() =&gt; {
    Prism.highlightAll();
  }, []);
  useEffect(() =&gt; {
    if (content) {
      editor?.commands.setContent(content);
    }
  }, [content]);
  return (
    &lt;div className=&quot;border-2&quot;&gt;
      &lt;ToolBar editor={editor} /&gt;
      &lt;EditorContent
        id=&quot;tiptap&quot;
        editor={editor}
        onClick={() =&gt; editor?.commands.focus()}
      /&gt;
    &lt;/div&gt;
  );
};

export default Tiptap;</code></pre>
<p>하지만 결과는 적용되지 않았다...!!!(그렇게 쉽게 될리가 없지...)
<img src="https://velog.velcdn.com/images/bae-sh/post/36742d4f-8efd-43d4-ac6a-d38d3d268fce/image.png" alt=""></p>
<p>그럼 원인이 뭘까? 고민을 해보고 몇가지 디버깅을 해보았다.</p>
<blockquote>
<ol>
<li>NEXTJS 환경에서 안되는건가?</li>
<li>Tiptap과 호환이 되지 않는걸까?</li>
<li>내 프로젝트와 의존성 문제가 발생했나?</li>
</ol>
</blockquote>
<p>일단 1번의 경우 이전 참고 사이트가 있는것을 보면 NEXT 환경에서도 된다는 것으로 판단하였다. 그럼 2번 문제가 가장 클것인데... 그래서 tiptap으로 작성된 html 코드를 정적으로 작성해보고 결과를 확인해보았다.    </p>
<pre><code class="language-tsx">&lt;pre&gt;
    &lt;code className=&quot;language-js&quot;&gt;
        const a = 1; const b= 2; console.log(a+b);
    &lt;/code&gt;
&lt;/pre&gt;</code></pre>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/60b6478b-6cfb-4d41-b940-39832a2b9047/image.png" alt=""></p>
<p>오! 작동이 잘 되는것을 확인할 수 있었다.!!</p>
<p>이 결과로 <code>Prism.highlight()</code> 함수는 해당 컴포넌트 의 codeblok을 확인하고 스타일링을 하는데 Tiptap의Content 내부로는 들어가지 않거나 혹은 게시글은 <strong>정적인 코드가 아닌 동적 코드</strong>이기에 useEffect가 계속해서 작동이 되지 않아서 그런걸 수도 있겠다라고 생각했다. </p>
<p>후자의 경우 dependency에 content상태를 넣어 타이핑을 칠떄마다 <code>Prism.highlight()</code>를 실행할 수도 있겠지만 그건 원하지 않았다. 그래서 Tiptap에서 추천하는 <code>lowlight</code> 라이브러리를 사용하였다.</p>
<h2 id="lowlight">lowlight</h2>
<p><a href="https://tiptap.dev/api/nodes/code-block-lowlight#lowlight">tiptap에서 lowlight</a> 사용법에 대해서 자세히 설명되어있으므로 그대로 구현했는데 다음과 같은 에러가 발생되었다. (억까의 시작)</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/a4c307a4-100c-4d9c-91f7-ffb9bba43f7a/image.png" alt=""></p>
<blockquote>
<p><em>이때 쫌 승질이 났다... 하라는대로 했는데 왜 없다는거야!!!</em></p>
</blockquote>
<p>Tiptap은 업데이트가 늦다는 지인의 말이 생각이나 설마 lowligt 버전 업데이트되면서 변경됐나 싶었다. 이럴땐 <a href="https://github.com/wooorm/lowlight#use">lowlight 공식문서</a>를 확인해보자.</p>
<p>역시나... tiptap 공식문서에서 사용되는 lowlight 버전은 <code>v2.7.0</code> 이고 현재 최신버전은 <code>v3.1.0</code> 이라 메이저 버전 업데이트로 인해 export 하는 값이 변경되었다. 이에 따라 사용법도 바뀐것이다.!! <del>버전 호환성 문제는 자주 발생하지만 적응이 안된다 하하하</del></p>
<p>그래서 먼저 <code>3.1.0</code> 버전으로 적용을 해보고 문제가 발생하면 공식문서 버전인 <code>2.7.0</code>으로 특정 버전을 설치하여 해결해보려 하였다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/aaca245b-7ead-4a13-b8ce-7e2beb110567/image.png" alt=""></p>
<p>오오~~ 드디어 codeblock에 색상이 들어오기 시작했다!👏 하지만 새로운 문제가 발생했다.</p>
<h3 id="문제상황">문제상황</h3>
<p><code>const a= 1;</code> 이라는 코드를 복사 붙여넣기를 했는데 이상하게 메타태그와 함께 html 코드까지 붙여넣기가 된다는 문제였다...</p>
<p>이거 만만치 않을거 같다는 예감이 있었다... 붙여넣기에서 문제가 발생한거 같으니 Tiptap에서 관련 정보가 있나 찾아보았다.</p>
<p><a href="https://tiptap.dev/guide/custom-extensions#node-views">참고</a>를 보면 정확하게 나의 상황과 같지는 않지만 <code>addNodeView</code> 와 같은 함수를 확인할 수 있었다.</p>
<blockquote>
<p>그럼 <code>addNodeView</code> 을 이용하여 현재 html에서 <code>내부 TEXT</code> 만 받아와서 return 값으로 넘겨주면 되지않을까?</p>
</blockquote>
<p>콘솔을 찍어가며 내부 프로퍼티 탐색 후 위 생각처럼 구현해보았는데 잘 되지않았다. 역시 쉬운건 없다... <em>Syntax Highligh 없어도 되지않을까?</em> 잠깐 유혹에 넘어갈뻔 했지만 다시 마음을 다잡고 에러 분석을 해보았다.</p>
<h3 id="디버깅">디버깅</h3>
<p>먼저 뭐가 문제일까 논리적으로 생각해보자.</p>
<blockquote>
<ol>
<li>이 또한 NEXTJS 환경문제?</li>
<li>나의 프로젝트와 의존성 문제?</li>
<li>lowlight의 3.1.0 버전의 문제?</li>
</ol>
</blockquote>
<p>스택오버플로우나 lowlight 깃이슈를 봐도 nextJS 문제는 확인할 수 없었다. 그럼 2,3번일 가능성이 있는데 2번 먼저 디버깅을 해보았다.</p>
<p>의존성 문제가 있는지 확인하기 위해서 나는 항상 새 프로젝트 에서 해당 라이브러리만 설치하여 동작 시켜본다. 예전에는 로컬에서 확인했었지만 이번에는 <a href="https://codesandbox.io/">CodeSandbox</a>라는 좋은 플랫폼을 알게되어 사용해 보았다. <a href="https://codesandbox.io/p/sandbox/optimistic-framework-qskfs9?file=%2Fpages%2F_app.js%3A11%2C1">코드</a> 직접 만든 독립적인 환경에서 실행해보니 다음과 같이 잘 작동하는 것을 알 수 있다. 그럼 2번 문제라고 생각이 되어서 어떻게 해결을 해야 하나 고민이 많았다.</p>
<p>하지만 생각보다 어이없게 <strong>yarn-lock 파일을 제거한 뒤 yarn install을 통해 재설치</strong>하니 의존성 문제가 해결되어 정상적으로 코드 복붙이 잘 되어버렸다.😫 아마도 기존에 깔려있는 모듈과 충돌이 발생해서 이러한 버그가 발생한게 아닐까 싶다.</p>
<h2 id="마무리">마무리</h2>
<p>끝은 조금 엉성하지만 문제가 발생했을 때 하나하나 디버깅을 해보며 독립적인 환경에서 어떠한 것이 문제인지 해결하는 능력을 키웠다.👍</p>
<blockquote>
<p>혹시 잘못된 부분이나 더 나은 해결방법이 있으면 댓글로 알려주시면 감사하겠습니다.🙇🏻‍♂️</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React-quill에서  tiptap 으로 - 1부]]></title>
            <link>https://velog.io/@bae-sh/React-quill%EC%97%90%EC%84%9C-tiptap-%EC%9C%BC%EB%A1%9C</link>
            <guid>https://velog.io/@bae-sh/React-quill%EC%97%90%EC%84%9C-tiptap-%EC%9C%BC%EB%A1%9C</guid>
            <pubDate>Sun, 05 Nov 2023 07:48:06 GMT</pubDate>
            <description><![CDATA[<h2 id="react-quill을-버린-이유">React-Quill을 버린 이유</h2>
<p>게시글 기능 구현을 위해 <a href="https://github.com/zenoamaro/react-quill">react-quill</a> 라이브러리를 사용했었다. 하지만 본인 프로젝트의 경우 NEXTJS 환경에서 구현을 하다보니 사용하면서 많은 걸림돌이 발생했었다.</p>
<h3 id="1-ssr의-지원-x">1. SSR의 지원 x</h3>
<p>먼저, Quill 에디터의 경우<code>document</code>에 접근하는 코드가 존재하는데 SSR 환경에서 렌더링 시 브라우저가 없지 않은가? 그럼 당연히 <strong>window 객체는 존재하지 않게되고</strong> 이미지와 같은 에러가 발생하였다.
<img src="https://velog.velcdn.com/images/bae-sh/post/83484652-9686-4cd9-b743-7f07ed17a0cd/image.png" alt=""></p>
<p>구글링을 하다가 <a href="https://github.com/zenoamaro/react-quill/issues/292#issuecomment-1057178885">링크</a>를 보며 <code>dynamic import</code>를 사용하여 동적으로 브라우저에서 CSR에서 시도하여 해결은 하였다.<del>(이때 오픈소스를 기여하고 싶었지만 방대한 코드의 양에 압도당해 미뤄두었다.😭)</del></p>
<h3 id="2-커스터마이징-불편">2. 커스터마이징 불편</h3>
<p>벨로그와 같은 깔끔한 디자인의 에디터를 제공하고 싶었는데 생각보다 커스터마이징 하기가 힘들었다. 또한, <code>react-quill</code>의 늦은 업데이트 속도는 다른 라이브러리 이전 욕구가 뿜뿜했다.</p>
<h2 id="직접-라이브러리-구현">직접 라이브러리 구현?</h2>
<blockquote>
<p>사실 처음에는 <strong>내가 직접 에디터 라이브러리를 만들어 보자!</strong> 로 시작하였다. <a href="https://github.com/velopert/velog-client">Velog</a>, <a href="https://github.com/ueberdosis/tiptap">Tiptap</a>, <a href="https://www.bucketplace.com/post/2020-09-18-%EC%9B%90%ED%99%9C%ED%95%9C-%EC%BD%98%ED%85%90%EC%B8%A0-%EC%9E%91%EC%84%B1%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%97%90%EB%94%94%ED%84%B0-%EA%B0%9C%EB%B0%9C%EA%B8%B0/">오늘의집</a> 등등 다양한 코드와 기술블로그를 학습하며 적용해보았으나 구문 parsing 하는 법, 디자인, 렌더링 등등 너무 고려할 부분이 많아 시간적으로 부담이 되었다. 다음 기회에 있으면 다시 도전해보려고 한다..😱</p>
</blockquote>
<h2 id="tiptap-도입">Tiptap 도입</h2>
<p>다른 라이브러리들 중에 <a href="https://tiptap.dev/">Tiptap</a>을 고른이유는 몇가지 있었다. 먼저 지인들의 추천으로 Tiptap 사이트를 들어갔었는데 너무나 깔끔한 디자인과 직관적인 example 구성방식은 주니어 개발자 나도 쉽게 따라할 수 있을거 같다는 느낌을 받았다. 또한, 다양한 프레임워크(React,Vue,<strong>NextJS</strong>,Svelt 등)를 지원한다는 점이 매력적이였다. 또한 수많은 extension plugin은 내가 Tiptap을 사용하지 않을 이유가 없었다.</p>
<h2 id="기능-구현">기능 구현</h2>
<p><a href="https://tiptap.dev/examples/default">Example</a> 이 너무 잘 나와 있어서 그대로 보면서 구현을 했다. 최종 작성한 Tiptap의 컴포넌트를 먼저 확인하고 세부 내용으로 들어가 보자.</p>
<pre><code class="language-tsx">//Tiptap.tsx
import React, { useEffect } from &#39;react&#39;;
import { useEditor, EditorContent } from &#39;@tiptap/react&#39;;
import ToolBar from &#39;../Toolbar&#39;;

//tiptap
import StarterKit from &#39;@tiptap/starter-kit&#39;;
import Highlight from &#39;@tiptap/extension-highlight&#39;;
import Image from &#39;@tiptap/extension-image&#39;;
import CodeBlockLowlight from &#39;@tiptap/extension-code-block-lowlight&#39;;

import { common, createLowlight } from &#39;lowlight&#39;;

interface TiptapProps {
  content: string;
}

const Tiptap = ({ content }: TiptapProps) =&gt; {
  const lowlight = createLowlight(common);
  const editor = useEditor({
    extensions: [
      StarterKit,
      Highlight,
      Image.configure({ inline: true, allowBase64: true }),
      CodeBlockLowlight.configure({
        lowlight,
      }),
    ],
  });

  useEffect(() =&gt; {
    if (content) {
      editor?.commands.setContent(content);
    }
  }, [content]);
  return (
    &lt;div className=&quot;border-2&quot;&gt;
      &lt;ToolBar editor={editor} /&gt;
      &lt;EditorContent
        id=&quot;tiptap&quot;
        editor={editor}
        onClick={() =&gt; editor?.commands.focus()}
      /&gt;
    &lt;/div&gt;
  );
};

export default Tiptap;</code></pre>
<h2 id="toolbar">Toolbar</h2>
<p>Toolbar의 경우 기본 제공하는 것이 이쁘지가 않아서 <a href="https://fonts.google.com/icons">google icon</a> 을 사용하여 커스터마이징을 하였다. css는 <strong>tailwindcss</strong>로 스타일링하였다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/bb089452-5882-4c79-92f8-4ff407f6ca12/image.png" alt=""></p>
<h4 id="toolbar-code">Toolbar code</h4>
<pre><code class="language-tsx">//Toolbar.tsx
import React from &#39;react&#39;;
import { Editor } from &#39;@tiptap/react&#39;;
import { Icon } from &#39;../icons&#39;;

interface ToolBarProps {
  editor: Editor | null;
}
function ToolBar({ editor }: ToolBarProps) {
  if (!editor) return null;

  return (
    &lt;div className=&quot;flex items-center justify-center gap-2 p-6 py-3 border-b-2 sm:gap-8&quot;&gt;
      &lt;div className=&quot;flex items-center justify-center gap-2&quot;&gt;
        &lt;Icon.H1 editor={editor} /&gt;
        &lt;Icon.H2 editor={editor} /&gt;
        &lt;Icon.H3 editor={editor} /&gt;
      &lt;/div&gt;
      &lt;div className=&quot;flex items-center justify-center gap-2&quot;&gt;
        &lt;Icon.Bold editor={editor} /&gt;
        &lt;Icon.Italic editor={editor} /&gt;
        &lt;Icon.Strikethrough editor={editor} /&gt;
        &lt;Icon.Code editor={editor} /&gt;
      &lt;/div&gt;

      &lt;div className=&quot;flex items-center justify-center gap-2&quot;&gt;
        &lt;Icon.Quote editor={editor} /&gt;
        &lt;Icon.AddPhoto editor={editor} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

export default ToolBar;</code></pre>
<p>여기서 특히 어려웠던 점은 <strong>이미지 업로드 툴바</strong>였다. <a href="https://tiptap.dev/examples/images">Tiptap의 이미지기능</a> 을 보면 다음과 같은 로직으로 editor에 사진이 설정되는 것을 알 수 있다.</p>
<ol>
<li>이미지의 URL을 입력한다.</li>
<li>이미지의 URL을 <code>&lt;img src={URL}/&gt;</code> 의 html로 변경한다.</li>
<li>이 변경된 html을 editor에 추가된다.</li>
</ol>
<p>하지만 Velog나 다른 에디터들을 보면 <strong>로컬에 있는 사진을 업로드 하거나 드래그 앤 드랍 방식</strong>으로 하는 것이 보편적인 것을 알 수 있다. 그럼 내가 직접 구현해야하는 것이 뭘까?</p>
<ol>
<li><code>&lt;input type=&#39;file&#39;/&gt;</code> 을 이용하여 이미지 추가 툴바 클릭 시 로컬에서 파일 받아오기 기능 구현</li>
<li>사진을 에디터로 드래그 앤 드랍 시 이 이미지의 정보를 저장하기</li>
</ol>
<p>그럼 차례대로 해결해 보자.</p>
<h2 id="addimage">AddImage</h2>
<h3 id="로컬에서-파일-받아오기">로컬에서 파일 받아오기</h3>
<img src = 'https://velog.velcdn.com/images/bae-sh/post/437a8a6d-a9bf-45f6-8f63-a496226bbef5/image.png' /> 

<p>해당 버튼 아이콘에 <code>&lt;input type=&#39;file&#39;/&gt;</code> 를 추가하여 아이콘 클릭 시 다음과 같은 창을 띄워 파일을 선택하려고 한다.
<img src="https://velog.velcdn.com/images/bae-sh/post/6063eabf-a612-4707-865e-c700f3830a4a/image.png" alt=""></p>
<p>그러기 위해서 아래 사진 같이 <code>&lt;input type=&#39;file&#39;/&gt;</code> 코드를 작성하면 나오는 input의 고유 버튼 인 파일 선택 버튼을 클릭해야하는데 file 타입의 스타일링이 조금 까다로워 버튼 이미지 크기를 파일 선택 버튼 크기로 꽉 채워서 해결을 하였다.
<img src="https://velog.velcdn.com/images/bae-sh/post/9012049c-6514-4b8d-a269-a279ac11e362/image.png" alt=""></p>
<h4 id="icon-적용-전">icon 적용 전</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/5b956dd9-51ac-4ec7-8bc2-3c545d8e116b/image.png" alt=""></p>
<h4 id="icon-적용-후">icon 적용 후</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/f79b6d2f-ba83-435a-95d3-6ea25c1305c1/image.png" alt=""></p>
<p>그럼 이제 버튼 클릭 시 파일을 불러올 수 있다. 그럼 어떻게 파일의 정보를 받아올까? 일반적으로 input 태그의 onChange 태그의 event를 통해서 값을 접근할 수 있다. 다음 코드를 보면 이해가 될 것이다.</p>
<pre><code class="language-tsx">function AddPhoto() 

  const handleUploadPhoto = async (files: FileList | null) =&gt; {
    if (files === null) return;
    const file = files[0];
    console.log(file);
  };
  return (
    &lt;button
      type=&quot;button&quot;
      className=&quot;relative w-8 h-8 cursor-pointer opacity-70 hover:opacity-40&quot;
    &gt;
      &lt;input
        type=&quot;file&quot;
        className=&quot;absolute top-0 left-0 w-8 h-8 outline-none opacity-0 file:cursor-pointer&quot;
        accept=&quot;image/*&quot;
        onChange={(e) =&gt; {
          handleUploadPhoto(e.target.files);
        }}
      /&gt;
      &lt;svg
        xmlns=&quot;http://www.w3.org/2000/svg&quot;
        width=&quot;32&quot;
        height=&quot;32&quot;
        viewBox=&quot;0 -960 960 960&quot;
      &gt;
        &lt;path d=&quot;M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h360v80H200v560h560v-360h80v360q0 33-23.5 56.5T760-120H200Zm480-480v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM240-280h480L570-480 450-320l-90-120-120 160Zm-40-480v560-560Z&quot; /&gt;
      &lt;/svg&gt;
    &lt;/button&gt;
  );
}</code></pre>
<p>필자는 input에 파일 데이터에 변화가 생기면 데이터를 백엔드에 Post 요청을 하여 저장을 하였고 응답 값으로 해당 URL을 받아오는 로직으로 구현을 하였다. </p>
<p>최종적으로 이미지 추가 버튼 코드이다.</p>
<pre><code class="language-tsx">//core
import React from &#39;react&#39;;
import { useSession } from &#39;next-auth/react&#39;;

//constants
import { ModalType, errorMessage } from &#39;@/constants/constant&#39;;

//service
import { BASE_URL } from &#39;@/service/base/api&#39;;
import { postManager } from &#39;@/service/post&#39;;

//third party
import { Editor } from &#39;@tiptap/react&#39;;
import { useModal } from &#39;@/hooks&#39;;

function AddPhoto({ editor }: { editor: Editor }) {

  const handleUploadPhoto = async (files: FileList | null) =&gt; {
    if (files === null || !editor) return;

    const file = files[0];
    const formData = new FormData();
    formData.append(&#39;file&#39;, file);

     const imgHash = await postManager.uploadImage(formData, accessToken);// 백엔드에게 이미지 Post요청 후 URL 받기
     const IMG_URL = `${BASE_URL}${imgHash}`;

     editor.commands.setImage({ src: IMG_URL });
  };
  return (
    &lt;button
      type=&quot;button&quot;
      className=&quot;relative w-8 h-8 cursor-pointer opacity-70 hover:opacity-40&quot;
    &gt;
      &lt;input
        type=&quot;file&quot;
        className=&quot;absolute top-0 left-0 w-8 h-8 outline-none opacity-0 file:cursor-pointer&quot;
        accept=&quot;image/*&quot;
        onChange={(e) =&gt; {
          handleUploadPhoto(e.target.files);
          e.target.value = &#39;&#39;; // 중복해서 데이터 넣을 경우 가능 예외 처리 
        }}
      /&gt;
      &lt;svg
        xmlns=&quot;http://www.w3.org/2000/svg&quot;
        width=&quot;32&quot;
        height=&quot;32&quot;
        viewBox=&quot;0 -960 960 960&quot;
      &gt;
        &lt;path d=&quot;M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h360v80H200v560h560v-360h80v360q0 33-23.5 56.5T760-120H200Zm480-480v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80ZM240-280h480L570-480 450-320l-90-120-120 160Zm-40-480v560-560Z&quot; /&gt;
      &lt;/svg&gt;
    &lt;/button&gt;
  );
}

export default AddPhoto;</code></pre>
<h3 id="drag-and-drop-미해결">Drag And Drop (미해결)</h3>
<p>두번째로 드래그 앤 드랍으로 에디터에 바로 사진을 넣고싶었다. 다행히도 <a href="https://tiptap.dev/api/extensions/file-handler">Tiptap pro의 File Handler</a> extension를 이용하면 쉽게 해결될 줄 알았지만 생각보다 난관이 있었다.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/9b116af1-e986-40e3-a64c-f269cd670532/image.png" alt=""></p>
<p>pro라는 어감이 유로 버전이라 생각 했지만 무료 버전도 어느정도 제공되는것 같아서 다행이었다. </p>
<h3 id="npmrc">.npmrc</h3>
<p>문서를 보며 따라하던 중 <code>.npmrc</code> 라는 새로운 파일을 알게되었다. <code>.npmrc</code> 파일에 나의 토큰 정보를 넣어서 npm 모듈 설치 할때 인증인가를 도와주는 파일이라는 것을 보고 npm 라이브러리가 private으로 제공할 수도 있구나라는 것을 알게되었다. npmrc는 인증인가 뿐 아니라 npm 환경설정, 패키지 scope 등 다양한 기능을 제공하는 것 같았다. <a href="https://medium.com/@pmmanav/what-is-a-npmrc-file-e7bd40bff3f0">참고</a></p>
<p>필자의 경우 node 패키지 관리툴로 yarn을 사용하고 있어 다음과 같이 <code>.yarnrc</code> 를 설정하였다.</p>
<pre><code class="language-.yarnrc">&quot;@tiptap-pro:registry&quot; &quot;https://registry.tiptap.dev/&quot;
&quot;//registry.tiptap.dev/:_authToken&quot; &quot;나의토큰정보&quot;</code></pre>
<p>그뒤 <code>yarn add @tiptap-pro/extension-unique-id</code> 를 실행 하였는데 다음과 같은 에러가 발생하였다.
<img src="https://velog.velcdn.com/images/bae-sh/post/c651a3e5-2de5-4eb7-bf6f-21b6c2531901/image.png" alt=""></p>
<p>공식문서의 예시대로 <strong>npm으로 설치할 경우에는 잘 되는 것</strong>을 보면 2가지 정도로 원인을 생각해 볼 수 있을거 같다.</p>
<ol>
<li>.yarnrc의 <strong>파일 설정을 잘못</strong>하여서 authCode를 읽지 못한경우</li>
<li>tiptap-pro의 경우 <strong>yarn 지원을 하지않는다?</strong></li>
</ol>
<p>아무래도 .yarnrc 파일을 처음 사용하다 보니 나의 문제라고 생각은 되지만 많은 구글링과 지피티를 사용해도 문법적인 오류는 발견을 못했다. (혹시 알고 계신분은 댓글로 제발 알려주세요😭😭)</p>
<p>그럼 두번째 경우로 tiptap 라이브러리 내부 문제인데 <strong>500에러</strong>라고 무조껀 서버 문제라고 단정지을수도 없을 뿐더러 npm은 되고 yarn은 안되는 경우를 겪은적이 없어서 당황스럽다. npm으로 설치를 하면 작동은 되겠지만 두가지의 노드패키지를 사용하기에는 관리 차원에서 부담스럽기도 하기에 이후에 해결하려고 남겨두었다. <del>(나중에 업데이트가 되면 될 수도 있지 않을까 하는 행복회로?)</del></p>
<p>작성하다 보니 글이 길어진 것 같아 <code>lowlight</code>를 활용하여 <code>code block</code>에 색상을 넣은 경험을 2부로 넘기려고 한다!</p>
<blockquote>
<p>혹시 잘못된 부분이나 더 나은 해결방법이 있으면 댓글로 알려주시면 감사하겠습니다.🙇🏻‍♂️</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[GPT 기능 구현 후 빌드 Error 해결 과정기]]></title>
            <link>https://velog.io/@bae-sh/GPT-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%ED%9B%84-%EB%B9%8C%EB%93%9C-Error-%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95%EA%B8%B0</link>
            <guid>https://velog.io/@bae-sh/GPT-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%ED%9B%84-%EB%B9%8C%EB%93%9C-Error-%ED%95%B4%EA%B2%B0-%EA%B3%BC%EC%A0%95%EA%B8%B0</guid>
            <pubDate>Mon, 23 Oct 2023 08:45:55 GMT</pubDate>
            <description><![CDATA[<p>프로젝트를 진행 중 OPEN AI의 API를 활용하여 GPT 기능을 넣어 보았다. 처음에는 비용적인 부분이 걱정이 되었지만 <a href="https://openai.com/pricing">OPENAI</a>의 가격정책을 보면 input, ouput이 1K token 라는 가정하에 0.0035$ 정도라 현재 유저가 많은 서비스가 아닌 만큼 크게 부담이 되지 않았다. OPEN AI의 API를 사용하여 GPT 기능을 구현하는 방법은 <a href="https://platform.openai.com/docs/api-reference/introduction">공식문서</a> 이외에도 많은 글들이 존재해 이번에는 CI 환경에서 빌드 중 발생한 에러 해결과정 및 배운점을 적어 보려고 한다.</p>
<h2 id="문제상황">문제상황</h2>
<p>로컬 환경에서는 에러 없이 잘 작동 하였는데 git action으로 build 테스트시에 다음과 같은 에러가 발생했다.
<img src="https://velog.velcdn.com/images/bae-sh/post/d26b31c2-b1fe-49e6-bfc8-ea8b4cedd7d4/image.png" alt=""></p>
<blockquote>
<p>응? OPEN_API_KEY가 없다고? KEY값을 어디서 사용하더라...</p>
</blockquote>
<p>에러 해결을 위해 사용되는 코드 부분을 찾아 보았다.</p>
<pre><code class="language-ts">//open AI의 api 사용을 위한 전처리 파일 입니다.
import OpenAI from &#39;openai&#39;;

const openai = new OpenAI({
  apiKey: process.env.NEXT_PUBLIC_OPEN_API,
  dangerouslyAllowBrowser: true,
});

async function getCodeReview({
  code,
  question,
}: {
  code: string;
  question: string;
}) {
  const completion = await openai.chat.completions.create({
    messages: [{ role: &#39;user&#39;, content: `${code} \n ${question} ` }],
    model: &#39;gpt-3.5-turbo&#39;,
    max_tokens: 1024,
  });

  return completion.choices[0].message.content;
}
export default getCodeReview;</code></pre>
<h2 id="해결시도-1-env파일-추가">해결시도 1 (env파일 추가)</h2>
<blockquote>
<p>아! 현재 API 키는 env.development에 저장되어 있고 gitignore에 env.development 파일이 설정되어 있기 때문에 빌드시에는 OPEN_API 값을 받아올 수 없기에 에러가 떴구나😅 
그럼 빌드 전에 env 파일을 넣어주면 되지 않을까?</p>
</blockquote>
<p>이러한 고민을 하여 yml 파일에 <code>Generate Environment Variables File for Production</code> 이름을 가진 코드를 추가하여 빌드 전 env 파일에 <code>NEXT_PUBLIC_OPEN_API</code> 값을 넣어주었다.</p>
<pre><code class="language-yml">name: &#39;test-lint-build&#39;

on:
  push:
  pull_request:

jobs:
  test:
    name: Test lint, build
    runs-on: ubuntu-latest
    env:
      working-directory: ./client

    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: &#39;18&#39;
      - name: Cache node modules
        uses: actions/cache@v2
        id: cache
        with:
          path: node_modules
          key: npm-packages-${{ hashFiles(&#39;**/package-lock.json&#39;) }}

      - name: Generate Environment Variables File for Production
        run: echo &quot;NEXT_PUBLIC_OPEN_API=${{ secrets.NEXT_PUBLIC_OPEN_API }}&quot; &gt;&gt; .env

      - name: Install Dependencies
        run: npm install
        working-directory: ${{ env.working-directory }}
      - run: npm run lint
        if: ${{ always() }}
        working-directory: ${{ env.working-directory }}
      - run: npm run build
        if: ${{ always() }}
        working-directory: ${{ env.working-directory }}</code></pre>
<p>해결될것 같았지만 여전히 같은 에러가 발생하였다. 무엇이 문제일까? 고민하다 두가지 의문점이 들었다.</p>
<ol>
<li>NEXTJS 환경에서 env 환경변수에 <code>NEXT_PUBLIC</code>이 의미하는게 뭘까?</li>
<li>개발 환경에서는 <code>.env.development</code> 를 사용하고 있었는데 .env, .env.development 차이가 뭘까?</li>
</ol>
<h3 id="1-next_public">1. NEXT_PUBLIC</h3>
<blockquote>
<p>Non-NEXT_PUBLIC_ environment variables are only available in the Node.js environment, meaning they aren&#39;t accessible to the browser (the client runs in a different environment).</p>
<p>In order to make the value of an environment variable accessible in the browser, Next.js can &quot;inline&quot; a value, at build time, into the js bundle that is delivered to the client, replacing all references to process.env.[variable] with a hard-coded value. To tell it to do this, you just have to prefix the variable with NEXT_PUBLIC_. <a href="https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables#bundling-environment-variables-for-the-browser">NEXTJS</a> </p>
</blockquote>
<p>요약하면, 일반적으로 env 변수는 NodeJS 환경에서만 사용할 수 있는데, 만약 브라우저 환경에서도 사용하고 싶으면 환경변수 앞에 접두사로 <code>NEXT_PUBLIC_</code>을 붙여야 한다.</p>
<p>결론적으로 나는 브라우저 환경에서 GPT 기능을 사용하기 위해 <code>NEXT_PUBLIC_</code> 을 추가적으로 붙여주었었고 이는 NODEJS 환경에서도 잘 작동하니 문제의 원인이 아니였다.</p>
<p>참고로 <code>process.env.NODE_ENV</code> 는 현재 실행 환경이 development,test,production 인지 구분해주는 내장 환경 변수이다. <strong>이를 활용하여 환경에 따라 의도적으로 다른 작업을 할 수도 있을 것 같다.</strong>
(예를들면 개발환경, 배포환경에서 서버에 요청하는 URL를 다르게 한다던지???)</p>
<h3 id="2-env">2. ENV</h3>
<p>매번 프로젝트 할떄마다 env 파일을 사용하지만 여러 env 종류와 의미를 알지못해 학습해 보았다.</p>
<h4 id="env-종류">ENV 종류</h4>
<blockquote>
<p>.env: 기본 파일.
.env.local: .env를 덮어쓰는 파일. Test를 제외한 모든 환경에서 로딩</p>
</blockquote>
<p>.env.development: 개발자 환경에서 로딩
.env.test: 테스트 환경에서 로딩
.env.production: 프로덕션 환경에서 로딩</p>
<blockquote>
</blockquote>
<p>.env.development.local, .env.test.local, .env.production.local: 각각 env.* 를 덮어쓰는 파일 <a href="https://flamingotiger.github.io/frontend/react/create-react-app-environment/?">참고</a></p>
<h4 id="env-실행-우선순위">ENV 실행 우선순위</h4>
<blockquote>
<p>npm start: .env.development.local &gt; .env.development &gt; .env.local &gt; .env
npm run build: .env.production.local &gt; .env.production &gt; .env.local, .env
npm test: .env.test.local &gt; .env.test &gt; .env (note .env.local is missing)</p>
</blockquote>
<p>정확히 알고 사용한 것은 아니였지만 개발환경에서는 .env.development 파일로, git action CI 빌드 환경에서는 .env 파일로 사용하여 이 또한 문제가 아니였다. (만약 빌드 환경에서 .env.development로 만들었다면 npm run build 실행 시 파일을 읽지 못했을 것이고 이를 원인이라고 생각했을 수도 있겠다.)</p>
<h2 id="해결시도-2-open-ai-코드-탐색">해결시도 2 (OPEN AI 코드 탐색)</h2>
<blockquote>
<p>env 설정은 잘해주었는데 왜 OPEN_API 값을 못찾을까?? 도대체 어떤 방식으로 코드를 짰길래...</p>
</blockquote>
<p>이러한 생각으로 OPEN_AI의 에러 처리 방식을 라이브러리를 뜯어보며 찾아보았다. 
<a href="https://github.com/openai/openai-node/blob/master/src/index.ts#L103-L112">open-ai/src/index.ts</a></p>
<pre><code class="language-ts">//open-ai/src/index.ts
...
  constructor({
    apiKey = Core.readEnv(&#39;OPENAI_API_KEY&#39;),
    organization = Core.readEnv(&#39;OPENAI_ORG_ID&#39;) ?? null,
    ...opts
  }: ClientOptions = {}) {
    if (apiKey === undefined) {
      throw new Errors.OpenAIError(
        &quot;The OPENAI_API_KEY environment variable is missing or empty; either provide it, or instantiate the OpenAI client with an apiKey option, like new OpenAI({ apiKey: &#39;My API Key&#39; }).&quot;,
      );
    }
...
</code></pre>
<p><a href="https://github.com/openai/openai-node/blob/master/src/core.ts#L964-L972">open-ai/src/core.ts</a></p>
<pre><code class="language-ts">//open-ai/src/core.ts
...
export const readEnv = (env: string): string | undefined =&gt; {
  if (typeof process !== &#39;undefined&#39;) {
    return process.env?.[env] ?? undefined;
  }
  if (typeof Deno !== &#39;undefined&#39;) {
    return Deno.env?.get?.(env);
  }
  return undefined;
};
...
</code></pre>
<p>이 코드를 보면서 OEPN AI의 코드 구현 방식을 생각해 보았다.</p>
<ol>
<li>생성자 인자로 apiKey가 들어오면 그 값을 사용</li>
<li>없는 경우는 readEnv 함수를 통해 env파일에 OPENAI_API_KEY 변수가 있나 확인하고 있으면 그대로 사용, 없으면 undefined 반환</li>
<li>apiKey가 undefined일 경우 Error 던짐</li>
</ol>
<p>나는 현재 아래와과 같이 NEXT_PUBLIC_OPEN_API(open ai에서 탐색하는 OEPNAI_API_KEY가 아님을 주의) 값을 설정해 놨었고 2번으로 분기 되지 않겠다는 것을 생각했다. 그럼 결론적으로 빌드 환경에서 OPENAI의 인자로 apiKey값이 들어가기는 할텐데 3번이(에러) 발생한다는 것은 apiKey가 undefined 라는 의미이고, 결국 아직도 build 환경에서는 env를 못찾는구나.</p>
<pre><code class="language-ts">//open AI의 api 사용을 위한 전처리 파일 입니다.
...
const openai = new OpenAI({
  apiKey: process.env.NEXT_PUBLIC_OPEN_API,
  dangerouslyAllowBrowser: true,
});
...</code></pre>
<h2 id="해결-방식-working-directory">해결 방식 (working-directory)</h2>
<p>너무나도 허무하게 문제는 <strong>env 파일 생성 경로를 잘못 지정</strong>해 주었기 때문이다.<del>(그러니 계속해서 env 파일을 못찾았지...😭)</del> 이 프로젝트 폴더 구조는 root-client 구조로 되어있어 프로젝트를 실행하기 위해서는 clinet 폴더로 변경하여 run을 실행시켜야했다. 하지만 <code>Generate Environment Variables File for Production</code> 에서는 <code>working-directory</code>를 변경하지 않아 <strong>env 파일은 root 폴더에 npm build는 client에서</strong> 실행되어 문제가 발생했던 것이다.</p>
<p>그래서 다음과 같이 <code>working-directory</code>를 추가한 yml 파일로 CI build 테스팅을 성공시킬 수 있었다.</p>
<pre><code class="language-yml">name: &#39;test-lint-build&#39;

on:
  push:
  pull_request:

jobs:
  test:
    name: Test lint, build
    runs-on: ubuntu-latest
    env:
      working-directory: ./client

    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js
        uses: actions/setup-node@v2
        with:
          node-version: &#39;18&#39;
      - name: Cache node modules
        uses: actions/cache@v2
        id: cache
        with:
          path: node_modules
          key: npm-packages-${{ hashFiles(&#39;**/package-lock.json&#39;) }}

      - name: Generate Environment Variables File for Production
        run: echo &quot;NEXT_PUBLIC_OPEN_API=${{ secrets.NEXT_PUBLIC_OPEN_API }}&quot; &gt;&gt; .env
        working-directory: ${{ env.working-directory }}

      - name: Install Dependencies
        run: npm install
        working-directory: ${{ env.working-directory }}
      - run: npm run lint
        if: ${{ always() }}
        working-directory: ${{ env.working-directory }}
      - run: npm run build
        if: ${{ always() }}
        working-directory: ${{ env.working-directory }}</code></pre>
<p>이번 에러를 통해 개발자의 성장은 에러를 해결하는 과정에서 가장 크다고 느꼈다. 만약 working-directory 설정을 안해줬다는것을 바로 알아차렸다면 해결은 빠르게 했을지 몰라도 NEXT_PUBLIC의 의미, ENV 종류와 특징, OPEN_AI가 API를 받아오는 과정을 학습할 수 없었을 것이다. 삽질하는 과정을 너무 부정적으로만 생각하지 말고 성장의 영양제라고 생각하자!😊</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NextJS의 꽃 pre-render]]></title>
            <link>https://velog.io/@bae-sh/NextJS%EC%9D%98-%EA%BD%83-pre-render</link>
            <guid>https://velog.io/@bae-sh/NextJS%EC%9D%98-%EA%BD%83-pre-render</guid>
            <pubDate>Tue, 23 May 2023 12:52:28 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>나도 예전에는 <strong><em>NextJS는 Server-side Rendering방식이야~</em></strong>  라고만 생각하며 개발을 한적이 있다. 
지금 생각해보면 NextJS가 유명하다고 무작정 따라하기만 했던거 같다... 😭
이번 기회에 공식문서를 정리를 해보며 NextJS의 강점인 pre-render에 대해 알아보자.</p>
</blockquote>
<p>NextJS 프레임 워크를 쓰는 가장 큰 이유 중 하나는 <strong>pre-render</strong> 방식이라고 생각을 한다.
이 <strong>pre-render</strong> 방식은 무엇이고 기존 React의 render와 무엇이 다른지 알아보자.</p>
<hr/>

<h3 id="사전지식">사전지식</h3>
<p>Next JS에서는 <strong>페이지 단위</strong>로 어떠한 렌더링 방식으로 작동할 것인지 선택할 수 있다. 이는 SPA(Single Page Application) 방식을 사용하는 React 방식과 차별점이라고 볼 수 있다. </p>
<h2 id="render">Render</h2>
<blockquote>
<p>렌더링 이란? HTML,CSS, 자바스크립트 등 개발자가 작성한 문서가 브라우저에서 출력되는 과정을 말한다.</p>
</blockquote>
<p>기존 CSR의 render 방식을 생각해 보자</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/c0ad1fde-da99-4b5b-b68c-d281d2236ceb/image.png" alt=""></p>
<p>사이트에 접속시 브라우저는 서버에게 HTML 및 JS 파일을 요청하게 된다.
HTML 파일에는 위 사진과 id가 root인 텅텅 빈 HTML 파일이 오게되고 이후 용량이 무거운 JS파일이 다운 후 DOM을 이 태그 안에 그리면서 rendering이 된다.</p>
<h3 id="단점">단점</h3>
<p>이렇게 rendering이 client에서 진행이 되면 속도가 느릴 뿐 아니라 웹 크롤러가 이 사이트가 어떤 사이트인지 알 수가 없어 SEO(검색 엔진 최적화)에 불리해 구글과 같은 검색엔진에 검색을 해도 노출이 잘 되지 않는다.</p>
<hr/>

<h1 id="pre-render">Pre-render</h1>
<blockquote>
<p>브라우저에 컴포넌트들이 rendering 되기 전 먼저 rendering 과정을 진행 하여 빠르게 화면에 보여주는 방식</p>
</blockquote>
<p>NextJS에서는 Static Site Generation (SSG) 방식과 Server-side Rendering (SSR) 방식으로 pre-render를 진행한다.</p>
<p>아래 사진과 같이 DB에 게시글 정보가 있다고 시나리오를 설정 하자.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/453ce1a4-24c9-4d21-a327-16218762bb19/image.png" alt=""></p>
<hr/>

<h2 id="static-site-generation-ssg">Static Site Generation (SSG)</h2>
<blockquote>
<p>하나의 페이지가 SSG 를 사용한다면, 이 페이지의 HTML은 <strong>build time</strong>에 생성된다.</p>
</blockquote>
<p>개발 -&gt; 빌드 -&gt; 배포 과정을 통해 유저가 웹사이트를 확인할 수 있다.</p>
<p>빌드를 하게 되면 코드를 수정하고 싶을 경우 다시 빌드 후 배포 과정을 밟아야 한다.</p>
<p>물론 <strong>블로그, 자기소개서나 소개페이지</strong> 같은 경우 자주 변경되는 페이지가 아니다 보니 이 SSG 방식을 쓰면 매우 유리하다. 그냥 완성되어 있는 HTML 정적 페이지 자체를 렌더링 시키는것은 브라우저 입장에서 별거 아니기 떄문에 매우매우 빠르기 때문이다.😎</p>
<p>매번 HTML 파일에다가 하드코딩을 해서 빌드 -&gt; 배포 할 수도 있다.
하지만, 서버에 게시글 정보를 저장하고 그 데이터를 fetching해서도 <code>정적 페이지</code>를 만들고 싶을때가 있을것이다.
그럴 경우에 <code>getStaticProps</code> 를 이용해보자.</p>
<h3 id="getstaticprops">getStaticProps</h3>
<img  src = "https://velog.velcdn.com/images/bae-sh/post/12e649fa-59ac-4ca9-acf3-83d7a2d74116/image.png" width="50%"/>

<p>위 코드의 경우 우리가 일반적으로 생각하는 정적페이지라고 생각해보자. 이때에는 서버로 부터 fetching 할 필요도 없으니 그냥 정적파일로 빌드 되어서 배포되면 된다.</p>
<p>하지만 게시글 정보가 서버로 부터 받아야 할 경우가 있을 것이다. 이때에는 <code>getStaticProps</code>를 사용해보자.</p>
<img  src = "https://velog.velcdn.com/images/bae-sh/post/41b63509-4bd1-4bc7-8b17-5ecb19821311/image.png" width="50%"/>


<p>이렇게 <code>getStaticProps</code>를 사용하면 <strong>build Time</strong>때 이 데이터를 fetching하여 게시글을 받아오고 이 데이터를 기반으로 정적 파일을 만든다.</p>
<p>그렇기에 계속해서 정적파일을 유지할 수 있다. <strong>하지만 Data base에 있는 게시글 정보가 수정 되면 어떻게 될까?</strong> 클라이언트 측에서는 빌드타임때 사용한 예전 게시글 정보를 기반으로 보여주기 때문에 새로 업데이트를 하기 위해서는 다시 빌드 후 배포를 진행해야 한다.</p>
<p>자주 변경되는 데이터의 경우 정적파일이 아무리 빠르게 렌더링이 되어서 좋다 하더라도 데이터가 바뀔때마다 <strong>빌드 -&gt; 배포</strong> 과정을 진행해야 하니 얼마나 비효율 적인가...</p>
<p>그래서 데이터가 잘 바뀌지 않는 포트폴리오나 블로그에 사용하자.</p>
<blockquote>
<p>한가지 더 추가하자면 NextJS Default는 SSG 이다. 
만약 페이지에 데이터를 동적으로 작용하는 것이 없다면 그 페이지 자체는 정적 파일로 만들어도 충분하지 않을까? Next JS는 내부적으로 판단 하여 해당 페이지를 정적 파일로 만들어 버린다.</p>
</blockquote>
<hr/>

<h2 id="server-side-rendering-ssr">Server-side Rendering (SSR)</h2>
<blockquote>
<p>SSG 렌더링을 사용하기에는 데이터가 자주 변하는데 그럼 어떻게 할까?</p>
</blockquote>
<p>이 때 SSR 방식을 사용하곤 한다. SSG의 경우 서버로 부터 데이터 요청을 <strong>빌드타임</strong>에 한다.</p>
<p>하지만, SSR의 경우 클라이언트가 <strong>프론트엔드 서버</strong>에게 Page request를 할때 프론트엔드 서버가 <strong>백엔드 서버</strong>에게 데이터를 요청한다.</p>
<p>대략적인 흐름을 보자.</p>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/b0cc8507-10fe-4396-bd86-4538d1688512/image.png" alt=""></p>
<blockquote>
<ol>
<li>클라이언트는 프론트 서버에게 브라우저에 보여줄 해당 페이지 자원(HTML,CSS,JS 등등)을 요청한다.</li>
<li>Front 서버는 Back 서버에게 필요한 정보(게시글)를 요청하여 받는다.</li>
<li>Front 서버는 이 데이터를 기반으로 HTML,CSS 등 필요한 자원을 만든다.</li>
<li>Client에게 페이지에 필요한 자원를 제공한다.</li>
</ol>
</blockquote>
<p>SSG와 다른점이 무엇일까? 먼저 데이터를 서버로 부터 언제 받는가이다.</p>
<p>SSR은 새로고침을 할때마다 매번 Client는 프론트서버에게 해당 페이지 자원을 요청하고 게시글 정보를 새롭게 받아오기 때문에 서버의 데이터가 변경되었다 하더라도 새로 빌드 -&gt; 배포 할 필요가 없다.</p>
<p>또한, CSR의 경우 클라이언트 렌더링 할때 데이터를 받아오는 반면 SSR의 경우 page request시 데이터를 요청하기에 빠르게 받아올 수 있다.
하지만, 데이터를 프론트서버로 부터 한번만 받아오기 떄문에 새로고침을 하지 않는 이상 데이터가 re-fetching이 되지 않는 것이 단점이다.</p>
<p>그렇기 때문에 자주 변경되는 정보들은 SSR 방식을 사용하면 현재 페이지에 보여지고 있는 정보와 실제 정보와 다를 수 있다.</p>
<h3 id="getserversideprops">getServerSideProps</h3>
<img  src = "https://velog.velcdn.com/images/bae-sh/post/794286e8-4fa5-472e-9771-55789b3741ae/image.png" width="50%"/>


<p>사용법은 SSG와 거의 다르지 않다.
page 파일 내에서 <code>getStaticProps -&gt; getServerSideProps</code> 로만 바꾸면 된다.</p>
<p>그럼 NextJS는 <em>아하, 이 데이터는 빌드타임이 아니라 server-side에서 요청해야겠다</em> 로 판단한다.</p>
<blockquote>
<p>지금까지 SSG와 SSR에서 데이터를 언제 어떻게 fetching 하여 HTML 파일을 만들어 rendering을 하는지 알아 보았다. Next는 Page 단위로 rendering 방식을 결정하고 있음을 기억하자.</p>
</blockquote>
<hr/>

<h2 id="client-side-rendering-csr">Client-Side Rendering (CSR)</h2>
<p>그럼 REACT의 CSR 방식에서는 데이터를 언제 어떻게 불러왔는가 생각을 해보며 글을 정리 해보자.</p>
<img  src = "https://velog.velcdn.com/images/bae-sh/post/335eb772-69d0-4ed2-a293-b94326f990f9/image.png" width="50%"/>

<blockquote>
<ol>
<li>클라이언트가 프론트 서버로 부터 빈 HTML 파일과 JS 파일을 받는다. </li>
<li>클라이언트에서 렌더링이 시작되고 Mypage가 렌더링이 되면 post 상태는 null로 초기화가 된다. </li>
<li>useEffect 훅을 통해 data를 fetching 한다.</li>
<li>setPost를 통해 data가 백엔드 서버로 부터 받은 정보를 기반으로 post상태를 변화시킨다.</li>
<li>클라이언트는 게시글을 볼 수 있다.</li>
</ol>
</blockquote>
<p>차이점이 보이는가? SSG와 SSR의 경우 1번일 때 이미 서버로 부터 데이터를 다 받아온 상태이다.
REACT의 CSR의 경우 3번에서 데이터를 받아온다.</p>
<p>REACT의 경우에는 1번에서 용량이 큰 JS 파일을 한번만 다운 받으면 그 뒤에는 페이지를 옮기 더라도 1번이 생략되어 빠르게 작동한다.</p>
<p>SSR의 경우 페이지 이동시 1번(서버로 부터 페이지 자원을 받음)부터 시작하기에 속도측면에서 역전당한다.</p>
<p>하지만 NextJS에서도 useEffect를 통해 데이터를 Client-sdie에서 받을 수 있다. </p>
<hr/>

<h2 id="결론">결론</h2>
<ol>
<li>NextJS는 페이지 단위로 렌더링을 결정한다.</li>
<li>SSG는 빌드타임에 데이터를 fetching 할 수 있다.</li>
<li>SSR은 백엔드 서버에게 페이지 자원 요청시 데이터를 fetching한다.</li>
<li>useEffect를 통해 NextJS에서도 Client-side에서 데이터를 fetching 할 수 있다.</li>
<li>개발자는 페이지마다 어떤 렌더링 방식을 사용할 것 인지는 페이지 특성을 고려하자.</li>
</ol>
<blockquote>
<p>출처 : <a src ='https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation'>NextJS Docs</a>
<a src ='https://watermelonlike.tistory.com/180'>Next.js - SSR이라고만 알고있었다.</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git merge에 관하여]]></title>
            <link>https://velog.io/@bae-sh/Git-merge%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@bae-sh/Git-merge%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Mon, 08 May 2023 16:57:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;우리 팀은 merge 전략을 <strong>rebase</strong>로 하자&quot; 라는 말과 함께 프로젝트를 진행하였다.
하지만 rebase merge를 하려는 중 얼마 못가 <strong>conflict</strong>를 마주쳤고 정확한 원인을 알지 못했다.
merge에 대해 정확하게 알지 못하고 있다고 깨닫고이번 기회에 <strong>merge</strong>에 대해 깊게 공부를 해보려 한다. 
이 글을 통해 다른 개발자들도 <strong>merge</strong> 개념의 이해에 도움이 되면 좋겠다.</p>
</blockquote>
<hr/>

<h1 id="merge-란">Merge 란?</h1>
<blockquote>
<p>브랜치 간에 변경 사항을 병합하는 작업이다. </p>
</blockquote>
<p>Merge에 방식에는 크게 
1-1. <strong>Fast forward</strong> merge
1-2. <strong>Recursive</strong> merge
2. <strong>Squash</strong> and merge
3. <strong>Rebase</strong> and merge
가 있다.</p>
<img src='https://velog.velcdn.com/images/bae-sh/post/44d3e47b-25c1-44f3-b126-171011653f22/image.png' width = "70%" height = "10%">

<p>깃허브에서 merge하는 방식을 보면 3가지 방식이 있는것 또한 확인할 수 있고
각각의 Merge의 특징을 살펴보며 차이점을 살펴보자.</p>
<hr/>

<h2 id="1-1-fast-forward-merge">1-1. Fast forward merge</h2>
<h4 id="before-merge">Before merge</h4>
 <img src='https://velog.velcdn.com/images/bae-sh/post/8e6acb28-0281-447a-a75b-15a67694c154/image.png' width = "70%" height = "10%">

<h4 id="after-merge">After merge</h4>
<img src='https://velog.velcdn.com/images/bae-sh/post/fea53a4a-7498-4feb-971b-b460606a930f/image.png' width = "70%" height = "10%">

<p>가장 기본적인 Merge 방식이다. main 브랜치에서 dev 브랜치로 분기 후 dev 브랜치에서 작업을 하다 이 작업 결과를 main 브랜치에 그대로 옮기는 방식이다.</p>
<p>dev의 B와 C 커밋 그 자체가 main에 복사 된다고 생각하면 된다. (커밋의 해쉬값 까지 동일하다)</p>
<h3 id="조건">조건</h3>
<p>이 <strong>Fast forward merge</strong>를 할 수 있는 경우는 dev가 main에서 분기 후 main에서는 <strong>어떠한 작업도 하지 않을 경우</strong>이다. </p>
<h2 id="1-2-recursive-merge">1-2. Recursive merge</h2>
<h4 id="before-merge-1">Before merge</h4>
 <img src='https://velog.velcdn.com/images/bae-sh/post/8e2c2558-a077-46a5-924a-d8f6b21993e1/image.png' width = "70%" height = "10%">

<h4 id="after-merge-1">After merge</h4>
<img src='https://velog.velcdn.com/images/bae-sh/post/6eda506f-e7eb-4b09-ac6e-42afa0b9ca67/image.png' width = "70%" height = "10%">

<p>가장 많이 사용하는 일반적인 Merge 이다.
개발을 진행해보며 Fast forward merge 처럼 main에서 어떠한 작업도 이루어 지지 않는 경우는 거의 없었다. 
Merge 하기 전 팀원들의 작업물 들이 main에 먼저 Commit된 경우가 대다수이기 때문이다.</p>
<p>이처럼 A Commit 에서 분기된 dev 브랜치가 B,C 작업을, A Commit 이후 main 브랜치에서 X,Y작업을 진행한 상황에서 dev 브랜치의 작업을 main에 Merge를 해야할 때가 있다.</p>
<p>이 때에는 Git 입장에서 Y Commit과 C Commit 중 어떠한 것이 먼저 일어난 것인지 알 수 없고 따라서 HEAD를 어디에 이동 시켜야 할지 모른다.</p>
<p>따라서 새로운 Z Commit 을 생성하고 dev 브랜치 작업물을 main에 merge 한다.
그럼 main에는 B,C,Z 3개의 Commit 내역이 추가된다.</p>
<p>이것이 <strong>Recursive merge</strong>(Commit 후 merge를 하기에 commit merge라고 많이 부르는것 같다)이다. </p>
<h3 id="충돌">충돌</h3>
<p>main에서 작업했던 것(X,Y)와 dev에 작업했던 것(B,C)이 충돌이 일어나곤 한다.
같은 파일을 수정하는 경우가 있기 때문이다.
이때에는 어떤 코드를 사용할 지는 사람이 결정해서 코드를 수정 후 Z와 같이 Commit을 진행한다.
이것은 <strong>Resolve conflict</strong> 라고 한다.</p>
<hr/>

<h2 id="2-squash-merge">2. Squash merge</h2>
<h4 id="before-merge-2">Before merge</h4>
 <img src='https://velog.velcdn.com/images/bae-sh/post/8e2c2558-a077-46a5-924a-d8f6b21993e1/image.png' width = "70%" height = "10%">

<h4 id="after-merge-2">After merge</h4>
<img src='https://velog.velcdn.com/images/bae-sh/post/0e5b457a-8eed-4d5f-910d-5e632385c980/image.png' width = "70%" height = "10%">

<p>개발을 하면서 한번도 사용한 적이 없는 Merge 방식이다.
공부를 하면서 이 merge 방식도 상당히 매력이 있다고 느꼈다.
main branch commit 내역에 dev의 commit이 합쳐져 하나의 Commit으로 깔끔하게 보이기 때문이다.</p>
<p>위와 같이 dev에서 main 으로 merge를 하기 전 B,C가 합쳐진 새로운 D Commit을 만들고 main으로 merge 된다. 
이때 main 브랜치는 B,C 커밋은 존재하지 않고 D만 존재하게 되어 깔끔한 Commit 이력을 유지할 수 있다.
<del>하지만 다른 브랜치에서 열심히 작성한 커밋 이력이 모두 없어지기에 잔디가 심어지지 않는다 ㅠㅠ</del></p>
<hr/>

<h2 id="3-rebase-merge">3. Rebase merge</h2>
<h4 id="before-merge-3">Before merge</h4>
 <img src='https://velog.velcdn.com/images/bae-sh/post/8e2c2558-a077-46a5-924a-d8f6b21993e1/image.png' width = "70%" height = "10%">

<h4 id="after-merge-3">After merge</h4>
<img src='https://velog.velcdn.com/images/bae-sh/post/e667c351-bb17-4733-805f-b989e0c6bc66/image.png' width = "70%" height = "10%">

<p>마지막으로 <strong>rebase merge</strong>이다. 이 게시글을 쓰게된 merge의 장본인이라고 할 수 있다.
이 merge 방식은 1-1의 Fast-forward merge 방식으로 볼 수 있는데 다른 점은 main에 어떠한 작업이 존재해도 merge를 할 수 있다는 것이다.</p>
<p>또한, Fast-forward merge 일 경우에 dev 브랜치의 Commit들이 그대로(해시값 유지) main 브랜치로 복사가 됐지만, rebase의 경우 커밋내용은 동일하지만 새로운 커밋들(해시값이 다름)이 main브랜치에 복사된다고 할 수 있다.</p>
<p>merge commit을 하지 않기 때문에 main 브랜치 하나로 작업한 것 처럼 보여 매우 깔끔한 commit내용을 유지할 수 있다.</p>
<h3 id="충돌-1">충돌</h3>
<p>그럼 1-1의 Fast-forward와 다르게 Rebase merge의 경우 dev 분기 후 main에 작업이 존재할 수 있고 이것은 dev와 충돌이 발생할 수 있다.
이때 dev와 main의 충돌 난 부분을 하나하나 해결하고 rebase를 다시 진행하면 해결할 수 있다.</p>
<img src='https://velog.velcdn.com/images/bae-sh/post/6a52246a-c80e-4b23-8699-280ee3c0d79b/image.png' width = "70%" height = "10%">

<p>하지만 나의 경우 위의 방식처럼 dev로 main을 먼저 Recursive merge을 진행한 뒤 다시 main에 Rebase merge를 진행하려고 하여 D의 Commit은 main의 부모를 가지고 이 브랜치 때문에 꼬여서 Rebase merge를 진행하지 못하였다.
이 경우에는 다시 main 브랜치로 Commit Merge를 진행할 수 밖에 없다.</p>
<blockquote>
<p>결론적으로 Rebase는 main 브랜치와 dev 브랜치가 충돌이 나지 않을 경우 merge를 진행할 수 있고 만약 충돌이 난다면 dev 브랜치에서 충돌난 부분을 하나하나 풀어 Rebase를 진행해야한다.
main의 브랜치를 dev로 merge 후 다시 main으로 merge를 진행하려고 하면 Rebase를 쓸 수 없고 commit Merge를 진행하자.</p>
</blockquote>
<hr/>


<blockquote>
<p>참고 : <a href="https://mangchhe.github.io/git/2021/09/04/GitMerge/">https://mangchhe.github.io/git/2021/09/04/GitMerge/</a>
<a href="https://kotlinworld.com/277">https://kotlinworld.com/277</a>
<a href="https://goddaehee.tistory.com/253">https://goddaehee.tistory.com/253</a>
<a href="https://wonyong-jang.github.io/git/2021/02/05/Github-Rebase.html">https://wonyong-jang.github.io/git/2021/02/05/Github-Rebase.html</a>
<a href="https://backlog.com/git-tutorial/kr/stepup/stepup2_8.html">https://backlog.com/git-tutorial/kr/stepup/stepup2_8.html</a> </p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[HTTP란?]]></title>
            <link>https://velog.io/@bae-sh/HTTP</link>
            <guid>https://velog.io/@bae-sh/HTTP</guid>
            <pubDate>Tue, 13 Sep 2022 14:59:37 GMT</pubDate>
            <description><![CDATA[<h1 id="httphyper-text-transfer-protocol">HTTP(Hyper Text Transfer Protocol)?</h1>
<blockquote>
<p>클라이언트와 서버가 통신할 때 <strong>하이퍼텍스트(HTML,CSS,JS 등등)을 교환하는 규약</strong>을 말한다.</p>
</blockquote>
<p>이 규약이 없으면 클라이언트와 서버가 데이터를 교환할 수가 없다.</p>
<h2 id="http의-특징">HTTP의 특징</h2>
<p>클라이언트가 어떠한 요청을 하면 그에 대한 응답을 서버는 해야 한다.
이때 서버는 클라이언트의 상태를 보존하지 않고(<strong>무상태 프로토콜</strong>) 연결을 계속 유지 하지도 않는다(<strong>비 연결성</strong>). </p>
<p>이러한 특징을 가지는 이유를 아래에서 알아보자.</p>
<h2 id="무상태-프로토콜---stateless">무상태 프로토콜 - Stateless</h2>
<p>간단하게 말하면 서버는 클라이언트들의 상태를 저장하지 않는 것이다. 즉, 매번 요청을 할때마다 필요한 상태들을 클라이언트에서 보내주어야 서버는 이해하게 된다. 아래의 상담원(서버)과의 예시를 보자.</p>
<blockquote>
<p>A(클라이언트) : 안녕하세요? A 입니다.
상담원A(서버) : 반갑습니다. A님. 무엇을 도와 드릴까요?
A(클라이언트) : B에게 5000원을 송금하고 싶습니다.
상담원B(서버) : 성함이 어떻게 되세요?</p>
</blockquote>
<p>위의 예시와 같이 상담원B의 경우 <strong>A &lt;-&gt; 상담원A</strong> 의 정보 교환을 알지 못한다. 그래서 상담원 B는 A의 성함을 알지 못한다. 이런 상황을 피하려면 한번에 </p>
<blockquote>
<p>&#39;제 이름은 A이고 B에게 5000원을 송금하고 싶습니다&#39; </p>
</blockquote>
<p>와 같이 필요한 상태를 한번에 보내야 할 것이다.</p>
<p>이러한 장점은 무엇일까? 먼저 서버에 부담을 덜어준다. 모든 클라이언트의 정보를 담고 있는것은 서버에 부담이 되기 때문에 필요할 경우에 클라이언트에서 보내주는것이 서버 입장에선 편하다. 그래서 보다 많은 클라이언트와 통신 할 수 있다.</p>
<p>하지만 단점의 경우 위에서 말했듯이 매번 클라이언트가 서버에게 상태를 보내야 하는 것이다.</p>
<p>그래서 매번 필요한 자주쓰이는 상태는 서버에 저장하고 상대적으로 덜 쓰이는 것은 클라이언트가 보내주는 것이 좋아 보인다.</p>
<h2 id="비-연결성---connectionless">비 연결성 - Connectionless</h2>
<p>HTTP 1.0 기준 HTTP는 연결을 유지하지 않는 모델이다.</p>
<p>왜 연결을 유지 하지 않을까? 위의 상담원 예시를 확장 해보자. 만약 연결성을 유지 한다고 가정하자.</p>
<blockquote>
<p>A(클라이언트) : 안녕하세요? A 입니다. B에게 5000원을 송금하고 싶습니다.
상담원A(서버) : 반갑습니다. A님. B에게 5000원이 송금 되었습니다.
A(클라이언트) : 감사합니다. (전화는 계속 유지된다.)
(1시간 뒤 )
A(클라이언트) : B에게 10000원을 송금하고 싶습니다.</p>
</blockquote>
<p>위의 예시와 같이 A는 다음 요청이 1시간 뒤에 발생하는데 연결성을 유지 될 경우에 상담원 A는 아무 일도 하지 못하고 A의 응답만 기다려야하는 비효율성이 나타난다. 따라서 연결성을 유지 하지 않고 필요할 경우에 다시 연결을 하는 방식이다. (이는 컴퓨터 구조시간에 배운 멀티 프로그래밍과 비슷하다고 생각한다. 진행중인 프로세스가 블록 상태가 된 경우 CPU는 다른 프로세스를 수행한다.)</p>
<h2 id="http-vs-https">HTTP vs HTTPS</h2>
<p>그런데 우리 주소창을 보면 http가 아닌 https로 되어 있는 것을 볼 수 있다. 이건 왜그럴까??</p>
<p>예전보다 보안이 훨씬 중요해진 요즘 보안은 필수이다. 하지만 http의 프로토콜의 경우 서버에서 부터 클라이언트로 전송되는 정보가 암호화되지 않는것이다. 만약 이것이 도난 당하면 큰일이기 때문에 SSL(보안 소켓 계층)을 사용하여 서버와 클라이언트의 정보 교환을 암호화 한다.</p>
<p>또한 구글에서도 검색 순위 결과에 약간의 가산점을 준다고 하는데 이는 구글에서도 HTTPS의 보안성을 강조함을 알 수 있다. 그리고 유저 또한 보안상 안전한 사이트를 방문할 수 밖에 없다.</p>
<blockquote>
<p>참고
<a href="https://developer.mozilla.org/ko/docs/Web/HTTP/Overview">https://developer.mozilla.org/ko/docs/Web/HTTP/Overview</a>
<a href="https://hanamon.kr/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-http-http%EB%9E%80-%ED%8A%B9%EC%A7%95-%EB%AC%B4%EC%83%81%ED%83%9C-%EB%B9%84%EC%97%B0%EA%B2%B0%EC%84%B1/">https://hanamon.kr/%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-http-http%EB%9E%80-%ED%8A%B9%EC%A7%95-%EB%AC%B4%EC%83%81%ED%83%9C-%EB%B9%84%EC%97%B0%EA%B2%B0%EC%84%B1/</a>
<a href="https://blog.wishket.com/http-vs-https-%EC%B0%A8%EC%9D%B4-%EC%95%8C%EB%A9%B4-%EC%82%AC%EC%9D%B4%ED%8A%B8%EC%9D%98-%EB%A0%88%EB%B2%A8%EC%9D%B4-%EB%B3%B4%EC%9D%B8%EB%8B%A4/">https://blog.wishket.com/http-vs-https-%EC%B0%A8%EC%9D%B4-%EC%95%8C%EB%A9%B4-%EC%82%AC%EC%9D%B4%ED%8A%B8%EC%9D%98-%EB%A0%88%EB%B2%A8%EC%9D%B4-%EB%B3%B4%EC%9D%B8%EB%8B%A4/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[클라이언트 사이드 렌더링과 서버 사이드 렌더링에 관하여]]></title>
            <link>https://velog.io/@bae-sh/%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EA%B3%BC-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</link>
            <guid>https://velog.io/@bae-sh/%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EA%B3%BC-%EC%84%9C%EB%B2%84-%EC%82%AC%EC%9D%B4%EB%93%9C-%EB%A0%8C%EB%8D%94%EB%A7%81%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC</guid>
            <pubDate>Wed, 31 Aug 2022 09:33:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><em>우리는 <code>SEO(검색 엔진 최적화)</code>가 중요하니 <code>SSR(서버 사이드 렌더링)</code> 방식으로 개발 진행 합시다!!</em></p>
</blockquote>
<p>개발 전 회의를 하다가 위와 같은 말이 나왔었다. 매번 회의할때 모르는 기술이 나오면 당황하고 자신감을 잃는다. 하지만 계속해서 질문을 하고 하나하나 알아가다 보면 생각보다 별거 아니였네? 라는 느낌을 받는다.
사람은 모르는 것을 마주치면 당황하는 것은 당연하다고 생각한다. 
모르는 것을 아는 것으로 바꿀때 자신이 성장함을 잊지 말자.</p>
<h2 id="csr클라이언트-사이드-렌더링">CSR(클라이언트 사이드 렌더링)</h2>
<p>SSR을 알아보기 전에 CSR을 먼저 알아보자. </p>
<p><code>CSR</code>이란 쉽게 말해서 클라이언트에서 모든 것을 처리하는 방식이다. 대표적으로 React가 있다.</p>
<h3 id="spa">SPA</h3>
<p>화면간 이동시 깜박임이 없는 <code>SPA(Single Page Application)</code>이 대세가 되면서 CSR이 급 부상했다. 예전 웹페이지의 경우 화면 이동시 서버에서 새로 html 파일을 받아 화면에 보여주어야 해서 깜박임이 존재했다.
하지만 SPA의 경우 데이터 교체만 일어나 UX가 좋은 장점이 있다.</p>
<h3 id="csr의-과정">CSR의 과정</h3>
<img src='https://velog.velcdn.com/images/bae-sh/post/8111d613-0d9a-4db3-8b12-be5d1cac67f1/image.png' width = "70%" height = "10%">



<p>위 흐름을 보면 먼저 서버로 부터 html 파일을 요청 한다. 이 html 파일의 경우 클라이언트에서 유저가 볼 컨텐츠 들이 아닌 모듈화 된 JS파일 링크만 들어있다. 그래서 현재 페이지는 빈화면이고 서버로부터 JS파일을 다운 받는다. 이 모듈화 된 JS파일은 클라이언트에서 사용할 모든 필요한 정보들이 들어있어 다운을 받는데 시간이 오래 걸린다. 다운이 완료되면 마침내 유저가 화면을 볼 수 있게 된다.</p>
<h3 id="react-를-사용하여-실습">React 를 사용하여 실습</h3>
<p>그림만 보는 것 보다 직접 눈으로 보는 것이 기억에 오래 남고 이해하기 쉽기 때문에 React를 이용하여 CSR의 진행 과정을 확인해 보자.</p>
<h4 id="localhosthtml">localhost.html</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/dd9e6991-57b0-4459-a863-d0683f8289d1/image.png" alt=""></p>
<p>npx create-react-app 을 통해 만들어진 프로젝트의 처음 화면이다.
Network를 보면 <code>localhost의 html</code> 파일을 볼수있다. 화면에는 리액트 마크와 여러 content가 있다. 하지만 body안에는 id가 root인 텅텅 빈 div 태그를 볼 수 있다.
이 파일이 위에서 말한 html 요청한 파일이다.</p>
<p>또한 script 부분을 보면 <code>bundle.js</code> 파일을 불러 오는 것을 알 수 있다.</p>
<h4 id="bundlejs">bundle.js</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/1a2ce94a-c1d4-4f2d-b26c-6437596584da/image.png" alt=""></p>
<p>이 부분은 <code>bundle.js</code> 파일 부분이다. 가독성이 떨어지는 엄청 복잡한 코드로 되어있다. 이는 우리가 필요한 CSS,JS, 이미지 등등 리소스들을 하나의 js 파일에 번들화 되어있기 때문이다. 번들화는 주로 <code>Webpack</code>을 이용한다. </p>
<h4 id="network">Network</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/69845f29-f94f-4450-902a-f94483224f34/image.png" alt=""></p>
<p>이번에는 파일이 어느 시점에 다운이 되는지 확인해보자</p>
<p>처음 시작부분에 0<del>23ms 까지 한번, 27ms</del>97ms 까지 한번 총 2구간이 진행됨을 알 수 있다.
처음에는 html부분을 다운받고 그 뒤 js 파일을 다운받는 것이다.</p>
<h3 id="csr의-단점">CSR의 단점</h3>
<h4 id="1-텅텅-빈-화면">1. 텅텅 빈 화면</h4>
<p>과정에서 알 수 있듯이 하나의 용량이 큰 JS파일을 다운 완료 전에는 유저는 빈 화면만 보게 된다. JS 파일이 크면 클수록 로딩시간은 더 길어지게 되고 유저는 지루함을 느낄 수 밖에 없다.
이를 해결하는 방법으로 코드 스플릿팅을 통해 하나의 JS가 아닌 여러개의 번들로 나누는 방법이 있다.</p>
<h4 id="2-seo검색-엔진-최적화의-문제">2. SEO(검색 엔진 최적화)의 문제</h4>
<p>구글과 네이버와 같은 검색엔진은 검색을 할 때 크롤링을 통해 사용자에게 사이트를 제공한다. 이 크롤링 방식에 있어서 검색엔진은 html 파일을 탐색하게 되는데 CSR의 경우 텅텅 빈 html을 보여주므로 검색에 비효율 적일 수 밖에 없다.</p>
<h2 id="ssr서버-사이드-렌더링">SSR(서버 사이드 렌더링)</h2>
<p>위와 같은 CSR의 단점을 해결하기 위해 SSR 렌더링을 사용한다. SSR의 경우 html과 css의 부분은 서버에서 먼저 받아와 CSR 단점 1번을 해결해준다. 그뒤 JS 파일을 받아오고 유저는 이 이후에 동적으로 웹을 사용할 수 있게된다. 또한 html에 컨텐츠들이 들어있기 때문에 CSR 단점 2번인 SEO의 문제도 해결할 수 있다.</p>
<h3 id="ssr의-과정">SSR의 과정</h3>
<img src='https://velog.velcdn.com/images/bae-sh/post/283bc91d-b897-43bf-827a-b771f6219169/image.png' width = "70%" height = "10%">

<p>먼저 서버에서 렌더된 HTML 파일을 브라우저에 제공한다. 브라우저는 즉각적으로 유저에게 컨텐츠를 보여준다. 그 뒤 JS 파일이 다시 다운받게 되고 유저는 JS에 관련된 기능들을 사용할 수 있게된다.</p>
<h3 id="next를-이용한-실습">Next를 이용한 실습</h3>
<p>SSR을 사용하기 위하여 Next.js를 이용하여 어떤식으로 파일을 받아오는지 확인해 보자.</p>
<h4 id="localhosthtml-1">localhost.html</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/3b4de0c3-b98b-41c4-85fb-ab68a7814d9b/image.png" alt=""></p>
<p>npx create-next-app 을 통해 만들어진 첫 화면이다.
이전 React와 달리 localhost html 파일 내부 body에 content들이 들어가 있다. 이것이 CSR과 SSR의 가장 큰 차이점이다. 이로써 유저는 조금 더 빠르게 첫 화면을 볼 수 있고 검색엔진이 크롤링 하기에 유리하다. </p>
<h4 id="network-1">Network</h4>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/182c3bcc-a959-4d68-a692-53c4a3eef5ff/image.png" alt="">
네트워크 창을 보면 0<del>100ms 까지 하나의 파일 120</del>270ms 정도에 여러개의 파일이 다운되는 것을 볼 수있다. 이는 처음에 html이 다운되는 시간, 이후에 js 파일이 다운 되는 것을 알 수 있다.</p>
<h3 id="ssr의-단점">SSR의 단점</h3>
<p>물론 SSR도 단점이 존재한다. 단점이 없으면 CSR을 쓰지 않지 않겠는가?</p>
<h4 id="1-화면-깜박임">1. 화면 깜박임</h4>
<p>CSR의 경우 SPA이기 때문에 페이지 이동시 부드럽게 넘어감을 알 수 있다. 하지만 SSR의 경우는 화면이 깜박 거리는 것을 볼 수 있는데 이는 UX 측면에서 좋지 않다.</p>
<h4 id="2-html과-js의-공백">2. html과 js의 공백</h4>
<p>네트워크 창에서 보면 html 다운되는 시간과 js가 다운되는 시간이 차이가 남을 알 수 있다. 이는 유저가 html 다운이 완료되어 첫 화면을 볼 수 있을 시점에 JS가 다운이 되어있지 않으면 기능이 정상 작동하지 않을 수 있다.</p>
<h3 id="csrssr">CSR+SSR</h3>
<p>하지만 Next에서는 Link 태그를 이용해 첫화면은 SSR 그 뒤에 화면 전환은 CSR처럼 사용할 수 있다. 이로 인해 선택적으로 유리한 렌더링 방식을 이용할 수 있다.</p>
<h4 id="참고">참고</h4>
<blockquote>
<p><a href="https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8">https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8</a>
<a href="https://www.youtube.com/watch?v=iZ9csAfU5Os">https://www.youtube.com/watch?v=iZ9csAfU5Os</a>
<a href="https://ctdlog.tistory.com/46">https://ctdlog.tistory.com/46</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[자신만의 Hook을 만들어 보자]]></title>
            <link>https://velog.io/@bae-sh/%EC%9E%90%EC%8B%A0%EB%A7%8C%EC%9D%98-Hook%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%96%B4-%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@bae-sh/%EC%9E%90%EC%8B%A0%EB%A7%8C%EC%9D%98-Hook%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%96%B4-%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 18 Aug 2022 13:00:05 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>지인의 부탁으로 쇼핑몰 제품 정보 관리 페이지를 제작중 문제가 발생하였다.
<img src="https://velog.velcdn.com/images/bae-sh/post/33f59eae-3a6c-4208-b894-6232eee39ea2/image.png" alt=""></p>
<p>위와 같이 하나의 제품에는 사진, 제품이름, 영어이름, 중국어이름, 운송장번호 등등 많은 Data값이 존재한다. 
이 데이터를 어떤식으로 관리할까 고민하다가 처음에는 아래와 비슷한 방식으로 하나의 상태에 모든 데이터를 넣었었다.</p>
<h3 id="잘못된-방식">잘못된 방식</h3>
<pre><code class="language-javascript">const [product, setProduct] = useState([
  {ko:&#39;사다리 모던 진열대&#39;,en : &#39;Ladder modern display&#39;, ... ,Hscode:&#39;9403209000&#39;,id :&#39;acxz123czxdas&#39;}
  ,{ko:&#39;대왕 거위 모찌 쿠션&#39;,en :&#39;Giant goose mochi cushion&#39;,...,Hscode:&#39;9403209000&#39;,id :&#39;sa5d135zxcsda&#39;}
  ,...]);</code></pre>
<p>처음에는 상위 컴포넌트에서 하나의 상태에 Data를 관리하여 하위 컴포넌트에게 props 전달하는 방식으로 사용하였다. 하지만 data의 양이 점점 늘어날 수록 데이터 변경 시 <strong>딜레이</strong>가 발생하기 시작했다.</p>
<p>위와 같은 상품 200개가 <code>product</code> 상태 하나에 전부 들어가 있다고 하자. 그럼 200개중 하나의 객체 프로퍼티값을 <code>setProduct</code>를 통해 수정을 하게되면 나머지 199개의 불필요한 중복이 발생하게 된다. 즉, 값이 변경되는 것은 하나의 상품인데 199개의 상품도 동시에 재렌더링이 발생하게 되는 것이다. 왜냐하면 리액트에서는 재렌더링 조건중 하나가 상태값이 변할 경우이기 때문이다. 그러다 보니 유저가 한글자씩 키보드에서 입력할떄마다 0.5~1 초 정도의 딜레이가 생기고 불만이 생긴 것이다.</p>
<p>이것을 해결하기 위해 <code>Custom Hook</code>을 구현하여 데이터 단위를 쪼개어 주었다.</p>
<p>여기서 <code>Hook</code>이란 무엇인지 알고 넘어가 보자.</p>
<h2 id="hook">Hook</h2>
<blockquote>
<p>Hook은 React 버전 16.8부터 React 요소로 새로 추가되었습니다. Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있습니다.</p>
</blockquote>
<p>React는 <code>클래스형 컴포넌트</code>와 <code>함수형 컴포넌트</code>로 구현할 수 있다.
클래스형 컴포넌트는 이해하기도 어렵고 클래스 컴포넌트 사이에서 상태로직을 재사용 하기가 어렵다. 
함수형태 컴포넌트에서는 비교적 쉽고 간단하기 때문에 선호하지만 이전에는<code>state</code>와 <code>Life Cycle</code>을 사용하기 위해선 어쩔수 없이 클래스형 컴포넌트를 사용할 수 밖에 없었다.</p>
<p>하지만 이 문제점을 해결하기 위해서 <code>Hook</code> 이 나온 것이다.
위에서 말한 <code>state</code>를 사용하기 위해서 <code>useState</code>라는 훅을 <code>Life Cycle</code>를 사용하기 위해서 <code>useEffect</code>라는 훅이 생긴 것이다.</p>
<p>즉, <code>함수형 컴포넌트</code>에서 <code>클래스형 컴포넌트</code>의 <strong>기능을 사용하기 위한 도구</strong>라고 보면 될 것 같다.</p>
<h2 id="custom-hook">Custom Hook</h2>
<p>그럼 나만의 Hook은 뭘까.</p>
<p>개발을 하다보면 상태 로직이 중복되는 경우가 발생한다.
 <img src="https://velog.velcdn.com/images/bae-sh/post/bc9dc851-3039-4ddf-b378-98b6c735f12b/image.png" alt=""></p>
<p>필자의 경우 위와 같은 데이터를 하나의 컴포넌트로 보면 데이터의 상태와 input box의 onChange로직은 상품 갯수만큼 중복된다고 볼 수 있다.</p>
<p>그래서 아까 같이 하나의 상태에 모든 제품들을 넣는 방식이 <strong>아닌</strong> State를 상품마다 독자적으로 만들어 주어야한다.</p>
<pre><code class="language-javascript">function useProduct(obj){
    const [product,setProduct] = useState(obj);

     const onChange = (e) =&gt; setProduct(e);
      //...
      // 상태 로직 관련 함수들

      return {product, setProduct, onChange}
}

export default useProduct;
// useProduct.js</code></pre>
<pre><code class="language-javascript">import useProduct from &#39;../hooks/useProduct&#39;;
//ex) obj = {ko:&#39;사다리 모던 진열대&#39;,en : &#39;Ladder modern display&#39;, ... ,Hscode:&#39;9403209000&#39;
//,id :&#39;acxz123czxdas&#39;}
function DataRow(obj){
    const {product, setProduct,onChage} = useProduct(obj);

      return &lt;div&gt;&lt;input value ={product.ko} onChage={onChage}/&gt;&lt;/div&gt;
}
// DataRow.js</code></pre>
<h2 id="결론">결론</h2>
<p>서버에서 데이터 배열을 받아오고 각각의 데이터 <code>obj</code>를 DataRow 배열에 넣어주면 Row당 상태값이 하나 생기게 된다. 이렇게 Custom Hook을 사용하면 만약 데이터 하나의 값을 변경한다고 할 때 이전 처럼 200개의 데이터가 들어있는 상태를 바꾸는 것이 아닌 하나의 Row에 대한 상태값만 수정되기 때문에 1개의 Row만 렌더링 되어 딜레이를 줄일 수 있었다. 물론 <code>Custom Hook</code>을 사용하지 않고 <code>DataRow.js</code> 파일에 모든 상태를 직접 만들어도 동작은 하겠지만 중복을 줄이기 위해 <code>Custom Hook</code>을 사용하는 것이 좋겠다.</p>
<h3 id="출처">출처</h3>
<blockquote>
<p><a href="https://ko.reactjs.org/docs/hooks-intro.html">https://ko.reactjs.org/docs/hooks-intro.html</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[CORS를 알아보자]]></title>
            <link>https://velog.io/@bae-sh/CORS%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@bae-sh/CORS%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 11 Aug 2022 07:29:57 GMT</pubDate>
            <description><![CDATA[<img src='https://velog.velcdn.com/images/bae-sh/post/984a4f1e-0cfd-4942-8b1e-ed134b7982b4/image.png' width = "60%" >

<p>웹 개발자라면 한번쯤은 봤을만한 <code>CORS</code> 정책을 정리해 보려고 한다.</p>
<p>우리는 개발을 하다보면 수많은 에러를 겪는다. 
그럴때 마다 해결에만 초점을 두는 것은 안좋은 습관이라 생각이 들고 그 에러가 뜬 <strong>근본적인 이유에 대해 생각</strong>해볼 필요가 있는거 같다.</p>
<p>주변 동료나 면접에서 어떠한 에러 관련 질문이 들어왔을때 직접 <strong>근본적인 이유에 대해 고민</strong>해보았던 부분과 <strong>단순히 해결</strong>만 했던 부분에 있어서 답변 수준은 확실하게 다르다고 느꼈다.</p>
<p>그럼 지금부터 근본적인 이유를 알아보자!</p>
<h1 id="corscross-origin-resource-sharing">CORS(Cross-origin resource sharing)</h1>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/d6a804ba-8092-4bb8-ae81-22d495f93c48/image.png" alt=""></p>
<blockquote>
<p>교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.</p>
</blockquote>
<p>위의 정의에서 <code>출처</code> 라는 부분이 무슨말일까? </p>
<h2 id="출처origin">출처(Origin)</h2>
<blockquote>
<p>웹 콘텐츠의 출처(origin)는 접근할 때 사용하는 URL의 스킴(프로토콜), 호스트(도메인), 포트로 정의됩니다. 두 객체의 스킴, 호스트, 포트가 모두 일치하는 경우 같은 출처를 가졌다고 말합니다.</p>
</blockquote>
<p>위의 정의에서도 모르는 용어가 팍팍 나온다... <code>스킴</code>, <code>호스트</code>, <code>포트</code> 를 알아보자.
<img src="https://velog.velcdn.com/images/bae-sh/post/5b8716f6-21a0-418c-a287-53c64bf67604/image.png" alt=""> 
구글(<a href="https://www.google.com/)%EC%9D%98">https://www.google.com/)의</a> URL를 기준으로 위의 예시에 대입을 해보자.</p>
<p><strong>스킴(프로토콜)</strong> : <code>https</code>
<strong>호스트(도메인)</strong> : <code>www.google.co.kr</code>
<strong>포트</strong> : <code>443</code>(생략)</p>
<p>포트는 스킴이 http일경우 <code>80</code>, https 일 경우 <code>443</code> 이라고 한다.</p>
<p>그래서 <a href="https://www.google.com:443/">https://www.google.com:443/</a>  으로 접속을 해도 <a href="https://www.google.com/">https://www.google.com/</a> 와 같다.</p>
<p>그럼 이제 우리는 출처가 같다는 말을 이해할 수 있다. 구글과 같은 출처를 가진것은 <a href="https://www.google.com/">https://www.google.com/</a><del>~</del> 와 같다고 할 수있다. </p>
<h3 id="다른-출처의-예제">다른 출처의 예제</h3>
<img src='https://velog.velcdn.com/images/bae-sh/post/f288881f-1541-46e9-8764-3d8529a1ab59/image.png' width = "80%" height = "10%">

<h2 id="sop동일-출처-정책">SOP(동일 출처 정책)</h2>
<p>그럼 왜 동일한 출처를 가진 경우에는 안전하고 다른 출처를 가진 경우 자원을 접근할 때 문제가 생겨 <code>CORS</code> 가 발생할까?</p>
<p>예를 들어보자
정상적인 클라이언트 출처  : <a href="https://kakao.com">https://kakao.com</a>
정상적인 서버 출처 : <a href="https://kakao.com">https://kakao.com</a>
해커 클라이언트 출처 : <a href="https://kakaoHacked.com">https://kakaoHacked.com</a></p>
<ol>
<li>유저가 해커의 클라이언트(kakaoHacked)에 실수로 들어갔다고 하자 (<del>필자의 어렸을때 기억으로 넥슨과 UI가 똑같이 만든 해커 사이트에 접속하여 로그인을 한 경우가 있는데 지금 생각해보면 개인정보가 유출 된 것이다.</del>)</li>
<li>정상적인 클라이언트와 UI가 같아 아무 의심 없이 로그인을 한다.</li>
<li>정상적인 서버에서 로그인 요청을 받았다. 하지만 이 요청의 출처는 해커(kakaoHacked) 이므로 리소스를 제공해 주지 않는다. (만약 다른 출처에도 리소스를 제공해준다면 유저의 개인정보는 해커의 손에 들어갈 것이다.)</li>
</ol>
<h2 id="해결책">해결책</h2>
<p>하지만 다른 출처를 가진 경우에도 리소스를 공유해야 하는 경우가 있지 않은가?
그럴 경우에 서버에서 <code>Access-Control-Allow-Headers</code> 에서 예외적으로 허용해 줄 출처를 설정한다.
설정해 주지 않은 상태에서 출처가 다른 어플리케이션 끼리 공유 할때 <code>CORS</code> 정책을 브라우저에서 알려주는 것이다.
또 다른 방식으로는 프록시 서버를 설정하여 <code>클라이언트 &lt;-&gt; 서버</code> 가 아닌 <code>클라이언트 &lt;-&gt; 프록시 서버 &lt;-&gt; 서버</code> 와 같이 중간에 프록시 서버를 두어 중개 역할을 하는 것이다.</p>
<h3 id="참고">참고</h3>
<blockquote>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/CORS">https://developer.mozilla.org/ko/docs/Web/HTTP/CORS</a>
<a href="https://developer.mozilla.org/ko/docs/Glossary/Origin">https://developer.mozilla.org/ko/docs/Glossary/Origin</a>
<a href="https://johngrib.github.io/wiki/why-http-80-https-443/">https://johngrib.github.io/wiki/why-http-80-https-443/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[공공 데이터 API를 통해 환율을 받아오자]]></title>
            <link>https://velog.io/@bae-sh/%EA%B3%B5%EA%B3%B5-%EB%8D%B0%EC%9D%B4%ED%84%B0-API%EB%A5%BC-%ED%86%B5%ED%95%B4-%ED%99%98%EC%9C%A8%EC%9D%84-%EB%B0%9B%EC%95%84%EC%98%A4%EC%9E%90</link>
            <guid>https://velog.io/@bae-sh/%EA%B3%B5%EA%B3%B5-%EB%8D%B0%EC%9D%B4%ED%84%B0-API%EB%A5%BC-%ED%86%B5%ED%95%B4-%ED%99%98%EC%9C%A8%EC%9D%84-%EB%B0%9B%EC%95%84%EC%98%A4%EC%9E%90</guid>
            <pubDate>Wed, 27 Jul 2022 15:01:49 GMT</pubDate>
            <description><![CDATA[<img src='https://velog.velcdn.com/images/bae-sh/post/d2298f6b-395c-464d-b032-e12271722c92/image.png' width = "80%" height = "10%">

<blockquote>
<p>개인 프로젝트 진행 중 <strong>환율 정보</strong>를 받아와야 하는 케이스가 생겼다.
공공 데이터의 경우 단순히 URL로 데이터를 요청해서 받아오는 경우 보다 복잡해서 정리해 보려고 한다!
참고로 <a href="https://unipass.customs.go.kr/csp/index.do">https://unipass.customs.go.kr/csp/index.do</a> 사이트를 들어가면 환율 정보를 알 수있다.</p>
</blockquote>
<p><a href="https://unipass.customs.go.kr/csp/framework/filedownload/kcs4gDownload.do?attchFileId=MYC-20200710-00037918211TiAQI">https://unipass.customs.go.kr/csp/framework/filedownload/kcs4gDownload.do?attchFileId=MYC-20200710-00037918211TiAQI</a> 를 클릭하면 관세청에서 제공하는 연계 가이드 문서를 볼 수 있어 이를 참고 하였다.</p>
<h2 id="사전-작업">사전 작업</h2>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/98423649-2aa9-4ebd-b43c-461c335f2da3/image.png" alt=""></p>
<p>이 API 를 사용하기 위해 우선적으로 UNI-PASS의 회원으로 가입되어 있어야 하며, 각각의 용도에 맞는 서비스를 신청한 후 승인을 받아야 사용할 수 있다. 우리는 관세환율이 필요하므로 이를 신청하자!
이 부분은 연계가이드에 친절하게 설명이 되어있기 때문에 생략하겠다.</p>
<h2 id="api-요청">API 요청</h2>
<p>연계 가이드 문서를 보면 관세 환율 정보는 ID값이 <strong>API012</strong>를 가지고 있다. 우리가 요청해야 하는 URL 주소는 다음과 같다.</p>
<blockquote>
<p>URL : <a href="https://unipass.customs.go.kr:38010/ext/rest/trifFxrtInfoQry/retrieveTrifFxrtInfo?crkyCn=%5B%EC%9D%B8%EC%A6%9D%ED%82%A4%5D&amp;qryYymmDd=%5B%EB%82%A0%EC%A7%9C%5D&amp;imexTp=2">https://unipass.customs.go.kr:38010/ext/rest/trifFxrtInfoQry/retrieveTrifFxrtInfo?crkyCn=[인증키]&amp;qryYymmDd=[날짜]&amp;imexTp=2</a></p>
</blockquote>
<p>인증키 부분은 <strong>사전작업</strong> 을 통해 받을 수 있고 날짜의 경우 원하는 날을 입력하면 된다 ex) 20150101
<strong>imexTp</strong>가 1 일경우 수출, 2일 경우 수입을 받아온다. 우리의 경우 <strong>수입</strong>을 예시로 진행할 것이다.</p>
<p><del>환율정보는 매주 일요일 마다 갱신이 되는것 같다.</del></p>
<p>Client 환경에서 우리가 원하는 데이터를 요청해보자. 필자는 React 환경에서 테스트를 해보았다.</p>
<h3 id="client-code">Client Code</h3>
<pre><code class="language-javascript">const getData = async () =&gt; {
  try {
    const data = await fetch(
      &#39;https://unipass.customs.go.kr:38010/ext/rest/trifFxrtInfoQry/retrieveTrifFxrtInfo?crkyCn=[인증키]&amp;qryYymmDd=20220725&amp;imexTp=2&#39;,
    );
    console.log(data);
  } catch (e) {
    console.log(e);
  }
};</code></pre>
<p><img src="https://velog.velcdn.com/images/bae-sh/post/d6a804ba-8092-4bb8-ae81-22d495f93c48/image.png" alt=""></p>
<p>적절한 인증키와 <code>20220725</code> 날짜에 해당하는 데이터 요청시 우리를 항상 괴롭히는 <code>CORS</code> 에러가 발생했다. 그래.. 이렇게 쉽게 원하는 걸 얻을리가 없지...</p>
<p>그럼 <code>CORS</code> 에 대해 간단하게 알아보고 해결해 보자.</p>
<h2 id="교차-출처-리소스-공유-cors">교차 출처 리소스 공유 (CORS)</h2>
<blockquote>
<p>교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.</p>
</blockquote>
<p>정의는 역시 어렵다. 간단하게 정리해보면 현재 Client에서 사용중인 도메인(현재는 localhost)과 서버의 도메인(UNI-PASS 서버)이 같아야지 서로 리소스(환율 정보)들을 공유할수 있다.(보안상의 이유라고 한다.) </p>
<p>하지만 요즘 다른 도메인 끼리 리소스를 공유해야 하는 경우도 많다. 
이럴 경우 필요에 따라 &#39;이 Client 도메인은 우리(서버)와 도메인이 달라도 리소스를 공유하겠습니다.&#39; 라고 서버에 설정 해야한다.</p>
<p>우리가 UNI-PASS 쪽 서버에 접근해 우리의 도메인을 추가 할 수는 없으므로 우리가 서버를 하나 만들어서 UNI-PASS로 부터 데이터를 받아오고 우리쪽 서버에서 우리의 도메인이 리소스를 받을 수 있게 설정하고 데이터를 제공하는 방식으로 해결했다.</p>
<h3 id="대략적인-flow">대략적인 flow</h3>
<img src='https://velog.velcdn.com/images/bae-sh/post/64ddd43d-af4e-4b9b-a13c-06b48cf20620/image.png' width = "50%" height = "10%">

<h2 id="server">Server</h2>
<p>자신의 서버가 있고 그곳에서 작업하면 좋겠지만 이번 프로젝트의 경우 그러지 못했다.
이것을 위해 서버를 띄운다 해도 너무 과한 느낌이 강하게 들어 나의 용도에 적합한 방법을 찾다 <code>AWS Lambda</code> 라는 서비스를 이용해보았다.</p>
<blockquote>
<p><strong>AWS Lambda</strong>는 서버를 프로비저닝하거나 관리하지 않고도 코드를 실행할 수 있게 해주는 컴퓨팅 서비스입니다.</p>
</blockquote>
<p>프론트 엔드 개발자가 간단하게 서버리스로 코드를 실행하고 싶을때 적합한 방식이라고 판단했다. </p>
<p>그럼 AWS Lambda 함수의 코드 구현을 보면 다음과 같다. </p>
<p>파이썬보다 Node.js가 편해서 <strong>Node.js</strong>로 구현하였다.</p>
<h2 id="code">Code</h2>
<h4 id="server-1">Server</h4>
<pre><code class="language-javascript">const https = require(&quot;https&quot;);
let url = `https://unipass.customs.go.kr:38010/ext/rest/trifFxrtInfoQry/retrieveTrifFxrtInfo?crkyCn=[인증키]&amp;qryYymmDd=${getToday()}&amp;imexTp=2`;

exports.handler = async (event) =&gt; {
    const resultObj = await new Promise((res, reject) =&gt; {
    https.get(url, (response) =&gt; {
      var result = &quot;&quot;;
      response.on(&quot;data&quot;, function (chunk) {
        result += chunk;
      });

      response.on(&quot;end&quot;, function () {

        const body = {
          headers: {
            &quot;Access-Control-Allow-Headers&quot; : &quot;Content-Type&quot;,
            &quot;Access-Control-Allow-Origin&quot;: &quot;*&quot;,
            &quot;Access-Control-Allow-Methods&quot;: &quot;OPTIONS,POST,GET&quot;
            },
          statusCode: 200,
          body: result,
        };
        res(body);
      });
    });
  });

    return resultObj;
};

function getToday(){
    var date = new Date();
    var year = date.getFullYear();
    var month = (&quot;0&quot; + (1 + date.getMonth())).slice(-2);
    var day = (&quot;0&quot; + date.getDate()).slice(-2);

    return year + month + day;
}</code></pre>
<p>그럼 관공서에서 제공해주는 형식인 <strong>XML</strong>로 다음과 같이 출력된다.
<img src='https://velog.velcdn.com/images/bae-sh/post/5eb4f936-a888-41e1-b3fa-00192b609d8b/image.png' width = "80%" height = "10%"></p>
<p>음.. 우리는 <code>JSON</code>이 <code>XML</code> 보다 다루기 편하므로 <code>xml-js</code>의 라이브러리를 이용해 변환을 진행하였다.</p>
<p>사실 이 부분에서 <code>lambda</code> 계층에 <code>xml-js</code> 라이브러리를 저장 후 JSON 파일을 전송하는 방법도 있지만
Client에서 JSON으로 변환하는 방식으로 진행하였다.</p>
<h4 id="client">Client</h4>
<pre><code class="language-javascript">import { useEffect, useState } from &#39;react&#39;;
import axios from &#39;axios&#39;;

var convert = require(&#39;xml-js&#39;);

const getData = async setData =&gt; {
  const response = await axios.get(
    &#39;AWS API 주소&#39;,
  );

  const jsonResponse = convert.xml2json(response.data);
  const USD = await getUSD(JSON.parse(jsonResponse));
  setData(USD);
};

const getUSD = data =&gt; {
  let USD = 0;

  data.elements[0].elements.forEach(e =&gt; {
    if (e.elements?.length &gt; 2) {
      if (e.elements[3].elements[0].text === &#39;USD&#39;) {
        USD = e.elements[2].elements[0].text;
      }
    }
  });

  return USD;
};

function App() {
  const [data, setData] = useState();

  useEffect(() =&gt; {
    getData(setData);
  }, []);
  return &lt;div className=&quot;App&quot;&gt;{data}&lt;/div&gt;;
}

export default App;
</code></pre>
<p>위와 같이 진행하면 아래처럼 USD 수입 환율을 받아올 수 있다.</p>
<img src='https://velog.velcdn.com/images/bae-sh/post/e3056063-0636-4bd9-97e7-1de94e91d650/image.png' width ="50%" height = "10%">

<h3 id="참고">참고</h3>
<blockquote>
<p><a href="https://developer.mozilla.org/ko/docs/Web/HTTP/CORS">https://developer.mozilla.org/ko/docs/Web/HTTP/CORS</a>
<a href="https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/welcome.html">https://docs.aws.amazon.com/ko_kr/lambda/latest/dg/welcome.html</a>
<a href="https://www.npmjs.com/package/xml-js">https://www.npmjs.com/package/xml-js</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>