<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>VIKING_J</title>
        <link>https://velog.io/</link>
        <description>모험을 떠나보자</description>
        <lastBuildDate>Tue, 05 Aug 2025 06:24:29 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>VIKING_J</title>
            <url>https://velog.velcdn.com/images/jay_chae/profile/a68241f5-3635-4bc6-9eab-6492dd4d5b56/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. VIKING_J. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jay_chae" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[FOUC 해결을 위한 `PreventFlash` 컴포넌트: 다크 모드 전환 시 깜빡임 방지]]></title>
            <link>https://velog.io/@jay_chae/FOUC-%ED%95%B4%EA%B2%B0%EC%9D%84-%EC%9C%84%ED%95%9C-PreventFlash-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%8B%A4%ED%81%AC-%EB%AA%A8%EB%93%9C-%EC%A0%84%ED%99%98-%EC%8B%9C-%EA%B9%9C%EB%B9%A1%EC%9E%84-%EB%B0%A9%EC%A7%80</link>
            <guid>https://velog.io/@jay_chae/FOUC-%ED%95%B4%EA%B2%B0%EC%9D%84-%EC%9C%84%ED%95%9C-PreventFlash-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EB%8B%A4%ED%81%AC-%EB%AA%A8%EB%93%9C-%EC%A0%84%ED%99%98-%EC%8B%9C-%EA%B9%9C%EB%B9%A1%EC%9E%84-%EB%B0%A9%EC%A7%80</guid>
            <pubDate>Tue, 05 Aug 2025 06:24:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jay_chae/post/ad8e0f5b-4900-46d0-b838-3d7eb19b4670/image.png" alt=""></p>
<p>웹 애플리케이션에서 다크 모드와 라이트 모드를 지원하는 것은 현대 사용자 경험(UX)의 필수 요소입니다. 하지만 테마 전환 시 <strong>FOUC</strong>(Flash of Unstyled Content)라고 불리는 깜빡임이 발생하면 사용자가 불편함을 느낄 수 있습니다. 이를 해결하기 위해 소개할 <code>PreventFlash</code> 컴포넌트는 <code>&lt;head&gt;</code> 태그에 위치하여 HTML 파싱 중에 미리 실행되며, 깜빡임을 방지합니다.</p>
<h2 id="1-깜빡임fouc-문제란">1. 깜빡임(FOUC) 문제란?</h2>
<p><strong>FOUC</strong>(Flash of Unstyled Content)는 웹 페이지가 로드될 때 잠깐 동안 잘못된 스타일이나 테마로 표시되는 현상을 뜻합니다. 특히 다크 모드와 라이트 모드 전환에서 자주 발생합니다.</p>
<h3 id="fouc-발생-예시">FOUC 발생 예시</h3>
<ul>
<li>다크 모드 사용자가 페이지를 열 때, 처음에 라이트 모드로 표시되다가 다크 모드로 전환되며 깜빡이는 경우.</li>
<li>라이트 모드 사용자가 잠깐 다크 모드 화면을 보게 되는 상황.</li>
</ul>
<h3 id="fouc의-주요-원인">FOUC의 주요 원인</h3>
<ol>
<li><strong>클라이언트 측 지연</strong>: React의 <code>useEffect</code> 같은 훅은 컴포넌트가 화면에 렌더링된 <em>후에</em> 실행됩니다. 이로 인해 초기 렌더링 시 기본 테마(예: 라이트 모드)가 먼저 표시되고, 이후 테마가 변경됩니다.</li>
<li><strong>서버 렌더링 한계</strong>: 서버에서 HTML을 생성할 때는 사용자의 테마 선호도를 알 수 없어 기본 테마로 렌더링됩니다. 클라이언트가 테마를 적용하기 전까지 잘못된 테마가 노출됩니다.</li>
</ol>
<h2 id="2-preventflash-컴포넌트">2. <code>PreventFlash</code> 컴포넌트</h2>
<pre><code class="language-tsx">//_document.tsx
import { Html, Head, Main, NextScript } from &#39;next/document&#39;

/**
 * Document 컴포넌트는 Next.js 애플리케이션의 HTML 문서 구조를 정의합니다.
 */
export default function Document() {
  return (
    &lt;Html className=&quot;bg-default&quot;&gt;
      &lt;Head&gt;
        {/* 다크 모드와 라이트 모드 전환 시 깜빡임을 방지 */}
        &lt;PreventFlash /&gt;
      &lt;/Head&gt;
      &lt;body&gt;
        &lt;Main /&gt;
        &lt;NextScript /&gt;
      &lt;/body&gt;
    &lt;/Html&gt;
  )
}

const background = {
  light: &#39;white&#39;,
  dark: &#39;#24292e&#39;,
}

function PreventFlash() {
  const setColorScheme = () =&gt; {
    const mql = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;)
    const scheme = localStorage.getItem(&#39;literal-color-scheme&#39;) ?? &#39;system&#39;
    if (scheme === &#39;&quot;dark&quot;&#39; || (scheme === &#39;&quot;system&quot;&#39; &amp;&amp; mql.matches)) {
      document.documentElement.classList.toggle(&#39;dark&#39;, true)
      document
        .querySelector(&#39;#theme-color&#39;)
        ?.setAttribute(&#39;content&#39;, background.dark)
    }
  }

  return (
    &lt;&gt;
      &lt;style&gt;{`
        .bg-default, .hover\\:bg-default:hover {
          background: ${background.light};
        }
        .dark.bg-default, .dark .bg-default, .dark .hover\\:bg-default:hover {
          background: ${background.dark};
        }
      `}&lt;/style&gt;

      &lt;script
        dangerouslySetInnerHTML={{
          __html: `const background=${JSON.stringify(background)}`,
        }}
      &gt;&lt;/script&gt;
      &lt;script
        dangerouslySetInnerHTML={{ __html: `(${setColorScheme})()` }}
      &gt;&lt;/script&gt;
    &lt;/&gt;
  )
}</code></pre>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/2f1ed905-6f60-4873-9f95-add5b2103410/image.png" alt=""></p>
<h3 id="head에-위치"><code>&lt;head&gt;</code>에 위치</h3>
<p><code>PreventFlash</code>는 <code>&lt;head&gt;</code> 태그 안에 삽입되어야 합니다. 이는 브라우저가 HTML을 위에서 아래로 파싱할 때 <code>&lt;head&gt;</code> 내의 <code>&lt;script&gt;</code> 태그를 만나면 즉시 실행하기 때문입니다. 따라서 페이지 렌더링이 시작되기 전에 테마를 설정할 수 있습니다.</p>
<h3 id="주요-기능">주요 기능</h3>
<ul>
<li><strong>테마 확인</strong>: 로컬 스토리지의 사용자 설정이나 시스템의 <code>prefers-color-scheme</code>를 확인합니다.</li>
<li><strong>즉시 실행</strong>: <code>&lt;head&gt;</code> 내 <code>&lt;script&gt;</code> 태그를 통해 HTML 파싱 중에 테마 설정 코드를 실행합니다.</li>
<li><strong>스타일 사전 정의</strong>: <code>&lt;style&gt;</code> 태그로 기본 스타일을 미리 정의해 깜빡임을 방지합니다.</li>
</ul>
<h2 id="3-dangerouslysetinnerhtml">3. <code>dangerouslySetInnerHTML</code></h2>
<ul>
<li><strong>React의 기본 원칙</strong>: React는 보안을 위해 JSX라는 안전한 방식으로만 HTML을 생성하게 해요. 그래서 <code>&lt;div&gt;</code>나 <code>&lt;p&gt;</code> 같은 태그는 괜찮지만, 직접 <code>&lt;script&gt;</code> 태그나 HTML 문자열을 삽입하려고 하면 제대로 작동하지 않습니다.</li>
<li><strong><code>dangerouslySetInnerHTML</code></strong>: 이 속성은 React에게 &quot;이 HTML 코드를 그대로 DOM에 넣어줘&quot;라고 지시하는 방법입니다.</li>
</ul>
<h3 id="dangerouslysetinnerhtml-사용-이유"><code>dangerouslySetInnerHTML</code> 사용 이유</h3>
<p>React에서 <code>dangerouslySetInnerHTML</code>을 사용하는 가장 큰 이유는 <strong>클라이언트 측에서 즉시 실행되는 JavaScript 코드를 삽입하거나, React 변수와 함수를 브라우저에 전달해야 할 때</strong>입니다. 일반적인 JSX로는 이런 작업이 안 되지 않습니다.</p>
<h4 id="예시-1-script-태그로-콘솔-출력하기">예시 1: <code>&lt;script&gt;</code> 태그로 콘솔 출력하기</h4>
<ul>
<li><strong>목표</strong>: 페이지에 <code>&lt;script&gt;</code> 태그를 넣어서 &quot;Hello&quot;를 콘솔에 출력하고 싶은 경우.</li>
<li><strong>잘못된 방법</strong>:<pre><code class="language-tsx">function MyComponent() {
  return &lt;script&gt;console.log(&#39;Hello&#39;)&lt;/script&gt;
}</code></pre>
이 코드는 동작하지 않습니다. React는 JSX에서 <code>&lt;script&gt;</code> 태그를 사용하면 보안상의 이유로 스크립트가 실행되지 않도록 처리합니다. 그래서 <code>&lt;script&gt;console.log(&#39;Hello&#39;)&lt;/script&gt;</code>는 DOM에 추가되지만, <code>console.log(&#39;Hello&#39;)</code>가 실행되지 않습니다.</li>
<li><strong>올바른 방법</strong>:<pre><code class="language-tsx">function MyComponent() {
  return (
    &lt;script
      dangerouslySetInnerHTML={{ __html: &quot;console.log(&#39;Hello&#39;)&quot; }}
    &gt;&lt;/script&gt;
  )
}</code></pre>
<ul>
<li>이렇게 하면 브라우저가 <code>&lt;script&gt;console.log(&#39;Hello&#39;)&lt;/script&gt;</code>를 제대로 실행해서 콘솔에 &quot;Hello&quot;를 출력합니다.</li>
<li><strong>왜 필요했나?</strong>: React가 <code>&lt;script&gt;</code>를 실행하지 않으니까, <code>dangerouslySetInnerHTML</code>로 강제로 HTML을 삽입합니다.</li>
</ul>
</li>
</ul>
<h4 id="예시-2-react-변수-전달하기">예시 2: React 변수 전달하기</h4>
<ul>
<li><strong>목표</strong>: React에서 정의한 변수를 클라이언트 측 JavaScript에서 사용하고 싶은 경우.</li>
<li><strong>코드</strong>:<pre><code class="language-tsx">function MyComponent() {
  const myVar = &#39;Hello from React&#39;
  return (
    &lt;&gt;
      &lt;script
        dangerouslySetInnerHTML={{ __html: `const myVar = &quot;${myVar}&quot;;` }}
      &gt;&lt;/script&gt;
      &lt;script
        dangerouslySetInnerHTML={{ __html: `console.log(myVar);` }}
      &gt;&lt;/script&gt;
    &lt;/&gt;
  )
}</code></pre>
<ul>
<li><strong>결과</strong>: 브라우저 콘솔에 &quot;Hello from React&quot;가 출력.</li>
<li><strong>설명</strong>:<ol>
<li>첫 번째 <code>&lt;script&gt;</code>는 React의 <code>myVar</code> 값을 클라이언트에 전달.</li>
<li>두 번째 <code>&lt;script&gt;</code>는 그 값을 사용해서 콘솔에 출력.</li>
</ol>
</li>
<li><strong>왜 필요했나?</strong>: React 컴포넌트 안의 변수(<code>myVar</code>)는 클라이언트 측 JavaScript에서 바로 접근할 수 없습니다. <code>dangerouslySetInnerHTML</code>로 <code>&lt;script&gt;</code>를 통해 값을 전달해야만 브라우저가 인식할 수 있습니다.</li>
</ul>
</li>
</ul>
<h4 id="preventflash-예시"><code>PreventFlash</code> 예시</h4>
<pre><code class="language-tsx">const background = {
  light: &#39;white&#39;,
  dark: &#39;#24292e&#39;,
}
function PreventFlash() {
 ...
  return (
    &lt;&gt;
      ...

      &lt;script
        dangerouslySetInnerHTML={{
          __html: `const background=${JSON.stringify(background)}`,
        }}
      &gt;&lt;/script&gt;
      &lt;script
        dangerouslySetInnerHTML={{ __html: `(${setColorScheme})()` }}
      &gt;&lt;/script&gt;
    &lt;/&gt;
  )
}</code></pre>
<ol>
<li><p><strong>첫 번째 <code>&lt;script&gt;</code></strong>:</p>
<ul>
<li><strong>목적</strong>: <code>background</code> 객체를 클라이언트 측에 전달합니다.</li>
<li><strong>왜 필요해요?</strong>: <code>setColorScheme</code> 함수가 <code>background.dark</code> 같은 값을 사용하려면, 이 객체를 브라우저가 알아야 해요. JSX로는 변수 전달이 안 되니까 <code>dangerouslySetInnerHTML</code>로 삽입합니다.</li>
</ul>
</li>
<li><p><strong>두 번째 <code>&lt;script&gt;</code></strong>:</p>
<ul>
<li><strong>목적</strong>: <code>setColorScheme</code> 함수를 페이지 로드 시점에 바로 실행합니다.</li>
<li><strong>왜 필요해요?</strong>: 페이지가 로드되자마자 테마를 설정해서, 사용자가 라이트 모드와 다크 모드 전환으로 깜빡이는 걸 보지 않게 합니다.</li>
</ul>
</li>
</ol>
<h3 id="dangerouslysetinnerhtml-안전성"><code>dangerouslySetInnerHTML</code> 안전성</h3>
<ul>
<li><strong>안전한 경우</strong>: 위 코드처럼 개발자가 직접 작성한 데이터(<code>background</code>)와 함수(<code>setColorScheme</code>)를 사용할 때는 보안 문제가 없습니다.</li>
<li><strong>위험한 경우</strong>: 사용자가 입력한 값(예: <code>&lt;script&gt;alert(&#39;해킹&#39;)&lt;/script&gt;</code>)을 <code>dangerouslySetInnerHTML</code>에 넣으면 악성 코드가 실행될 수 있어요. 하지만 이런 경우가 아니라면 괜찮습니다.</li>
</ul>
<p><code>dangerouslySetInnerHTML</code>를 쓰지 않으면 어떻게 될까요?</p>
<h2 id="4-사용하지-않았을-경우">4. 사용하지 않았을 경우</h2>
<pre><code class="language-tsx">import { useEffect } from &#39;react&#39;

function App() {
  useEffect(() =&gt; {
    const theme = localStorage.getItem(&#39;theme&#39;) || &#39;light&#39;
    document.documentElement.classList.add(theme)
  }, [])

  return &lt;div&gt;컨텐츠&lt;/div&gt;
}</code></pre>
<h3 id="과정">과정</h3>
<ol>
<li>서버에서 기본 테마(라이트 모드)로 HTML을 렌더링합니다.</li>
<li>브라우저가 HTML과 CSS를 로드해 라이트 모드로 화면을 표시합니다.</li>
<li>React가 마운트되고 <code>useEffect</code>가 실행되어 로컬 스토리지에서 <code>them</code> 정보를 가져옵니다.</li>
<li>화면이 라이트에서 다크로 바뀌며 깜빡임이 발생합니다.</li>
</ol>
<h2 id="5-사용한-경우">5. 사용한 경우</h2>
<p><code>PreventFlash</code>가 <code>&lt;head&gt;</code>에 위치하여 HTML 파싱 중에 실행되는 점이 핵심입니다.</p>
<h3 id="브라우저-렌더링-단계">브라우저 렌더링 단계</h3>
<ol>
<li><strong>HTML 파싱</strong>: HTML을 다운로드하고 DOM 트리를 생성합니다.</li>
<li><strong>CSS 파싱</strong>: CSS를 파싱해 CSSOM을 만듭니다.</li>
<li><strong>렌더 트리 생성</strong>: DOM과 CSSOM을 결합해 렌더 트리를 생성합니다.</li>
<li><strong>레이아웃</strong>: 요소의 크기와 위치를 계산합니다.</li>
<li><strong>페인팅</strong>: 화면에 픽셀을 그립니다.</li>
</ol>
<h3 id="head에서-script-실행"><code>&lt;head&gt;</code>에서 <code>&lt;script&gt;</code> 실행</h3>
<ul>
<li>브라우저는 HTML을 위에서 아래로 파싱하며, <code>&lt;head&gt;</code> 내 <code>&lt;script&gt;</code>를 만나면 파싱을 멈추고 스크립트를 실행합니다.</li>
<li><code>PreventFlash</code>의 <code>&lt;script&gt;</code>는 이 시점에 테마를 설정하므로, 렌더 트리가 생성되기 전에 올바른 테마가 적용됩니다.</li>
</ul>
<h3 id="실제-사용-예시">실제 사용 예시</h3>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/2f1ed905-6f60-4873-9f95-add5b2103410/image.png" alt=""></p>
<h2 id="6-렌더링-비교">6. 렌더링 비교</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>PreventFlash 미사용</th>
<th>PreventFlash 사용</th>
</tr>
</thead>
<tbody><tr>
<td>HTML 파싱</td>
<td>기본 테마로 진행</td>
<td><code>&lt;head&gt;</code>에서 <code>&lt;script&gt;</code> 실행, 테마 설정</td>
</tr>
<tr>
<td>렌더 트리 생성</td>
<td>기본 테마로 생성</td>
<td>올바른 테마로 생성</td>
</tr>
<tr>
<td>페인팅</td>
<td>라이트 → 다크 (깜빡임)</td>
<td>처음부터 다크 모드 표시</td>
</tr>
</tbody></table>
<h2 id="7-정리">7. 정리</h2>
<pre><code class="language-tsx">// 라이트 모드와 다크 모드의 배경색을 정의합니다.
const background = {
  light: &#39;white&#39;,
  dark: &#39;#24292e&#39;,
}

/**
 * PreventFlash 컴포넌트는 다크 모드와 라이트 모드 전환 시 깜빡임을 방지합니다.
 * - 초기 렌더링 시 사용자의 테마 선호도(로컬 스토리지 또는 시스템 설정)에 따라 올바른 스타일을 적용합니다.
 * - `dangerouslySetInnerHTML`을 사용하여 `&lt;script&gt;` 태그에 클라이언트 측에서 즉시 실행되는 JavaScript 코드를 삽입하며,
 *   이는 외부 import가 빠른 새로고침(fast refresh)을 방해하기 때문에 여기서 직접 구현됩니다.
 * - 목적: 페이지 로드 시 테마 전환으로 인한 깜빡임(FOUC)을 방지하고 사용자 경험을 개선합니다.
 * @returns {JSX.Element} 스타일과 스크립트를 포함한 JSX 요소
 */
function PreventFlash() {
  // setColorScheme 함수는 클라이언트 측에서 테마를 설정합니다.
  const setColorScheme = () =&gt; {
    // 시스템의 다크 모드 선호 여부를 확인합니다.
    const mql = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;)
    // 로컬 스토리지에서 사용자 테마 설정을 가져오며, 기본값은 &#39;system&#39;입니다.
    const scheme = localStorage.getItem(&#39;literal-color-scheme&#39;) ?? &#39;system&#39;

    // 다크 모드 조건: 사용자가 &#39;dark&#39;를 선택했거나, &#39;system&#39;이고 시스템이 다크 모드일 때
    if (scheme === &#39;&quot;dark&quot;&#39; || (scheme === &#39;&quot;system&quot;&#39; &amp;&amp; mql.matches)) {
      // HTML 요소에 &#39;dark&#39; 클래스를 추가하여 다크 모드 스타일을 적용합니다.
      document.documentElement.classList.toggle(&#39;dark&#39;, true)
      // 테마 색상을 다크 모드 배경색으로 업데이트합니다.
      document
        .querySelector(&#39;#theme-color&#39;)
        ?.setAttribute(&#39;content&#39;, background.dark)
    }
  }

  return (
    &lt;&gt;
      {/* 배경색을 정의하는 CSS 스타일을 삽입합니다. */}
      &lt;style&gt;{`
        .bg-default, .hover\\:bg-default:hover {
          background: ${background.light};
        }
        .dark.bg-default, .dark .bg-default, .dark .hover\\:bg-default:hover {
          background: ${background.dark};
        }
      `}&lt;/style&gt;
      {/* background 객체를 클라이언트 측에서 사용할 수 있도록 JSON 형태로 삽입합니다. */}
      &lt;script
        dangerouslySetInnerHTML={{
          __html: `const background=${JSON.stringify(background)}`,
        }}
      &gt;&lt;/script&gt;
      {/* setColorScheme 함수를 즉시 실행하여 초기 테마를 설정합니다. */}
      &lt;script
        dangerouslySetInnerHTML={{ __html: `(${setColorScheme})()` }}
      &gt;&lt;/script&gt;
    &lt;/&gt;
  )
}</code></pre>
<p><code>PreventFlash</code> 컴포넌트는 <code>&lt;head&gt;</code> 태그에 삽입되어 HTML 파싱 중에 실행되며, 테마를 즉시 적용해 FOUC를 방지합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 성능 최적화 ]]></title>
            <link>https://velog.io/@jay_chae/%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@jay_chae/%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Wed, 05 Feb 2025 07:18:30 GMT</pubDate>
            <description><![CDATA[<p>아래 링크에 정리</p>
<p>이미지 최적화 - LCP
<a href="https://velog.io/@jay_chae/LCP-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94">https://velog.io/@jay_chae/LCP-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94</a></p>
<p>폰트 최적화 - FCP
<a href="https://velog.io/@jay_chae/Next-font-%EC%B5%9C%EC%A0%81%ED%99%94">https://velog.io/@jay_chae/Next-font-%EC%B5%9C%EC%A0%81%ED%99%94</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[그렇게 쓰면 아무도 안 읽습니다]]></title>
            <link>https://velog.io/@jay_chae/%EA%B7%B8%EB%A0%87%EA%B2%8C-%EC%93%B0%EB%A9%B4-%EC%95%84%EB%AC%B4%EB%8F%84-%EC%95%88-%EC%9D%BD%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@jay_chae/%EA%B7%B8%EB%A0%87%EA%B2%8C-%EC%93%B0%EB%A9%B4-%EC%95%84%EB%AC%B4%EB%8F%84-%EC%95%88-%EC%9D%BD%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Mon, 20 Jan 2025 13:36:10 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jay_chae/post/696fb224-2c22-4db3-b744-db448243d0d7/image.png" alt=""></p>
<p>“가장 우선해야 하는 것은 사용자의 과업의 완성을 위해 적정한 정보를 생산하고 전달하는 일이고, 오직 우리가 집중해야 할 문제는 정보를 어떻게 디자인해서 사용자와 진정한 의미의 대화를 할지이다.”</p>
<p>: 이력서 보는 사람의 과업</p>
<p>what you say 그다음 how you say</p>
<p>선택하기 → 선택</p>
<p>주어1 동사1, 중복 피하기</p>
<p>무엇인가요?(문장형) ←x→  ~선택(명사구)</p>
<p>일관성 중요!</p>
<p>글쓰기도 코드처럼  컨벤션 필요 </p>
<p>해요체 vs 하십시오 체</p>
<p>높임의 정도 차이 편할거냐 대접받을거냐</p>
<p>보통 섞이기 마련.</p>
<p>뭘 부탁할 때는 해보세요.</p>
<p>유머 좋아. 근데 너무 자주 나오면 안됨</p>
<p>사용자 이름 부르기</p>
<p>친근과 부담 균형 맞추기</p>
<p>이름 부르면 별 내용 아니더라도 외면하기 힘들다. → 칵테일 파티 효과</p>
<p>: 음 인사 담당자인지 개발자인지, 리더인지 선택하기</p>
<p>리스크도 있다. 너무 자주 부르면 안됨</p>
<p>독이 될 수 있는 공감 표현</p>
<p>예단해 버리는 느낌, 민감한 부분일 수 있다.(대출)</p>
<p>고난이도다</p>
<p>아이콘 밑에 텍스트는 있으면 좋다</p>
<p>팝업</p>
<p>최소한, 중요한, 무조건 받아야 할 때</p>
<p>버튼 먼저 읽고 제목을 읽는다.</p>
<p>본문은 1문장 최소 2문장</p>
<p>타이틀 본문은 중복 내용 없어야 함</p>
<p>버튼 하나 → 의문 X</p>
<p>버튼 둘 → 평서문 x</p>
<p>버튼 3개는 좀...</p>
<p>왼쪽에는 후퇴동작 오른쪽 전진동작(프라이머리 색, 순서 바꾸지 마!→ 골탕 먹이려 하지마 기분 나쁨)</p>
<p>버튼→ 간결한 동사</p>
<p>텍스트 버튼→ 링크→ 명사(도착 페이지)</p>
<p>세로로 정렬된 버튼에는 좀 길게 휴대폰에 저장하기로 텍스트 하고 본문 없애는 방법도 있다. 버튼이 비어보일수 있으니까</p>
<p>버튼의 텍스트는 사용자의 보이스다. 사용자의 말투를 어떻게 할 것인가?</p>
<p>대화체→ 직접 말하는 느낌→ 자기 입장을 표현한 느낌→ 좋음</p>
<p>그러나 길어지는 위험 그리고 권력 관계의 미묘한 조정</p>
<p>토스트는 피드백 그러나 그렇게 중요한게 아닐때</p>
<p>2~3초 최대 2줄 10단어 이하 → 다 못 읽었는데 없어지면 찝찝</p>
<p>툴팁: 말풍선</p>
<p>사용자가 모를 것 같은 정보만 넣는다</p>
<p>많아지면 구차해짐 많아지면 실력이 좋지 않다는 말</p>
<p>오류 해결 방안 제시</p>
<p>부정적, 저자세(굽신) 안됨</p>
<p>fail→실패라는 번역 너무 부정적임</p>
<p>다크 패턴 좀 별로 → 바보 취급하지 마랓</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UX 7가지 디자인]]></title>
            <link>https://velog.io/@jay_chae/UX-7%EA%B0%80%EC%A7%80-%EB%94%94%EC%9E%90%EC%9D%B8</link>
            <guid>https://velog.io/@jay_chae/UX-7%EA%B0%80%EC%A7%80-%EB%94%94%EC%9E%90%EC%9D%B8</guid>
            <pubDate>Mon, 20 Jan 2025 13:34:44 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jay_chae/post/e2c60756-f337-499f-8bd7-bc0c9de9fe18/image.png" alt=""></p>
<p>“UX 디자인은 사용자가 제품을 구입하기 전부터 구입해서 사용하고 폐기하는 전 과정을 포함하기 때문에 전 회사 차원에서 협력해야 달성할 수 있는 목표다”</p>
<p>: 사용하기 전 후 과정 다 고려해야 함. 이력서 열기 전, 후 과정을 고려 → 이력서 보기 직전 화면에 메시지를 넣는다면, 재밌고 좋지 않을까. 말을 건다던지 왜냐면 pdf든 노션이든 이력서가 뜨기 전에는 무조건 지루한 흰바탕일테니까. 만일 나에게 말을 건다면 집중력이 벌써 증가할 것 같다. </p>
<p>“사용자가 처음 보는 제품을 사용할 때 사용자의 장기기억에 저장된 의미를 활용해서 제품의 사용법에 의미를 부여하고 해석할 수 있도록 인터랙션을 설계해야 한다.” ... “요컨대 사용자는 자신에게 의미 없는 것을 잘 기억하지 못한다.”</p>
<p>: 이력서를 보는 사람 머리에는 어떤 의미가 들어 있을까. 내가 겪었던 문제, 제목을 직관적이고 의미 파악이 쉽게 한다. → 문제해결능력: “만들어주신 공동 컴포넌트 수정해도 돼요?”, 협업능력: “마감기한이 늦춰졌어요”</p>
<p>필수요소와 매력요소</p>
<p>: 필수요소는 적당히 매력요소를 열심히. 만족도 효율이 좋다.</p>
<p>: 우리 사이트의 매력요소는? splitview? 재미? </p>
<p>기능이 많아지면 UI를 익숙하게 해서 긴장도를 해소하자</p>
<p>피드백</p>
<p>0.1초→ 즉각적이네, 1초→ 느리네 1초보다 크면→ 끊기네, 10초→ 중단됐네</p>
<p>2초 이상은 무조건 진행 상태를 알려줘야 함</p>
<p>: split view가 일단 바로 나와야 함 컨텐츠는 느리더라도. </p>
<p>“설계원칙 5: 모든/특정 사용자의 시나리오를 고려하라” </p>
<p>“ux 디자이너는 모든 상황을 고려해서 사용자가 어려움을 겪지 않도록 ux를 디자인 한다.”</p>
<p>: 모든 상황을 고려하라... 내 이력서를 공유할 수 있게 할까?</p>
<p>→ 마우스 없는 상황 고려하기</p>
<p>“설계원칙 6: 사용자의 실수를 방지, 복구할 수 있는 방법을 제공하라”</p>
<p>“설계원칙 10: 중복코딩 이점 활용하고 간섭이 일어나지 않도록 설계하라”</p>
<p>색깔+이미지</p>
<p>“설계원칙11: 모든 상황에서 반드시 의도된 행위를 유발하도록 설계하라”</p>
<p>:유도 유도 유도해야 함. 어떻게 split view를 열게 하지? 어떻게 이것저것 눌러보게 하지?</p>
<p>“즉 제품이나 서비스 경쟁은 결국 고객의 시간을 누가 많이 뺐느냐의 싸움이란 것이다.”</p>
<p>: 시간→ 정→ 합격</p>
<p>설계원칙 16: 화면 전환 시에 시각적인 연속성을 부여해야 한다.</p>
<p>: 그 하일라이트 해야 한다는 아주 좋은 근거</p>
<p>설계원칙 17: 사용자가 기억하게 하지 마라 ‘회상보다 인식’</p>
<p>cli보다 gui가 편한 이유</p>
<p>: 그 태그 검색해야 하는 좋은 근거 아주 좋아 </p>
<p>설계원칙 18: 인터페이스를 접어라</p>
<p>힉의 법칙: 의사결정을 내리는 데 선택 가능한 대안의 수가 많을수록 오랜 시간이 걸린다.</p>
<p>모든 것을 나열하지 말고 중요하고 필수적인 것은 옵션에 넣어라</p>
<p>: 이력서에 핵심만 넣고 splitview는 접어두는 용도</p>
<p>: 힉의 법칙에 따라 링크를 넣고 시각적인 연속성을 위해 splitview</p>
<p>설계원칙 19: 자주 쓰이고 중요한 것을 더 쉽게 접근</p>
<p>가깝고 크게</p>
<p>: 블록 전체를 클릭 vs 자세히 보기 버튼</p>
<p>좋은 ux 떠올리는 비결</p>
<p>좋은 비유 찾기</p>
<p>사이트 → 논문 → 구글</p>
<p>“좋은 메타포는 제품과 전혀 닮지 않았는데 기능적 유사성이나 내적 연관성이 크고 메타포의 도입으로 보이지 않았던 새로운 특징이 드러나게 된다.”</p>
<p>:클로드 아주 좋아</p>
<p>“사용자가 처음 보는 제품을 사용할 때 사용자의 장기기억에 저장된 의미를 활용해서 제품의 사용법에 의미를 부여하고 해석할 수 있도록 인터랙션을 설계해야 한다.” ... “요컨대 사용자는 자신에게 의미 없는 것을 잘 기억하지 못한다.”</p>
<p>: 이력서를 보는 사람 머리에는 어떤 의미가 들어 있을까. 내가 겪었던 문제, 제목을 직관적이고 의미 파악이 쉽게 한다. → 문제해결능력: “만들어주신 공동 컴포넌트 수정해도 돼요?”, 협업능력: “마감기한이 늦춰졌어요”</p>
<p>필수요소와 매력요소</p>
<p>: 필수요소는 적당히 매력요소를 열심히. 만족도 효율이 좋다.</p>
<p>: 우리 사이트의 매력요소는? splitview? 재미? </p>
<p>기능이 많아지면 UI를 익숙하게 해서 긴장도를 해소하자</p>
<p>피드백</p>
<p>0.1초→ 즉각적이네, 1초→ 느리네 1초보다 크면→ 끊기네, 10초→ 중단됐네</p>
<p>2초 이상은 무조건 진행 상태를 알려줘야 함</p>
<p>: split view가 일단 바로 나와야 함 컨텐츠는 느리더라도.</p>
<p>“설계원칙 5: 모든/특정 사용자의 시나리오를 고려하라” </p>
<p>“ux 디자이너는 모든 상황을 고려해서 사용자가 어려움을 겪지 않도록 ux를 디자인 한다.”</p>
<p>: 모든 상황을 고려하라... 내 이력서를 공유할 수 있게 할까?</p>
<p>“설게원칙 6: 사용자의 실수를 방지, 복구할 수 있는 방법을 제공하라”</p>
<p>“설계원칙 10: 중복코딩 이점 활용하고 간섭이 일어나지 않도록 설계하라”</p>
<p>색깔+이미지</p>
<p>“설계원칙11: 모든 상황에서 반드시 의도된 행위를 유발하도록 설계하라”</p>
<p>:유도 유도 유도해야 함. 어떻게 split view를 열게 하지? 어떻게 이것저것 눌러보게 하지?</p>
<p>“즉 제품이나 서비스 경쟁은 결국 고객의 시간을 누가 많이 뺐느냐의 싸움이란 것이다.”</p>
<p>: 시간→ 정→ 합격</p>
<p>설계원칙 16: 화면 전환 시에 시각적인 연속성을 부여해야 한다.</p>
<p>: 그 하일라이트 해야 한다는 아주 좋은 근거</p>
<p>설계원칙 17: 사용자가 기억하게 하지 마라 ‘회상보다 인식’</p>
<p>cli보다 gui가 편한 이유</p>
<p>: 그 태그 검색해야 하는 좋은 근거 아주 좋아 </p>
<p>설계원칙 18: 인터페이스를 접어라</p>
<p>힉의 법칙: 의사결정을 내리는 데 선택 가능한 대안의 수가 많을수록 오랜 시간이 걸린다.</p>
<p>모든 것을 나열하지 말고 중요하고 필수적인 것은 옵션에 넣어라</p>
<p>: 이력서에 핵심만 넣고 splitview는 접어두는 용도</p>
<p>: 힉의 법칙에 따라 링크를 넣고 시각적인 연속성을 위해 splitview</p>
<p>설계원칙 19: 자주 쓰이고 중요한 것을 더 쉽게 접근</p>
<p>가깝고 크게</p>
<p>: 블록 전체를 클릭 vs 자세히 보기 버튼</p>
<p>좋은 ux 떠올리는 비결</p>
<p>좋은 비유 찾기</p>
<p>사이트 → 논문 → 구글</p>
<p>“좋은 메타포는 제품과 전혀 닮지 않았는데 기능적 유사성이나 내적 연관성이 크고 메타포의 도입으로 보이지 않았던 새로운 특징이 드러나게 된다.”</p>
<p>:클로드 아주 좋아</p>
<p>태스크 vs 목적</p>
<p>사용하는 이유→ 목적→ 싸게 산다(마트)</p>
<p>목적을 위해 필요한 거 → 태스크 → 쿠폰 이용, 전단지에 있는 제품 구매</p>
<p>태스크를 분석 잘해야 함</p>
<p>: 이력서를 보는 이유→ 좋은 놈인지 알아보기 위해</p>
<p>:태스크→ 공고 올리기, 이력서 받기, 이력서 열기, 읽기, 분석(여기가 복잡), 합격, 불합격 메일 보내기</p>
<p>: 분석→ 역량 파악, 블로그 깃헙 들어가기, 등등 </p>
<p>: 합격 메일 보내기 기능+ 불합격 통보 이걸 쉽게 할 수 있게 한다?</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[📏단일 책임 원칙으로 렌더링 개선(react next js)]]></title>
            <link>https://velog.io/@jay_chae/%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84-%EC%9B%90%EC%B9%99%EC%9C%BC%EB%A1%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B0%9C%EC%84%A0react-next-js</link>
            <guid>https://velog.io/@jay_chae/%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84-%EC%9B%90%EC%B9%99%EC%9C%BC%EB%A1%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EA%B0%9C%EC%84%A0react-next-js</guid>
            <pubDate>Sun, 22 Dec 2024 10:11:08 GMT</pubDate>
            <description><![CDATA[<h2 id="😤-문제">😤 문제</h2>
<p>“어떤 문제가 있었나요”</p>
<p>splitView 내용만 바뀌는 데 splitView 컨테이너까지 리렌더링 된다.</p>
<h2 id="🧐-문제-원인">🧐 문제 원인</h2>
<p>“문제 원인이 무엇인가요”</p>
<p>splitView 컨테이너 열고 닫는 로직과 내용 바꾸는 로직이 같은 컴포넌트에 있다. 그래서 내용이 바뀔 때 컨테이너 까지 리렝더링</p>
<h2 id="😎-해결책">😎 해결책</h2>
<p>“해결책”</p>
<p>단일 책임 원칙으로 splitView 안에 내용을 결정하는 컴포넌트와 splitView 열고 닫는 컴포넌트로 분리한다.</p>
<h2 id="🤔-이유">🤔 이유</h2>
<p>“왜 이런 해결책 사용했나요”</p>
<p>splitView 안에 내용을 결정하는 컴포넌트가 분리되면 안에 내용이 바뀌어을 때 컨테이너는 영향을 받지 않는다.</p>
<ul>
<li>splitViewContainer prop 가독성이 더 좋아졌다. 클린 코드!</li>
</ul>
<h2 id="🤩-적용">🤩 적용</h2>
<p>“어떻게 적용했나요”</p>
<p>비포</p>
<pre><code class="language-js">//분리 전
import SecondaryViewContainer from &quot;./SecondaryViewContainer &quot;
function SplitViewContainer({ PrimaryView }: SplitViewContainerProp) {
  //여는 로직
  const isOpen = useStoreSelector(splitViewStore, (state) =&gt; state.isOpen)
  //안에 내용 결정 로직
  const id = useStoreSelector(splitViewStore, (state) =&gt; state.id)
  const Content = id != null ? ssulContents[id] : () =&gt; null

  return (
    &lt;div className=&quot;relative flex size-full justify-end&quot;&gt;
      &lt;div
        className={`absolute size-full lg:w-3/6 ${
          isOpen ? &#39;lg:left-0&#39; : &#39;lg:left-1/2 lg:-translate-x-1/2&#39;
        } overflow-y-scroll transition-all duration-300 ease-out`}
      &gt;
        {PrimaryView}
      &lt;/div&gt;
      {isOpen &amp;&amp; (
        &lt;div className=&quot;size-full animate-slide-in-right lg:w-5/12&quot;&gt;
          &lt;SecondaryViewContainer&gt;
            &lt;Content /&gt;
          &lt;/SecondaryViewContainer&gt;
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  )
}</code></pre>
<p>애프터</p>
<pre><code class="language-js">//컨테이너(열고 닫는 로직만 담당)
function SplitViewContainer({
  PrimaryView,
  SecondaryView,
}: SplitViewContainerProp) {
  const isOpen = useStoreSelector(splitViewStore, (state) =&gt; state.isOpen)

  return (
    &lt;div className=&quot;relative flex size-full justify-end&quot;&gt;
      &lt;div
        className={`absolute size-full lg:w-3/6 ${
          isOpen ? &#39;lg:left-0&#39; : &#39;lg:left-1/2 lg:-translate-x-1/2&#39;
        } overflow-y-scroll transition-all duration-300 ease-out`}
      &gt;
        {PrimaryView}
      &lt;/div&gt;
      {isOpen &amp;&amp; (
        &lt;div className=&quot;size-full animate-slide-in-right lg:w-5/12&quot;&gt;
          {SecondaryView}
        &lt;/div&gt;
      )}
    &lt;/div&gt;
  )
}
//컨텐트 (안의 내용물 결정 로직만 담당)
function SecondaryViewContent({ id }: SecondaryViewContentProps) {
  const id = useStoreSelector(splitViewStore, (state) =&gt; state.id)
  const Content = id != null ? ssulContents[id] : () =&gt; null

  return (
    &lt;article className=&quot;prose size-full !max-w-full py-5 md:prose&quot;&gt;
      &lt;Content /&gt;
    &lt;/article&gt;
  )
}</code></pre>
<p>분리하고 나니까 prop도 더 가독성이 좋아졌다.</p>
<pre><code class="language-js">//before
//세컨더리뷰는 어딨지? 의문인 코드
function Page() {
  return &lt;SplitViewContainer PrimaryView={&lt;PrimaryViewContainer/&gt;} /&gt;
}
//after
//프라이머리 뷰와 세컨더리 뷰가 각각 어떤 컴포넌트지 알수 있다.
function Page() {
  return (
    &lt;SplitViewContainer
      PrimaryView={&lt;PrimaryViewContainer/&gt;}
      SecondaryView={&lt;SecondaryViewContainer/&gt;}
    /&gt;
  )
}</code></pre>
<p>렌더링 갯수</p>
<p>비포: 5개</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/b65e8b28-8e47-4b07-bb45-2d1063acc250/image.png" alt=""></p>
<p>애프터: 4개</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/53972750-f020-4b67-b5fa-774e8f213b95/image.png" alt=""></p>
<p>렌더링 시간</p>
<p>비포: 5.7ms</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/722c619c-0bd3-468b-a743-2bda4da9803a/image.png" alt=""></p>
<p>애프터</p>
<p>렌더링 시간 3.3ms</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/00519db3-8628-4967-baef-cc7f6bdccfee/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[⭕ LCP 이미지 최적화]]></title>
            <link>https://velog.io/@jay_chae/LCP-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@jay_chae/LCP-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Sun, 22 Dec 2024 10:07:50 GMT</pubDate>
            <description><![CDATA[<h2 id="😤-문제">😤 문제</h2>
<p>“어떤 문제가 있었나요”</p>
<p>LCP 점수 나쁨</p>
<h2 id="🧐-문제-원인">🧐 문제 원인</h2>
<p>“문제 원인이 무엇인가요”</p>
<p>이미지가 늦게 뜬다</p>
<h2 id="😎-해결책">😎 해결책</h2>
<p>“해결책”</p>
<p>next Image에 priorty 속성 true 적용</p>
<h2 id="🤔-이유">🤔 이유</h2>
<p>“왜 이런 해결책 사용했나요”</p>
<p>priorty 적용하면 이미지를 preload 한다.</p>
<p>공식 문서</p>
<blockquote>
<p>priority가 true로 설정되면 해당 이미지는 높은 우선순위로 간주되어 <strong>사전 로드(preload)</strong>됩니다. 
...
priority 속성은 이미지가 Largest Contentful Paint (LCP) 요소로 감지될 때 사용해야 합니다. 다양한 뷰포트 크기에 따라 서로 다른 이미지가 LCP 요소가 될 수 있으므로, 여러 개의 우선순위 이미지를 지정하는 것이 적절할 수 있습니다. 또한 이 속성은 <strong>화면 상단(above the fold)</strong>에 표시되는 이미지에만 사용해야 합니다. 기본값은 false입니다. Largest Content인 이미지에 priority 속성을 주자</p>
</blockquote>
<h2 id="🤩-적용">🤩 적용</h2>
<p>“어떻게 적용했나요”</p>
<p>LCP 11초 개선</p>
<p>before</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/ab59b9a3-de05-405f-a137-6c1c6c164d6c/image.png" alt=""></p>
<p>after</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/59fcad3c-7be5-4637-83f4-0be8b72f79fe/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔠Next font 최적화]]></title>
            <link>https://velog.io/@jay_chae/Next-font-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@jay_chae/Next-font-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Sun, 22 Dec 2024 10:05:22 GMT</pubDate>
            <description><![CDATA[<h2 id="😤-문제">😤 문제</h2>
<p>“어떤 문제가 있었나요”</p>
<p>폰트를 다운로드 하는 데 오래 걸렸다.</p>
<h2 id="🧐-문제-원인">🧐 문제 원인</h2>
<p>“문제 원인이 무엇인가요”</p>
<p>CDN으로 받아오는 데 시간이 오래 걸린다.</p>
<h2 id="😎-해결책">😎 해결책</h2>
<p>“해결책”</p>
<p>폰트를 다운로드하고 next/font를 사용한다. </p>
<h2 id="🤔-이유">🤔 이유</h2>
<p>“왜 이런 해결책 사용했나요”</p>
<p>셀프 호스팅 + next font</p>
<p>다른 곳에 요청을 해서 폰트를 받지 않고 내 서버에서 받으면 빠르기 때문에 폰트를 다운로드해서 빌드해둔다.</p>
<p>next font (next 공식문서) </p>
<blockquote>
<p>next/font는 폰트(맞춤형 폰트 포함)를 자동으로 최적화하고 외부 네트워크 요청을 제거하여 개인정보 보호와 성능을 향상시킵니다.
next/font는 모든 폰트 파일에 대해 자동 자체 호스팅을 기본적으로 제공합니다. 이를 통해 size-adjust CSS 속성을 활용하여 레이아웃 이동 없이 웹 폰트를 최적의 방식으로 로드할 수 있습니다.
이 새로운 폰트 시스템은 Google Fonts도 성능과 개인정보 보호를 고려하여 편리하게 사용할 수 있도록 합니다. CSS와 폰트 파일은 빌드 시 다운로드되며 나머지 정적 자산과 함께 자체 호스팅됩니다. 브라우저에서 Google로 요청이 전송되지 않습니다.</p>
</blockquote>
<p>즉 셀프 호스팅 + fallback font 최적화로 레이아웃 이동 최소화해준다.</p>
<p>원하는 폰트와 가장 비슷한 폰트를 자동으로 fall back font를 넣어주는 게 참 신기하다</p>
<h2 id="🤩-적용">🤩 적용</h2>
<p>“어떻게 적용했나요”</p>
<pre><code class="language-js">import localFont from &#39;next/font/local&#39;

const pretendard = localFont({
  src: &#39;../public/fonts/PretendardVariable.woff2&#39;,
  display: &#39;swap&#39;,
  weight: &#39;45 920&#39;,
  preload: true,
})

export default function RootLayout({
  children,
}: Readonly&lt;{
  children: React.ReactNode
}&gt;) {
  return (
    &lt;html lang=&quot;ko&quot; className={`${pretendard.className} size-full`}&gt;
      &lt;body className=&quot;size-full&quot;&gt;
        &lt;div className=&quot;size-full overflow-x-hidden&quot;&gt;{children}&lt;/div&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  )
}</code></pre>
<p><strong>FCP 1초 개선!</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[⛏️selector로 렌더링 최적화 (react,next js)]]></title>
            <link>https://velog.io/@jay_chae/selector%EB%A1%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94-reactnext-js</link>
            <guid>https://velog.io/@jay_chae/selector%EB%A1%9C-%EB%A0%8C%EB%8D%94%EB%A7%81-%EC%B5%9C%EC%A0%81%ED%99%94-reactnext-js</guid>
            <pubDate>Sun, 22 Dec 2024 10:01:05 GMT</pubDate>
            <description><![CDATA[<h2 id="😤-문제">😤 문제</h2>
<p>“어떤 문제가 있었나요”</p>
<p>버튼을 누르면 누른 버튼 색이 바뀐다.
그런데 버튼 하나를 누르면 색이 변하지 않는 모든 버튼이 리렌더링 된다.</p>
<h2 id="🧐-문제-원인">🧐 문제 원인</h2>
<p>“문제 원인이 무엇인가요”</p>
<p>버튼 컴포넌트가 전역 상태를 가지고 있기 때문에 렌더링 변화가 없어도 전역상태가 바뀌면 리렌더링이 된다.</p>
<h2 id="😎-해결책">😎 해결책</h2>
<p>“해결책”</p>
<p>selector의 반환 값을 해당 버튼이 활성화 되었는지에 대한 값으로 바꾸어 값이 변하는 경우만 렌더링 되게 한다. </p>
<pre><code class="language-js">//before: 전역 상태 id를 사용 =&gt; isActive 값이 변하지 않아도 id만 변해도 리렌더링
const id = useStoreSelector(splitViewStore, (state) =&gt; state.id)
const isActive = splitViewId === id

//after: isActive 값이 바뀔 때만 리레더링
const isActive = useStoreSelector( splitViewStore,(state) =&gt; state.id === splitViewId)
</code></pre>
<h2 id="🤔-이유">🤔 이유</h2>
<p>“왜 이런 해결책 사용했나요”</p>
<p>전역 상태를 사용하는 컴포넌트는 그 전역 상태 값이 바뀔 때 업데이트 해야 하는 컴포넌트로 콜백에 등록이 된다.</p>
<p>그런데 selector를 사용하여 어떤 값이 어떻게 변경 되었을 떄 업데이트 시켜줄지 정해줄 수 있다 </p>
<h2 id="🤩-적용">🤩 적용</h2>
<p>“어떻게 적용했나요”</p>
<pre><code class="language-js">//before
function OpenSplitView({ children, splitViewId }: OpenSplitViewProps) {
  const id = useStoreSelector(splitViewStore, (state) =&gt; state.id)
  const isActive = splitViewId === id

  const handleOpenSplitView = () =&gt; {
    splitViewStore.setState({
      isOpen: true,
      id: splitViewId,
    })
  }

//after
function OpenSplitView({ children, splitViewId }: OpenSplitViewProps) {
  const isActive = useStoreSelector(
    splitViewStore,
    (state) =&gt; state.id === splitViewId,
  )

  const handleOpenSplitView = () =&gt; {
    splitViewStore.setState({
      isOpen: true,
      id: splitViewId,
    })
  }
</code></pre>
<hr>
<p>렌더링 갯수:</p>
<p>before: 버튼을 누르면 모든 버튼 4개가 리렌더링 </p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/d75c5ea5-23ae-4250-a186-13269e788381/image.png" alt=""></p>
<p>after: 눌린 버튼, 해제된 버튼 2개만 리렌더링  </p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/f760ed6d-b912-47f2-9f27-7bf7a399e8e7/image.png" alt=""></p>
<hr>
<p>렌더링 시간:</p>
<p>before: 5.8ms</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/8e90976e-3d2f-4402-96b8-cb5580118892/image.png" alt=""></p>
<p>after: 2.4ms</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/289cd930-ead3-48a5-b9bf-4fe5af0e668d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[지역 상태의 문제점 ]]></title>
            <link>https://velog.io/@jay_chae/%EC%A7%80%EC%97%AD-%EC%83%81%ED%83%9C%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90</link>
            <guid>https://velog.io/@jay_chae/%EC%A7%80%EC%97%AD-%EC%83%81%ED%83%9C%EC%9D%98-%EB%AC%B8%EC%A0%9C%EC%A0%90</guid>
            <pubDate>Tue, 19 Nov 2024 12:44:40 GMT</pubDate>
            <description><![CDATA[<hr>
<h2 id="요약">요약</h2>
<h3 id="문제">문제:</h3>
<p>지역 상태를 컴포넌트 간에 공유하려면 prop drilling과 리렌더링 문제가 생긴다.</p>
<h3 id="해결방법">해결방법:</h3>
<ol>
<li>context:하위 트리마다 특정 값을 공유해야 할 때 사용한다.</li>
<li>subscribe: 트리 외부에 store를 두고 그 값을 특정 컴포넌트가 가져다가 쓰는 모델</li>
</ol>
<hr>
<h2 id="지역-상태의-문제점">지역 상태의 문제점</h2>
<p>지역 상태(useState)는 선언된 컴포넌트 내에서만 사용 가능하다. 
그런데 컴포넌트 간에 같은 상태를 공유하고 싶다면 어떻게 해야 할까?</p>
<p>공통 부모 컴포넌트까지 올라가서 지역 상태를 선언해 준 후 prop으로 내려줘야 한다.
이럴 경우 먼저 prop drilling이 심하게 일어날 가능성이 높다.</p>
<p>게다가 렌더링 측면에서도 좋지 않다.
예를 들어 멀리 떨어진 컴포넌트 간에 상태를 공유할 경우를 생각해 보자.
심할 경우 최상위 부모에 상태 선언 후 prop으로 내려줘야 할 수도 있다.
이 경우 상태가 변했을 때 문제가 생긴다.
최상위가 리렌더링 되기 때문에 + 내려오는 하위 prop들이 변하기 때문에 어플 전체가 리렌더링 된다.</p>
<p>즉 컴포넌트 간 같은 상태를 공유하고자 할 때, 지역 상태는 2가지 문제점이 발생한다.</p>
<ol>
<li>prop drilling</li>
<li>re-rendering</li>
</ol>
<h2 id="context">context</h2>
<p>해결 방법은 크게 두 가지가 있다.
첫 번째는 context이고 그 다음이 구독 모델이다.</p>
<p>리액트에서는 context api를 제공해 주고 있다.
이 친구를 사용하면 prop drill을 하지 않고 prop 점프를 할 수 있게 된다.</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/c18824ff-a428-42fd-8990-9f893876325e/image.png" alt=""></p>
<p>위 그림을 보면 최상단 부모에서 상태가 쭉쭉 걸어 내려오는 것을 확인 할 수 있다.
그러나 context를 사용하면 걸어 내려오지 않습니다.</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/1ae6213d-d46a-4de9-9cb6-bb9fec9be8d3/image.png" alt=""></p>
<p>위 그림처럼 최상위에서 상태가 필요한 컴포넌트로 바로 점프해서 도달하게 된다.
이런 방식을 사용해서 상태를 사용하는 컴포넌트만 렌더링 시키는 것이 가능해진다.</p>
<p>그러나 이 방식에는 뼈아픈 단점이 있다.
먼저 상태 값이 객체인 경우 렌더링 최적화가 되지 않는다.
{a:1,b:2} 인 상태 값을 사용한다고 해보자.
컴포넌트 A에는 a 값만을 사용하고 컴포넌트 B에서는 b만을 사용한다.
이 경우 a 값을 변경 했을 경우, A만 리렌더링 하면 좋겠지만 B도 함께 리렌더링 된다.
context를 사용할 때에 꼭 알아야 하는 리렌더링 이슈다.
이 문제는 객체를 여러 context로 나누는 방법이 있다고 한다.
그러나 그럴 경우 컴포넌트를 감싸는 provider가 많아지는 등, 더티하고 유지 보수하기 힘든 코드가 된다.</p>
<p>다음은 문제는 아니지만 context 사용하는 적절한 상황이라는 게 존재한다.
context는 &#39;문맥&#39;이라는 뜻, 즉 이름처럼 어떤 문맥을 형성할 때 사용하는 것이 좋다.
예를 들어 한 남자의 인생을 다룬 소설을 읽고 있다고 해보자
책 초반부에서 &#39;사랑&#39;이라는 단어가 나올 때, 그 단어의 뜻은 로맨틱한 &#39;에로틱 사랑&#39;을 의미했다.
그러나 남자가 늙어버린 책 후반부에서 &#39;사랑&#39;은 인류애적인 &#39;아가페 사랑&#39;을 의미하게 된다.
같은&#39;사랑&#39;이라는 단어지만 문맥에 따라 다른 뜻으로 사용된다.
context도 비슷한 상황에서 쓰인다.
어떤 상태를 하위 트리에서 일관되게 사용하고 싶을 때 사용한다.
하위 트리마다 다르게 쓰여야 할 때, provider를 사용하면 효과적으로 사용할 수 있다.</p>
<p>즉, 하위 트리에 문맥을 형성하는 상태 값이 있을 때 사용하라고 있는 친구다.
그러므로 context는 어떤 전역 상태에는 적절하지 않을 수 있다.
예를 들어서 모달이 열렸는지 닫혔는지에 대한 정보를 저장하기에는 부적절하다.
왜나하면 특정 하위 트리 전체에 의미 있는 값이 아니기 때문이다.
그저 무작위적으로 모달을 사용하는 컴포넌트에서 사용되는 값이다.
이런 경우 어쩔 수 없이 최상위에 context provider를 주어야 한다.
가능은 하지만 context가 정체성에 그다지 맞는 사용법은 아니라고 할 수 있다.
마치 포크로 밥을 퍼먹는 느낌? 솔직히 잘 퍼먹을 수 있지만, 포크는 찍어먹을 때 쓰라고 만든 거긴 해 ㅎㅎ</p>
<h2 id="subscribe">subscribe</h2>
<p>지역 상태 해결 방법 두 번째는 바로 구독 모델이다.
이 경우 context로 사용하기에는 부적절해 보이는 전역 상태를 사용하기에 적합한 모델이다.</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/95b1af45-adef-4357-83c3-1f3e9fb6b03c/image.gif" alt=""></p>
<p>위 그림처럼 전역 상태가 컴포넌트 상위에 있지 않는다.
store는 전역 상태가 들어있는 창고인 store를 따로 빼둔다.
그 다음 해당 전역 상태를 사용하는 컴포넌트가 저 창고에서 빼오는 식으로 사용한다.
값을 사용하는 컴포넌트가 상태값을 구독해서 배달받는 느낌으로 이름을 이렇게 지은게 아닌가 싶다.(걍 내 생각, 적절한 네이밍 좋았다)</p>
<p>이런 모델을 사용하는 대표적인 예는 zustand이다.</p>
<p>이렇게 될 경우
prop drilling은 손쉽게 해결된다.
그리고 context에서 언급된 객체인 경우 렌더링 최적화가 되지 않는다는 문제는 어떨까?
동일한 문제가 발생은 하지만 selector라는 것을 활용해 해결할 수 있다.
{a:1,b:2}일 때 필요한 a만 select하는 기법이다.
구현하는 방식은 라이브러리마다 다르고 어떻게 구현하느냐에 따라 달라질 테니 설명은 생략, 그러나 간단하니 필요하면 찾아보자.
context보다는 조금 손쉽 효율적인 해결책이라고 한다.</p>
<p>구독 모델은 전역상태 관리에 모델로 널리 쓰이고 있으면 대표적인 예시가 zustand라고 할 수 있다.
zustand를 사용할 때 그러니 꼭 selector 기능을 사용해 렌더링 최적화를 하라.</p>
<p>구독 모델은 같은 구조의 상태를 하위 트리 문맥에 따라 다르게 사용하지 못한다.
이런 경우는 context가 좋다.
그래서 이 둘은 혼합해서 사용할 수 있다.
두 가지 장점을 모두 취하는 방식으로 만들어진 대표적인 라이브러리가 바로 jotai다.
jotai에는 context처럼 provider를 제공하니 활용할 수 있으면 활용해 보자.</p>
<hr>
<h2 id="next">NEXT</h2>
<p>나는 모달이나 split view처럼 열고 닫아야 하는 값을 전역 상태로 관리하고자 한다.
이 경우 구독 모델이 더 적절해 보이니, 구독 모델 전역 상태를 직접 구현해 보고자 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[NEXT MIDDLEWARE]]></title>
            <link>https://velog.io/@jay_chae/NEXT-MIDDLEWARE</link>
            <guid>https://velog.io/@jay_chae/NEXT-MIDDLEWARE</guid>
            <pubDate>Sat, 26 Oct 2024 05:58:53 GMT</pubDate>
            <description><![CDATA[<p>next js로 프로젝트 할 경우
middleWare 활용법</p>
<p>로그인 안 했으면 접근 못하는 페이지의 경우 미들웨어 쓰면 좋다</p>
<p>예를 들면 마이페이지는 로그인 하지 않으면 로그인 페이지로 리다이렉트 시켜주어야 한다.</p>
<p>브라우저에서 리다이렉트 할 경우</p>
<p>브라우저  -&gt; 마이 페이지요청 -&gt; 프런트 서버
브라우저 &lt;- 마이 페이지 html &lt;- 프런트 서버
브라우저 로그인 유뮤 검사
브라우저-&gt; 로그인 페이지 요청 -&gt; 프런트 서버
브라우저 &lt;- 로그인 페이지 html &lt;- 프런트 서버</p>
<hr>
<p>미들웨어 사용해 리다이렉트 할 경우</p>
<p>브라우저  -&gt; 마이 페이지요청 -&gt; 프런트 서버
미들웨어가 로그인 유뮤 검사
브라우저 &lt;- 로그인 페이지 html &lt;- 프런트 서버</p>
<hr>
<p>이렇게 줄어드는 것을 확인 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[디버깅 툴 활용해 주세요]]></title>
            <link>https://velog.io/@jay_chae/%EB%94%94%EB%B2%84%EA%B9%85-%ED%88%B4-%ED%99%9C%EC%9A%A9%ED%95%B4-%EC%A3%BC%EC%84%B8%EC%9A%94</link>
            <guid>https://velog.io/@jay_chae/%EB%94%94%EB%B2%84%EA%B9%85-%ED%88%B4-%ED%99%9C%EC%9A%A9%ED%95%B4-%EC%A3%BC%EC%84%B8%EC%9A%94</guid>
            <pubDate>Mon, 21 Oct 2024 13:47:52 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jay_chae/post/91fdd75f-73aa-4d43-993b-ae254b0377a8/image.png" alt=""></p>
<p>결론: 복잡해지기 전에 미리 미리 디버깅 툴을 활용해서 개발하자</p>
<p>profiler를 통해 불필용한 렌더링 줄이자 회색 좋아요, 노란색 싫어요</p>
<p>부모 렌더링 되면 자식 다 렌더링 되니까, state는 최소 컴포넌트로 쪼개자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전역 상태 관리 selector]]></title>
            <link>https://velog.io/@jay_chae/%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-selector</link>
            <guid>https://velog.io/@jay_chae/%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC-selector</guid>
            <pubDate>Wed, 16 Oct 2024 15:54:55 GMT</pubDate>
            <description><![CDATA[<h3 id="결론">결론:</h3>
<p>전역 상태에서 필요한 값만 select해서 가져오면 렌더링을 최적화 할 수 있다.</p>
<hr>
<p>예를 들어 전역 상태가 객체 {a:1,b:2}라고 해보자.</p>
<p>이때 컴포넌트A가 a만을 사용하고, 컴포넌트B는 b만 사용한다.</p>
<p>만일 이 때 B가 b의 값을 바꾸었을 때, 컴포넌트 A는 리렌더링이 되지 말아야 한다.</p>
<p>전역 상태 관리 라이브러리는 이런 최적화가 되어 있다.</p>
<p>그래서 해당 컴포넌트에서 필요한 값만 select해서 쓰면 렌더링 최적화를 할 수 있다.</p>
<p>전역 상태관리 라이브러리는 기존의 상태로 새로운 상태를 만들 수 있고, select도 제공하고 있다. 이 기능을 잘 활용해 보자.</p>
<hr>
<h3 id="예시">예시</h3>
<pre><code class="language-js">//jotai

const counterState = atom(0);
const biggerThan10 = atom((get)=&gt;get(counterState) &gt; 10);

function Temp(){
  const count = useAtomValue(biggerThan10);
}

function Temp2(){
  const [,setCount] =  useAtom(counterStae);
  ...
  //setCount 3에서 2로 변경 
}
</code></pre>
<p>Jotai를 예시로 들어 보자.</p>
<p>Atom은 전역 상태 단위를 의미한다.</p>
<p>counterState라는 상태로 biggerThan10이라는 상태를 만들었다. </p>
<p>대신 10보다 클 경우에만 가져온다.</p>
<p>이 경우 biggerThan10을 사용하는 Temp 컴포넌트는 10보다 큰 경우에만 변화가 감지된다.</p>
<p>즉 10보다 큰 수로 바뀔 때만 렌더링이 일어난다.</p>
<p>아래는 나쁜 예시다.</p>
<pre><code class="language-js">//jotai

const counterState = atom(0);
const biggerThan10 = atom((get)=&gt;get(counterState) &gt; 10);

function Temp(){
  const count = useAtomValue(counterStae);
  cosnt biggerThanTen = count &gt; 10 ? count:null;

  ///biggerThanTen 사용하는 렌더링 있다고 하자
}
</code></pre>
<p>이런 경우 다른 컴포넌트에서 counterState가 10보다 작은 값으로 변경이 될 때마다, 이 컴포넌트는 무의이하게 리렌더링 될 거다.</p>
<hr>
<p>다시 결론, 전역 상태 관리를 할 때 select 해서 쓰면 렌더링 최적화를 이룰 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전역 상태 관리]]></title>
            <link>https://velog.io/@jay_chae/%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@jay_chae/%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Tue, 15 Oct 2024 15:22:53 GMT</pubDate>
            <description><![CDATA[<h3 id="결론">결론:</h3>
<h4 id="컴포넌트-내부에서만-사용할-수-있는-usestate를-극복하기-위해-전역상태관리-라이브러리-등장-작동방식은-컴포넌트-밖-어딘가에-상태를-둔다컴포넌트-최상단-또는-격린된-파일-그리고-이-외부-상태-변경을-각자의-방식으로-감지해-컴포넌트를-렌더링-한다-예를-들면-변경이-되면-usestate의-set-함수를-돌려-렌더링-시킨다">컴포넌트 내부에서만 사용할 수 있는 useState를 극복하기 위해 전역상태관리 라이브러리 등장. 작동방식은 컴포넌트 밖 어딘가에 상태를 둔다.(컴포넌트 최상단 또는 격린된 파일) 그리고 이 외부 상태 변경을 각자의 방식으로 감지해 컴포넌트를 렌더링 한다. 예를 들면 변경이 되면 useState의 set 함수를 돌려 렌더링 시킨다.</h4>
<hr>
<h3 id="전역-상태-관리-조건-3가지">전역 상태 관리 조건 3가지</h3>
<ol>
<li>여러 컴포넌트에서 접근 가능해야 한다.</li>
<li>상태를 바꾸면 이 상태를 가지고 있는 모든 컴포넌트에서 리렌더링이 일어나야 한다.</li>
<li>상태가 객체인 경우, 예를 들어 {a:1,b:2} 경우 a만을 사용하는 컴포넌트는 b가 변경 되었을 때 렌더링이 일어나서는 안 된다.</li>
</ol>
<hr>
<h3 id="직접-구현">직접 구현</h3>
<p>전역 상태 store를 만든다.
아래 코드 참고</p>
<pre><code class="language-js">type Initializer&lt;T&gt; = T extends unknown ? T | ((prev: T) =&gt; T) : never;

type Store&lt;State&gt; = {
  get: () =&gt; State
  set: (action: Initializer&lt;State&gt;) =&gt; State 
  subscribe: (callback: () =&gt; void) =&gt; () =&gt; void // 변경을 감지하고 싶은 컴포넌트들의 setState 동작 등록
};

export const createStore = &lt;State&gt;(
  initialState: Initializer&lt;State&gt;,
): Store&lt;State&gt; =&gt; {

  let state = typeof initialState !== &#39;function&#39; ? initialState : initialState();

  // 콜백 함수를 저장하는 곳
  const callbacks = new Set&lt;() =&gt; void&gt;();

  const get = () =&gt; state;
  const set = (nextState: State | ((prev: State) =&gt; State)) =&gt; {
    state = typeof nextState === &#39;function&#39; ? (nextState as (prev: State) =&gt; State)(state) : nextState;

    // 값이 변경됐으므로 콜백 목록을 순회하면서 모든 콜백을 실행한다.
    callbacks.forEach((callback) =&gt; callback());

    return state;
  }

  // 
  const subscribe = (callback: () =&gt; void) =&gt; {
    // 콜백 등록
    callbacks.add(callback);

    // 클린업 실행 시 삭제해 반복적으로 추가되는 것 방지
    return () =&gt; {
      callbacks.delete(callback);
    }
  }

  return {get, set, subscribe};
}</code></pre>
<p>그러나 여기 set을 써서 state 값을 바꾼다고 해서 렌더리이 일어나지는 않는다.</p>
<p>set 함수를 보면 state 값을 바꾼 후에 쌓아둔 콜백 함수들을 실행하는 것을 볼 수 있다.</p>
<p>변경을 감지하고 싶은 컴포넌트들의 useState setter 함수를 전부 돌리기 위함이다.</p>
<p>이 전역 변수 쓰는 모든 컴포넌트를 리렌더링 하기 위해서다. </p>
<p>이 로직은 useStore에 제작, </p>
<pre><code class="language-js">export type State = { counter: number; text: string };

const useStoreSelector = (
  store: Store&lt;State&gt;,
  selector: (state: State) =&gt; unknown, // store 값에서 어떤 값을 가져올지 정의하는 함수
) =&gt; {
  const [state, setState] = useState(() =&gt; selector(store.get()));

  useEffect(() =&gt; {
    const unsubscribe = store.subscribe(() =&gt; {
      const value = selector(store.get());
      setState(value);
    });

    return unsubscribe;
  }, [store, selector]);
  return state;
};</code></pre>
<p>여기서 보면 useState를 사용해서 스토어 값을 return state 해주는 것을 확인 할 수 있다. 그리고 setState를 store 콜백에 저장해주고 있다.</p>
<p>사용법은 아래와 같다.</p>
<pre><code class="language-js">// Counter.tsx
const Counter = (props: {
  store: Store&lt;{ counter: number; text: string }&gt;;
}) =&gt; {
  const { store } = props;
  const counter = useStoreSelector(
    store,
    useCallback(state =&gt; state.counter, []),
  );

  function handleClick() {
    store.set(prev =&gt; ({ ...prev, counter: prev.counter + 1 }));
  }
  return (
    &lt;&gt;
      &lt;h1&gt;Counter&lt;/h1&gt;
      &lt;h3&gt;{counter}&lt;/h3&gt;
      &lt;button onClick={handleClick}&gt;+&lt;/button&gt;
    &lt;/&gt;
  );
};</code></pre>
<p>이렇게 되면 useStore를 사용하는 모든 컴포넌트의 전역 상태를 바라보는 지역 useState가 생긴다. 그리고 지역 useState의 setter 함수가 전역 콜백으로 들어가 전역 set 함수가 샐행될 때 실행되므로 리렌더링이 발생한다.</p>
<pre><code class="language-js">useCallback(state =&gt; state.counter, []),
</code></pre>
<p>이 친구(selector)의 정체는 만일 전역 상태가 객체일 때 객체의 일부만 사용하느 경우를 위해서 작성되었다.
{a:1,b:2}일 때 b만 사용한 경우 a가 바뀌었을 때 useState의 setter가 돌지 않도록, 즉 리렌더링을 막을 수 있게 된다.</p>
<p>useCallBack을 사용한 이유는 리렌더링이 되었을 경우 불필요한 동작을 막기 위해서다. useStoreSelector 안 useEffect를 보면 의존성 배열에 selector가 들어가 있다.
컴포넌트가 리렌더링 되어서 이 함수가 새로 생성이 되면 같은 로직인 함수 임에도 불구하고 useEffect가 의미없이 한 번 돌아가기 때문이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[import { cookies } from 'next/headers']]></title>
            <link>https://velog.io/@jay_chae/import-cookies-from-nextheaders</link>
            <guid>https://velog.io/@jay_chae/import-cookies-from-nextheaders</guid>
            <pubDate>Sun, 13 Oct 2024 15:25:45 GMT</pubDate>
            <description><![CDATA[<p>결론: </p>
<p>쿠키를 다루고 싶은 경우, 프런트 서버에서 돌아가는 코드는 &quot;next/headers&quot;의 cookies 메소드를 사용하면 된다. 그러나 서버 컴포넌트에서 set cookies는 해줄 수는 없다. 클라이언트에서도 &quot;next/headers&quot;의 cookies 메소드를 사용할 수는 있지만 프런트 서버와 통신을 한다.</p>
<p>이유: </p>
<p>next가 제공하는 쿠키 메소드는 모두 서버 액션이다. get은 프런트 서버가 브라우저로부터 들어오는 요청의 헤더를 읽는 메소드, set은 프런트 서버가 브라우저로 보내는 요청에 쿠키를 set해주라고 명령하는 메소드다.</p>
<p>적용: </p>
<p>왠만하면 클라이언트에서 돌아가는 코드는 브라우저 저장소에 있는 쿠키 값을 활용하는 게 좋겠다. 예를 들어 document.cookie를 사용하면 된다.</p>
<hr>
<h3 id="쿠키란">쿠키란?</h3>
<p>브라우저가 가지고 있는 작은 데이터다.
이렇게 브라우저 store에 로컬 스코리지 처럼 있는 것을 확인 할 수 있다.
브라우저는 어떤 요청을 보낼 때 이 쿠키를 자동으로 요청 헤더에 넣어서 보낸다.
<img src="https://velog.velcdn.com/images/jay_chae/post/5ac68b33-960f-40b4-a6b6-f12828454ab4/image.png" alt=""></p>
<hr>
<h3 id="next-cookie">Next cookie</h3>
<p>넥스트는 &quot;next/headers&quot;에서 쿠키와 관련된 메소드를 제공하고 있다.
<a href="https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options">https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options</a></p>
<p>&quot;next/headers&quot;는 서버 컴포넌트에서만 import 할 수 있다.
그러니까 관련 메소드들이 전부 프런트 서버에서 작동하는 서버 액션이라는 의미다.</p>
<p>그러나 이 액션을 클라이언트에서 사용하고 싶으면 어떻게 하면 좋을까?</p>
<p>라이브러리를 사용해도 좋고, 아래와 같이 util 함수로 빼서 클라이언트 컴포넌트에 import 해서 사용하면 된다고 한다.</p>
<pre><code class="language-js">&quot;use server&quot;;

import { cookies } from &quot;next/headers&quot;;

export async function setCookie(name: string, value: string) {
  const oneDay = 24 * 60 * 60 * 1000; // Set expiry for one day
  cookies().set(name, value, { expires: Date.now() + oneDay });
}

export async function deleteCookie(name: string) {
  cookies().delete(name);
}

export async function getCookie(name: string) {
  return cookies().get(name)?.value;
}

</code></pre>
<hr>
<h3 id="클라이언트에서-next-cookies를-사용한-경우">클라이언트에서 next cookies를 사용한 경우</h3>
<pre><code class="language-js">//test 페이지
&quot;use client&quot;;

import React, { useEffect } from &quot;react&quot;;

import { getCookie, setCookie } from &quot;@/utils/next-cookie&quot;;

export default function TestPage() {
  useEffect(() =&gt; {
    async function testCookieAction() {
      await setCookie(&quot;test&quot;, &quot;cookie&quot;);
      await getCookie(&quot;test&quot;);
    }

    testCookieAction();
  }, []);
  return &lt;div&gt;TestPage&lt;/div&gt;;
}
</code></pre>
<p>이런 코드가 있다고 해보자.</p>
<p>그런 경우 브라우저 네트워크 창을 확이하면 아래와 같이 프런트 서버와 통신을 한 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/476e7dba-9d57-42b4-b5d9-671d650de7eb/image.PNG" alt=""></p>
<p>가장 먼저 set cookie를 하기 위해 프런트 서버에 요청을 보냈다. 그리고 오는 리스폰스에 Set-Cookie가 있다. 이걸 받은 브러우저는 쿠키를 만들게 된다.</p>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/30cd0c61-984a-4e9f-9d92-b08d81b3522b/image.PNG" alt=""></p>
<p>그 다음 get cookie를 하기 위해서 프런트 서버에 또 요청을 하는 것을 확인 할 수 있다. 응답으로 쿠키 값을 주고 있다.</p>
<p>즉, next/headers가 제공하고 있는 cookies 메소드들을 클라이언트에서 사용할 경우 프런트 서버와 통신이 일어난다는 점이다.</p>
<p>이를 클라이언트 코드에 남발할 경우 통신을 많이 하게 돼서 성능에 좋지 않을 수 있다.</p>
<hr>
<h3 id="get">get</h3>
<blockquote>
<p>The cookies function allows you to read the HTTP incoming request cookies from a Server Component or write outgoing request cookies in a Server Action or Route Handler.</p>
</blockquote>
<p>넥스트 공식문서를 확인하면 위와 같이 설명하고 있다.</p>
<p>예를 들어 get은 프런트 서버가 프런트 서버에서 오는 요청의 헤더를 읽는 메소드, set은 프런트 서버가 브라우저로 보내는 요청에 쿠키를 set해주라고 명령하는 메소드다.</p>
<p>이걸 어떻게 활용할 수 있을까?</p>
<p>예를 들어 로그인이 되어 있다면 버튼 글자가 프로필, 아니면 로그인인 경우를 생각해 보자.
이 로그인 버튼을 서버 사이드 렌더링 하고 싶다면 어떻게 하는 게 좋을까? 
로그인 유무에 대한 정보를 어디에 담으면 좋을까? 
로컬 스토리지와 같은 곳은 프런트 서버가 접근하지 못한다.
그런데 쿠키에 저장하면 어떨까?
쿠키가 서버 측에 보내는 요청에 담겨서 가게 된다.
그러면 이때 next/headers를 읽고 사용하면 된다.</p>
<pre><code class="language-js">import { cookies } from &quot;next/headers&quot;;

export default function ServerComponent() {
  const cookieStore = cookies();
  const loggedIn = !!cookieStore.get(&quot;accessToken&quot;);
  return (
    &lt;button&gt;
        {loggedIn ? &quot;프로필&quot;:&quot;로그인&quot;}
    &lt;/button&gt;
  );
}
</code></pre>
<hr>
<h3 id="set">set</h3>
<blockquote>
<p>HTTP does not allow setting cookies after streaming starts, so you must use .set() in a Server Action or Route Handler.</p>
</blockquote>
<p>주의해야 할 점이 하나 있다. </p>
<p>위에서 보면 스트리밍 중일 때는 쿠키를 set 할 수 없다는 얘기를 하고 있다.</p>
<pre><code class="language-js">import { cookies } from &quot;next/headers&quot;;

export default function Test() {
  cookies().set(&quot;test&quot;, &quot;cookie&quot;);
  return &lt;div&gt;Test&lt;/div&gt;;
}

</code></pre>
<p>예를 들어 위와 같은 코드가 있다고 해보자.
의도는 이 이 컴포넌트가 돌아갈 때마다 쿠키를 set 해주는 거다.
그러나 위와 같은 코드를 돌려보면 에러가 뜬다.</p>
<p>이는 스트리밍과 연관이 있다.
스트리밍이란 서버에서 렌더링 한 걸 조금씩 넘겨주는 걸 말한다.
만일 스트리밍을 하지 않는다면 전부 렌더링한 뒤에 넘겨줘야 한다.&#39;</p>
<p>그래서 내 생각에는 저렇게 서버 컴포넌트가 돌아가면 바로 스트리밍이 시작되는데 그 중간에 Set-Cookie를 헤더에 넣어주지 않는다는 말인 것 같다.</p>
<p>결론은 서버 컴포넌트에서는 구조적으로 setCookie를 해줄 수 없다.</p>
<hr>
<h3 id="다시-결론">다시 결론</h3>
<p>쿠키를 다루고 싶은 경우, 프런트 서버에서 돌아가는 코드는 &quot;next/headers&quot;의 cookies 메소드를 사용하면 된다. 그러나 서버 컴포넌트에서 set cookies는 해줄 수는 없다. 클라이언트에서도 &quot;next/headers&quot;의 cookies 메소드를 사용할 수는 있지만 프런트 서버와 통신을 한다. 그래서 왠만하면 클라이언트에서 돌아가는 코드는 브라우저 저장소에 있는 쿠키 값을 활용하는 게 좋겠다. 예를 들어 document.cookie를 사용하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[카카오 소셜 로그인]]></title>
            <link>https://velog.io/@jay_chae/%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@jay_chae/%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Fri, 11 Oct 2024 16:00:02 GMT</pubDate>
            <description><![CDATA[<p>프런트 개발 들어가기 전에!</p>
<p>카카오 소셜 로그인 4단계로 가기 전에 알아야 할 기초 지식들</p>
<ul>
<li><p>인증</p>
<ul>
<li>인증은 &quot;너는 누구냐?&quot;를 묻는 단계</li>
<li>ex) 카카오가 아는 놈인지 모르는 놈인지 확인</li>
<li>ex) 로그인</li>
</ul>
</li>
<li><p>인가</p>
<ul>
<li>인가는 “너 어디까지 할 수 있어?” 묻는 단계</li>
<li>권한을 확인하는 단계</li>
</ul>
</li>
<li><p>토큰</p>
<ul>
<li>토큰은 일종의 증명서, 신분증이다.</li>
<li>토큰 값으로  인증,인가를 간편하게 한다.</li>
</ul>
</li>
</ul>
<p>프런트 구현 전에 필요한 세팅</p>
<ul>
<li>kakao developers에서 진행중인 프로젝트 id 받아놓아야 합니다.</li>
<li>개발 test app인 경우 kakao developers에 등록된 이메일만 로그인 가능합니다.</li>
<li>인가 코드를 받을 redirect_url도 등록 해놓아야 합니다.</li>
</ul>
<hr>
<h2 id="카카오-소셜-로그인-구현-과정-4단계">카카오 소셜 로그인 구현 과정 4단계</h2>
<ol>
<li><p><strong><a href="https://kauth.kakao.com/oauth/authorize">https://kauth.kakao.com/oauth/authorize</a> ← 일단 여기로 가서 로그인 합니다.</strong></p>
<ul>
<li><p>위 링크에 searchParams을 꼭 넣어줘야 함</p>
<ul>
<li><p>response_type: code로 고정</p>
</li>
<li><p>client_id: 카카오한테 받은 우리 서비스 id</p>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<pre><code>    - **redirect_url**: 로그인 성공하고 동의하고 가야하는 페이지, 3번 단계에 나옴

    - **scope: 어떤 정보 받을지**

    - ex) `https://kauth.kakao.com/oauth/authorize?response_type=code&amp;client_id=1ndma,fnlemsm&amp;redirect_uri=https://localhost:3000/auth/login/oauth/kakao&amp;scope=profile_nickname,account_email`
     저 링크로 들어가면 카카오 로그인 화면이 뜹니다</code></pre><ol start="2">
<li><strong>로그인(인증)에 성공하면 이제 인가 단계</strong><ul>
<li>동의 화면이 나옵니다.</li>
<li>특정 정보에 접근하는 거에 동의합니다.</li>
</ul>
</li>
</ol>
<ol start="3">
<li><strong>인가에 성공하면 카카오 측에서  redirect_url로 이동시켜 줌</strong><ul>
<li><strong>redirect_url</strong>에 해당하는 페이지를 만들어놔야 함</li>
<li>카카오 측에서 인가 코드를 searchParams에 넣어줌
 ex) <code>https://localhost:3000/redirect_url?code=oauthPermittedCode</code></li>
</ul>
</li>
</ol>
<ol start="4">
<li><strong>인가 코드로 카카오 측에 토큰을 요청한다.</strong>
 <a href="https://kauth.kakao.com/oauth/token">https://kauth.kakao.com/oauth/token</a> ← 요기로 인가 코드 넘겨주면 토큰을 준다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/ab768eff-0549-4049-bbc7-825fe70d0345/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[hydrate]]></title>
            <link>https://velog.io/@jay_chae/hydrate</link>
            <guid>https://velog.io/@jay_chae/hydrate</guid>
            <pubDate>Thu, 10 Oct 2024 17:39:09 GMT</pubDate>
            <description><![CDATA[<p>결론: SSR 시 서버 렌더링(트리) 된 것과 클라이언트에서 렌더링(트리) 된 것이 다르면 안 된다.(hydration 오류)</p>
<p>이유: paint 과정이 두 번 발생해서 좋지 않다.</p>
<p>설명:</p>
<ol>
<li>ReactDom.hydrate</li>
</ol>
<pre><code class="language-js">ReactDom.hydrate(리액트컴포넌트, 서버에서 만든 HTML)</code></pre>
<p>서버에서 받은 html을 받는다. -&gt; 화면에 보여준다. -&gt; hydrate가 서버에서 만든 HTML과 리액트 컴포넌트를 받는다 -&gt; hydrate가 리액트 컴포넌트를 토대로 다시 DOM을 만든다, -&gt; 만든 DOM을 서버에서 받은 HTML과 비교한다. -&gt; 일치하면 자바스크립트를 적용해서 interactive하게 만든다. -&gt; 일치하지 않으면 새로 만든 DOM으로 화면에 다시 그린다.(hydration 오류)</p>
<ol start="2">
<li>hydrate error 발생 예시</li>
</ol>
<p><img src="https://velog.velcdn.com/images/jay_chae/post/c1411860-3a4f-49d3-8b95-ad1ac2b3c061/image.png" alt=""></p>
<ol start="3">
<li>데이터 패칭 경우</li>
</ol>
<p>서버에서 서버 패치 받은 후 SSR 했을 경우, 클라이언트에서 hydration 하기 위해 또 패치 받아서 렌더링 후 비교하고 js 적용하면 패치를 두 번 받게 된다. 이런 경우 패치를 한 번 받도록 처리를 해준다. 프레임워크가 이를 잘 해주고 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSR vs SPA]]></title>
            <link>https://velog.io/@jay_chae/SSR-vs-SPA</link>
            <guid>https://velog.io/@jay_chae/SSR-vs-SPA</guid>
            <pubDate>Thu, 10 Oct 2024 08:08:25 GMT</pubDate>
            <description><![CDATA[<p>결론: SPA와 SSR 둘 다 알아야 함
이유: SPA와 SSR 의 장단점을 섞어서 개발해야 하기 때문</p>
<p>적용: 최초 진입은 SSR로 하고 그 이후는 내려 받은 자바스크립트로 SPA처럼 작동하게 한다.</p>
<hr>
<p>spa: 최초 접근할 때 브라우저가 필요한 자바스크입트를 모두 받는다. 이후 라우팅은 이미 받은 자바스크립를 사용해 페이지를 만든다.</p>
<p>ssr: 페이지 마다 서버에 요청해서 받는다.</p>
<p>spa는 최초에 전부 받아야 하기 때문에 최초 진입이 느리다. 그리고 검색 엔진 못은 자바스크립트를 돌리지 않기 때문에 SEO에 약하다. 대신 한 번 진입하면 매우 부드럽고 좋은 사용자 경험을 제공한다.</p>
<p>ssr 장점</p>
<ul>
<li>최초 진입 빠르다.</li>
<li>SEO 좋다.</li>
<li>누적 레이아웃 변경이 적다.</li>
<li>사용자 디바이스 성능에서 자유</li>
<li>보안에 좋다</li>
</ul>
<p>단점</p>
<ul>
<li>서버를 고려하면서 개발해야 함</li>
<li>서버 구축해야 함</li>
<li>렌더링 지연 가능성</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[화면 크기 전역 상태로 관리]]></title>
            <link>https://velog.io/@jay_chae/%ED%99%94%EB%A9%B4-%ED%81%AC%EA%B8%B0-%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C%EB%A1%9C-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@jay_chae/%ED%99%94%EB%A9%B4-%ED%81%AC%EA%B8%B0-%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C%EB%A1%9C-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sun, 22 Sep 2024 11:19:34 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>우주윗미 프로젝트 리팩토링 했어요.</p>
<p>문제:
화면을 조금만 바꿔도 handleResize가 엄청 많이 돌아간다.</p>
<h2 id="상황">상황</h2>
<pre><code class="language-js">// 커스텀 훅 제작
&quot;use client&quot;;

import { useEffect, useState } from &quot;react&quot;;

const useIsMobile = (): boolean =&gt; {
  const [isMobile, setIsMobile] = useState(false);

  useEffect(() =&gt; {
    const handleResize = () =&gt; {
      setIsMobile(window.innerWidth &lt;= 480);
    };

    handleResize();

    window.addEventListener(&quot;resize&quot;, handleResize);

    return () =&gt; {
      window.removeEventListener(&quot;resize&quot;, handleResize);
    };
  }, []);

  return isMobile;
};

export default useIsMobile;</code></pre>
<p>상황1: 화면 크기가 모바일인지 아닌지에 따라서 조건부 렌더링이 필요.
상황2: 화면 크기를 검사하는 useIsMobile 훅을 만듬.
상황3: 조건부 렌더링이 필요한 컴포넌트가 여러 곳에 있다.
상황4: 여러 곳에서 useIsMobile 훅을 사용 중.</p>
<div style="display: flex; justify-content: center; align-items: center;">
  <img src="https://velog.velcdn.com/images/jay_chae/post/b04a9506-607f-4a53-886a-8a05184f0e7b/image.PNG" width="50%" height="50%" />
  <img src="https://velog.velcdn.com/images/jay_chae/post/c4e35535-7e3b-4cb6-aa4e-b78a3c272161/image.PNG" width="50%" height="50%" />
</div>



<hr>
<h2 id="문제-원인">문제 원인</h2>
<p>원인1:
resize는 화면 크기가 바뀔 때 마다 코드가 돌아간다.</p>
<p>원인2:
useIsMobile를 여러 곳에서 사용했기 때문에 동일한 로직의 이벤트 리스너가 여러 번 등록된다.</p>
<p>원인1 + 원인2:
동일한 이벤트 핸들러 여러 개가 화면 크기가 바뀔 때 마다 돌아간다.</p>
<hr>
<h2 id="해결">해결</h2>
<h4 id="해결1">해결1:</h4>
<p><strong>isMobile 변수를 zotai를 활용해 전역 상태로 만든다.</strong></p>
<pre><code class="language-js">//전역 상태  생성
import { atom } from &quot;jotai&quot;;

/** 모바일 화면인지 아닌지 */
const isMobileAtom = atom&lt;boolean&gt;(false);

export default isMobileAtom;</code></pre>
<p>=&gt; 화면 크기는 어떤 컴포넌트에서나 똑같다. 그러므로 동일한 값을 가진 상태를 여러개 만들 이유가 없다. 예를 들어 다크모드이거나 언어가 영어로 설정 된다면 모든 컴포넌트에서 다크이고 영어다. 그래서 이런 경우 전역상태로 관리하곤 한다. 화면 크기도 마찬가지다.</p>
<h4 id="해결2">해결2:</h4>
<p><strong>화면 크기가 필요한 곳에서는 전역 상태 값을 이용하도록 한다.</strong></p>
<pre><code class="language-js">// 훅 로직 변경
&quot;use client&quot;;

import { useAtom } from &quot;jotai&quot;;

import { isMobileAtom } from &quot;@/stores&quot;;

const useIsMobile = (): boolean =&gt; {
  const [isMobile] = useAtom(isMobileAtom);

  return isMobile;
};

export default useIsMobile;</code></pre>
<p>=&gt; 이벤트 리스너를 등록하고 빼는 로직은 전부 삭제. 여러 곳에서 동일한 이벤트 리스너를 등록할 필요가 없다.</p>
<h4 id="해결3">해결3:</h4>
<p><strong>화면 크기를 검사하는 로직을 가진 컴포넌트 하나를 제작해서 최상위에 넣는다.</strong></p>
<pre><code class="language-js">// example
export default function RootLayout({
  children,
}:{
  children: React.ReactNode;
}) {
  return (
    &lt;html lang=&quot;ko&quot;&gt;
      &lt;body&gt;
        &lt;MobileSizeWatcher /&gt; // 이 친구!
        &lt;Header /&gt;
        {children}
      &lt;/body&gt;
    &lt;/html&gt;
  );
}</code></pre>
<p>=&gt; Next.js &amp; app router를 사용 중이기 때문에 최상단 레이아웃에 컴포넌트를 넣어주었다. 저 컴포넌트 안에서만 화면 크기를 검사하고 isMobile 전역상태를 업데이트 해준다.</p>
<h4 id="해결4">해결4:</h4>
<p><strong>resize 대신 matchMedia 사용.</strong></p>
<pre><code class="language-js">&quot;use client&quot;;

import { useAtom } from &quot;jotai&quot;;
import { useEffect } from &quot;react&quot;;

import { isMobileAtom } from &quot;@/stores&quot;;

export default function MobileSizeWatcher() {
  const [, setIsMobile] = useAtom(isMobileAtom);
  useEffect(() =&gt; {
    const mediaQuery = window.matchMedia(&quot;(max-width: 480px)&quot;);

    const handleMediaQueryChange = (e: MediaQueryListEvent) =&gt; {
      setIsMobile(e.matches);
    };

    // 초기 렌더링 시 확인
    setIsMobile(mediaQuery.matches);

    // 미디어 쿼리 변경 시 이벤트 등록
    mediaQuery.addEventListener(&quot;change&quot;, handleMediaQueryChange);

    return () =&gt; {
      mediaQuery.removeEventListener(&quot;change&quot;, handleMediaQueryChange);
    };
  }, [setIsMobile]);

  return null;
}
</code></pre>
<p>=&gt; matchMedia는 미디어 쿼리가 일치하는지 검사하는 메소드. 이 메소드를 사용해서 모바일 사이즈보다 작아졌을 때만 함수가 돌아가도록 했다.</p>
<hr>
<h2 id="결과">결과</h2>
 <img src="https://velog.velcdn.com/images/jay_chae/post/ff04f909-d8fd-48a1-aae2-b1d8fbf0647c/image.PNG" width="50%" height="50%" />

<p><img src="https://velog.velcdn.com/images/jay_chae/post/cf2dabdd-ca08-4c27-af0e-6d4c25722a5d/image.png" alt=""></p>
<p>결과1:
모바일인지 아닌지 정보를 가진 변수를 전역 변수 하나로만 관리 됨.</p>
<p>결과2:
이벤트 리스너가 하나만 등록됨.</p>
<p>결과3:
화면 크기가 모바일로 줄어들 때 한번만 함수가 돈다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[matchMedia]]></title>
            <link>https://velog.io/@jay_chae/matchMedia</link>
            <guid>https://velog.io/@jay_chae/matchMedia</guid>
            <pubDate>Sun, 22 Sep 2024 10:27:52 GMT</pubDate>
            <description><![CDATA[<p>핵심: <code>미디어 쿼리 조건이 현재 환경과 일치하는지 확인하는 메서드.</code>
실천: <code>matchMedia()를 통해 미디어 쿼리를 감지하고, 화면 크기나 디스플레이 조건이 변경될 때마다 해당 이벤트를 처리하고자 할 때 사용</code></p>
<hr>
<pre><code class="language-js">
///matchMedia와 change 이벤트 활용
let mediaQuery = window.matchMedia(&quot;(max-width: 600px)&quot;);

mediaQuery.addEventListener(&quot;change&quot;, (e) =&gt; {
  if (e.matches) {
    console.log(&quot;화면 너비가 600px 이하로 변경되었습니다.&quot;);
  } else {
    console.log(&quot;화면 너비가 600px 이상으로 변경되었습니다.&quot;);
  }
});
</code></pre>
<p>vs</p>
<pre><code class="language-js">//reszie활용
window.addEventListener(&quot;resize&quot;, () =&gt; {
  console.log(&quot;브라우저 창 크기가 변경되었습니다.&quot;);
});
</code></pre>
<p><code>change</code>는 미디어 쿼리 조건이 변하는 순간에 반응.
<code>resize</code>는 창 크기 자체가 변화할 때 반응하는 이벤트.</p>
<p>=&gt; 브라우저 크기가 특정 이상이나 이하 일 때 로직이 필요하다면 matchMedia와 change 이벤트 활용하는 게 더 효율적이다. 왜냐하면 matchMedia로 특정 크기 일 때만 코드를 돌리도록 할 수 있기 때문. resize는 화면 크기가 좀만 변해도 코드를 돌리게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[하위 폴더에서 husky 설정]]></title>
            <link>https://velog.io/@jay_chae/%ED%95%98%EC%9C%84-%ED%8F%B4%EB%8D%94%EC%97%90%EC%84%9C-husky-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@jay_chae/%ED%95%98%EC%9C%84-%ED%8F%B4%EB%8D%94%EC%97%90%EC%84%9C-husky-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Wed, 18 Sep 2024 10:10:53 GMT</pubDate>
            <description><![CDATA[<p>상황: husky 폴더가 .git 폴더보다 하위에 있는 폴더 구조.</p>
<p>문제: husky와 git 파일은 동일 선상에 없으면 돌아가지 않는다.</p>
<p>해결: package.json에 script 추가, husky 파일에 코드 추가</p>
<hr>
<p>문제 상황</p>
<pre><code class="language-bash">├── project
│    ├── git //git 폴더
│   ├── backend
│   └── frontend
│       └── husky //허스키 폴더가 git 폴더 보다 하위에 있다.
│            ├──commit-msg
│           ├── pre-commit
│           └── pre-push</code></pre>
<p>프로젝트 폴더 구조는 위와 같다.
backend 폴더와 frontend 폴더가 한 곳에 있다.
커밋을 포함해 깃 관리를 project 최상 위에서 하고 있다. 
그래서 git 폴더가 최상위에 있는 상황이다.</p>
<blockquote>
<p>git init 명령어를 실행하면 현재 디렉토리가 Git 저장소로 초기화. 이때 .git이라는 숨겨진 디렉토리가 생성된다. .git 폴더 안에는 Git이 버전 관리를 수행하는 데 필요한 파일과 데이터가 저장됩니다.</p>
</blockquote>
<p>백과 프런트를 한 곳에서 관리하기로 했기 때문에 이런 폴더 구조가 생겼다.
이러한 상황에서 frontend 폴더 안에 있는 코드만 husky를 사용해 린트 등을 검사하고 싶었다.</p>
<blockquote>
<p>Husky는 Git 훅(Git Hooks)을 쉽게 설정하고 관리할 수 있게 도와주는 도구다.
Git 훅은 Git에서 특정 이벤트가 발생할 때 자동으로 실행되는 스크립트다.
예를 들어, 커밋을 하기 전에 코드 스타일을 검사하거나, 푸시 전에 테스트를 실행하는 등의 작업을 자동화할 수 있다.</p>
</blockquote>
<p>그러나 역시나 husky는 돌아가지 않았다.</p>
<p>git 폴더의 위치 때문일 수도 있겠다는 생각이 들었다.
아무래도 git 관리를 담당하고 있는 폴더와 husk는 같은 선상에 있어야  하지 않을까?
정답이었다.</p>
<hr>
<p>문제 1</p>
<p>간단한 해결책으로는 npm이나 yarn으로 최상 위에 husky를 설치하고 husky 폴더를 최상 위로 뺴는 방법이 있었다.</p>
<p>그러나 그렇게 되면 backend 코드에도 frontend lint나 fomatter등이 적용되고 말거다. 그러므로 이 방법은 사용할 수 없다.</p>
<hr>
<p>해결책 2</p>
<pre><code class="language-js">
//package.json
&quot;scripts&quot;: {
    &quot;postinstall&quot;: &quot;cd .. &amp;&amp; husky ./frontend/.husky&quot;
  },
</code></pre>
<p>먼저 frontend 안에 있는 package.json에 script를 추가했다.</p>
<p><code>scritps</code></p>
<blockquote>
<p>scripts는 package.json 파일 내에서 정의되는 속성 중 하나다. Node.js 프로젝트에서 자주 사용하는 명령어들을 관리하고 자동화하는 역할을 한다. 프로젝트 작업에 필요한 명령어들을 간편하게 실행할 수 있도록 도와준다.</p>
</blockquote>
<p><code>postinstall</code></p>
<blockquote>
<p>postinstall은 install 명령어가 실행된 후 자동으로 실행된다. 즉, 프로젝트에 필요한 패키지들을 설치한 후에 postinstall 스크립트가 실행되도록 설정해준다.
husky 패키지는 설치된 후 husky install 이라는 명령어가 실행되어야 하기 때문에 패키지가 설치 된 후 실행되는 라이프 사이클 스크립트를 사용해야 합니다. NPM을 사용하는 경우 prepare라는 스크립트를 사용할 수 있지만 Yarn 2+은 prepare 스크립트를 지원하지 않기 때문에 postinstall이라는 스크립트를 사용해야 한다.</p>
</blockquote>
<p><code>&quot;cd .. &amp;&amp; husky ./frontend/.husky&quot;</code></p>
<blockquote>
<p>cd .. 으로 일단 상위 폴더로 이동한다. -&gt; 상위 폴더에서 frontend/.husky 폴더에 있는 Husky 설정을 설치하고 적용한다.</p>
</blockquote>
<hr>
<p>문제 2</p>
<p>위 과정을 완료하면 husky가 돌아는 간다.
그러나 문제는 잘 안 돌아간다.</p>
<p>husky 폴더 안에 있는 이 3가지 파일을 작성했다.</p>
<ol>
<li>commit-msg (커밋 메시지가 특정 규칙을 따르는지 검사)</li>
<li>pre-commit (커밋이 실행되기 전에 실행됩니다.)</li>
<li>pre-push(코드를 원격 저장소로 푸시하기 전에 실행됩니다.)</li>
</ol>
<p>커밋 전에, 푸쉬 전에 코드를 검사하고자 했다.</p>
<p>그런데 이번 문제는 기 때문에 발생한다.
husky가 실행될 때 위에 <code>cd ..</code>으로 인해 최상위에 있게 된다.
그런데 lint 관련 설정 파일이 frontend 폴더 안에 있어서 lint를 제대로 검사해주지 못한다.</p>
<hr>
<p>해결</p>
<p><code>cd ./frontend</code>
명령어를 husky 파일에 추가해준다.</p>
<pre><code class="language-js">// commit-msg 
#!/bin/sh
cd ./frontend
npx --no-install commitlint --edit &quot;$1&quot;
</code></pre>
<blockquote>
<p>cd ./frontend는 현재 디렉토리에서 frontend라는 폴더로 이동하라는 뜻입니다.
이유는, commitlint의 설정 파일 또는 실행 환경이 frontend 폴더 안에 존재하기 때문입니다.</p>
</blockquote>
<hr>
<p>정리</p>
<p>husky와 git 폴더는 같은 선상에 있어야 돌아간다.
그러나 만일 다른 경우 package.json 의 postintall scripts를 활용해 같은 선상에서 husky를 적용할 수 있도록 한다.</p>
]]></description>
        </item>
    </channel>
</rss>