<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>deli-ght</title>
        <link>https://velog.io/</link>
        <description>PRE-FE에서 PRO-FE로🚀🪐!</description>
        <lastBuildDate>Thu, 14 Mar 2024 04:31:22 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>deli-ght</title>
            <url>https://images.velog.io/images/deli-ght/profile/2aa540f5-efe6-4173-b350-8b3ff71d2ce1/IMG_5426.JPG</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. deli-ght. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/deli-ght" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[husky 설치하기]]></title>
            <link>https://velog.io/@deli-ght/husky-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@deli-ght/husky-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 14 Mar 2024 04:31:22 GMT</pubDate>
            <description><![CDATA[<h2 id="설치">설치</h2>
<p><a href="https://typicode.github.io/husky/get-started.html">https://typicode.github.io/husky/get-started.html</a></p>
<p>pnpm 기준</p>
<pre><code class="language-shell">pnpm add --save-dev husky</code></pre>
<p>다음 스크립트를 사용해 기본 셋팅을 간편하게 설정해줍니다.</p>
<pre><code class="language-shell">pnpm exec husky init</code></pre>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/bbaac2da-9345-41b2-b586-0252b4a3639b/image.png" alt=""></p>
<p>스크립트 실행 후, <code>.husky</code> 파일에 <code>pre-commit</code> 스크립트가 생성되고, <code>package.json</code>에 이를 실행하는 <code>prepare</code> 명령어가 추가됩니다.</p>
<h2 id="hook-추가하기">Hook 추가하기</h2>
<p>husky는 hook 실행 전에 로컬 명령어를 실행할 수 있도록 해줍니다. 다음 파일들로부터 명령어를 읽습니다.</p>
<ul>
<li><code>$XDG_CONFIG_HOME/husky/init.sh</code></li>
<li><code>~/.config/husky/init.sh</code></li>
<li><code>~/.huskyrc</code> (deprecated)</li>
</ul>
<p>On Windows: C:\Users\yourusername.config\husky\init.sh</p>
<h2 id="git-hook-생략하기">git hook 생략하기</h2>
<h3 id="single-command">single command</h3>
<p>git hook을 생략하기 위해선, <code>-n/--no-verify</code> 옵션을 붙여줍니다.</p>
<pre><code class="language-shell">git commit -m &quot;...&quot; -n # Skips Git hooks</code></pre>
<p>해당 옵션 없이도 생략하도록 하기 위해 <code>HUSKY=0</code>을 설정해줍니다.</p>
<pre><code class="language-shell">HUSKY=0 git ... # Temporarily disables all Git hooks
git ... # Hooks will run again</code></pre>
<h3 id="multiple-command">multiple command</h3>
<p>여러 코멘드에서 생략하기 위해선, 시작 전 <code>export HUSKY=0</code>을 선언하고 git 명령어 입력 후 <code>unset HUSKY</code>을 통해 종료해줍니다.</p>
<pre><code class="language-shell">export HUSKY=0 # Disables all Git hooks
git ...
git ...
unset HUSKY # Re-enables hooks</code></pre>
<h3 id="global">Global</h3>
<p>컴퓨터에 기본 설정으로 hook을 사용하게 하지 않기 위해 <code>~/.config/husky/init.sh</code> 파일에 다음과 같이 설정해줍니다.</p>
<pre><code class="language-shell"># ~/.config/husky/init.sh
export HUSKY=0 # Husky won&#39;t install and won&#39;t run hooks on your machine</code></pre>
<h2 id="commit-테스트">Commit 테스트</h2>
<p>Commit을 테스트 하기 위해 실제 커밋이 동작하지 않도록 하는 방법도 있습니다. <code>pre-commit</code> 파일에 <code>exit 1</code>을 넣어주면, commit 명령어를 입력해도 실제 commit이 되지 않습니다.</p>
<pre><code class="language-shell"># .husky/pre-commit

# Your WIP script
# ...

exit 1</code></pre>
<pre><code class="language-shell">git commit -m &quot;testing pre-commit code&quot;
# A commit will not be created</code></pre>
<h2 id="commitlint-적용하기">Commitlint 적용하기</h2>
<p><a href="https://commitlint.js.org/guides/getting-started.html">https://commitlint.js.org/guides/getting-started.html</a></p>
<pre><code class="language-shell">pnpm add --save-dev @commitlint/cli @commitlint/config-conventional </code></pre>
<h3 id="configuration">configuration</h3>
<pre><code class="language-shell">echo &quot;export default { extends: [&#39;@commitlint/config-conventional&#39;] };&quot; &gt; commitlint.config.js</code></pre>
<h3 id="husky를-이용해-hook-적용하기">husky를 이용해 hook 적용하기</h3>
<pre><code>echo &quot;npx --no -- commitlint --edit \$1&quot; &gt; .husky/commit-msg</code></pre><p><img src="https://velog.velcdn.com/images/deli-ght/post/49d7fea0-4c82-41e3-9e44-4829a6dc81ff/image.png" alt=""></p>
<p>js로 기본 설치된 config 파일을 내부에서 ts를 사용하고 있는 경우, ts로 변경해주기</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[next build시 lint가 동작하지 않는 문제]]></title>
            <link>https://velog.io/@deli-ght/next-build%EC%8B%9C-lint%EA%B0%80-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@deli-ght/next-build%EC%8B%9C-lint%EA%B0%80-%EB%8F%99%EC%9E%91%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Wed, 06 Mar 2024 09:59:45 GMT</pubDate>
            <description><![CDATA[<h2 id="reoccur-scenario">Reoccur scenario</h2>
<ol>
<li><p>임의의 컴포넌트 페이지에서 생성해 하나를 page에서 사용한다. </p>
<pre><code class="language-jsx"> // Test.tsx
 export const Test = () =&gt; {
   return &lt;div&gt;Test&lt;/div&gt;;
 };

 // Page1.tsx
 ...
     &lt;Test/&gt;
 ...</code></pre>
</li>
<li><p><code>next build</code> 실행</p>
</li>
<li><p>기존의 Test 컴포넌트 파일을 Test1로 변경하고, Test 파일을 새로 생성해 그 안에 Test 컴포넌트를 생성한다.</p>
</li>
<li><p><code>next build</code> 실행</p>
</li>
<li><p>Text1의 Text를 사용하도록 변경한다 → <code>next build</code> 에러 (error msg : <code>Text1의 Text가 unused</code>)</p>
</li>
</ol>
<ol start="6">
<li>Test1 삭제 후, Test의 Test를 사용하도록 한다 → lint 에러 안남 (맞음)</li>
<li>다시 Test1 생성 후, Test1의 Test를 사용하도록 한다 → lint 에러 안남(..???)
.next/cache/eslint 삭제하면..? 드디어 터짐 🤯</li>
</ol>
<h2 id="결론">결론</h2>
<p>파일 rename시, <code>next lint</code> 를 생활화합시다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/7c80f738-4565-4304-80ab-7202e0269b5c/image.png" alt=""></p>
</blockquote>
<p>next에서는 빌드시 lint가 적용된 캐싱된 파일을 사용하기 때문에 별도로 lint가 실행되지 않는다.</p>
<blockquote>
<p><a href="https://nextjs.org/docs/basic-features/eslint">https://nextjs.org/docs/basic-features/eslint</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Safari scroll flickering]]></title>
            <link>https://velog.io/@deli-ght/Safari-scroll-flickering</link>
            <guid>https://velog.io/@deli-ght/Safari-scroll-flickering</guid>
            <pubDate>Wed, 06 Mar 2024 09:53:16 GMT</pubDate>
            <description><![CDATA[<h2 id="issue">Issue</h2>
<p>SVG로 꾸민 페이지가 safari에서 스크롤시 흰 배경이 나오면서 로딩이 생김</p>
<h2 id="references">References</h2>
<p><a href="https://github.com/vercel/next.js/issues/34455">https://github.com/vercel/next.js/issues/34455</a></p>
<p>이미지에만 한정된 PR이라 enterprise 이슈에도 해당되는지 모르겠음.</p>
<p>찾아보니 safari에서 div 스크롤시 하얀배경(body의 배경)이 나오면서 로딩이 걸리는 이슈가 있는 듯함 → next + emotion 조합의 문제는 아닌건가</p>
<p>css hack 사용 → 실패</p>
<pre><code class="language-jsx">-webkit-transform: translate3d(0, 0, 0);
will-change : transform</code></pre>
<h2 id="원인">원인</h2>
<p>배경에 깔려있던 블러 이미지 때문.
svg를 컴포넌트로 불러와 사용하도록 했으나 이게 레이아웃 계산에서 문제가 생기고 있었음
background로 불러와 사용하는 것이 좋음</p>
<pre><code class="language-js">// ❌
import EclipseIcon from &quot;/__assets/icons/Eclipse.svg&quot;
...
&lt;EclipseIcon/&gt;
...

// ⭕️
const WaitlistSection = styled(Section)`
  position: relative;
  overflow: hidden;
  background-color: ${COLORS.gray[900]};

  &amp;::after {
    content: &quot;&quot;;
    display: block;
    position: absolute;
    top: -50%;
    left: -20%;
    min-width: 900px;
    width: 100%;
    height: 600px;
    background: url(&quot;/__assets/icons/Eclipse.svg&quot;) no-repeat center center;
  }
`;</code></pre>
<blockquote>
<p>svg를 사용하는 다양한 방법
<a href="https://svgontheweb.com/ko/#implementation">https://svgontheweb.com/ko/#implementation</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[React 18 update]]></title>
            <link>https://velog.io/@deli-ght/React-18-update</link>
            <guid>https://velog.io/@deli-ght/React-18-update</guid>
            <pubDate>Sat, 17 Feb 2024 07:29:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>2022년 3월 29일에 올라왔던 <a href="https://react.dev/blog/2022/03/29/react-v18">React v18.0</a>을 읽고 해당 내용을 정리한 글입니다.번역본이 아니기 때문에 빠지거나 주관적인 내용이 있습니다.</p>
</blockquote>
<h2 id="what-is-concurrent-react">What is Concurrent React?</h2>
<p>React 18은 concurrent renderer 위에서 개발되었습니다. </p>
<ul>
<li><code>Concurrency</code> : 하나의 기능이라기 보단, <strong>동시에</strong> 여러 버전의 UI가 준비될 수 있는 매커니즘. 세부 실행 방식으로도 이해할 수 있음.</li>
</ul>
<p>이를 구현하기 위해 priority queue나 multiple buffering같은 섬세한 기술들을 이용했지만, 개발자들이 이에 대해 이해하고 있을 필요는 없도록 설계되었습니다. 따라서 개발자들은 어떤 방식으로 concurrency가 이뤄질 지 고민하는 것보단, <strong>유저가 어떤 유저 경험을 가지면 좋겠는지, 그 경험을 어떻게 전달할 것인지</strong>에 집중하며 개발하면 됩니다. 또한 Concurrency가 무엇인지만 이해하고 있으면 됩니다.</p>
<p>Concurrency에서 가장 중요한 포인트는 <strong>렌더링이 언제든 방해될 수 있다</strong>는 점입니다. </p>
<ul>
<li>구 버전: <code>uninterrupted</code>, <code>synchronous transaction</code><ul>
<li>기존에는 렌더링되는 동안 interaction이 불가</li>
</ul>
</li>
<li>React 18 : <code>interrupted</code> <ul>
<li>렌더링 중간에 방해받더라도, 동시에 UI가 나타남이 보장됨. 이게 가능하기 위해, 전체 트리가 계산이 끝날 때 까지 DOM mutation이 기다림.</li>
<li>메인 쓰레드를 방해하지 않고, 새로운 스크린을 그릴 수 있음.</li>
<li>큰 렌더링이 생기더라도 유저의 반응에 즉각적으로 응답할 수 있게됨.</li>
<li><code>Reusable state</code> : 이전의 UI를 재사용하여 세션별로 렌더링을 쪼개 보여줄 수 있음.<ul>
<li>이를 구현하기 위한 <code>&lt;offscreen&gt;</code>이라는 컴포넌트 구현 예정</li>
<li><a href="https://react.dev/blog/2024/02/15/react-labs-what-we-have-been-working-on-february-2024#offscreen-renamed-to-activity">2024.02 update</a> <code>&lt;Activity&gt;</code>로 이름이 변경되고, 개발 우선순위가 낮아짐</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="gradually-adopting-concurrent-feature">Gradually Adopting Concurrent Feature</h2>
<p>대부분의 이전 컴포넌트에서 별다른 변경없이도 &quot;그냥 작동&quot;은 하고 있지만, 어떤 컴포넌트들은 마이그레이션이 필요합니다. 전체적인 마이그레이션 방법은 기존 코드의 변경 없이 어플리케이션이 React 18에서 동작하도록 하는 것입니다. <code>&lt;StrictMode&gt;</code>를 사용하면 production에는 영향을 미치지 않고, 개발 중에 concurrent 관련 에러를 잡기 조금 수월할겁니다.</p>
<p>업그레이드를 하면 바로 concurrency 관련 기능들을 사용할 수 있습니다 (<code>startTransition</code>이나 <code>useDefferredValuer</code>같은). 장기적으로는concurrency를 직접적으로 사용하는 적용보다는 concurrent가 가능한 라이브러리나 프레임워크를 사용하길 기대하고 있습니다. (=maintainer들아 일해라)</p>
<h2 id="suspense-in-data-frameworks">Suspense in Data Frameworks</h2>
<p>몇몇 라이브러리에서는 data fetching을 위한 <code>Suspense</code>를 사용할 수 있습니다. 즉각적인 data fetching에서 Suspense를 사용하는 것이 기술적으로는 가능하지만, 보편적인 방식으로는 권장하지 않습니다.</p>
<blockquote>
<ol>
<li><p>Reading the data
data를 읽을 때마다 로딩상태에서 무엇을 보여줄 지 결정해야함</p>
</li>
<li><p>Spectify the loading state
한 페이지 내에 두 컴포넌트가 로딩 상태를 가질 때 이 두 컴포넌트의 로딩 상태를 합치고 싶은 경우
<img src="https://velog.velcdn.com/images/deli-ght/post/a55cf0e4-07fb-4816-a4c3-4753833b7e5d/image.png" alt="">
두 문제를 해결하기 위해 suspense를 사용</p>
</li>
</ol>
</blockquote>
<p>추후에는 특정 프레임워크가 아니더라도 suspense를 사용할 수 있도록 하는게 목표입니다. 가장 suspense가 잘 동작하는 방법은 <strong>본인의 어플리케이션 아키텍쳐에 깊에 통합되는 것</strong>입니다. </p>
<p>이전 버전에서는 suspense가 React.lazy와 함께 클라이언트에서 코드를 분리하는 역할로 사용되었지만, 코드를 기다리는 것 너머의 역할이 될 것입니다. (선언된 같은 suspense fallback에서 비동기적 오퍼레이션들을 다룰 수 있도록 - data fetching만을 위한게 아닌 비동기적인 이미지를 위한 fallback이라던가..)</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/151ca449-cf70-41eb-bb11-367778e76df1/image.png" alt=""></p>
<p>(image from React 18 conf keynote)</p>
<h2 id="server-components-is-still-in-development">Server Components is Still in Development</h2>
<p><code>Server component</code>는 서버와 클라이언트를 걸쳐 어플리케이션을 개발할 수 있도록 해주는 기능입니다.Concurrent rendering과 기술적으로 연관되어있지는 않지만, 함께 사용했을 때 더욱 잘 동작할 수 있도록 설계되었습니다.
(현재 Next에서 app directory와 함께 사용 가능 - <a href="https://react.dev/learn/start-a-new-react-project#bleeding-edge-react-frameworks">사용 가능한 프레임워크들은 여기에 업데이트될 예정</a>)</p>
<h2 id="what-s-new-in-react-18">What&#39; s New in React 18</h2>
<h3 id="new-feature--automatic-batching">New feature : Automatic Batching</h3>
<p>기존 방식의 경우 React event handler 내부의 업데이트만 <code>batch</code>가 실행되고, <code>Promises</code>, <code>setTimeout</code>, <code>native event handler</code> 들은 리액트에서 기본적으로 배치를 지원하지 않았습니다.</p>
<p>하지만 데이터를 가져온 뒤에 상태를 업데이트하는 경우, (기본 event handler X가 아닌 경우) batch가 실행되지 않고</p>
<pre><code class="language-js">    function handleClick () {
    // (1) 렌더링
       fetchSth().then(() =&gt; {
    //    (2) 렌더링
          setCount(c =&gt; c+1);
          setFlag(f =&gt; !f);
       })
    }</code></pre>
<p>callback에서는 (1) event가 종료된 이후에 (2)를 실행하기 때문에 (1)번과 (2)번 두 번의 렌더링이 발생됩니다. (본 글의 표현을 빌리자면 run &quot;after&quot; not &quot;during&quot;)</p>
<p>반면 React 18에서는 어떤 이벤트에서 실행됐는지와 관계없이 항상 <code>batch</code>로 동작합니다. (Promises, setTimeout, native event handler 다 포함)</p>
<pre><code class="language-js">    function handleClick () {
       fetchSth().then(() =&gt; {
          setCount(c =&gt; c+1);
          setFlag(f =&gt; !f);
       })
   // 딱 끝나는 시점에서 렌더
    }
</code></pre>
<h3 id="new-feature-transitions">New feature: Transitions</h3>
<p>transition은 급한(urgent) 업데이트와 급하지 않은(non-urgent)를 구분하기 위한 개념입니다.</p>
<ul>
<li>urgent transition : 타이핑, 클릭과 같은 유저 인터랙션 즉각 반영<ul>
<li>UI가 즉시 바뀌지 않으면 에러로 간주됨.</li>
</ul>
</li>
<li>transition updates : 다른 뷰로 UI가 변경되는 것<ul>
<li>유저가 변경이 바로 이뤄질 것이라고 기대하지 않음</li>
</ul>
</li>
</ul>
<p>예를 들어, 필터버튼을 클릭했을 때,</p>
<ul>
<li>필터 버튼이 변경됨 = urgent transition</li>
<li>필터된 내용이 로딩 이후 나타남 = transition updates</li>
</ul>
<p><code>startTranstion</code> API를 이용해 어떤 액션이 중요도를 가지는 지 알려줄 수 있습니다.</p>
<pre><code class="language-js">import { startTransition } from &#39;react&#39;;

// Urgent
setInputValue(input);


startTransition(() =&gt; {
  // Transition
  setSearchQuery(input);
});</code></pre>
<p><code>startTransition</code>에 들어간 업데이트는 다른 중요한 업데이트가 생기는 경우 언제든 우선순위가 밀려날 수 있습니다. 만약 transition이 유저에 의해 중단되면 React는 완료되지 않은 오래된 렌더링 작업을 버리고 최신 업데이트만 렌더링합니다.</p>
<ul>
<li><code>useTransition</code> : transition을 시작하기 위한 hook. <code>[isPending, startTransition]</code>을 리턴</li>
<li><code>startTransition</code> : hook을 사용할 수 없을 때, transition을 시작하기 위한 method.</li>
</ul>
<h2 id="new-suspense-features">New Suspense Features</h2>
<p>Suspense를 사용하면 컴포넌트 트리의 일부가 아직 표시될 준비가 되지 않은 경우 로딩 상태를 선언적으로 지정할 수 있습니다:</p>
<pre><code class="language-js">&lt;Suspense fallback={&lt;Spinner /&gt;}&gt;
  &lt;Comments /&gt;
&lt;/Suspense&gt;</code></pre>
<p>Suspense는 UI의 로딩상태를 일급 선언적 개념으로 만들어, 그 위에 고차원적인 기능을 구축할 수 있게 합니다. 추후 기능은 더 확장될 예정이며, transition과 함께 사용시 충분한 데이터를 받기 전까지 렌더링을 미뤄, 안좋은 로딩 상태가 보여지는 것을 막을 수 있게됩니다.</p>
<h2 id="new-client-and-server-rendering-apis">New Client and Server Rendering APIs</h2>
<h3 id="react-dom-client">React DOM Client</h3>
<p><code>react-dom/client</code>에서 export되는 API들</p>
<ul>
<li><code>createRoot</code> : render나 unmount를 위한 루트를 생성하는 메소드. ReactDOM.render 대신에 사용되며, 리액트 18의 새로운 기능들은 createRoot를 사용해야 적용됨</li>
<li><code>hydrateRoot</code> : 서버에서 렌더된 어플리케이션을 hydrate하기 위한 메소드. ReactDOM.hydrate 대신 사용되며, 리액트 18의 새로운 기능들은 hydrateRoot 사용해야 적용됨</li>
</ul>
<p>두 메소드 모두 새 옵션인 <strong>렌더나 hydration 중 에러가 발생했다가 회복된 경우 로그에 안내하주는</strong> <code>onRecoverableError</code>를 옵션으로 받습니다. 옛날 브라우저에서는 reportError나 console.error로 동작합니다.</p>
<h3 id="react-dom-server">React DOM Server</h3>
<p><code>react-dom/server</code>에서 export되는 API들</p>
<ul>
<li><code>renderToPipeableStream</code> : Node 환경에서 스트리밍용</li>
<li><code>renderToReadableStream</code> : edge 환경에서 스트리밍용 (Deno, Cloudflare)</li>
</ul>
<p>기존에 있던 <code>renderToString</code>도 동작하지만 권장되지는 않습니다.</p>
<h2 id="new-strict-mode-behaviors">New Strict Mode Behaviors</h2>
<p>리액트에서 상태를 유지하며 세션별 UI를 더하거나 뺄 수 있도록 개발될 예정입니다.이게 동작하기 위해선 같은 컴포넌트 상태를 unmount하고 remount할 때 사용해야 합니다. 이에 컴포넌트는 탄력적으로 대응할 수 있어야합니다. 이 문제 해결을 위해 Strict Mode에서 모든 컴포넌트가 첫번째 렌더 이후 두번째로 이전 상태를 복구해 자동으로 unmount되고 remount 되는 옵션을 추가했습니다.</p>
<h2 id="new-hooks">New hooks</h2>
<h3 id="useid">useId</h3>
<p>서버와 클라이언트에서 모두 유니크한 아이디를 만들어줄 수 있는 훅입니다. 서버와 클라이언트간 hydration 불일치를 해결해줍니다. 데이터 key로의 사용은 권장되지 않습니다.</p>
<h3 id="usetransition">useTransition</h3>
<p>업데이트가 급하지 않은 상태임을 표시하는 훅입니다. 해당 훅이 사용되지 않는 업데이트는 모두 urgent update로 간주됩니다. </p>
<h3 id="usedefferedvalue">useDefferedValue</h3>
<p>non-urgent 업데이트의 리렌더링을 미루는 훅입니다. 디바운싱과 비슷하지만, 고정된 딜레이 시간 없습니다.</p>
<h3 id="usesyncexternalstore">useSyncExternalStore</h3>
<p>외부 스토어가 스토어 업데이트를 강제로 동기화하여 동시 읽기를 지원할 수 있는 훅입니다. 외부 데이터를 얻을 때, useEffect를 사용하지 않아도 됩니다. 해당 코드는 application의 직접적인 사용보단, 라이브러리에서의 사용을 위해 개발되었습니다.</p>
<h3 id="useinsertioneffect">useInsertionEffect</h3>
<p>CSS-in-js 라이브러리의 렌더링에서 스타일 주입의 성능 문제 해결을 위한 훅입니다. DOM mutaion 이후, layout effect가 새 layout을 읽기 전에 동작합니다. 이미 CSS-in-js를 구축해두었다면 사용을 권장하지 않습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[enter입력시 폼 submit을 막고싶다면 (button이 분리된 상태에서 submit을 컨트롤 할 방법)]]></title>
            <link>https://velog.io/@deli-ght/enter%EC%9E%85%EB%A0%A5%EC%8B%9C-%ED%8F%BC-submit%EC%9D%84-%EB%A7%89%EA%B3%A0%EC%8B%B6%EB%8B%A4%EB%A9%B4-button%EC%9D%B4-%EB%B6%84%EB%A6%AC%EB%90%9C-%EC%83%81%ED%83%9C%EC%97%90%EC%84%9C-submit%EC%9D%84-%EC%BB%A8%ED%8A%B8%EB%A1%A4-%ED%95%A0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@deli-ght/enter%EC%9E%85%EB%A0%A5%EC%8B%9C-%ED%8F%BC-submit%EC%9D%84-%EB%A7%89%EA%B3%A0%EC%8B%B6%EB%8B%A4%EB%A9%B4-button%EC%9D%B4-%EB%B6%84%EB%A6%AC%EB%90%9C-%EC%83%81%ED%83%9C%EC%97%90%EC%84%9C-submit%EC%9D%84-%EC%BB%A8%ED%8A%B8%EB%A1%A4-%ED%95%A0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 24 Sep 2023 08:03:47 GMT</pubDate>
            <description><![CDATA[<p>폼 내부에 input 필드를 사용해 구성하던 중, input 내부에서 유저가 입력을 다 하고, 엔터를 누르니 폼이 입력되는 현상이 발생했다.</p>
<p><strong>as-is</strong> : input 내부에서 enter 클릭시, 해당 폼의 submit이 동작
<strong>to-be</strong> : input 내부에서 enter 클릭시, 해당 폼의 submit이 동작하지 않았으면 좋겠음.</p>
<p>심지어 현재 폼의 submit 버튼은 폼 내부에 있지 않고 lnb에 위치해 폼에 대한 상태를 공유하고 있지 않고, form ID로만 연결되어 있어서 별도의 조작이 불가능한 상태였다.</p>
<pre><code class="language-jsx">// lnb
&lt;button form=&quot;new-form&quot;&gt;
    Submit
&lt;/button&gt;

// form
&lt;form id=&quot;new-form&quot;&gt;
 ...
&lt;/form&gt;</code></pre>
<p>그래서 <strong>button이 분리된 상태에서 submit을 컨트롤 할 방법</strong>을 찾아보기로 했다.</p>
<h3 id="enter시-어떤-이벤트에-의해-submit이-실행되는가">Enter시 어떤 이벤트에 의해 submit이 실행되는가</h3>
<p>enter는 키니깐 키 이벤트가 실행되겠지 ㅎㅎ → <span style="color:red">아님 ❌</span></p>
<p>submit버튼에 onKeyPress, onKeyDown 이벤트가 실행되는 타이밍에 콘솔을 찍게 했지만 아무 동작도 하지 않았다.</p>
<p>놀랍게도 폼 내부에서 엔트키를 누르는 경우, <code>onClick</code>이벤트가 실행된다.</p>
<p>그러니 우리는 onClick 이벤트에서 <strong>유저가 버튼을 클릭한건지, 아니면 엔터키에 의한 클릭인지 구분해야한다</strong>는 의미이다.</p>
<h3 id="onclick의-이벤트-객체엔-detail이라는-속성이-있더라">onClick의 이벤트 객체엔 <a href="https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail"><code>detail</code></a>이라는 속성이 있더라</h3>
<p>유저가 버튼을 직접 눌렀을 때와 엔터를 입력한 뒤의 각각 이벤트 객체를 뜯어보았다.</p>
<p>다른 곳에는 큰 차이가 없었는데, detail 속성이 각각 다르게 나타난 것을 확인 할 수 있었다.</p>
<p>엔터 입력시 : <code>detail = 0</code>
유저가 버튼 클릭시 : <code>detail = 1</code></p>
<p>detail 속성은 클릭 이벤트에서는 유저가 클릭 한 수만큼 증가하고, mouseup/down 이벤트에서는 클릭수 + 1 의 값을 가진다. 그리고 이외의 이벤트에서는 0을 나타낸다.</p>
<p>그래서 유저가 직접 클릭한 경우에만 값이 1이 되고 (혹은 그 이상), 엔터를 통해 제출된 경우에는 0인 값이 나왔던 것이었다.</p>
<p>따라서 detail값이 0인 경우에는 제출을 막아 원하는 폼 형태를 만들 수 있었다.</p>
<pre><code class="language-jsx">&lt;button 
    form=&quot;new-form&quot;
    onClick={(ev) =&gt; {
    if (ev.detail === 0) {
      ev.preventDefault();
    }
  }}
&gt;
 Submit
&lt;/button&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next 13] Loading UI and Streaming]]></title>
            <link>https://velog.io/@deli-ght/next-13-Loading-UI-and-Streaming</link>
            <guid>https://velog.io/@deli-ght/next-13-Loading-UI-and-Streaming</guid>
            <pubDate>Sat, 08 Jul 2023 11:40:39 GMT</pubDate>
            <description><![CDATA[<h1 id="loading-ui">Loading UI</h1>
<p><code>loading.js</code> 파일은 React suspense를 이용해 의미있는 로딩화면을 보여주기 위해 사용되는 파일</p>
<h2 id="instant-loading-states">Instant Loading States</h2>
<p>즉각적으로 사용자에게 보여지는 fallback UI
<strong>페이지</strong>에서 데이터 fetching이 발생하는 경우, 해당 페이지의 같은 폴더 내 위치한 <code>loading.js</code> 를 fallback UI 로 보여준다. (Suspense가 없어도)</p>
<pre><code class="language-tsx">import type { Category } from &#39;#/app/api/categories/category&#39;;
import { getBaseUrl } from &#39;#/lib/getBaseUrl&#39;;
import { SkeletonCard } from &#39;#/ui/skeleton-card&#39;;
import { notFound } from &#39;next/navigation&#39;;

export default async function Page({
  params,
}: {
  params: { categorySlug: string };
}) {

  // 🍀 해당 데이터가 fetching 되는 동안 가장 가까운 상위 계층에 위치한 loading을 보여줌
  const res = await fetch(
    // We intentionally delay the response to simulate a slow data
    // request that would benefit from `loading.js`
    `${getBaseUrl()}/api/categories?delay=1000&amp;slug=${params.categorySlug}`,
    {
      // We intentionally disable Next.js Cache to better demo
      // `loading.js`
      cache: &#39;no-cache&#39;,
    },
  );

  if (!res.ok) {
    // Render the closest `error.js` Error Boundary
    throw new Error(&#39;Something went wrong!&#39;);
  }

  const category = (await res.json()) as Category;

  if (!category) {
    // Render the closest `not-found.js` Error Boundary
    notFound();
  }

  return (
    &lt;div className=&quot;space-y-4&quot;&gt;
      &lt;h1 className=&quot;text-xl font-medium text-gray-400/80&quot;&gt;{category.name}&lt;/h1&gt;

      &lt;div className=&quot;grid grid-cols-1 gap-6 lg:grid-cols-3&quot;&gt;
        {Array.from({ length: category.count }).map((_, i) =&gt; (
          &lt;SkeletonCard key={i} /&gt;
        ))}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}</code></pre>
<p>loading.js도 page.js와 마찬가지로 함께 위치한 layout으로 감싸진 채 보여진다.</p>
<p>Next.js의 네비게이션은 즉각적이며, 탈취 가능해 화면이 다 로딩되지 않더라도 다른 화면으로 이동 가능하다.
공유된 레이아웃(layout.js)은 내부 요소가 로딩될 동안 (완전히 렌더된 상태가 아니더라도) 인터랙션이 가능하다.</p>
<h2 id="streaming-with-suspense">Streaming with suspense</h2>
<p>next 13의 <a href="https://nextjs.org/blog/next-13#streaming">주요 기능</a> 중 하나 </p>
<p>UI의 렌더링된 단위를 점진적으로 렌더링하고 클라이언트에 점진적으로 스트리밍하는 기능으로,
데이터가 필요없는 부분은 먼저 보여주고, 데이터 fetchiing이 필요한 부분은 데이터를 받아오기 전까지 fallback ui를 보여준다.</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/5df4a8e1-efcf-44a8-9497-031c122065db/image.png" alt="">
<code>loading.js</code> 말고도 해당 컴포넌트에 커스컴 suspense boundary 설정 가능하다.</p>
<pre><code class="language-tsx">import { Suspense } from &#39;react&#39;
import { PostFeed, Weather } from &#39;./Components&#39;

export default function Posts() {
  return (
    &lt;section&gt;
      &lt;Suspense fallback={&lt;p&gt;Loading feed...&lt;/p&gt;}&gt;
        &lt;PostFeed /&gt;
      &lt;/Suspense&gt;
      &lt;Suspense fallback={&lt;p&gt;Loading weather...&lt;/p&gt;}&gt;
        &lt;Weather /&gt;
      &lt;/Suspense&gt;
    &lt;/section&gt;
  )
}</code></pre>
<h3 id="what-is-streaming">What is streaming?</h3>
<p>스트리밍에 대해 알기 위해선 SSR과 그 한계를 아는 것이 중요
SSR에서는 사용자가 페이지를 보고 상호작용하기 전에 완료해야하는 일련의 단계가 존재</p>
<ol>
<li>페이지에 필요한 데이터를 서버로 부터 받아옴</li>
<li>서버는 페이지의 HTML을 렌더링함</li>
<li>HTML, CSS, JS가 클라이언트로 보내짐</li>
<li>HTML, CSS를 이용해 만들어진 유저와 상호작용하지 않는 UI들이 보여짐</li>
<li>React의 hydrate를 통해 인터랙션이 활성화됨.</li>
</ol>
<p>이러한 단계는 순차적이고 차단적이므로 서버는 모든 데이터를 가져온 후에만 페이지의 HTML을 렌더링할 수 있다. 클라이언트에서는 페이지의 모든 컴포넌트에 대한 코드가 다운로드된 후에만 React가 UI에 dydrate할 수 있다.</p>
<p>Streaming은 페이지의 HTML을 작은 청크로 나눈 뒤, 점진적으로 서버에서 클라이언트로 보낸다. 페이지의 부분 요소들이 모든 데이터를 기다릴 필요 없이 더 빠르게 렌더될 수 있다. 중요도에 따라 먼저 보내지고 hydration도 먼저 시작할 수 있게 된다. 결과적으로 큰 데이터를 가져올 때 생기는 blocking이나, TTFB, FCP, TTI 의 blocking을 최소화할 수 있다.</p>
<blockquote>
<p>Suspense의 장점</p>
</blockquote>
<ol>
<li>Streaming server rendering</li>
<li>Selective Hydration</li>
</ol>
<h2 id="seo">SEO</h2>
<p>Streaming UI 전에 <code>generateMetadata</code> 의 데이터를 먼저 받아온다. 이건 stream된 첫번째 응답이 <code>&lt;head&gt;</code> 태그임을 보장한다.</p>
<p>스트리밍은 서버에서 렌더되므로 SEO에 영향을 미치지 않는다. </p>
<p>아래의 링크를 통해 구글 웹 크롤링이 어떻게 이뤄질지 확인할 수 있다.
<a href="https://search.google.com/test/mobile-friendly">https://search.google.com/test/mobile-friendly</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next 13] Dynamic Routes]]></title>
            <link>https://velog.io/@deli-ght/next-13-Dynamic-Routes</link>
            <guid>https://velog.io/@deli-ght/next-13-Dynamic-Routes</guid>
            <pubDate>Sat, 08 Jul 2023 09:38:24 GMT</pubDate>
            <description><![CDATA[<h1 id="dynamic-routes">Dynamic Routes</h1>
<p>기존 next의 dynamic route와 동일한 기능.</p>
<p><strong>동적 세그먼트</strong> : 요청 시 다운 받거나, 빌드 시 미리 렌더링됨</p>
<h2 id="convention">Convention</h2>
<p><code>[folderName]</code></p>
<h2 id="example">Example</h2>
<pre><code class="language-tsx">export default function Page({ params }: { params: { slug: string } }) {
  return &lt;div&gt;My Post: {params.slug}&lt;/div&gt;
}</code></pre>
<p>페이지인 경우 <code>params</code> props로 dynamic route 값이 넘어옴. 
<code>app/blog/[slug]/page.js</code> -&gt; <code>params : {slug : &#39;a&#39;}</code></p>
<h3 id="generatestaticparams">generateStaticParams</h3>
<p><code>generateStaticParams</code> : 빌드 시점에 경로를 정적으로 생성 가능하도록 해주는 함수</p>
<pre><code class="language-ts">export async function generateStaticParams() {
  const posts = await fetch(&#39;https://.../posts&#39;).then((res) =&gt; res.json())

  return posts.map((post) =&gt; ({
    slug: post.slug,
  }))
}</code></pre>
<p>generateStaticParams 내부에서 fetch된 데이터는 다른 페이지의 generateStaticParams에서 데이터를 fetch 하더라도 한번만 데이터를 받아옴.</p>
<h1 id="catch-all-segments">Catch-all segments</h1>
<p>동적 세그먼트는 모든 하위 세그먼트를 가져오도록 확장 될 수 있음</p>
<h2 id="convention-1">Convention</h2>
<p><code>[...folderName]</code></p>
<p><code>app/shop/[...slug]/page.js</code> 에 <code>/shop/a/b/c</code> 로 접근시, <code>param : {slug : [ &#39;a&#39;, &#39;b&#39;, &#39;c&#39;]}</code></p>
<h1 id="optional-catch-all-segments">Optional Catch-all segments</h1>
<h2 id="convention-2">Convention</h2>
<p><code>[[...folderName]]</code></p>
<p>기존 catch-all과의 차이점은 <code>/shop</code> 으로 접근시 param에 잡히는지의 여부</p>
<p>Catch-all segments 에서는 아무것도 잡히지 않지만,
Optional Catch-all segments 에서는 <code>{}</code> 로 잡힌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next 13] Route groups]]></title>
            <link>https://velog.io/@deli-ght/next-13-Route-groups</link>
            <guid>https://velog.io/@deli-ght/next-13-Route-groups</guid>
            <pubDate>Sat, 08 Jul 2023 09:25:32 GMT</pubDate>
            <description><![CDATA[<h1 id="route-groups">Route Groups</h1>
<p>App 디렉토리에서 일반적으로 중첩된 폴더는 URL 경로와 매핑된다.
Route group을 이용하면 URL에 연결되지 않는 폴더를 만들 수 있다.</p>
<h2 id="convention">Convention</h2>
<p><code>(folderName)</code></p>
<h2 id="example">Example</h2>
<h3 id="1-라우트를-url에-영향-없이-구성하기">1. 라우트를 URL에 영향 없이 구성하기</h3>
<p>경로 그룹의 이름은 <strong>정리를 위한 것</strong> 외에는 특별한 의미가 없다. URL 경로에는 영향을 미치지 않는다.</p>
<p>따라서 다른 그룹 내에 있더라도, 같은 URL을 가리키는 경우가 발생해 오류가 생길 수 있다.</p>
<pre><code class="language-js">❌
app
 ㄴ (shirts)
     ㄴ about
         ㄴ layout.jsx
        ㄴ page.jsx   // /about
 ㄴ (pants)
      ㄴ about
         ㄴ layout.jsx
        ㄴ page.jsx   // /about</code></pre>
<p>ex) <code>(shirts)/about/page.js</code>와 <code>(pants)/about/page.js</code>는 모두 <code>/about</code> 링크를 나타냄</p>
<h3 id="2-레이아웃에-특정-세그먼트-선택하고-싶다면-route-group으로-묶어주기">2. 레이아웃에 특정 세그먼트 선택하고 싶다면, route group으로 묶어주기</h3>
<p>각각의 폴더에 각자의 layout을 정할 수도 있다.</p>
<p>만약 도메인에 따라 다른 레이아웃을 가져가고 싶다면, route group을 이용해 서로 다른 레이아웃을 가지도록 할 수 있다.</p>
<ul>
<li>shirts 관련 페이지의 레이아웃에는 상의 목록이 나타나고, pants 관련 페이지에는 하의 목록이 나타나길 원하는 경우, <code>(shirts)/layout.js</code> 과 <code>(pants)/layout.js</code> 으로 분리해 설정 가능</li>
</ul>
<p>최상위 위치에 (app/*) layout파일 없이 여러 루트 레이아웃을 사용하는 경우, 메인 페이지 파일은 경로그룹 중 하나에 정의되어야 한다.</p>
<pre><code class="language-js">❌
app
 ㄴ (shirts)
     ㄴ layout.jsx
 ㄴ (pants)
     ㄴ layout.jsx
 ㄴ page.jsx

 ✅
 app
 ㄴ (shirts)
     ㄴ layout.jsx
    ㄴ page.jsx    
 ㄴ (pants)
     ㄴ layout.jsx</code></pre>
<p>여러 루트 레이아웃을 사용할 때, 레이아웃이 다른 페이지로 이동하는 경우 클라이언트 측 탐색이 아닌 (soft navigation X) 전체 페이지가 새로 로드된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next 13] Linking and Navigating]]></title>
            <link>https://velog.io/@deli-ght/next-13-Linking-and-Navigating</link>
            <guid>https://velog.io/@deli-ght/next-13-Linking-and-Navigating</guid>
            <pubDate>Sat, 08 Jul 2023 08:42:47 GMT</pubDate>
            <description><![CDATA[<p>Next.js router는 <code>server-centric routing</code>과 <code>client-side navigation</code>를 사용하고 있다.
이런 사용 방식은 <strong>즉각적인 로딩 상태</strong>와 <strong>동시 렌더링</strong>이 가능하도록 해주는데, 이는 클라이언트측의 상태를 보존하고, 비용이 많이 드는 리렌더링을 피하며, 중단이 가능하고 교착상태의 원인이 되지 않도록 한다. </p>
<h3 id="route로-이동하는-두가지-방법">Route로 이동하는 두가지 방법</h3>
<ol>
<li>Link 컴포넌트 사용</li>
<li>useRouter 훅 사용</li>
</ol>
<h1 id="link-컴포넌트"><code>&lt;Link&gt;</code> 컴포넌트</h1>
<p><a href="https://nextjs.org/docs/app/api-reference/components/link">https://nextjs.org/docs/app/api-reference/components/link</a>
링크 컴포넌트는 리액트의 컴포넌트로 <code>&lt;a&gt;</code> 태그를 확장해 prefetching을 제공하고, 경로간 클라이언트에서의 이동을 지원한다. Next.js에서 경로를 이동하는 기본적인 방법</p>
<p>next/link를 import해서 사용하며 href prop으로 이동 경로를 받는다.</p>
<pre><code class="language-tsx">import Link from &#39;next/link&#39;

export default function Page() {
  return &lt;Link href=&quot;/dashboard&quot;&gt;Dashboard&lt;/Link&gt;
}</code></pre>
<h2 id="동적인-segment에-연결하기">동적인 segment에 연결하기</h2>
<pre><code class="language-tsx">import Link from &#39;next/link&#39;

export default function PostList({ posts }) {
  return (
    &lt;ul&gt;
      {posts.map((post) =&gt; (
        &lt;li key={post.id}&gt;
          {* use template literal *}
          &lt;Link href={`/blog/${post.slug}`}&gt;{post.title}&lt;/Link&gt;
        &lt;/li&gt;
      ))}
    &lt;/ul&gt;
  )
}</code></pre>
<h2 id="활성화된-링크-확인하기">활성화된 링크 확인하기</h2>
<p><code>usePathname()</code> 훅을 이용해 링크가 현재 활성화된 상태인지(지금 위치한 링크인지) 확인할 수 있다.</p>
<pre><code class="language-tsx">&#39;use client&#39;

import { usePathname } from &#39;next/navigation&#39;
import { Link } from &#39;next/link&#39;

export function Navigation({ navLinks }) {
  const pathname = usePathname()

  return (
    &lt;&gt;
      {navLinks.map((link) =&gt; {
        const isActive = pathname.startsWith(link.href)

        return (
          &lt;Link
            className={isActive ? &#39;text-blue&#39; : &#39;text-black&#39;}
            href={link.href}
            key={link.name}
          &gt;
            {link.name}
          &lt;/Link&gt;
        )
      })}
    &lt;/&gt;
  )
}</code></pre>
<p><a href="http://localhost:3000/snippets/search-params?sort=asc&amp;page=2&amp;perPage=100">http://localhost:3000/snippets/search-params?sort=asc&amp;page=2&amp;perPage=100</a> 링크에 있는 경우, pathname은 <code>/snippets/search-params</code> 을 리턴한다.</p>
<h2 id="id로-이동하기">ID로 이동하기</h2>
<p><code>&lt;Link&gt;</code> 의 기본적인 동작은 변경되는 segment의 상단으로 스크롤 하는 것
href에 id가 명시되어 있다면, 해당 id가 위치한 곳으로 스크롤이 이동
스크롤이 상단으로 올라가는 걸 막고 싶다면, scroll props의 값을 false로 두고 hash(#)된 id 값 넣어주기</p>
<pre><code class="language-tsx">import Link from &#39;next/link&#39;

export default function Page() {
  return &lt;Link href=&quot;/dashboard#title&quot; scroll={false}&gt;Dashboard&lt;/Link&gt;
}</code></pre>
<h1 id="userouter-hook">useRouter() hook</h1>
<p>useRouter 훅은 클라이언트 컴포넌트 내부에서 프로그래밍 방식으로 이동이 가능</p>
<pre><code class="language-tsx">&#39;use client&#39;

import { useRouter } from &#39;next/navigation&#39;

export default function Page() {
  const router = useRouter()

  return (
    &lt;button type=&quot;button&quot; onClick={() =&gt; router.push(&#39;/dashboard&#39;)}&gt;
      Dashboard
    &lt;/button&gt;
  )
}</code></pre>
<blockquote>
<p>훅은 클라이언트단에서만 사용 가능하기 때문에, 가급적 link 컴포넌트 사용해 이동하는 것이 권장됨.</p>
</blockquote>
<h2 id="네비게이션-작동-방식">네비게이션 작동 방식</h2>
<ul>
<li>라우트 이동은 link 태그나 router.push()를 이용</li>
<li>라우터는 브라우저 주소창의 URL을 업데이트함</li>
<li>라우터는 클라이언트 캐시에서 변경되지 않은 segment의 재사용을 통해 불필요한 일을 줄이려고 함. (e.g. layout / partial rendering)</li>
<li>soft navigation에서는 서버보다 캐시에서 세그먼트 데이터를 가져오지만, hard navigation에서는 서버로 부터 서버 컴포넌트를 받아옴.<ul>
<li>soft navigation : 탐색 시 탐색 중인 경로에 동적 세그먼트가 포함되어 있지 않거나 현재 경로와 동일한 동적 매개변수가 있는 경우 Next.js는 소프트 탐색을 사용.<ul>
<li>Navigating from <code>/dashboard/team-red/*</code> to <code>/dashboard/team-red/*</code> will be a soft navigation.</li>
<li>Navigating from <code>/dashboard/team-red/*</code> to <code>/dashboard/team-blue/*</code> will be a hard navigation.</li>
</ul>
</li>
</ul>
</li>
<li>생성된 경우 페이로드를 가져오는 동안 서버에서 로딩 UI가 표시됩니다.</li>
</ul>
<h2 id="클라이언트에서-렌더된-서버컴포넌트-캐싱하기">클라이언트에서 렌더된 서버컴포넌트 캐싱하기</h2>
<p>클라이언트측 캐시는 서버사이드의 Next.js HTTP cache와 다름</p>
<p>새 라우터는 서버컴포넌트의 렌더링된 결과를 갖는 클라이언트 사이드 내부 메모리 캐시를 가집니다. 이 캐시는 route segment에 따라 구분되며, 모든 레벨에서 캐시 만료(무효화)를 허용하고 동시 렌더링 간 일관성을 보장합니다.</p>
<p>user가 앱 내부를 돌아다니면 라우터는 이전 세그먼트와 미리 fetch된 segment 들을 보관합니다. → 서버에서 매번 받아올 필요 X</p>
<h3 id="캐시-유효성-검사">캐시 유효성 검사</h3>
<p>Server Action은 데이터를 필요시에 재확인하기 위한 용도로 사용됨.</p>
<ul>
<li>path 별 : <code>revalidatePath</code></li>
<li>cache tag 별 : <code>revalidateTag</code></li>
</ul>
<p>근데 아직 알파 기능</p>
<h1 id="prefetching">prefetching</h1>
<p>해당 페이지에 방문하기 전에 백단에서 미리 로드해놓는 방법
클라이언트 캐시에 캐싱된 값을 렌더링해준다.</p>
<p>Link로 연결된 페이지들은 기본적으로 <strong>prefetch</strong></p>
<p>useRouter를 사용하는 경우 prefetch 함수 사용</p>
<h2 id="static-and-dynamic-routes">Static and Dynamic Routes:</h2>
<ul>
<li>경로가 정적인 경우, 세그먼트에 대한 모든 서버 컴포넌트가 prefetch 됨</li>
<li>경로가 동적인 경우, 첫번째 공유 레이아웃부터 첫 <code>loading.js</code> 까지 prefetch.<ul>
<li>전체 경로를 동적으로 prefetch하는 데 드는 비용을 줄일 수 있고, 동적 경로에 대한 즉각적인 로딩 상태가 가능해짐</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>prefetching</strong></p>
</blockquote>
<ul>
<li>production에서만 가능</li>
<li><code>&lt;Link&gt;</code>태그에 prefetch={false} 값을 전달해 비활성화 가능</li>
</ul>
<h3 id="soft-navigation">Soft Navigation</h3>
<p>해당 세그먼트에 대한 데이터가 캐싱되어 있는 경우, 서버에서 다시 받아오는게 아니라 캐시를 사용</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next 13] Pages and Layouts]]></title>
            <link>https://velog.io/@deli-ght/next-13-Pages-and-Layouts</link>
            <guid>https://velog.io/@deli-ght/next-13-Pages-and-Layouts</guid>
            <pubDate>Sat, 10 Jun 2023 06:12:23 GMT</pubDate>
            <description><![CDATA[<h2 id="pages">Pages</h2>
<blockquote>
<p>하나의 라우트에 해당하는 UI를 가진 파일</p>
</blockquote>
<h4 id="특징">특징</h4>
<ul>
<li>Page는 항상 라우트 트리의 끝단</li>
<li>Pages의 파일 형식은 항상 <code>.js</code> , <code>.jsx</code> , <code>.tsx</code></li>
<li>Page.js 파일은 공개적으로 접근 가능한 라우트를 만드는데 필요함</li>
<li>기본적으로는 서버 컴포넌트이나, 클라이언트 컴포넌트도 가능</li>
<li>데이터 fetching 가능</li>
</ul>
<h2 id="layouts">Layouts</h2>
<blockquote>
<p>여러 페이지간 공유되는 UI를 가진 파일</p>
</blockquote>
<p>네비게이션 상에서 layouts은</p>
<ul>
<li>상태를 보존</li>
<li>상호작용을 유지하고</li>
<li>리렌더링하지 않음</li>
<li>하위폴더에서도 사용 가능</li>
</ul>
<p>만약 layout.js 파일에서 layout을 default로 export한 경우,
컴포넌트는 반드시 <code>children</code> prop을 받아야 함.
→ 렌더링되는 동안 보여질 예정</p>
<pre><code class="language-tsx">export default function DashboardLayout({
  children, // will be a page or nested layout
}: {
  children: React.ReactNode
}) {
  return (
    &lt;section&gt;
      {/* Include shared UI here e.g. a header or sidebar */}
      &lt;nav&gt;&lt;/nav&gt;

      {children}
    &lt;/section&gt;
  )
}</code></pre>
<h4 id="특징-1">특징</h4>
<ul>
<li>가장 최상위 layout (Root layout)은 전역에 적용되며, <code>html</code> 과 <code>body</code> 를 포함해야함. (필수 컴포넌트)</li>
<li>최상위 레이아웃을 제외한 서브 레이아웃들은 optional. 한번 생성되면 위치한 폴더의 하위 페이지까지 적용됨</li>
<li>Layout은 기본적으로 중첩되어 사용됨</li>
<li>route 그룹을 사용해 공유 레이아웃 안밖에서 특정 route segment를 선택 할 수 있음.</li>
<li>Layout도 기본적으로 서버 컴포넌트</li>
<li>데이터 fetching 가능</li>
<li>상위 레이아웃에서 하위 레이아웃으로 데이터 전달 불가능. 하지만 같은 데이터를 여러번 받아와도 리액트에서 중복되는 호출을 알아서 제거해주기 때문에 성능에 영향을 미치지 않음</li>
<li>layout을 통해 현재 위치한 route segment에 접근 불가. 접근하기 위해선 client 컴포넌트의 <code>useSelectedLayoutSegment</code> 나 <code>useSelectedLayoutSegments</code> 사용</li>
<li>layout 파일과 page 파일은 같은 폴더내에 위치 가능하며, 해당 Page를 layout이 감쌈</li>
</ul>
<h3 id="root-layout-필수">Root layout (필수)</h3>
<pre><code class="language-tsx">export default function RootLayout({ children }: {
  children: React.ReactNode
}) {
  return (
    &lt;html lang=&quot;en&quot;&gt;
      &lt;body&gt;{children}&lt;/body&gt;
    &lt;/html&gt;
  )
}</code></pre>
<h4 id="특징-2">특징</h4>
<ul>
<li>app 디렉토리는 반드시 하나의 root layout을 가지고 있어야 함.</li>
<li>next.js가 자동적으로 <code>html</code> 과 <code>body</code> 를 생성해주지 않기 때문에, 반드시 root layout에 포함되어야 함</li>
<li>해당 html에 <a href="https://nextjs.org/docs/app/building-your-application/optimizing/metadata">내장 SEO support</a> 사용 가능</li>
<li>route group을 이용해 여러 root layout 생성 가능<ul>
<li>app 폴더에 바로 layout을 만들지 않더라도, app 폴더의 하위 폴더들에 <code>html</code>과 <code>body</code>를 포함한 폴더만의 root layout을 만들어 적용 가능</li>
</ul>
</li>
<li>pages directory → app directory로 마이그레이션시, 기존 <code>_app.js</code> 와 <code>_document.js</code> 를 <code>layout</code> 로 변경</li>
</ul>
<h3 id="nested-layout">Nested layout</h3>
<p>상위 레이아웃은 하위 레이아웃 및 파일들을 children props 를 통해 감싸게 됨.</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/8ad9f7c5-902d-4761-9f6d-1b4965d693b9/image.png" alt=""></p>
<h2 id="templates">Templates</h2>
<p>하위 layout이나 page를 감싼다는 점에서 layout과 비슷
경로 전체에서 상태를 유지하는 layout과 달리 <strong>template은 각 하위 항목에 대해 새 인스턴스 생성</strong>
→ template을 공유하는 페이지간 이동시, 상태는 reset되고 effect는 재실행됨</p>
<h4 id="사용-예시">사용 예시</h4>
<ul>
<li>입장/퇴장 애니메이션이 있는 라이브러리</li>
<li>useEffect이나 useState 기반의 기능</li>
<li>프레임워크의 기본 동작을 바꾸는 경우, (ex, suspense는 처음 로드시에만 fallback을 보여주는데, 페이지 이동시마다 fallback을 보여줘야 하는 경우)</li>
</ul>
<p>→ 위 상황이 아니라면 layout 사용이 권장됨.</p>
<p>export를 default로 하는 경우, 중첩된 segment 전역에 적용
→ 그게 아니라면 self로 호출해야하나?
    - self로 호출시, unique key를 넘겨주어야 함.</p>
<pre><code class="language-tsx">&lt;Layout&gt;
  {/* Note that the template is given a unique key. */}
  &lt;Template key={routeParam}&gt;{children}&lt;/Template&gt;
&lt;/Layout&gt;</code></pre>
<h2 id="modifying-head">Modifying <code>&lt;head&gt;</code></h2>
<p><code>metadata</code> 객체나 <code>generateMetadata</code>를 export해 <code>layout</code> 이나 <code>page</code> 에서 사용함으로써 적용 가능</p>
<pre><code class="language-tsx">import { Metadata } from &#39;next&#39;

export const metadata: Metadata = {
  title: &#39;Next.js&#39;,
}

export default function Page() {
  return &#39;...&#39;
}</code></pre>
<p>metadata를 <code>&lt;head&gt;</code>에 수동으로 입력할 필요 없이 <a href="https://nextjs.org/docs/app/building-your-application/optimizing/metadata">Metadata api</a>를 사용해 <code>&lt;head&gt;</code> 요소의 스트리밍 및 중복 제거와 같은 고급 요구사항을 자동으로 처리</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next 13] Routing Fundamentals]]></title>
            <link>https://velog.io/@deli-ght/next-13-Routing-Fundamentals</link>
            <guid>https://velog.io/@deli-ght/next-13-Routing-Fundamentals</guid>
            <pubDate>Sat, 10 Jun 2023 06:03:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Nextjs Documents
[Routing Fundamentals]
(<a href="https://nextjs.org/docs/app/building-your-application/routing">https://nextjs.org/docs/app/building-your-application/routing</a>)
[Defining Route]
(<a href="https://nextjs.org/docs/app/building-your-application/routing/defining-routes">https://nextjs.org/docs/app/building-your-application/routing/defining-routes</a>)</p>
</blockquote>
<p><strong>목표</strong> : Routing에 필요한 개념들 이해하기</p>
<h1 id="routing-fundamentals">Routing Fundamentals</h1>
<p>모든 application의 뼈대 == <code>routing</code>
한 어플리케이션에서 구조를 나타내기 위해 사용되는 링크 요소를 의미함.</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/896e29b2-2a8b-4090-88ef-fb697ebbcfbf/image.png" alt=""></p>
<p><strong>url segment</strong> : 도메인 이후 <code>/</code>로 구분되는 부분
<strong>url path</strong> : 도메인 이후 모든 부분</p>
<h2 id="router-of-folders-and-files">Router of folders and files</h2>
<p>next.js는 file based router</p>
<p><strong>folder</strong> : router를 결정하기 위한 것 (page.js를 포함)
<strong>file</strong> : UI를 만들기 위함 (special files)</p>
<h2 id="route-segment">Route segment</h2>
<p><strong>folder는 route segment로 사용됨</strong></p>
<p>ex) <code>www.dana.com</code>이라는 도메인을 사용하는 프로젝트에 app 하위에 <code>introduce</code> 라는 폴더가 있는 경우(+ 해당 폴더에 <code>page.js</code> 존재), <code>www.dana.com/introduce</code> 에 접근 가능</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/d3babdc9-0514-4019-9e2d-c529d667302c/image.png" alt=""></p>
<h2 id="colocation">Colocation</h2>
<p><code>page.js</code> / <code>router.js</code> 만 router로 사용 가능하기 때문에 <strong>같은 폴더 내 UI component 포함 가능</strong></p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/c911375c-eb69-4582-939a-32bce5a77346/image.png" alt=""></p>
<h2 id="server-centric-routing">Server centric routing</h2>
<p>client가 route map 다운로드 X
link 컴포넌트와 같이 client-side인 경우, 유저가 새 링크로 이동
→ 페이지를 reload X
→ 변경되는 컴포넌트(segment)만 render</p>
<p>유저가 접근한 페이지의 서버 컴포넌트 데이터를 segment별로 나뉜 client side cache에 저장</p>
<ul>
<li>모든 수준에서 무효화를 허용</li>
<li>동시 렌더링에서 일관성을 보장</li>
</ul>
<p>→ 이전에 사용된 segment의 재사용이 가능해 더 나은 성능을 갖게 된다는 의미</p>
<h2 id="partial-rendering">Partial rendering</h2>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/aab18974-6aee-406b-82bc-ad76af2dc176/image.png" alt=""></p>
<p>(e.g. /dashboard/settings and /dashboard/analytics)</p>
<p>형제 라우터들이 서로 이동하는 경우, 레이아웃은 그대로 유지되고 부분적으로만 새로운 컴포넌트가 렌더링됨. (segment만 렌더링)</p>
<h2 id="advanced-rounting-patterns">Advanced Rounting Patterns</h2>
<h3 id="parallel-routes">Parallel routes</h3>
<p>동시에 두개 이상의 페이지를 독립적으로 이동 가능한 하나의 뷰로 보여주는 방법</p>
<h3 id="intercepting-routes">Intercepting routes</h3>
<p>route를 가로채 다른 route의 내용을 보여주는 방법
현재 페이지가 중요해 해당 내용을 유지하기 위해 사용
ex) 하나의 업무를 수정하면서 다른 모든 업무를 확인해야할 때, 피드에서 사진을 확대할 때</p>
<h1 id="defining-route">Defining Route</h1>
<p>Next.js는 <strong>file-system</strong> based router</p>
<h3 id="folder">Folder</h3>
<p>폴더가 곧 route segment
하위 라우트를 만들기 위해선 해당 폴더 안에 하위 라우트 폴더를 만들면 됨.</p>
<h3 id="pagejs">page.js</h3>
<p>page.js 파일은 공개적으로 접근 가능하도록 해줌.
해당 파일이 없는 폴더는 컴포넌트 저장소나 스타일시트, 이미지 등등 으로 활용가능</p>
<h2 id="creating-ui">Creating UI</h2>
<p>Special file convention은 각각의 route segment에 UI를 생성하기 위해 사용됨.</p>
<p><code>page.jsx</code>는 하나의 라우트에 해당하는 UI를 보여주고,
<code>layout.jsx</code>는 여러 라우트에 공유되는 UI를 보여줌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[next13] Server components]]></title>
            <link>https://velog.io/@deli-ght/next13-Server-components</link>
            <guid>https://velog.io/@deli-ght/next13-Server-components</guid>
            <pubDate>Sun, 04 Jun 2023 08:50:50 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 <a href="https://nextjs.org/docs/getting-started/react-essentials">https://nextjs.org/docs/getting-started/react-essentials</a> 를 기반으로 작성되었습니다.</p>
</blockquote>
<h1 id="react-essentials">React Essentials</h1>
<p><strong>목표</strong> : next 13을 본격적으로 공부하기 전에, 메인이 되는 서버 컴포넌트가 정확히 무엇인지 이해하기</p>
<p><strong>다룰 내용</strong></p>
<ul>
<li>서버 컴포넌트 VS 클라이언트 컴포넌트</li>
<li>서버 컴포넌트를 언제 사용하면 좋을지</li>
<li>추천하는 사용 패턴</li>
</ul>
<h2 id="서버-컴포넌트">서버 컴포넌트</h2>
<p>기존에는 default가 client 베이스였고, 페이지에 따라 SSR과 CSR을 나눌 수 있었음.
이제는 페이지별로 나누는게 아닌 <strong>컴포넌트</strong>별로 나눌 수 있게 됨.</p>
<p>기본 렌더링 방식은 SSR이고 내부는 Client 컴포넌트로 구성 가능
-&gt; next.js의 서버 우선 접근 방식과 일치</p>
<h3 id="왜-서버-컴포넌트를-사용해야-하는가">왜 서버 컴포넌트를 사용해야 하는가</h3>
<ol>
<li><p>개발자들이 서버 인프라를 더 잘 활용할 수 있게 해줌</p>
<ul>
<li>데이터를 가져오는 부분을 DB랑 더 가까운 서버로 옮김</li>
<li>JS 번들 크기에 영향을 미치던 대규모 종속성을 서버에 유지해 성능 개선</li>
<li>서버 컴포넌트 사용시 PHP나 Ruby on Rails 처럼 사용하면서도, React의 장점(UI지향형 컴포넌트, 유연성 등)을 살릴 수 있음.</li>
</ul>
</li>
<li><p>초기 페이지의 로딩이 빠르고, Client-side 번들 사이즈가 줄어듦</p>
<ul>
<li>기본 client-side runtime 캐싱 가능하고 크기가 예측 가능</li>
<li>그 외에 서버 컴포넌트로 사용되는 코드들은 크기게 영향을 주지 않음 (서버에서 처리되기 때문)</li>
</ul>
</li>
</ol>
<blockquote>
<p>Next 13의 <strong>App directory에 있는 모든 컴포넌트는 <code>Server component</code></strong>
(page 뿐만 아니라 special files 및 같은 파일 내 존재하는 컴포넌트 포함)</p>
</blockquote>
<ul>
<li>special files : error.tsx, layout.tsx ...</li>
<li>next 13부터는 app 내에 페이지 뿐만 아니라 컴포넌트도 함께 위치시킬 수 있음 (app/main/page.tsx 와 app/main/input.tsx는 같은 main 폴더 내에 있지만, routing은 page만 가능)</li>
</ul>
<h2 id="클라이언트-컴포넌트">클라이언트 컴포넌트</h2>
<p><code>&quot;use client&quot;</code>을 파일 맨 앞에 명시해 클라이언트 컴포넌트로 지정 가능
명시된 파일에 사용된 모든 모듈들은 client 번들에 포함됨.</p>
<h2 id="언제-어떤-컴포넌트를-사용하는게-좋을까">언제 어떤 컴포넌트를 사용하는게 좋을까?</h2>
<p>클라이언트에서 반드시 사용되어야하는 상황이 아니라면 서버 컴포넌트 권장</p>
<ul>
<li>event handler 사용 (onClick, onChange ...)</li>
<li>useHook 사용시 (useState, useEffect ...)</li>
<li>위의 hook을 사용한 custom hook 사용시</li>
<li>browser API 사용시</li>
<li>React class component 사용시</li>
</ul>
<h2 id="사용-패턴">사용 패턴</h2>
<h3 id="1-클라이언트-컴포넌트를-dom-트리의-가장-끝단으로-만들어-사용하기">1. 클라이언트 컴포넌트를 DOM 트리의 가장 끝단으로 만들어 사용하기</h3>
<h3 id="2-클라이언트-컴포넌트와-서버-컴포넌트-병합하기">2. 클라이언트 컴포넌트와 서버 컴포넌트 병합하기</h3>
<h4 id="react에서-렌더링하는-방식">React에서 렌더링하는 방식</h4>
<ul>
<li>클라이언트에 결과를 전송하기 전에 모든 서버 컴포넌트 렌더링<ul>
<li>이 때, 클라이언트 컴포넌트에 포함된 서버 컴포넌트도 포함</li>
<li>클라이언트 컴포넌트 자체는 스킵됨</li>
</ul>
</li>
<li>클라이언트에서 렌더링하면서, 서버에서 만들어진 결과물을 끼워 넣음</li>
</ul>
<h4 id="nextjs에서-렌더링하는-방식">Next.js에서 렌더링하는 방식</h4>
<ul>
<li>일단 서버에서 서버 컴포넌트와 클라이언트 컴포넌트 모두 HTML로 변환(pre-render)</li>
</ul>
<h3 id="3-클라이언트-컴포넌트-안에-서버-컴포넌트-넣기">3. 클라이언트 컴포넌트 안에 서버 컴포넌트 넣기</h3>
<pre><code class="language-tsx">&#39;use client&#39;;

// This pattern will **not** work!
// You cannot import a Server Component into a Client Component.
import ExampleServerComponent from &#39;./example-server-component&#39;;

export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, setCount] = useState(0);

  return (
    &lt;&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;{count}&lt;/button&gt;
      &lt;ExampleServerComponent /&gt; // ❌❌❌
    &lt;/&gt;
  );
}</code></pre>
<p>이렇게 클라이언트 컴포넌트 내부에 서버 컴포넌트를 바로 넣어두는 것은 지원하지 않는 패턴
앞서 언급했듯 이렇게 되면 서버컴포넌트를 클라이언트에서 한번 더 계산해 줘야하기 때문 (round trip)</p>
<p><strong>해결 방법</strong> : 클라이언트 컴포넌트에 서버 컴포넌트 <code>Props</code>로 전달하기</p>
<p>주로 <code>children</code> 형식으로 넘겨주는 방식이 사용됨.
props 형태로 넘겨주는 이유는 클라이언트 컴포넌트 자체에서 들어오는 <strong>props가 무엇인지 알 필요가 없고</strong> 단지 이 props가 <strong>어디에 들어갈지</strong> 위치만 알고 있으면 되기 때문</p>
<pre><code class="language-tsx">&#39;use client&#39;;

import { useState } from &#39;react&#39;;

export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, setCount] = useState(0);

  return (
    &lt;&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;{count}&lt;/button&gt;
      {children}
    &lt;/&gt;
  );
}</code></pre>
<h3 id="4-서버-컴포넌트에서-클라이언트-컴포넌트로-props-전달하기-serialization">4. 서버 컴포넌트에서 클라이언트 컴포넌트로 props 전달하기 (serialization)</h3>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/3b0354a0-a763-4466-92d7-5fd07044f008/image.png" alt=""></p>
<p>서버에서 클라이언트로 전달되는 props들은 serialization 과정을 거침.
따라서 Props로 전달 시, functions, Date와 같은 형식은 전달 불가</p>
<blockquote>
<p><a href="https://developer.mozilla.org/en-US/docs/Glossary/Serialization">Serialization?</a>
네트워크나 스토리지에 전달되기 위한 파일 형태로 변환하는 것.
JS에서는 <code>Json.stringify()</code>를 이용해 서버에 데이터를 보내는 방식을 의미한다. 따라서 deep copy가 불가능한 것들(unserializable)은 props로 보낼 수 없다. </p>
</blockquote>
<p><a href="https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy">https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy</a></p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/55faec7d-9c2a-4a37-97cb-c3b5729a6b86/image.png" alt=""></p>
<h3 id="5-클라이언트-컴포넌트-밖에서-server-only-code-유지하기">5. 클라이언트 컴포넌트 밖에서 Server-only code 유지하기</h3>
<p>서버에서만 돌아가야 하는 코드가 클라이언트 컴포넌트에 침투하게 될 수 있음.
이를 막기 위해 <code>server-only</code> packages 사용</p>
<pre><code class="language-bash">npm install server-only</code></pre>
<p>설치 후, 서버에서만 사용되어야 하는 모듈에 import 해줌.
클라이언트 컴포넌트에서 해당 모듈 사용시, build-time error 발생</p>
<blockquote>
<p>자매품 <code>client-only</code>도 있음</p>
</blockquote>
<blockquote>
<p>서버에서만 사용되는 케이스
<code>next_public</code>이 붙지 않은 환경 변수는 서버에서만 접근 가능하기 때문에, 서버에서 안전하게 key를 이용해서 데이터를 받아와야 하는 경우, 클라이언트에서 해당 모듈을 사용하면 안됨.</p>
</blockquote>
<pre><code class="language-ts">import &#39;server-only&#39;;
&gt;
export async function getData() {
  const res = await fetch(&#39;https://external-service.com/data&#39;, {
    headers: {
      authorization: process.env.API_KEY,
    },
  });
&gt;
  return res.json();
}</code></pre>
<h3 id="6-외부-라이브러리-사용">6. 외부 라이브러리 사용</h3>
<p>대부분의 라이브러리들은 client 전용으로 만들어졌지만, <code>&quot;use client&quot;</code>가 표기되어 있지 않기 때문에, 서버 컴포넌트에서 호출 시 에러가 발생하게됨.</p>
<p>이를 막기 위해 외부 라이브러리의 컴포넌트를 별도의 컴포넌트로 다시 만들어서 사용해주어야 함.</p>
<pre><code class="language-tsx">&quot;use client&quot;

import { clientComponent } from &quot;third-party-library&quot;

export default clientComponent</code></pre>
<h2 id="context">Context</h2>
<p>Server는 상태(state)를 갖지 않기 때문에 context는 클라이언트에서만 지원됨.
따라서 Provider도 위의 라이브러리처럼 client로 감싸서 사용해야함.</p>
<p>서버 컴포넌트간 데이터 공유가 필요한 경우,
데이터 자체를 공유하는게 아닌 DB에 대한 엑세스를 공유 (global singleton)
동일한 접근자를 가지고 각각의 컴포넌트에서 각자 데이터 호출</p>
<p>어처피 중복된 fetch 호출에 대해 <a href="https://nextjs.org/docs/app/building-your-application/data-fetching#automatic-fetch-request-deduping">next가 중복을 제거해 처리</a>하기 때문에, 반복 호출에 대해 걱정할 필요가 없음.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Yarn 명령어]]></title>
            <link>https://velog.io/@deli-ght/Yarn-%EB%AA%85%EB%A0%B9%EC%96%B4</link>
            <guid>https://velog.io/@deli-ght/Yarn-%EB%AA%85%EB%A0%B9%EC%96%B4</guid>
            <pubDate>Wed, 12 Apr 2023 08:13:05 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>하나씩 공부할 때마다 추가되는 Yarn 명령어 모음집</p>
</blockquote>
<h2 id="yarn-workspace-foreach-명령어-이름"><a href="https://yarnpkg.com/cli/workspaces/foreach">Yarn workspace foreach &lt;명령어 이름&gt;</a></h2>
<blockquote>
<p>사용 전, workspace tools를 사용해줘야 함
<code>yarn plugin import workspace-tools</code></p>
</blockquote>
<p>모든 workspace에 해당 command를 실행</p>
<h3 id="옵션">옵션</h3>
<ul>
<li><code>-p</code> : 병렬로 실행</li>
<li><code>-v</code> : 어떤 workspace의 실행 결과인지 앞에 표시</li>
<li><code>-t</code> : 의존적인 workspace가 있는 경우, 선행 되어야하는 workspace가 종료된 후 실행</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[next.js의 rewrite와 redirect]]></title>
            <link>https://velog.io/@deli-ght/nextrewrite%EC%99%80-redirect</link>
            <guid>https://velog.io/@deli-ght/nextrewrite%EC%99%80-redirect</guid>
            <pubDate>Tue, 17 Jan 2023 13:37:19 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>tldr;</strong> 유저가 어떤 path로 접근하는 경우, 특정 사이트로 옮겨주는 next 자체 기능 2가지
공식 문서 확인하기 : <a href="https://nextjs.org/docs/api-reference/next.config.js/rewrites">rewrites</a> / <a href="https://nextjs.org/docs/api-reference/next.config.js/redirects">redirect</a></p>
</blockquote>
<h1 id="공통점과-차이점">공통점과 차이점</h1>
<h2 id="공통점">공통점</h2>
<p>next.js에서는 <code>rewrite</code>와 <code>redirect</code> 기능을 제공한다. 
두가지 모두 유저가 특정 path로 이동시 정해진 화면이 보이도록 한다.</p>
<h2 id="차이점">차이점</h2>
<p>두 설정의 다른 점은
<strong>rewrite</strong>는 유저가 입력한 url 그대로 유저에게 보여져 유저는 화면이 변경된지 모르는 반면,
<strong>redirect</strong>는 정해진 path로 url이 바뀌게 된다.</p>
<p>예를 들어 유저가 <code>www.minju.com/old</code> 라는 path로 진입시, <code>www.minju.com/new</code>의 페이지를 보여주고 싶다면,
<strong>rewrite</strong>는 주소창의 url이 <code>www.minju.com/old</code> 인채로 <code>/new</code>의 화면이 보여지고
<strong>redirect</strong>는 <code>www.minju.com/old</code>을 <code>www.minju.com/new</code>로 리다이렉트 해주어 유저가 입력한 값과 달리 <code>www.minju.com/new</code>가 주소창에 나타나게 된다. </p>
<h1 id="설정-방법">설정 방법</h1>
<p>설정 방법은 <code>next.config.js</code> 파일에 다음과 같은 설정값을 추가해주면 된다.</p>
<pre><code class="language-js">module.exports = {
  // rewrite
  async rewrites() {
    return [
      {
        // source : 유저가 진입할 path
        // destination : 유저가 이동할 path
        source: &#39;/about&#39;,
        destination: &#39;/&#39;,
      },
    ]
  },
  // redirect
  async redirects() {
    return [
      {
        source: &#39;/about&#39;,
        destination: &#39;/&#39;,
        permanent: true,
      },
    ]
  },
}</code></pre>
<blockquote>
<p>permanent 속성에 대해선 아래 속성에서 함께 소개하도록 하겠다.</p>
</blockquote>
<h1 id="source와-destination">source와 destination</h1>
<h2 id="path-매칭">path 매칭</h2>
<h3 id="path">:path</h3>
<p><code>:</code> 를 이용해 다이나믹한 path 값을 받아올 수 있다.</p>
<pre><code class="language-js">source: &#39;/old-blog/:slug&#39;,
destination: &#39;/news/:slug&#39;,</code></pre>
<p>이렇게 설정되어있는 경우,
<code>/old-blog/apple</code> 입력시 <code>/news/apple</code>로
<code>/old-blog/banana</code> 입력시 <code>/news/banana</code>로 이동된다.</p>
<p>만약 <code>/old-blog/apple/green</code>으로 입력시, 일치하는 경로가 없어 입력한 url(🍏) 그대로 이동된다. </p>
<h3 id="path-1">:path*</h3>
<p>하지만 다이나믹 path 옆에 <code>*</code>을 붙인다면?</p>
<pre><code class="language-js">source: &#39;/old-blog/:slug*&#39;,
destination: &#39;/news/:slug*&#39;,</code></pre>
<p><code>/old-blog/apple/green</code> -&gt; <code>/news/apple/green</code>
<code>/old-blog/apple/green/delicious</code> -&gt; <code>/news/apple/green/delicious</code>
경로 길이에 상관 없이 rewrite 혹은 redirect 된다. </p>
<h3 id="와일드-카드--정규식">와일드 카드 / 정규식</h3>
<p>도 사용 가능하다^ㅁ^</p>
<h2 id="rewrite에서만-적용되는-자동-쿼리">rewrite에서만 적용되는 자동 쿼리</h2>
<p>rewrite에서는 생략된 path에 대해 자동 쿼리로 넘겨주는 속성이 있다. 
예를 들어 다음과 같이 설정했을 때, </p>
<pre><code class="language-js">source: &#39;/old-blog/:slug&#39;,
destination: &#39;/news&#39;,</code></pre>
<p>source에서는 다이나믹 Path를 받았지만, destination 그 어디에도 받아온 slug를 사용하는 곳이 없다. 이런 경우 slug로 입력받은 값은 destination의 쿼리로 넘겨진다. </p>
<pre><code class="language-ts">module.exports = {
  reactStrictMode: true,
  async rewrites() {
    return [
      {
        source: &quot;/test/:slug&quot;,
        destination: &quot;/episodes&quot;,
      },
    ];
  },</code></pre>
<pre><code class="language-ts">// Episodes.tsx
const Episodes: NextPage = () =&gt; {
  const router = useRouter();
  console.log(router.query);
  ...
}</code></pre>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/2b5ca977-2f48-4a26-b727-a3aac85716c2/image.png" alt=""></p>
<blockquote>
<p><strong>만약 *를 이용해 여러 path를 받는 경우엔..?</strong>
<code>/test/a/b/c/e/d</code> 를 입력한 경우 배열로 해당 값을 받게 된다. 
<img src="https://velog.velcdn.com/images/deli-ght/post/5694ba09-98ca-4c52-ae3c-5a0b51edad40/image.png" alt=""></p>
</blockquote>
<h1 id="다른-속성들">다른 속성들</h1>
<p>기본적인 <code>source</code>와 <code>destination</code> 속성 외에도 다양한 속성들이 있다. </p>
<h2 id="has--missing">has &amp; missing</h2>
<p>rewrite나 redirect를 path뿐만 아니라 헤더나 쿠키, 쿼리 값이 일치하거나(<code>has</code>) 일치하지 않는 경우(<code>missing</code>)에만 실행할 수 있도록 사용하는 속성</p>
<p><code>has</code>에 있는 속성은 <strong>전부</strong> 일치해야하며
<code>missing</code>에 있는 속성은 <strong>전부</strong> 일치해선 안된다.</p>
<h3 id="속성-타입">속성 타입</h3>
<pre><code class="language-ts">{
    type : &#39;header&#39; | &#39;cookie&#39; | &#39;host&#39; | &#39;query&#39;,
    key : String,
    value : String | undefined
}</code></pre>
<h3 id="사용-예시">사용 예시</h3>
<pre><code class="language-js">module.exports = {
  // rewrite
  async rewrites() {
    return [
      {
        source: &#39;/:path((?!another-page$).*)&#39;,
          has: [
            {
              type: &#39;header&#39;,
              key: &#39;x-redirect-me&#39;,
            },
          ],
            permanent: false,
              destination: &#39;/another-page&#39;,
      },
      {
        source: &#39;/:path((?!another-page$).*)&#39;,
        missing: [
          {
            type: &#39;header&#39;,
            key: &#39;x-do-not-redirect&#39;,
          },
        ],
        permanent: false,
        destination: &#39;/another-page&#39;,
      },
      ]
  }
}</code></pre>
<h2 id="basepath">basepath</h2>
<p>만약 <code>module.exports</code>에 <code>basepath</code> 설정을 해둔 경우, source path에 적용 여부를 나타낸다. <code>false</code>는 destination에 <strong>외부 링크가 사용된 경우</strong>에만 사용할 수 있는 속성이다.</p>
<pre><code class="language-js">module.exports = {
  basePath: &#39;/docs&#39;,

  async redirects() {
    return [
      {
        source: &#39;/with-basePath&#39;, // automatically becomes /docs/with-basePath
        destination: &#39;/another&#39;, // automatically becomes /docs/another
        permanent: false,
      },
      {
        // does not add /docs since basePath: false is set
        source: &#39;/without-basePath&#39;,
        destination: &#39;https://example.com&#39;,
        basePath: false,
        permanent: false,
      },
    ]
  },
}</code></pre>
<h2 id="permanent">permanent</h2>
<blockquote>
<p>✅ redirect에만 존재하는 속성값</p>
</blockquote>
<p><strong>permanent</strong> 속성값은 유저나 검색 엔진에서 해당 리다이렉트 값을 영구적으로 저장할 것인지에 대한 여부다. 만약 이벤트 페이지 이거나 임시 페이지인 경우는 <code>false</code>로 지정해주면 된다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2022년을 마무리하며]]></title>
            <link>https://velog.io/@deli-ght/2022%EB%85%84%EC%9D%84-%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@deli-ght/2022%EB%85%84%EC%9D%84-%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0</guid>
            <pubDate>Tue, 10 Jan 2023 04:12:11 GMT</pubDate>
            <description><![CDATA[<p>2022 회고를 어떻게 시작해야할까 고민하느라 쉽게 글을 작성하지 못했다. 2022는 내가 진짜 개발자로서 일을 시작한 해이기도 했고, 한 해의 대부분이 개발로 점철된 해였어서 개발 회고로 할 말이 너~~무 많아서 정리하는데도 시간이 많이 걸렸다. 2022 회고.. 가보자고..!</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/859d1661-49d7-4229-b225-d2594322a8d4/image.png" alt=""></p>
<h2 id="개인-공부-😭">개인 공부 😭</h2>
<p>올해 가장 못한 것 탑 3에 드는 개인 공부.. 약간의 찍먹들을 했다고 말하기 민망해서 큼지막하게 했다고 말할 수 있는 공부만 골라보았다. 근데 사실 이것도 맘에 안듦.. 내년에는 개인 공부하는 시간을 확보하고 계획을 세워서 진짜 알차게 보내고 싶다. </p>
<h3 id="uiux-공부">UI/UX 공부</h3>
<p>작년 내 회고글에도 적혀있던 UI/UX 공부. 도널드 노먼씨의 책을 올해 초에 구매했으나 약 11개월 뒤인 12월에 부랴부랴 올해 버킷리스트를 채우기 위해 읽었다. <code>감성디자인</code>과 <a href="https://velog.io/@deli-ght/%EB%94%94%EC%9E%90%EC%9D%B8%EA%B3%BC-%EC%9D%B8%EA%B0%84%EC%8B%AC%EB%A6%AC-1.-%EC%83%9D%ED%99%9C%EC%9A%A9%ED%92%88%EC%9D%98-%EC%A0%95%EC%8B%A0-%EB%B3%91%EB%A6%AC%ED%95%99"><code>디자인과 인간심리</code></a>를 읽었는데, 아무래도 실무적인 UI/UX보단 이론에 가까워 실무적인 공부를 하고 싶다는 생각이 들었다. 프로젝트하는데 이왕이면 디자인도 하면 좋으니깐..!</p>
<blockquote>
<p>🍀 2023 목표 : <strong>피그마 공부하기</strong></p>
</blockquote>
<h3 id="http-완벽가이드">HTTP 완벽가이드</h3>
<p>올해 생일선물로 받았던 HTTP 완벽가이드. 이것도 올해 안에 읽기를 목표로 하고 있었으나 어느덧 12월이 되어버렸고, 12월 목표로 <em>HTTP 1회독하기</em> 로 결정했다. 원래 목표는 읽으면서 벨로그에 정리하는 것이었는데, 읽다보니 배워야할 것들이 방대해 매일 하나씩 정리하기에는 버겁다고 느꼈고, 읽다가 지치는 것보다 완주가 목표였기 때문에 일단 1회독하고 추후 다시 천천히 읽으며 정리해보기로 했다. 그리고 그게 올해 목표...^ㅁ^..</p>
<p>얼레벌레 읽은 1회독이긴 했지만 내용이 굉장히 알차고 재밌었다. 그동안 추상적으로 알던 개념들을 퍼즐 맞추듯이 읽고 있어서 맞는 자리를 찾을 때마다 유레카를 외치게 된다. 물론 이게 10,000,000,000피스짜리 퍼즐인게 함정이지만..차근차근 정리하면서 공부하다보면 어떻게든 자리를 찾아가지 않을까..?</p>
<blockquote>
<p>🍀 2023 목표 : <strong>HTTP 완벽 가이드 딥하게 공부하기</strong></p>
</blockquote>
<h3 id="자바스크립트">자바스크립트</h3>
<p>올해 코어자바스크립트와 딥다이브 스터디를 진행했었다. 코어 자바스크립트를 먼저 공부하고 딥다이브를 진행했는데, 중요한 개념들을 미리 훑고 딥하게 공부해서 다행이다 싶었다. 물론 모든 내용을 기억하긴 어렵지만 자바스크립트를 사용하면서 의문을 가졌던 것들이나 새로운 기능들을 공부할 수 있어서 공부하는 동안 흥미진진했다. 딥다이브를 한번 더 읽어보고 싶지만 시간이 오래 걸릴 듯 하고 코어 자바스트립트는 몇번씩 반복해서 읽어봐야겠다. </p>
<h3 id="타입스크립트">타입스크립트</h3>
<p>타입스크립트는 올해 처음 사용 및 공부하기 시작했다. 진짜 얕은 타입스크립트 실력으로 실무에도 투입되었고, 실무에서 타입스크립트를 사용하면서 레거시 코드를 만들어내는 나 자신을 용서하기 힘들었다. 그래서 올해 퇴사 후에 꼭 타입스크립트 공부를 제대로 하고 싶었고, 퇴사하자마자 타입스크립트 강의를 결제했다. 왜 업무하면서 공부하지 못했는지 궁금하겠지만 이건 차차 아래에서 다루도록 하고... 강의를 결제한 이유는 <code>기강잡기</code>와 <code>감잡기</code> 두가지 이유에서였다. 기강잡기는 내 심리적인 문제가 컸고, 감잡기는 길을 잘못든 타입스크립트 사용을 바로 잡아줄 멘토가 필요해서였다. 아무튼 좋은 선택이었고, 아는만큼 보인다고 이제는 고칠 수 없는 지난 회사 코드들을 당장이라고 회사로 찾아가 고치고 싶은 마음이 굴뚝같아져 더욱 괴로워졌지만, 앞으로 사용하는 타입스크립트는 더이상의 이런 불상사가 없도록 더 공부하고 더 많이 사용하고 싶다. </p>
<blockquote>
<p>🍀 2023 목표 : <strong>이펙티브 타입스크립트 다시 도전하기</strong></p>
</blockquote>
<h2 id="진행한-프로젝트">진행한 프로젝트</h2>
<p>되게 올해가 빨리간 기분이었는데, 프로젝트 정리를 하면서 이게 올해였다고..??! 화들짝 놀랐다. 시간 가는게 아무 것도 없이 빠르게 지나간 줄 알았는데 나름 알차게 보냈구나 싶어서 혼자 괜시리 뿌듯했다,, 작년에 프론트엔드를 공부하기로 결정하고 가장 하고싶었던게 프로젝트였는데 아쉬웠던 점을 이뤄 보람찼다. 올해에는 좀 더 양질의 프로젝트를 할 수 있기를..!</p>
<h3 id="감귤마켓">감귤마켓</h3>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/628a83cb-e11d-4cd2-ad0f-8be6905455c0/image.png" alt=""></p>
<blockquote>
<p>✔️ 프로젝트 진행 과정 확인하기
<a href="https://velog.io/@deli-ght/%EA%B0%90%EA%B7%A4%EB%A7%88%EC%BC%93-1">감귤마켓 1</a>
<a href="https://velog.io/@deli-ght/%EA%B0%90%EA%B7%A4%EB%A7%88%EC%BC%932">감귤마켓 2</a></p>
</blockquote>
<p>HTML, CSS, JS로 진행한 프로젝트로 멋사 수업에 포함되어있던 팀프로젝트였다. 프로젝트 경험이 별로 없었지만 그간의 경험들을 토대로 PM 역할을 하기 위해 노력했다. 초보 얼레벌레 리더였지만 잘 따라와준 동료들 덕분에 필수 구현 사항들을 전부 구현할 수 있었고, 마지막 동료 피드백으로 칭찬을 받아서 매우 뿌듯했다. (피드백 결과를 5월에서야 받긴 했지만 예상치 못한 선물이라 기분이 좋았다 😉)
<img src="https://velog.velcdn.com/images/deli-ght/post/025f2676-2ae2-4fd1-80a8-c61b7e6bd577/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/331f0e12-95dc-411b-8d8b-e062e5a6e57f/image.png" alt=""></p>
<h3 id="스페이스로그">스페이스로그</h3>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/a3efeaa8-0547-4a6b-a8c7-3e227d431758/image.png" alt=""></p>
<blockquote>
<p>스페이스로그 진행과정 확인하기
<a href="https://velog.io/@deli-ght/series/velog-clone-coding">스페이스로그 글 목록</a></p>
</blockquote>
<p>멋사 수업에서 가장 좋았던 점은 개발자 친구들을 사귈 수 있다는 점이었다. 비전공자의 숙원같은 느낌이랄까.. 친구를 사귀어야지 하고 복학했는데 코로나가 터져버린 복학생의 비애랄까.. 암튼 그런 한(?)이 있었는데, 이곳에서 스데브를 만났다. 규리님을 주축으로 진짜 제대로 프로젝트 한번 해 보자! 하고 모였고 벨로그를 따라하되, 우리만의 무언가를 넣어보자! 하고 시작한 프로젝트였다. (여담이지만 &#39;우리만의 무언가&#39;로 다크모드를 추가했는데, 개발 도중 벨로퍼트님이 만들어버리셨다. 😨)</p>
<p>개발하면서 생각보다 벨로그에 기능이 많다는걸 느꼈으며, 이 많은 기능들을 구현하면서 그만큼 많이 성장했다. 특히 이 때 <code>next.js</code>로 개발을 진행했는데, 이 때 구현하고 공부한 많은 기능들이 실무에서 굉장히 큰 도움이 되었다.</p>
<h3 id="테오의-구글-스프린트--뽀프만">테오의 구글 스프린트 : 뽀프만</h3>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/1dac58f7-4660-42d6-b0b1-d0dd0c94f353/image.png" alt=""></p>
<blockquote>
<p>스프린트 후기
<a href="https://velog.io/@deli-ght/%ED%85%8C%EC%98%A4%EC%9D%98-%EA%B5%AC%EA%B8%80-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-4%EA%B8%B0-%ED%9B%84%EA%B8%B0-npm-%EB%B0%B0%ED%8F%AC">npm에 react 컴포넌트 배포 🙌 ( + 테오의 구글 스프린트 4기 후기 )</a></p>
</blockquote>
<p>프로젝트에 목이 마른 복학생이었던 졸업생이 발견한 테오 스프린트. 내가 테오의 글을 볼 땐 2기가 끝난지라 댓글에 추후 열리게 되면 알려달라고 작성해두었고, 테오가 메일을 보내주었다.
<img src="https://velog.velcdn.com/images/deli-ght/post/debc0563-71c8-458b-87dd-ad65b7957591/image.png" alt=""></p>
<p>짧지만 알차게 프로젝트를 만들어갈 수 있다는 생각에 바로는 아니고 조금 고민하다가 신청했다. 3기 신청 메일이었으나 조금 늦게 결심해 4기로 들어갔다. 하지만 너무 좋은 선택이었고, 좋은 사람들을 만나 즐거운 프로젝트를 할 수 있었다. 프로젝트가 끝난 이후에도 준의 추진력으로 함께 스터디를 진행했고 많이 배울 수 있는 사람들을 만나 올해 더 성장할 수 있었다. 동기 유발 인간들.. 💪</p>
<p>다음에는 꼭 직접 npm 배포해보기!</p>
<h3 id="loglog">Loglog</h3>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/426c4a3a-b3bf-4d1c-bc42-5a9770a73363/image.png" alt=""></p>
<blockquote>
<p>loglog 진행과정 확인하기 
<a href="https://velog.io/@deli-ght/log-log-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%A4%EB%AA%85">log log 프로젝트 ) 프로젝트 설명</a></p>
</blockquote>
<p>1년 전 규리님과 함께 기획+개발 모두 포함 이틀동안 진행했던 <a href="https://velog.io/@deli-ght/goodbye-2021">goodbye2021</a>이라는 회고 프로젝트가 있었다. 1년 회고용으로 만들어두었지만 월별 회고용도로 1년동안 잘 쓰다가 연말이 다가오고 사용자 입장에서 수정이 필요하다고 생각했던 부분들을 디벨롭해 프로젝트를 업데이트 했다. 기획단계에서 더 많은 기능 추가를 하고 싶었으나 올해 말에 다시 돌아오기로 했다. 일단 1년 회고를 가장한 한달 회고용 웹으로 잘 써먹어야지. </p>
<h3 id="amoo"><a href="https://amoo.pages.dev/">Amoo</a></h3>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/a19c8b41-1830-4826-8fa1-4e5991357960/image.png" alt=""></p>
<p>연말에 준이 플젝 하나 하자고 제안해서 나오게된 아무 게임! 이것도 회고글이랑 고민했던 부분 글을 적어야하는데 테스트 케이스 적용하고 해야지 했다가 아직까지 못쓰고 있다. 허허. 타입스크립트를 막 제대로 공부하고 진행했던 프로젝트라서 규모가 크진 않지만 공부한 여러가지를 적용해볼 수 있어서 재밌었다. 연말까지 알차게 보냄..!</p>
<blockquote>
<p>🍀 2023 목표 : <strong>회고글 작성하기 / 테스트 코드 적용하기</strong></p>
</blockquote>
<h2 id="취업">취업</h2>
<p>올해 초 회사에서 쓰일 프로덕트의 프로젝트 제안을 받고 프로덕트를 만들다가 4월 입사 제안을 받아 입사했다. 제안을 준 프론트엔드 CTO와 나와 함께 입사한 (역시) 프론트엔드 신입 개발자분 이렇게 셋으로 개발팀이 꾸려졌고, 5월에 기획이 엎어져 8월 오픈을 목표로 처음부터 프로덕트를 다시 만들게 되었다.</p>
<p>웹 및 앱 오픈이 회사의 사활이 걸린 문제였기 때문에 밤낮 평일 주말할 것 없이 최선을 다해 개발에 임했고, 계속해서 바뀌는 기획에 맞춰 열심히 커뮤니케이션하며 8월 오픈을 향해 달렸다. 프론트로만 이뤄진 개발팀이다보니 자연스럽게 백엔드도 다루게 되었고, API 개발과 프론트 개발, 팀 내에서 사용할 어드민 페이지까지 모두 다뤄야만 했다.</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/93386179-cf32-406d-941e-b46706d1de1e/image.png" alt=""></p>
<p>(눈물없이 볼 수 없는 커밋...)</p>
<p>약속했던 8월이 오고 슬프게도 오픈이 성공적이지는 못했고 갑작스런 CTO의 퇴사로 다른 시니어분이 오기까지 한달여간 신입 두명이서 신규 기능 오픈 + 에러 해결을 해야하는 상황이 되었다. 8월이 내 지난 인생에서 가장 힘들었던 달이었다. 그치만 힘든 시간이었지만 인생 업적에 남길 수 있는 일이라고 좋게 생각하려고 한다.  🥲 한달간 웹에서 터지던 에러를 잡고, 안정화에 포커스를 두고 개발했다. 시니어분이 오시고 인수인계라고 할 것도 없지만,, 약 한달 정도의 업무 정리를 끝으로 퇴사했다.</p>
<p>🌞 <strong>GOOD</strong></p>
<ul>
<li>결국에 남는 건 사람이라고 했던가. 사람에게 받은 상처 사람으로 회복한다고, 힘든 시간들을 함께 보내고 나니 회사 사람들과 더욱 끈끈해졌다. 아쉽게도 회사를 떠나게 되었지만, 함께 울고 웃던 나날들 덕분에 그 힘든 시간들을 견딜 수 있었다. </li>
<li>프론트엔드 일에만 한정되지 않고 백엔드 영역과 PM, 기획 영역까지 넘나들어본 것. 역할이 분배되어 있지 않던 상황이었고, 누군가는 해야하는 상황이었기 때문에 내가 했다. 덕분에 한 분야에만 한정되지 않고 더 다양한 업무를 할 수 있었다.</li>
</ul>
<p>🌧 <strong>BAD</strong></p>
<ul>
<li>업무를 하면서 내가 부족하다고 굉장히 많이 느끼게 되었다. 일을 하고 있던 중에는 당장의 문제를 해결해야했기 때문에 나 자신에 대한 질문까진 닿지 않았지만, 일을 관두고 난 뒤에 굉장히 고민이 많아졌다. 크게 타입스크립트에서부터 시작해서, 디자인 패턴이라던가 테스트코드라던가, 구조를 잡고 프로젝트에 들어갔다면 놓치는 부분을 줄일 수 있지 않았을까? 최적화 할 수 있는 방법이 더 없었을까? 등등 질문들이 꼬리에 꼬리를 물어 아직 배울 공부가 많다고 느껴졌다. </li>
<li>일하면서 해결한 코드들을 정리해두지 않은 것. 완벽한 글이 아니더라도 정리해둘 걸 그랬다. 회사 코드라서 어디서부터 어디까지 오픈할 수 있을까 고민이 많았고 이에 답해줄 사수도 없어서 어영부영 넘어가버렸다..</li>
</ul>
<p>🌈 <strong>FOR BETTER</strong></p>
<ul>
<li>업무를 시작하게 되면 꼭 업무 일지 쓰기</li>
<li>회사 사람들과 일 진행 사항에 대한 오버 커뮤니케이션 하기</li>
<li>공부하고 싶은 것들 주저하지 말고 그때 그때 공부하기</li>
<li>프론트에 한정된 공부하지 않기. 모든 상황에 대응하기 위해선 두루두루 할 수 있어야한다. (근데 일단 프론트 공부부터 제대로 하고..)</li>
</ul>
<h2 id="총평">총평</h2>
<p>정말이지 다사다난한 해였다. 그만큼 많이 성장한 해이기도 하고, 그만큼 성장해야겠다고 결심한 해이기도 하다. 회사를 관두고 혼자 생각할 시간이 많아지면서 개발을 관둬야하나라는 생각까지 갔으나 10년은 더 해보기로 마음먹었다. 10년은 해봐야 해봤다고 말할 수 있지 않을까. </p>
<p>올해는 목표 설정을 만다라트로 했는데 이 중 개발과 커리어 테마의 목표를 공유해둘까 한다. 목표는 모름지기 많이 공유할 수록 성공 확률이 높아지는 법이니깐. </p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/b166382a-09fc-4c16-b3b3-56b6773dfc24/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/ac769ee0-7d2a-451d-a409-9c76242a5017/image.png" alt=""></p>
<p>전부 다 달성보다는 여러가지 목표를 정해놓고 최대한 많이 달성하는데 의미를 두려고 한다. 2023년도 한번 더 가보자고~</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/5cbb021a-006d-4076-9d39-8c45ea5d89be/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Github Lighthouse CI 적용]]></title>
            <link>https://velog.io/@deli-ght/%EA%B9%83%ED%97%99-Lighthouse-CI-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@deli-ght/%EA%B9%83%ED%97%99-Lighthouse-CI-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Thu, 05 Jan 2023 16:42:24 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://dev.to/joerismits/ensure-your-nextjs-apps-performance-is-top-notch-with-lighthouse-ci-and-github-actions-4ne8">Ensure your Next.js app&#39;s performance is top-notch with Lighthouse CI and GitHub Actions</a>을 읽고 따라해보며 겪은 이슈</p>
</blockquote>
<pre><code>Error: No URLs provided to collect
    at startServerAndDetermineUrls (/Users/minjukim/futurama.vercel.com/node_modules/@lhci/cli/src/collect/collect.js:149:36)</code></pre><p>해당 글을 무작정 따라하다가 다음과 같은 에러가 생겼다🥲 그래서 제대로 <a href="https://github.com/GoogleChrome/lighthouse-ci">공식문서 : Google Chrome Lighthouse ci</a> 읽으면서 따라해본 방법 정리</p>
<h2 id="설치-방법">설치 방법</h2>
<h3 id="1-lhcicli-설치">1. @lhci/cli 설치</h3>
<pre><code class="language-bash">npm i --save-dev @lhci/cli
yarn add @lhci/cli --dev</code></pre>
<h3 id="2-lighthousercjs-파일-생성">2. lighthouserc.js 파일 생성</h3>
<pre><code class="language-js">module.exports = {
  ci: {
    collect: {
      startServerCommand: &quot;npm run start&quot;,
      startServerReadyPattern: &quot;ready on&quot;,
      url: [&quot;http://localhost:3000&quot;],
    },
    upload: {
      target: &quot;temporary-public-storage&quot;,
    },
  },
};</code></pre>
<blockquote>
<p>기본적으로 lighthouse 검사를 3번 실행하게 된다. 만약 횟수를 정하고 싶다면 <code>&quot;numberOfRuns&quot;: 1(횟수)</code> 설정을 추가해주면 된다.</p>
</blockquote>
<pre><code class="language-js">&quot;collect&quot;: {
    &quot;startServerCommand&quot;: &quot;npm run start&quot;,
    &quot;startServerReadyPattern&quot;: &quot;ready on&quot;,
    &quot;url&quot;: [
        &quot;http://localhost:3000&quot;
        ],
    &quot;numberOfRuns&quot;: 1,
}</code></pre>
<h3 id="3-secret-key-발급-받기">3. Secret key 발급 받기</h3>
<p><a href="https://github.com/apps/lighthouse-ci">Lighthouse ci market</a>에 접속해 <code>LHCI_GITHUB_APP_TOKEN</code>를 발급받는다. </p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/944f4d2f-bf1e-45e8-b760-8f59fff9b23c/image.png" alt=""></p>
<p>발급 받은 키를 잘 복사해 해당 프로젝트 레포지토리의 Settings &gt; Secrets &gt; Actions 에 들어가 <code>new repository secret</code> 버튼을 클릭한뒤</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/db686245-52c7-4b9b-a5a0-e2b42f895464/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/adf98e2c-5359-48b1-b780-95a427ddbc21/image.png" alt=""></p>
<p>복사한 키를 입력해 저장한다.</p>
<h3 id="4-프로젝트에-ci-파일-생성">4. 프로젝트에 ci 파일 생성</h3>
<p><code>.github/workflows</code> 에 <code>ci.yml</code> 파일을 생성한다.
<img src="https://velog.velcdn.com/images/deli-ght/post/8781b6fa-645a-45b4-830c-b5895dc72f46/image.png" alt=""></p>
<blockquote>
<p>.github 폴더는 프로젝트 파일의 최상위에 생성하면 된다.</p>
</blockquote>
<p><code>ci.yml</code>에 다음과 같이 작성해준다</p>
<pre><code class="language-yml">name: CI
on: [push]
jobs:
  lighthouseci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: 16
      - run: npm install &amp;&amp; npm install -g @lhci/cli@0.8.x
      - run: npm run build
      - run: lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}</code></pre>
<h3 id="5-gitignore-수정">5. gitignore 수정</h3>
<p>로컬에서 <code>lhci</code> 명령어를 돌리게 되면 .lighthouseci라는 폴더가 생성되면서 로그가 찍히게 된다. 이 파일이 깃헙에 올라가는 걸 막기 위해 <code>.gitignore</code> 파일에 <code>.lighthouseci/</code>를 추가해준다.</p>
<h3 id="6-끝">6. 끝!</h3>
<p>요렇게 돌아가면서
<img src="https://velog.velcdn.com/images/deli-ght/post/e43ddc63-6610-4022-a046-1444a3d001d3/image.png" alt=""></p>
<p>실패시 다음과 같이 뜨고
<img src="https://velog.velcdn.com/images/deli-ght/post/a88b6600-26b2-4902-b847-59cb45eae051/image.png" alt=""></p>
<p>성공시에는 다음과 같이 뜬다.
<img src="https://velog.velcdn.com/images/deli-ght/post/748fb710-205e-4f84-81f5-fd7347c9be3d/image.png" alt=""></p>
<p>성공 여부와 관계없이 <code>lhci/url</code> 이 생긴걸 확인할 수 있는데, details를 클릭하면 
<img src="https://velog.velcdn.com/images/deli-ght/post/caf40f1f-4fe4-4f23-b6a5-55f774099003/image.png" alt=""></p>
<p>이렇게 lighthouse 결과지 페이지를 생성해준다.</p>
<h2 id="lighthousercjs-파일-설명">lighthouserc.js 파일 설명</h2>
<h3 id="왜-언급된-글에서는-lighthousercjson-파일을-생성하라고-했는데-lighthousercjs가-제대로-동작했을까">왜 언급된 글에서는 .lighthouserc.json 파일을 생성하라고 했는데, lighthouserc.js가 제대로 동작했을까?</h3>
<p>Lighthouse CI가 configuration 파일을 다음과 같은 우선순위로 찾는다고 한다.</p>
<ol>
<li><code>.lighthouserc.js</code></li>
<li><code>lighthouserc.js</code></li>
<li><code>.lighthouserc.json</code></li>
<li><code>lighthouserc.json</code></li>
<li><code>.lighthouserc.yml</code></li>
<li><code>lighthouserc.yml</code></li>
<li><code>.lighthouserc.yaml</code></li>
<li><code>lighthouserc.yaml</code></li>
</ol>
<blockquote>
<p>최상위에 파일이 존재하는게 아니라 다른 곳에 파일을 두고 싶다면 <code>lhci</code> 명령어 옆에 항상 <code>--config=./대충/파일/위치</code> 를 붙여 파일 위치를 알려주기!</p>
</blockquote>
<p>여기서 주의할 점은 어떤 파일이던 상관없지만 파일 확장자에 따라 형식을 맞춰 작성해주어야 한다.</p>
<h4 id="js">js</h4>
<pre><code class="language-js">module.exports = {
  ci: {
    collect: {
      // collect options here
    },
    assert: {
      // assert options here
    },
    upload: {
      // upload options here
    },
    server: {
      // server options here
    },
    wizard: {
      // wizard options here
    },
  },
};</code></pre>
<h4 id="json">json</h4>
<pre><code class="language-Json">{
  &quot;ci&quot;: {
    &quot;collect&quot;: {
      // collect options here
    },
    &quot;assert&quot;: {
      // assert options here
    },
    &quot;upload&quot;: {
      // upload options here
    },
    &quot;server&quot;: {
      // server options here
    },
    &quot;wizard&quot;: {
      // wizard options here
    }
  }
}</code></pre>
<h4 id="yml">yml</h4>
<pre><code class="language-yml">ci:
  collect:
    # collect options here

  assert:
    # assert options here

  upload:
    # upload options here

  server:
    # server options here

  wizard:
    # wizard options here</code></pre>
<h3 id="lighthouserc의-다양한-설정들">lighthouserc의 다양한 설정들</h3>
<p>위에 파일을 보면 알겠지만 lighthouserc에는 다양한 설정들이 있다. 기본적으로 <code>collect</code>와 <code>upload</code>만 사용하긴 했지만, 공식문서에서는 assert 사용도 권장하고 있다. (assert 이외의 설정 설명이나 디테일한 설명은 <a href="https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/configuration.md">여기</a> 참고하기!)</p>
<p>assert는 lighthouse 실행 결과에서 낮은 점수가 나오는 경우 CI 단계에서 에러가 나도록 해주는 설정이다.
assert 설정 안에서도 <code>preset</code>, <code>assertion</code> 등 세부설정이 있다. </p>
<p><strong>preset</strong>은 <code>&quot;lighthouse:all&quot;</code>, <code>&quot;lighthouse:recommended&quot;</code>, <code>&quot;lighthouse:no-pwa&quot;</code> 중 선택할 수 있다. </p>
<table>
<thead>
<tr>
<th>preset</th>
<th>설정</th>
</tr>
</thead>
<tbody><tr>
<td><code>lighthouse:all</code></td>
<td>100점 맞지 않는 이상 에러 (난이도 : 완전 어려움 ⭐️⭐️⭐️⭐️⭐️)</td>
</tr>
<tr>
<td><code>lighthouse:recommended</code></td>
<td>퍼포먼스 제외 모든 검사 100점 (난이도는 어렵지만 가장 추천)</td>
</tr>
<tr>
<td><code>lighthouse:no-pwa</code></td>
<td><code>lighthouse:recommended</code>와 똑같지만 PWA 검사를 하지 않음</td>
</tr>
</tbody></table>
<p><strong>assertion</strong> 은 검사 항목들의 기준을 수정할 수 있다. <code>검사항목 : level | [level, options]</code>의 형식으로 작성된다. <code>검사항목</code>들은 <a href="https://github.com/GoogleChrome/lighthouse/blob/v5.5.0/lighthouse-core/config/default-config.js#L375-L407">여기</a>에서 확인 할 수 있다. <code>level</code> 설정은 세가지로 나뉜다. </p>
<table>
<thead>
<tr>
<th>level</th>
<th>설정</th>
</tr>
</thead>
<tbody><tr>
<td><code>off</code></td>
<td>해당 검사를 진행하지 않음</td>
</tr>
<tr>
<td><code>warn</code></td>
<td>검사를 진행하고 결과도 출력하지만 에러로 내뱉지는 않음</td>
</tr>
<tr>
<td><code>error</code></td>
<td>에러 발생</td>
</tr>
</tbody></table>
<p><code>option</code>의 기본값은 <code>{&quot;aggregationMethod&quot;: &quot;optimistic&quot;, &quot;minScore&quot;: 1}</code> 이다. <code>aggregationMethod</code>는 <strong>검사 점수 측정 방법</strong>으로 세가지 항목으로 이뤄져 있다. </p>
<table>
<thead>
<tr>
<th>aggregationMethod</th>
<th>설정</th>
</tr>
</thead>
<tbody><tr>
<td><code>median</code></td>
<td>실행 평균값</td>
</tr>
<tr>
<td><code>optimistic</code></td>
<td>실행 점수 최대값</td>
</tr>
<tr>
<td><code>pessimistic</code></td>
<td>실행 점수 최솟값</td>
</tr>
</tbody></table>
<pre><code class="language-js">{
  &quot;ci&quot;: {
    &quot;assert&quot;: {
      &quot;assertions&quot;: {
        &quot;first-contentful-paint&quot;: &quot;off&quot;,
        &quot;installable-manifest&quot;: [&quot;warn&quot;, {&quot;minScore&quot;: 1}],
        &quot;uses-responsive-images&quot;: [&quot;error&quot;, {&quot;maxLength&quot;: 0}]
      }
    }
  }
}</code></pre>
<blockquote>
<h3 id="🍀-tmi">🍀 TMI</h3>
<p>모든 Clickable한 요소의 크기는 48*48 이상이어야 한다. 
<img src="https://velog.velcdn.com/images/deli-ght/post/57ac3b9e-b6dc-47f8-8455-6f32be82e231/image.png" alt=""></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[log log 프로젝트 ) 프로젝트 설명]]></title>
            <link>https://velog.io/@deli-ght/log-log-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%A4%EB%AA%85</link>
            <guid>https://velog.io/@deli-ght/log-log-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%A4%EB%AA%85</guid>
            <pubDate>Fri, 23 Dec 2022 10:06:18 GMT</pubDate>
            <description><![CDATA[<h1 id="loglog--httpsloglogcokr">[LOGLOG]  (<a href="https://loglog.co.kr/">https://loglog.co.kr/</a>)</h1>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/33b29faa-038b-48ed-8667-067dce68a335/image.png" alt="">
⬆️ 모바일에서도 사용해보세요!</p>
<h2 id="프로젝트-설명">프로젝트 설명</h2>
<blockquote>
<p>loglog는 <strong>1년을 가볍게 회고할 수 있는 웹 서비스</strong>입니다. 여섯개의 키워드를 선택해 키워드에 해당하는 사진을 선택하거나 키워드에 대한 글을 작성할 수 있습니다. 당신의 한 해는 어떤 해였나요? 결과페이지에서 포인트 색상을 정해 색상을 입히고 이미지를 저장해 친구와 공유해보세요!</p>
</blockquote>
<p>작년에 규리님과 이틀에 걸쳐 급하게 만들었던 <a href="https://velog.io/@deli-ght/goodbye-2021">연말 회고 페이지</a>를 리뉴얼하기로 했다. 그동안 업데이트를 하고 싶었으나, 너무 바쁜 일상으로 인해,, 손 댈 엄두를 내지 못했는데 연말이 다가오고 규리님이 이제 슬슬 업데이트를 해볼까요 하고 제안을 주셔서 드디어 한을 풀 수 있게 되었다.</p>
<p>우선 프로젝트 이름을 변경했다. 기존 프로젝트 이름은 <code>goodbye-2021</code>로 21년 회고에 포커싱되어있기 때문에, 년도에 상관없이 사용할 수 있도록 이름을 <code>loglog</code>로 변경했다.</p>
<p>스펙은 이전과 동일하게 가져가되, 예전 기능에서 우리가 사용하면서 필요하다고 생각했던 여러 기능을 추가하기로 했다. 서버가 필요하지 않은 기능들이다보니 프론트 스택만을 사용했다. </p>
<h2 id="member">member</h2>
<table>
  <tr>
    <td align="center">
      <a href="https://github.com/deli-ght"
        ><img
          src="https://avatars.githubusercontent.com/deli-ght"
          width="100px;"
          alt=""
        /><br /><sub><b>김민주</b></sub></a
      ><br />
    </td>
    <td align="center">
      <a href="https://github.com/jae04099"
        ><img
          src="https://avatars.githubusercontent.com/jae04099"
          width="100px;"
          alt=""
        /><br /><sub><b>이규리</b></sub></a
      ><br />
    </td>
  </tr>
</table>

<h1 id="product">Product</h1>
<p><a href="https://www.figma.com/file/VrvICIz64ikHzC2GcwjekS/remind2021?node-id=19%3A90&amp;t=0oqb42e89f8dlOWa-1">피그마 이미지</a>
<a href="https://github.com/jae04099/loglog.co.kr">github</a>
<a href="https://loglog.co.kr/">loglog 서비스 페이지</a></p>
<h2 id="stack">stack</h2>
<h3 id="frontend">Frontend</h3>
<ul>
<li>React</li>
<li>styled-component</li>
<li>Recoil</li>
</ul>
<h3 id="library">library</h3>
<ul>
<li><a href="https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1.-react-grid-layout">react-grid-layout (최종 적용 ❌)</a></li>
<li><a href="https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2.-html2canvas">html2canvas</a></li>
<li><a href="https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3.-tinycolor2">tinycolor2</a></li>
</ul>
<h2 id="페이지">페이지</h2>
<table>
  <tr>
    <td align="center">
      <img
          src="https://i.imgur.com/Lqqe4X4.png"
          width="300px;"
          alt=""
        /><br/><sub><b>메인 페이지</b></sub>
    </td>
    <td align="center">
      <img
          src="https://i.imgur.com/3jZ22Cp.png"
          width="300px;"
          alt=""
        /><br/><sub><b>키워드 선택 페이지</b></sub>
    </td>
       <td align="center">
      <img
          src="https://i.imgur.com/8I081mX.jpg"
          width="300px;"
          alt=""
        /><br/><sub><b>결과 페이지</b></sub>
    </td>
  </tr>
</table>

<h2 id="상세-기능">상세 기능</h2>
<p>이번 업데이트는 다음과 같은 기능들이 추가되었다.</p>
<h3 id="1-전체적인-ui-수정">1. 전체적인 UI 수정</h3>
<h4 id="1-1-키워드-페이지-통합">1-1. 키워드 페이지 통합</h4>
<p>기존 페이지는 키워드 선택 페이지와 회고 입력 페이지가 분리되어, 만약 작성 중 키워드를 수정하고 싶다면 이전 페이지로 돌아가야했다. 유저 입장에서 사용하면서 키워드를 중간에 수정할 수 없음에 불편함을 느꼈고, 페이지를 통합하기로 했다. </p>
<table>
  <tr>
    <td align="center">
      <img
          src="https://velog.velcdn.com/images/deli-ght/post/bd2efce8-89e5-4b59-bace-80ff4bc52389/image.png"

<pre><code>      alt=&quot;&quot;
    /&gt;&lt;br/&gt;&lt;sub&gt;&lt;b&gt;키워드 선택 페이지 (전)&lt;/b&gt;&lt;/sub&gt;
&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;
  &lt;img
      src=&quot;https://velog.velcdn.com/images/deli-ght/post/5956eaf0-ee3b-441b-b751-a9b7a419210c/image.png&quot;

      alt=&quot;&quot;
    /&gt;&lt;br/&gt;&lt;sub&gt;&lt;b&gt;회고 입력 페이지 (전)&lt;/b&gt;&lt;/sub&gt;
&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;
  &lt;img
      src=&quot;https://i.imgur.com/3jZ22Cp.png&quot;

      alt=&quot;&quot;
    /&gt;&lt;br/&gt;&lt;sub&gt;&lt;b&gt;통합된 키워드 선택 페이지 (후)&lt;/b&gt;&lt;/sub&gt;
&lt;/td&gt;</code></pre>  </tr>
</table>

<h4 id="1-2-메인-컬러셋-변경">1-2. 메인 컬러셋 변경</h4>
<p>컬러를 수정할 수 없던 기존 버전과 달리 이번 버전에서부터는 컬러를 수정할 수 있기 때문에 전체 컬러를 블랙앤 화이트로 맞춰 유저가 선택한 어떤 컬러에도 어울릴 수 있도록 했다.</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/c860f8a0-b936-4573-b32e-e21a357e26c4/image.png" alt=""></p>
<h4 id="1-3-결과-레이아웃-수정">1-3. 결과 레이아웃 수정</h4>
<p>글의 길이에 따라 컨텍스트의 높이가 달라지는 레이아웃을 <code>mansonry</code>라고 한다. (나도 이번에 처음 알게 되었다.) 대표적인 예시로 핀터레스트 UI가 있다. 
<img src="https://velog.velcdn.com/images/deli-ght/post/6c41a5bd-d6aa-447f-9f8b-68326045be67/image.png" alt=""></p>
<p>이 레이아웃을 만들기 위해 처음에는 라이브러리를 사용해볼까 했으나 이미지의 경우 높이를 일정하게 해주는 예외사항이 있어 커스텀이 가능하도록 코드로 구현하는 방법을 찾아보았다. </p>
<pre><code class="language-jsx">export const MasonryGrid = ({ datas, pointColor }) =&gt; {
  useEffect(() =&gt; {
    masonryLayout();
  }, []);

  return (
    &lt;GridContainer className=&quot;masonry-container&quot;&gt;
      {datas?.map((data, index) =&gt; {
        const { content, keyword } = data;

        if (content)
          return (
            &lt;AnswerBlock
              key={keyword}
              data={data}
              data-type=&quot;text&quot;
              bgColor={getRandomColor(index, pointColor)}
            /&gt;
          );
        return &lt;AnswerImage key={keyword} data={data} data-type=&quot;image&quot; /&gt;;
      })}
    &lt;/GridContainer&gt;
  );
};

const GridContainer = styled.div`
  display: grid;
  box-sizing: border-box;
  width: 100%;
  grid-template-columns: repeat(2, 1fr);
  gap: 5px;
  grid-auto-rows: 5px;
`;
</code></pre>
<p>먼저 컨텍스트를 담을 컨테이너를 그리드로 만들어준다. 이때 그리드를 모눈종이처럼 촘촘이 나눠줘야하기 때문에 나는 5px로 높이를 설정해주었다. gap의 경우 역시 너무 간격이 크면 줄 길이에 따라 하단이 너무 길어지는 UI가 나오기 때문에 열 높이와 똑같이 5px로 설정해주었다.</p>
<p>이렇게 만들어진 grid에 <strong>각각의 컨텍스트가 길이에 따른 높이값을 가지도록 계산</strong>해준다. </p>
<pre><code class="language-js">export const masonryLayout = () =&gt; {
  const container = document.querySelector(&quot;.masonry-container&quot;);
  const masonryContainerStyle = getComputedStyle(container);

  const autoRows = parseInt(
    masonryContainerStyle.getPropertyValue(&quot;grid-auto-rows&quot;),
  );

  document.querySelectorAll(&quot;.masonry-item&quot;).forEach((elt) =&gt; {
    const scrollHeight = elt.scrollHeight;

    elt.style.gridRowEnd = `span ${Math.ceil(scrollHeight / autoRows / 1.7)}`;
  });

  document.querySelectorAll(&quot;.masonry-item-image&quot;).forEach((elt) =&gt; {
    const imgWidth = container.scrollWidth / 2 - 10;

    elt.style.gridRowEnd = `span ${Math.ceil(imgWidth / autoRows / 2)}`;
  });
};
</code></pre>
<p>각각이 가지게 될 높이를 열 높이로 나눠준다. 그렇게 나눠진 값만큼 grid의 높이를 차지하는 방식으로 계산하는데, 이 때 나는 하단이 너무 길어지는 현상이 생겨 1.7로 한번 더 나누어 주었다. 1.7이라는 깔끔하지 않은 숫자로 나눈 이유는.. 1과 2 사이에서 열심히 테스트해보다가 1.7이 가장 깔끔한 UI가 나와 1.7로 지정했다.</p>
<p><code>이미지</code>의 경우, 처음에는 1:1 비율로 지정해주고 싶어 처음에는 별다른 계산 없이 <code>aspect-ratio</code>를 이용해 1:1 비율로 지정한 뒤, 넓이만큼 높이값을 가지도록 했으나 이런 경우 <strong>aspect-ratio의 특성에 의해 높이에 따라 넓이가 계산되고 넓이에 따라 높이가 계산되고가 반복되어 UI가 깨지는 문제가 발생</strong>했다. 어쩔 수 없이 aspect-ratio 설정을 빼고 글자와 똑같이 넓이값을 받아와 행 높이에 맞춰 나눠주도록 했다.</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/d9d0b0f1-9ff7-4de9-9d3a-73ca41743f0d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/6d15e340-97b9-4c63-9c43-2a04e3a1f06e/image.png" alt=""></p>
<h3 id="2-컬러-선택-기능">2. 컬러 선택 기능</h3>
<p>배포 후 가장 많은 요청을 받았던 컬러 선택 기능을 드디어 추가했다. 모든 칸의 컬러를 선택하는 방법도 있겠지만, 모든 컬러를 선택하도록 하는 것이 유저에게 피로감을 줄 수 있겠다는 생각을 해 포인트 컬러를 기반으로 명도가 변경되도록 수 정했다.</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/7ecbf7fa-0e45-40a6-8150-c372c6c44a14/image.png" alt=""></p>
<h4 id="2-1-포인트컬러-직접-선택">2-1. 포인트컬러 직접 선택</h4>
<p>포인트 컬러 직접 선택 input의 경우, <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/color">기본 input</a>을 이용했다. input의 타입을 color로 지정해주면, 유저가 컬러 피커를 이용할 수 있다. </p>
<table>
  <tr>
    <td align="center">
      <img
          src="https://velog.velcdn.com/images/deli-ght/post/1c3032f4-6a89-4269-bcdf-838de4798795/image.png"
          width="300px;"
          alt=""
        /><br/><sub><b>크롬 컬러 피커</b></sub>
    </td>
    <td align="center">
      <img
          src="https://velog.velcdn.com/images/deli-ght/post/13971861-4f86-4adf-b96a-5eaf7c32705a/image.png"
          width="300px;"
          alt=""
        /><br/><sub><b>사파리 (웹) 컬러 피커</b></sub>
    </td>
    <td align="center">
      <img
          src="https://velog.velcdn.com/images/deli-ght/post/7137dc32-20bb-48bb-b3a2-aed9f58719ed/image.png"
          width="300px;"
          alt=""
        /><br/><sub><b>사파리 (모바일) 컬러 피커</b></sub>
    </td>
  </tr>
</table>


<p>기본 input 모양을 커스터마이징 하고 싶어 input의 <code>display : none</code> 처리 하고 label을 클릭하면 컬러 피커가 뜨도록 코드를 작성해주었다. 이렇게 코드를 작성했더니 크롬에서는 원하는대로 동작하지만 사파리에서는 동작하지않는 문제가 발생했다...!!😨</p>
<p>이 문제를 해결하기 위해 display 속성이 아닌 <code>visibility : hidden</code> 을 사용해보았으나 역시나 뜨지 않았고, <code>opacity : 0</code> 을 설정해주었더니 그제서야 비로소 동작했다.</p>
<pre><code class="language-js">const ColorInput = styled.input`
  opacity: 0;
`;</code></pre>
<h4 id="2-2-포인트컬러-랜덤-선택">2-2. 포인트컬러 랜덤 선택</h4>
<p>컬러를 랜덤으로 설정하는 코드는 tinycolor2 라이브러리를 이용하였다! 해당 코드에 대한 설명은 글로 따로 적어두었으니 <a href="https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3.-tinycolor2">tinycolor2 라이브러리 사용 후기</a>에서 확인하기!</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/8cd57804-b3f6-4102-9875-7d4baf2a6241/image.gif" alt=""></p>
<blockquote>
<p>해당 기능은 가장 반응이 좋았다! 😎</p>
</blockquote>
<h3 id="3-이미지-회고-타입-추가">3. 이미지 회고 타입 추가</h3>
<p>텍스트만 입력 가능하던 기존 버전과 달리 이번 버전에서는 이미지도 입력할 수 있도록 기능이 추가되었다! 
<img src="https://velog.velcdn.com/images/deli-ght/post/9f81b634-4719-499b-a39c-a259b43544a6/image.png" alt=""></p>
<p>데이터를 서버에 저장하거나 공유할 필요가 없어 이미지 서버를 따로 구축하진 않았고 blob 형태로 프로젝트 내에서 계속 사용하도록 했다. </p>
<pre><code class="language-js">const onFileChanged = (ev) =&gt; {
    const {
      target: { files },
    } = ev;
    if (files) {
      const { length: FileLength } = files;
      for (let i = 0; i &lt; FileLength; i++) {
        const reader = new FileReader();
        reader.addEventListener(&quot;loadend&quot;, (event) =&gt; {
          if (!event.target) return;
          const { result } = event.target;
          if (result) {
            handleSaveImg(result);
          }
        });
        reader.readAsDataURL(files[i]);
      }
    }
    ev.target.value = &quot;&quot;;
  };</code></pre>
<h3 id="4-이미지-저장-기능">4. 이미지 저장 기능</h3>
<p>이미지 저장 기능은 라이브러리를 사용해 쉬울 것이라고 생각했으나, 예상외의 복병이 2가지 존재했다 🥲</p>
<h4 id="4-1-html2canvas에서-지원하지-않는-css-속성">4-1. html2canvas에서 지원하지 않는 css 속성</h4>
<p>유저가 추가한 이미지를 비율에 맞춰 보여주기 위해 처음에는 background에 이미지를 넣어주었다. 하지만 background로 이미지를 넣는 경우 이미지가 심하게 깨지는 문제가 있어 이를 해결하기 위해 <code>image</code> 태그에 넣는 것으로 변경했다.</p>
<p>image 태그에 넣어 이미지의 위치를 조절하기 위해 image 자체에 <code>object-fit</code>를 넣어주었다. 웹 상에서는 원하는 대로 잘 나타났지만.. 이미지를 다운로드 받았더니 이미지가 확대되는 문제가 발생했다. (아놔)</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/0d557889-5245-4f24-be55-0827d6b7d2eb/image.png" alt=""></p>
<p>사이트에서 살펴보니 <code>object-fit</code>을 지원하고 있지 않고 있었다. </p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/8c0b23e6-1a35-4395-8620-8131d4ad6caf/image.png" alt=""></p>
<p>문제를 해결하기 위해 이미지 크기를 수동으로 조절하는 코드를 추가했다. 만약 이미지의 넓이가 높이보다 크다면 높이를 높이에 맞춰 이미지를 채우고, 높이가 넓이값보다 크다면 넓이에 맞춰 이미지를 채우는 방식을 사용했다.</p>
<pre><code class="language-js"> useEffect(() =&gt; {
    const myImgs = document.querySelectorAll(&quot;.my-images&quot;);

    myImgs.forEach((myImg) =&gt; {
      const imgWidth = myImg.clientWidth;
      const imgHeight = myImg.clientHeight;

      if (imgWidth &gt; imgHeight) {
        myImg.style.height = &quot;100%&quot;;
      } else {
        myImg.style.width = &quot;100%&quot;;
      }
    });
  }, []);
</code></pre>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/0f5d7d7d-c64d-4b7c-8984-febb189fe1a2/image.png" alt=""></p>
<h4 id="4-2-인앱-브라우저에서-이미지-다운로드-불가">4-2. 인앱 브라우저에서 이미지 다운로드 불가</h4>
<p>어느정도 기능 구현이 완료된 이후, 주변에 베타 테스트를 부탁했다. 카카오톡을 이용해 링크를 전달했는데 문제가 발생했다. 인앱 브라우저에서는 이미지 다운로드가 불가능했던 것..! 앱이 자체 앱이 아니라 사파리나 크롬과 같은 브라우저로 이동시키는 코드를 추가 할 수 없었고, 웹 자체에 이를 대처하는 코드를 추가해야했다.</p>
<p>방법을 찾던 도중 다음의 블로그를 발견했다.
<a href="https://burndogfather.tistory.com/257">카카오, 네이버 인앱에서 외부 브라우저 띄우는 방법 정리</a></p>
<p>기존에는 링크 프로토콜 변경을 통해 ios에서 사파리가 열리도록 하는 편법을 썼으나 업데이트 이후로는 이 방법이 막혔다고 한다,, 현재로서는 유저가 직접 링크를 복사해서 이동하는 방법밖에 없어서 블로그에 나와있던 코드를 다음과 같이 수정했다.</p>
<pre><code class="language-js">const copyUrl = () =&gt; {
    window.navigator.clipboard.writeText(&quot;https://loglog.co.kr&quot;).then(() =&gt; {
      alert(&quot;링크가 복사되었습니다! 즐거운 한해 마무리 하세요~&quot;);
    });
  };
  if (
    navigator.userAgent.match(
      /inapp|NAVER|KAKAOTALK|Snapchat|Line|WirtschaftsWoche|Thunderbird|Instagram|everytimeApp|WhatsApp|Electron|wadiz|AliApp|zumapp|iPhone(.*)Whale|Android(.*)Whale|kakaostory|band|twitter|DaumApps|DaumDevice\/mobile|FB_IAB|FB4A|FBAN|FBIOS|FBSS|SamsungBrowser\/[^1]/i,
    )
  ) {
    if (navigator.userAgent.match(/iPhone|iPad/i)) {
      return (
        &lt;Container&gt;
          &lt;Caution&gt;
            더 나은 환경을 위해 외부 브라우저로 이동해 이용해주세요! 🥲
          &lt;/Caution&gt;
          &lt;CopyButton type=&quot;button&quot; onClick={copyUrl}&gt;
            링크 복사하기
          &lt;/CopyButton&gt;
        &lt;/Container&gt;
      );
    } else {
      window.location.href =
        &quot;intent://&quot; +
        window.location.href.replace(/https?:\/\//i, &quot;&quot;) +
        &quot;#Intent;scheme=http;package=com.android.chrome;end&quot;;
    }
  } else {
    return (
      &lt;Container&gt;
        &lt;Header&gt;
          안녕&lt;span&gt;2022&lt;/span&gt;
        &lt;/Header&gt;
        &lt;Description&gt;
          &lt;Strong&gt;키워드&lt;/Strong&gt;와 &lt;Strong&gt;색&lt;/Strong&gt;으로
          &lt;br /&gt;
          나의 2022 기록하기
        &lt;/Description&gt;
        &lt;Wrapper&gt;
          &lt;BookImage src=&quot;/image/favorite-book.png&quot; alt=&quot;&quot; /&gt;
          &lt;FormName /&gt;
          &lt;Button toLink={&quot;/answer&quot;} children={&quot;2022 정리하기&quot;} /&gt;
        &lt;/Wrapper&gt;
      &lt;/Container&gt;
    );
  }</code></pre>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/143f6266-0b51-4e86-a2da-16c53e9c102d/image.gif" alt=""></p>
<h1 id="마무리">마무리</h1>
<p>12월 15일을 데드라인으로 정했으나 생각치 못한 이슈들도 있고, 수정하고 싶은 부분들도 생겨 딜레이되었다..! 계속해서 수정사항들을 업데이트 할 예정이니 많관부!
<img src="https://velog.velcdn.com/images/deli-ght/post/7647316f-f9d8-4bf5-882f-1cf50efa9e65/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[loglog 프로젝트 ) 3. tinycolor2]]></title>
            <link>https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3.-tinycolor2</link>
            <guid>https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3.-tinycolor2</guid>
            <pubDate>Wed, 14 Dec 2022 14:07:18 GMT</pubDate>
            <description><![CDATA[<h1 id="라이브러리-설명">라이브러리 설명</h1>
<blockquote>
<p>여러 타입의 컬러를 다양하게 변형 및 활용할 수 있는 라이브러리</p>
</blockquote>
<h2 id="공식-문서">공식 문서</h2>
<p><a href="https://bgrins.github.io/TinyColor/">테스트 홈페이지</a>
<a href="https://github.com/bgrins/TinyColor">깃헙 코드</a></p>
<h1 id="사용방법">사용방법</h1>
<h2 id="설치">설치</h2>
<pre><code class="language-bash">npm install tinycolor2
yarn add tinycolor2</code></pre>
<h2 id="사용">사용</h2>
<h3 id="다양한-타입의-컬러-형식-사용-가능">다양한 타입의 컬러 형식 사용 가능</h3>
<pre><code class="language-js">// Hex, 8-digit (RGBA) Hex
tinycolor(&quot;#000&quot;);
tinycolor(&quot;000&quot;);

// RGB, RGBA
tinycolor(&quot;rgb (255, 0, 0)&quot;);
tinycolor(&quot;rgb 255 0 0&quot;);

// HSL, HSLA
tinycolor(&quot;hsl(0, 100%, 50%)&quot;);
tinycolor(&quot;hsla(0, 100%, 50%, .5)&quot;);

// HSV, HSVA
tinycolor(&quot;hsv(0, 100%, 100%)&quot;);
tinycolor(&quot;hsva(0, 100%, 100%, .5)&quot;);

// Named
tinycolor(&quot;RED&quot;);
tinycolor(&quot;blanchedalmond&quot;);</code></pre>
<h3 id="지원하는-기능">지원하는 기능</h3>
<p>지원하는 여러가지 기능 중 내가 유익하다고 생각한 것들을 몇가지만 뽑아보았다.</p>
<h4 id="1-islight--isdark">1. isLight() / isDark()</h4>
<p>해당 컬러가 밝은 색인지, 어두운 색인지 boolean 값으로 알려주는 함수
이번 프로젝트에서는 현재 백그라운드 컬러에 따라 폰트 컬러가 바뀌는 기능에 사용해보았다.</p>
<pre><code class="language-js">let color1 = tinycolor(&quot;#fff&quot;);
color1.isLight(); // true

let color2 = tinycolor(&quot;#000&quot;);
color2.isLight(); // false</code></pre>
<pre><code class="language-js">// styledComponent
color: ${({ bgcolor }) =&gt;
    tinycolor(bgcolor).isDark() ? &quot;#ffffff&quot; : &quot;#000000&quot;};</code></pre>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/f54eddd5-792a-436d-8031-0ff3279b6f20/image.gif" alt=""></p>
<h4 id="2-lighten--darken">2. lighten() / darken()</h4>
<p>원하는 만큼 현재색에서 밝은 컬러 혹은 어두운 컬러를 뽑아내준다.</p>
<pre><code class="language-js">tinycolor(&quot;#f00&quot;).lighten().toString(); // &quot;#ff3333&quot;
tinycolor(&quot;#f00&quot;).lighten(100).toString(); // &quot;#ffffff&quot;

tinycolor(&quot;#f00&quot;).darken().toString(); // &quot;#cc0000&quot;
tinycolor(&quot;#f00&quot;).darken(100).toString();</code></pre>
<p>지금 프로젝트에서는 랜덤값에 따라 칸이 포인트 컬러보다 밝거나 어둡게 설정되도록 코드를 작성하였다.</p>
<pre><code class="language-js">const bgColor = Math.floor(Math.random() * 1)
      ? tinycolor(pointColor).darken(randomValue * 10)
      : tinycolor(pointColor).brighten(randomValue * 10);</code></pre>
<h4 id="3-random">3. random()</h4>
<p>랜덤으로 색상을 뽑아준다.
진짜 좋다고 생각한 기능..!</p>
<pre><code class="language-js">let randomColor = tinycolor.random().toHexString() // #ffe234;</code></pre>
<h1 id="궁금증">궁금증</h1>
<h2 id="어떻게-글씨체-darklight를-구분할까">어떻게 글씨체 dark/light를 구분할까</h2>
<pre><code class="language-js">tinycolor.prototype = {
    isDark: function() {
        return this.getBrightness() &lt; 128;
    },
    isLight: function() {
        return !this.isDark();
    },
    getBrightness: function() {
        //http://www.w3.org/TR/AERT#color-contrast
        var rgb = this.toRgb();
        return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
    ...</code></pre>
<p>코드를 살펴보면 <code>getBrightness()</code> 함수를 이용해 밝기 정도를 계산한 뒤, 절반인 128 이하면 dark, 이상이면 light로 구분하였다. <a href="https://www.w3.org/TR/AERT/#color-contrast">주석 처리된 링크</a>로 들어가보면 계산식을 확인할 수 있는데, RGB 컬러를 YIQ 형식으로 변경할 때 사용되며 컬러 밝기 정도를 확인하는 공식이라고 한다. 계산값은 0~255 사이의 값이 나온다.
<img src="https://velog.velcdn.com/images/deli-ght/post/c3701709-53d4-4eda-be3e-7e96a19ab3fe/image.png" alt=""></p>
<h2 id="lighten-darken-방식">lighten/ darken 방식</h2>
<pre><code class="language-js">function lighten (color, amount) {
    amount = (amount === 0) ? 0 : (amount || 10);
    var hsl = tinycolor(color).toHsl();
    hsl.l += amount / 100;
    hsl.l = mathMin(1, mathMax(0, hsl.l));
    return tinycolor(hsl);
}</code></pre>
<p>코드를 살펴보니 <code>HSL</code>이라는 값을 이용했다. HSL이란 인간의 시각적 특징을 고려해 만들어진 <strong>색공간</strong>으로, <code>H</code>는 적색을 기준으로한 색상 각도인 <strong>Hue</strong>를 의미하고 <code>S</code>는 <strong>채도</strong>를 의미한다. <code>L</code>는 <strong>명도(Lightness)</strong>의 줄임말로 해당 코드에서는 명도의 값을 조절하며 컬러값의 밝기를 조절하는 것을 확인할 수 있다. </p>
<p>비슷한 방식으로 brighten 함수가 있는데, 이건 rgb를 이용하고 있다.</p>
<pre><code class="language-js">function brighten(color, amount) {
    amount = (amount === 0) ? 0 : (amount || 10);
    var rgb = tinycolor(color).toRgb();
    rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100))));
    rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100))));
    rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100))));
    return tinycolor(rgb);
}</code></pre>
<p>코드를 보다보면 <code>mathMin(a, mathMax(b, value))</code>; 형식의 함수가 비슷하게 연달아 나온다. 이렇게 사용하는 이유는 a부터 b사이의 값만 나오도록 로직을 설계하기 위해서이며 Min안의 a 값은 항상 max 안의 b 값보다 커야한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[loglog 프로젝트 ) 2. html2canvas]]></title>
            <link>https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2.-html2canvas</link>
            <guid>https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2.-html2canvas</guid>
            <pubDate>Wed, 14 Dec 2022 09:14:21 GMT</pubDate>
            <description><![CDATA[<h1 id="라이브러리-설명">라이브러리 설명</h1>
<blockquote>
<p>웹의 특정 컴포넌트를 canvas 형태로 변경하고 싶을 때 사용하는 라이브러리</p>
</blockquote>
<h2 id="공식-문서">공식 문서</h2>
<p><a href="https://html2canvas.hertzen.com/">공식 홈페이지</a>
<a href="https://github.com/niklasvh/html2canvas">깃헙 코드</a></p>
<h1 id="사용-방법">사용 방법</h1>
<h2 id="설치">설치</h2>
<pre><code class="language-bash">npm install --save html2canvas
yarn add html2canvas</code></pre>
<h2 id="사용">사용</h2>
<pre><code class="language-js">html2canvas(element).then(function(canvas) {
    document.body.appendChild(canvas);
});</code></pre>
<p><code>html2canvas</code>의 파라미터로 canvas형식으로 변환을 원하는 노드 객체를 넣어주면 된다. </p>
<pre><code class="language-js">  const captureResult = () =&gt; {
    html2canvas(document.getElementById(&quot;capture&quot;), {
      backgroundColor: &quot;#000000&quot;,
    }).then(function (canvas) {
      const downloadLink = document.createElement(&quot;a&quot;);
      downloadLink.download = &quot;filename.png&quot;;
      downloadLink.href = canvas.toDataURL();
      downloadLink.click();
    });
  };</code></pre>
<p>내가 작성한 코드의 경우, 버튼 클릭시 캔버스를 이미지 형식으로 다운 받을 수 있도록 작성한 코드로 promise의 결과값으로 canvas를 받아 png 형식으로 다운로드 받을 수 있도록 해주었다.</p>
<h1 id="원리">원리</h1>
<p>처음 캔버스로 바꿀 노드를 받아오게 되면, 해당 노드를 캔버스 내에 그리기 위해 클론하는 과정을 거친다.</p>
<pre><code class="language-js">// html2canvas/src/index.ts
const renderElement = async (element: HTMLElement, opts: Partial&lt;Options&gt;): Promise&lt;HTMLCanvasElement&gt; =&gt; {
    if (!element || typeof element !== &#39;object&#39;) {
        return Promise.reject(&#39;Invalid element provided as first argument&#39;);
    }

  // ... 생략

    const cloneOptions: CloneConfigurations = {
        allowTaint: opts.allowTaint ?? false,
        onclone: opts.onclone,
        ignoreElements: opts.ignoreElements,
        inlineImages: foreignObjectRendering,
        copyStyles: foreignObjectRendering
    };

    const documentCloner = new DocumentCloner(context, element, cloneOptions);
    const clonedElement = documentCloner.clonedReferenceElement;
    if (!clonedElement) {
        return Promise.reject(`Unable to find element in cloned iframe`);
    }</code></pre>
<p>노드를 클론하는 <code>DocumentCloner</code>는 재귀 형식으로 노드를 타고 내려가면서 clone을 생성한다. 
<img src="https://velog.velcdn.com/images/deli-ght/post/5bf92533-0c9c-4eae-90bf-c15dcd19f40b/image.png" alt=""></p>
<p><a href="https://github.com/niklasvh/html2canvas/blob/6020386bbeed60ad68e675fdcaa6220e292fd35a/src/dom/document-cloner.ts#L48">https://github.com/niklasvh/html2canvas/blob/6020386bbeed60ad68e675fdcaa6220e292fd35a/src/dom/document-cloner.ts#L48</a></p>
<p>클론이 끝나고 나면, 원하던 컴포넌트의 높이와 넓이에 맞는 캔버스 객체를 하나 생성해준다.</p>
<pre><code class="language-js">// html2canvas/src/render/canvas/canvas-renderer.js
 constructor(context: Context, options: RenderConfigurations) {
        super(context, options);
        this.canvas = options.canvas ? options.canvas : document.createElement(&#39;canvas&#39;);
        this.ctx = this.canvas.getContext(&#39;2d&#39;) as CanvasRenderingContext2D;
        if (!options.canvas) {
            this.canvas.width = Math.floor(options.width * options.scale);
            this.canvas.height = Math.floor(options.height * options.scale);
            this.canvas.style.width = `${options.width}px`;
            this.canvas.style.height = `${options.height}px`;
        }
        this.fontMetrics = new FontMetrics(document);
        this.ctx.scale(this.options.scale, this.options.scale);
        this.ctx.translate(-options.x, -options.y);
        this.ctx.textBaseline = &#39;bottom&#39;;
        this._activeEffects = [];
        this.context.logger.debug(
            `Canvas renderer initialized (${options.width}x${options.height}) with scale ${options.scale}`
        );
    }</code></pre>
<p>앞서 클론한 노드들을 바탕으로 해당 노드의 타입을 구분에 이에 맞춰 만들어진 캔버스안에 그려내는 방식으로 동작한다.</p>
<pre><code class="language-js">async renderNodeContent(paint: ElementPaint): Promise&lt;void&gt; {
        this.applyEffects(paint.getEffects(EffectTarget.CONTENT));
        const container = paint.container;
        const curves = paint.curves;
        const styles = container.styles;
        for (const child of container.textNodes) {
            await this.renderTextNode(child, styles);
        }

        if (container instanceof ImageElementContainer) {
            try {
                const image = await this.context.cache.match(container.src);
                this.renderReplacedElement(container, curves, image);
            } catch (e) {
                this.context.logger.error(`Error loading image ${container.src}`);
            }
        }

        if (container instanceof CanvasElementContainer) {
            this.renderReplacedElement(container, curves, container.canvas);
        }

        if (container instanceof SVGElementContainer) {
            try {
                const image = await this.context.cache.match(container.svg);
                this.renderReplacedElement(container, curves, image);
            } catch (e) {
                this.context.logger.error(`Error loading svg ${container.svg.substring(0, 255)}`);
            }
        }
        ...
        ...</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[loglog 프로젝트 ) 1. react-grid-layout]]></title>
            <link>https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1.-react-grid-layout</link>
            <guid>https://velog.io/@deli-ght/loglog-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1.-react-grid-layout</guid>
            <pubDate>Wed, 14 Dec 2022 05:37:13 GMT</pubDate>
            <description><![CDATA[<h1 id="라이브러리-설명">라이브러리 설명</h1>
<blockquote>
<p>그리드를 레이아웃안에서 유저가 크기 조절 가능하도록 만들고싶을 때 사용할 수 있는 라이브러리</p>
</blockquote>
<h2 id="사용-예시">사용 예시</h2>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/3eb5ee82-6c5e-446b-93ec-83acb350e51c/image.gif" alt=""></p>
<h2 id="공식-문서">공식 문서</h2>
<p><a href="https://react-grid-layout.github.io/react-grid-layout/examples/0-showcase.html">공식 홈페이지</a>
<a href="https://github.com/react-grid-layout/react-grid-layout">깃헙 코드</a></p>
<h1 id="사용-방법">사용 방법</h1>
<h2 id="설치">설치</h2>
<pre><code class="language-bash">npm install react-grid-layout</code></pre>
<p>최상위 폴더에서 css 파일 임포트</p>
<pre><code class="language-js">// index.js
import &quot;/node_modules/react-grid-layout/css/styles.css&quot;;
import &quot;/node_modules/react-resizable/css/styles.css&quot;;</code></pre>
<h2 id="gridlayout">GridLayout</h2>
<p><strong>정해진 크기</strong> 내에서 레이아웃을 만들 수 있는 컴포넌트</p>
<h3 id="layout">layout</h3>
<p>GridLayout 컴포넌트에는 layout이라는 props가 있다. 해당 데이터는 GridLayout에 들어갈 개별 요소들의 성질을 각각 객체에 담아 <code>배열</code> 형식으로 만든 것이다.</p>
<pre><code class="language-js">const layout = [
  { i: &quot;a&quot;, x: 0, y: 0, w: 1, h: 2, static: true },
  { i: &quot;b&quot;, x: 1, y: 0, w: 3, h: 2, minW: 2, maxW: 4 },
  { i: &quot;c&quot;, x: 4, y: 0, w: 1, h: 2 },
];</code></pre>
<h4 id="속성값">속성값</h4>
<table>
<thead>
<tr>
<th align="center">key</th>
<th align="center">value type</th>
<th align="left">설명</th>
<th>필수 여부</th>
</tr>
</thead>
<tbody><tr>
<td align="center">i</td>
<td align="center">string</td>
<td align="left">각 요소의 id값과 일치하는 식별자값</td>
<td>✅</td>
</tr>
<tr>
<td align="center">x</td>
<td align="center">number</td>
<td align="left">요소가 어느 x 지점에서 시작할 것인지</td>
<td>✅</td>
</tr>
<tr>
<td align="center">y</td>
<td align="center">number</td>
<td align="left">요소가 어느 y 지점에서 시작할 것인지</td>
<td>✅</td>
</tr>
<tr>
<td align="center">w</td>
<td align="center">number</td>
<td align="left">요소가 몇칸의 넓이를 차지할 것인지</td>
<td>✅</td>
</tr>
<tr>
<td align="center">h</td>
<td align="center">number</td>
<td align="left">요소가 몇칸의 높이를 차지할 것인지</td>
<td>✅</td>
</tr>
<tr>
<td align="center">minW</td>
<td align="center">number</td>
<td align="left">요소가 최소 넓이 몇칸을 유지할 수 있는지</td>
<td></td>
</tr>
<tr>
<td align="center">maxW</td>
<td align="center">number</td>
<td align="left">요소가 최대 넓이 몇칸을 유지할 수 있는지</td>
<td></td>
</tr>
<tr>
<td align="center">minH</td>
<td align="center">number</td>
<td align="left">요소가 최소 높이 몇칸을 유지할 수 있는지</td>
<td></td>
</tr>
<tr>
<td align="center">maxH</td>
<td align="center">number</td>
<td align="left">요소가 최대 높이 몇칸을 유지할 수 있는지</td>
<td></td>
</tr>
<tr>
<td align="center">static</td>
<td align="center">boolean</td>
<td align="left">크기와 위치가 고정됨</td>
<td></td>
</tr>
<tr>
<td align="center">isDraggable</td>
<td align="center">boolean</td>
<td align="left">드래그 가능 여부</td>
<td></td>
</tr>
<tr>
<td align="center">isResizable</td>
<td align="center">boolean</td>
<td align="left">크기 조절 가능 여부</td>
<td></td>
</tr>
<tr>
<td align="center">resizeHandles</td>
<td align="center">array &lt;&#39;s&#39;, &#39;w&#39; , &#39;e&#39; , &#39;n&#39; , &#39;sw&#39; , &#39;nw&#39; , &#39;se&#39; , &#39;ne&#39;&gt;</td>
<td align="left">사이즈 조절 핸들 위치 배열</td>
<td></td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/7c06e225-b4ba-433e-a6ef-0a1450e54ed7/image.png" alt=""></p>
<blockquote>
<p><code>static</code>이 <strong>false</strong>인 경우 <code>isDraggable : false</code> , <code>isResizable : false</code> 로 설정값과 상관없이 덮어쓰기 된다.</p>
</blockquote>
<h3 id="예시-코드">예시 코드</h3>
<pre><code class="language-js">import GridLayout from &quot;react-grid-layout&quot;;
import &quot;./App.css&quot;;

function App() {
  const layout = [
    { i: &quot;a&quot;, x: 0, y: 0, w: 1, h: 2, static: true },
    { i: &quot;b&quot;, x: 1, y: 0, w: 3, h: 2, minW: 2, maxW: 4 },
    { i: &quot;c&quot;, x: 4, y: 0, w: 1, h: 2 },
  ];
  return (
    &lt;div id=&quot;container&quot;&gt;
      &lt;GridLayout
        className=&quot;layout&quot;
        layout={layout}
        cols={12}
        rowHeight={30}
        width={1200}
      &gt;
        &lt;div key=&quot;a&quot;&gt;a&lt;/div&gt;
        &lt;div key=&quot;b&quot;&gt;b&lt;/div&gt;
        &lt;div key=&quot;c&quot;&gt;c&lt;/div&gt;
      &lt;/GridLayout&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<p>다른 props 들을 살펴보면 다음과 같다.
<code>cols</code> : 몇개의 열로 레이아웃을 구성할 지 정한다.
<code>rowHeight</code> : 한 행의 높이를 지정할 수 있다.
<code>width</code> : 레이아웃 전체의 넓이를 지정한다.</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/348edcd3-1a22-4d8f-982d-9a0f1e787930/image.png" alt=""></p>
<h3 id="실행-화면">실행 화면</h3>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/ef3d624f-94de-4da3-a8b3-48cecb4eb64b/image.gif" alt=""></p>
<p>실행 화면에서 보이다싶이 GridLayout의 문제점은 레이아웃의 넓이가 고정되어있어 반응형을 고려하지 못한다는 것이다. 이를 보완하기 위해 반응형 레이아웃을 제공한다.</p>
<h2 id="responsivegridlayout">ResponsiveGridLayout</h2>
<h3 id="layouts">layouts</h3>
<pre><code class="language-js"> const LAYOUTS = {
    lg: [
      { i: &quot;a&quot;, x: 0, y: 0, w: 1, h: 1, minW: 1, maxW: 1, minH: 1, maxH: 2 },
      { i: &quot;b&quot;, x: 1, y: 0, w: 1, h: 2, minW: 1, maxW: 1, minH: 1, maxH: 2 },
      { i: &quot;c&quot;, x: 2, y: 0, w: 1, h: 1, minW: 1, maxW: 1, minH: 1, maxH: 2 },
    ],
    md: [
      { i: &quot;a&quot;, x: 0, y: 0, w: 1, h: 1, minW: 1, maxW: 1, minH: 1, maxH: 2 },
      { i: &quot;b&quot;, x: 1, y: 0, w: 1, h: 2, minW: 1, maxW: 1, minH: 1, maxH: 2 },
      { i: &quot;c&quot;, x: 0, y: 1, w: 1, h: 1, minW: 1, maxW: 1, minH: 1, maxH: 2 },
    ],
  };</code></pre>
<p>반응형 레이아웃에서는 사이즈별 아이템의 위치를 지정할 수 있기 때문에 사이즈를 키로 한 <code>객체</code>를 layout props 값으로 넘겨준다.</p>
<p><img src="https://velog.velcdn.com/images/deli-ght/post/81cc4ac3-84c9-474e-b78a-1a8d89b673ea/image.gif" alt=""></p>
<h3 id="예시-코드-1">예시 코드</h3>
<pre><code class="language-js">import &quot;./App.css&quot;;
import { Responsive, WidthProvider } from &quot;react-grid-layout&quot;;

const ResponsiveGridLayout = WidthProvider(Responsive);

function App() {
  const LAYOUTS = {
    lg: [
      { i: &quot;a&quot;, x: 0, y: 0, w: 1, h: 1, minW: 1, maxW: 1, minH: 1, maxH: 2 },
      { i: &quot;b&quot;, x: 1, y: 0, w: 1, h: 2, minW: 1, maxW: 1, minH: 1, maxH: 2 },
      { i: &quot;c&quot;, x: 2, y: 0, w: 1, h: 1, minW: 1, maxW: 1, minH: 1, maxH: 2 },
    ],
    md: [
      { i: &quot;a&quot;, x: 0, y: 0, w: 1, h: 1, minW: 1, maxW: 1, minH: 1, maxH: 2 },
      { i: &quot;b&quot;, x: 1, y: 0, w: 1, h: 2, minW: 1, maxW: 1, minH: 1, maxH: 2 },
      { i: &quot;c&quot;, x: 0, y: 1, w: 1, h: 1, minW: 1, maxW: 1, minH: 1, maxH: 2 },
    ],
  };
  return (
    &lt;div&gt;
      &lt;ResponsiveGridLayout
        className=&quot;layout&quot;
        layouts={LAYOUTS}
        breakpoints={{ lg: 1000, md: 600 }}
        cols={{ lg: 3, md: 2 }}
      &gt;
        {LAYOUTS.lg.map((el) =&gt; (
          &lt;div key={el.i} {...el}&gt;
            &lt;h1&gt;영화 🎬&lt;/h1&gt;
            &lt;p&gt;
              세상에 이렇게 행복한 일이 있었나? 나는 잘 모르겠다.
              가나다라마바사가바나ㅏㄹ어니ㅏㅇ러니ㅏ러니ㅏ어ㅣ라넝리ㅏ너ㅣ아ㅓㄹ니아러ㅣㄴ아러니아러니ㅏ어린아ㅓ린아ㅓ리나ㅓㅇ리아ㅓ리나어리나.
            &lt;/p&gt;
          &lt;/div&gt;
        ))}
      &lt;/ResponsiveGridLayout&gt;
    &lt;/div&gt;
  );
}

export default App;</code></pre>
<p>기본 GridLayout과 같은 형식이지만 breakpoint 지정을 통해 반응형 넓이에 따라 행의 수가 바뀌도록 지정해줄 수 있다.</p>
<pre><code class="language-js">breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}</code></pre>
<h1 id="라이브러리를-사용하면서-생긴-궁금증">라이브러리를 사용하면서 생긴 궁금증</h1>
<h2 id="어떻게-breakpoint가-변경됨을-인식하고-레이아웃을-변경할까">어떻게 breakpoint가 변경됨을 인식하고 레이아웃을 변경할까?</h2>
<p>// react-grid-layout/lib/ResponsiveReactGridLayout.jsx</p>
<pre><code class="language-js">  componentDidUpdate(prevProps: Props&lt;*&gt;) {
    // Allow parent to set width or breakpoint directly.
    if (
      this.props.width != prevProps.width ||
      this.props.breakpoint !== prevProps.breakpoint ||
      !isEqual(this.props.breakpoints, prevProps.breakpoints) ||
      !isEqual(this.props.cols, prevProps.cols)
    ) {
      this.onWidthChange(prevProps);
    }
  }</code></pre>
<p><code>componentDidUpdate</code>는 변동사항이 생기는 경우, Update 이전의 상태값을 props로 넘겨준다. 따라서 넓이가 조정되는 경우, 넓이를 다시 계산하는 함수를 실행하도록 해두었다. </p>
<pre><code class="language-js">  /**
   * When the width changes work through breakpoints and reset state with the new width &amp; breakpoint.
   * Width changes are necessary to figure out the widget widths.
   */
// 넓이 변경 함수
  onWidthChange(prevProps: Props&lt;*&gt;) {
    const { breakpoints, cols, layouts, compactType } = this.props;
    // 현재 넓이에 따른 breakpoint 찾기
    const newBreakpoint =
      this.props.breakpoint ||
      getBreakpointFromWidth(this.props.breakpoints, this.props.width);

    // 이전 breakpoint
    const lastBreakpoint = this.state.breakpoint;

    // 새로운 breakpoint에 따른 행 갯수
    const newCols: number = getColsFromBreakpoint(newBreakpoint, cols);

    // 레이아웃 객체
    const newLayouts = { ...layouts };

    // Breakpoint 변경된 경우
    if (
      lastBreakpoint !== newBreakpoint ||
      prevProps.breakpoints !== breakpoints ||
      prevProps.cols !== cols
    ) {
      // 만약 최근 레이아웃에 대한 내용이 새로운 레이아웃 객체에 없는 경우를 대비해 레이아웃 클론
      if (!(lastBreakpoint in newLayouts))
        newLayouts[lastBreakpoint] = cloneLayout(this.state.layout);

      // Find or generate a new layout.
      let layout = findOrGenerateResponsiveLayout(
        newLayouts,
        breakpoints,
        newBreakpoint,
        lastBreakpoint,
        newCols,
        compactType
      );

      // This adds missing items.
      layout = synchronizeLayoutWithChildren(
        layout,
        this.props.children,
        newCols,
        compactType,
        this.props.allowOverlap
      );

      // Store the new layout.
      newLayouts[newBreakpoint] = layout;

      // callbacks - 레이아웃 변경에 따른 콜백 실행
      this.props.onLayoutChange(layout, newLayouts);
      this.props.onBreakpointChange(newBreakpoint, newCols);

      this.setState({
        breakpoint: newBreakpoint,
        layout: layout,
        cols: newCols
      });
    }

    const margin = getIndentationValue(this.props.margin, newBreakpoint);
    const containerPadding = getIndentationValue(
      this.props.containerPadding,
      newBreakpoint
    );

    // breakpoint가 아니라 width가 변경된 경우에도 실행되도록 
    // 이러면 재귀가 되지 않나..?🤔
    this.props.onWidthChange(
      this.props.width,
      margin,
      newCols,
      containerPadding
    );
  }</code></pre>
]]></description>
        </item>
    </channel>
</rss>