<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>생각과 기록</title>
    <link>https://okdohyuk.tistory.com/</link>
    <description>생각과 기록을 통하여 훌륭한 개발자가 되기를 꿈꾸는 초급 개발자의 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 18 May 2026 00:59:27 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>유도혁</managingEditor>
    <image>
      <title>생각과 기록</title>
      <url>https://tistory1.daumcdn.net/tistory/3910334/attach/4ab6352645d84e3ab845af5409ef4350</url>
      <link>https://okdohyuk.tistory.com</link>
    </image>
    <item>
      <title>리액트 텍스트 에디터 구현기(WYSIWYG Editor)</title>
      <link>https://okdohyuk.tistory.com/154</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 커뮤니티가 html을 기반으로 한 텍스트에디터를 사용한다. 그것은 당연하게도 렌더링할 때 서버에서 받아온 내용 그대로 출력하기에 용이하고, 그 이상의 요구사항이 필요 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 어느 서비스에서 특화된 컴포넌트를 보여주어야 하거나 모바일 앱과도 연계해야 한다면 어떨까? 이 순간 텍스트에디터는 진화하여야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;만난계기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 요구사항의 웹 서비스를 구현해야 했는데 맨붕이왔다. 당연히도 그럴 것이 앱에서는 이미 독자 규격으로 컴포넌트형 콘텐츠를 사용한 커뮤니티가 서비스 중이었기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lead와 Delete는 받아온 데이터를 규격에 따라 컨버팅하고 맞는 컴포넌트에 렌더링하여 해결하였다. 문제는 역시 Create와 Update인데 얼마 남지 않은 개발기간 동안 텍스트에디터에 손대보지 않다가 이것을 처음 구현하기란 어려웠다. 주변에서는 적당한 라이브러리를 찾아서 사용하면 되는 것이 아니냐고 하였지만, 사용자의 개인 데이터를 불러오고 그중에 원하는 것을 추가하여 공유하는 스펙이 있었다. 구현해야 하는 디자인도 그렇고 단순히 html을 편집하는 방식은 아닌 것 같았다. 참고를 위해 노션이나 네이버 블로그나 카페에서 사용하는 에디터로 글을 작성해 보며 어떻게 동작하는 걸까 고민했다. 그런데 마침 타이밍 좋게 &lt;b&gt;FECONF 2022&lt;/b&gt; 를 보고 있었는데 짠! 하고 텍스트 에디터에 대한 내용이 나오는 게 아니겠는가&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;image.CLFWA2.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdWsau/btssTmOJpqS/kQ1V2M8oQVNUBsWJKkgQl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdWsau/btssTmOJpqS/kQ1V2M8oQVNUBsWJKkgQl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdWsau/btssTmOJpqS/kQ1V2M8oQVNUBsWJKkgQl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdWsau%2FbtssTmOJpqS%2FkQ1V2M8oQVNUBsWJKkgQl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;2250&quot; data-filename=&quot;image.CLFWA2.png&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;2250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;contenteditable&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 요소를 편집할 수 있는지 나타내는 특성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 프레젠테이션 (1).jpg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMfEU/btssS1RxzYm/BNoz1KCNR8juEKCET7Hnn0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMfEU/btssS1RxzYm/BNoz1KCNR8juEKCET7Hnn0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMfEU/btssS1RxzYm/BNoz1KCNR8juEKCET7Hnn0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMfEU%2FbtssS1RxzYm%2FBNoz1KCNR8juEKCET7Hnn0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;540&quot; data-filename=&quot;제목 없는 프레젠테이션 (1).jpg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;텍스트에디터의 역사&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1세대&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;contenteditable을 기반으로 html 형식에 의존하여 내용을 작성하면 그 자체가 html로 기록되어 바로 보여주며, html 편집으로도 작성할 수 있다. html이다 보니 웹브라우저 이외의 환경에서 사용이 불가하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로는 메일 편집기나 티스토리 html로 글 작성과 같은 방법이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;150BF9244BF554C905.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nct3T/btssTsVG5DN/ZbToE9uzVgkBlzCINRNdUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nct3T/btssTsVG5DN/ZbToE9uzVgkBlzCINRNdUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nct3T/btssTsVG5DN/ZbToE9uzVgkBlzCINRNdUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNct3T%2FbtssTsVG5DN%2FZbToE9uzVgkBlzCINRNdUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;369&quot; data-filename=&quot;150BF9244BF554C905.png&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;369&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.5세대&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;json을 기본 데이터형식으로 사용하여 멀티 디바이스에 대응할 수 있다. 블록 단위 편집해야 하고 다른 블록의 텍스트를 함께 선택하는 것이 불가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2세대&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 커서와 인풋 버퍼를 사용하여 어색한 부분을 해결하였지만, 브라우저 네이티브 기능을 사용하기 어려움과 다국어에 따라 입력방식이 다른 점을 대응하기 힘듦.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3세대 에디터&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 프레젠테이션 (2).jpg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfZivt/btssSDpTNUj/ZNpgJMnL7WPgVsQkDHKgjK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfZivt/btssSDpTNUj/ZNpgJMnL7WPgVsQkDHKgjK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfZivt/btssSDpTNUj/ZNpgJMnL7WPgVsQkDHKgjK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfZivt%2FbtssSDpTNUj%2FZNpgJMnL7WPgVsQkDHKgjK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;540&quot; data-filename=&quot;제목 없는 프레젠테이션 (2).jpg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;React&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vDOM으로 데이터 변경시 컴포넌트 단위 리렌더링 기능 제공&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MobX&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상태관리 라이브러리로 React와 함께 사용하면 리렌더링에 관여할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;observer: 상태 변화를 감지하여 렌더링에 관여할 수 있음.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ContentEditable&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텐츠를 편집할 수 있음.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 기술과의 충돌&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 프레젠테이션 (3).jpg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgyQTE/btssUJ3IEYi/UQV02OmTScROks5GFFGYB1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgyQTE/btssUJ3IEYi/UQV02OmTScROks5GFFGYB1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgyQTE/btssUJ3IEYi/UQV02OmTScROks5GFFGYB1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgyQTE%2FbtssUJ3IEYi%2FUQV02OmTScROks5GFFGYB1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;540&quot; data-filename=&quot;제목 없는 프레젠테이션 (3).jpg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 기술의 데이터를 관리하는 절차의 차이로 데이터의 싱크가 깨지는 문제가 발생한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;싱크 문제 해결&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;제목 없는 프레젠테이션 (4).jpg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBrOuD/btssUHEPeUi/aFsTtMggkptzocz11ZpgPk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBrOuD/btssUHEPeUi/aFsTtMggkptzocz11ZpgPk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBrOuD/btssUHEPeUi/aFsTtMggkptzocz11ZpgPk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBrOuD%2FbtssUHEPeUi%2FaFsTtMggkptzocz11ZpgPk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;540&quot; data-filename=&quot;제목 없는 프레젠테이션 (4).jpg&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MobX의 untracked API를 이용하여 contenteditable을 이용하는 상황에는 react의 반응을 중단하고 있다가, 백스페이스나 엔터와 같이 데이터 그룹에 영향이 가는 부분은 store를 업데이트하고 vDom을 업데이트할 수 있도록 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function Edit({ content }: EditProps) {
  const {
    contentEditableRef,
    lines,
    upstreaming,
    handleInput,
    handleKeyDown,
    ...
  } = useEdit(content);

  const Content = () =&amp;gt; {
    const render = () =&amp;gt; {...};

    if (upstreaming) {
      return &amp;lt;span&amp;gt;{untracked(render)}&amp;lt;/span&amp;gt;;
    }

    return &amp;lt;span&amp;gt;{render()}&amp;lt;/span&amp;gt;;
  };

  return (
    &amp;lt;Editer
      ref={contentEditableRef}
      contentEditable={true}
      suppressContentEditableWarning={true}
      onInput={handleInput}
      onKeyDown={handleKeyDown}
      ...
    &amp;gt;
      &amp;lt;Content /&amp;gt;
    &amp;lt;/Editer&amp;gt; 
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;function useEdit(content: Content | 'post') {
  const contentEditableRef = useRef&amp;lt;HTMLDivElement | null&amp;gt;(null);
  const {
    lines,
    upstreaming,
    setLines,
    setUpstreaming,
    updateStore,
    keyDownEnter,
    keyDownBackspace,
    ...
  } = useStore&amp;lt;EditorStore&amp;gt;('editorStore');

  const handleInput = () =&amp;gt; {
    setUpstreaming(true);
  };

  const handleKeyDown = (e: React.KeyboardEvent&amp;lt;HTMLDivElement&amp;gt;) =&amp;gt; {
    if (e.nativeEvent.isComposing) return e.preventDefault();

    switch (e.key) {
      case 'Enter':
        return keyDownEnter(e);
      case 'Backspace':
        return keyDownBackspace(e);
      ...
    }
  };

  useEffect(() =&amp;gt; {
    const mo = new MutationObserver((mutations) =&amp;gt; {
      if (!upstreaming) return;

      mutations.forEach(updateStore);
    });

    if (contentEditableRef.current)
      mo.observe(contentEditableRef.current, {...});

    return () =&amp;gt; {
      mo.disconnect();
    };
  }, []);

  return {
    contentEditableRef,
    lines,
    upstreaming,
    handleInput,
    handleKeyDown,
    ...
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고링크들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=xDyUFE1pmmY&quot;&gt;https://www.youtube.com/watch?v=xDyUFE1pmmY&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://deview.kr/data/deview/session/attach/[114]중요한_건_꺾이지_않는_마음_최종.pdf&quot;&gt;https://deview.kr/data/deview/session/attach/[114]중요한_건_꺾이지_않는_마음_최종.pdf&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프론트엔드/React | Next</category>
      <category>contentEditable</category>
      <category>mobx</category>
      <category>untracked</category>
      <category>WYSIWYG</category>
      <category>리액트 텍스트 에티터</category>
      <category>에디터</category>
      <category>위지윅</category>
      <category>텍스트에디터</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/154</guid>
      <comments>https://okdohyuk.tistory.com/154#entry154comment</comments>
      <pubDate>Sat, 2 Sep 2023 16:28:33 +0900</pubDate>
    </item>
    <item>
      <title>프론트엔드와 백엔드 개발자의 전문성과 강점(feat.ChatGPT)</title>
      <link>https://okdohyuk.tistory.com/153</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 프론트엔드와 백엔드에 대한 대화를 나누는 중에 백엔드 개발자가 &quot;프론트엔드 개발자보다 실력이 압도적으로 높다&quot;는 질문을 받았습니다. 하지만 이러한 주장은 전문성과 강점을 고려하지 않은 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드와 백엔드는 모두 웹 개발에서 필수적인 역할을 하고 있습니다. 그러나 프론트엔드와 백엔드 개발자는 서로 다른 전문성을 가지고 있습니다. 프론트엔드 개발자는 사용자 경험을 중심으로 웹 사이트 또는 앱의 디자인, 레이아웃, 인터페이스 및 기능을 구현합니다. 반면 백엔드 개발자는 데이터 처리, 서버 및 데이터베이스 구현, API 디자인 및 보안과 같은 서버 측 기능에 집중합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 중급과 고급 수준에서는 프로그래밍, 알고리즘, 데이터 구조, 네트워크 및 데이터베이스와 같은 CS 지식은 서로 비슷할 것으로 예상됩니다. 그러나 프론트엔드와 백엔드 개발자의 전문성과 강점은 다릅니다. 프론트엔드 개발자는 사용자 경험에 대한 전문성을 가지며, 디자인, 레이아웃, 인터페이스 등의 영역에서 뛰어납니다. 반면, 백엔드 개발자는 서버 측 프로그래밍과 데이터 처리, 보안 등의 분야에서 전문성을 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 전문성과 강점을 고려할 때, 프론트엔드와 백엔드 개발자는 서로 비교할 수 없는 분야입니다. 서로 다른 분야에서 다른 전문성을 가지고 있으며, 이를 결합하여 웹 개발 프로젝트를 성공적으로 완료할 수 있습니다. 따라서 프론트엔드와 백엔드 개발자 모두가 중요한 역할을 수행하며, 서로의 전문성을 존중하고 협업하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, 프론트엔드와 백엔드 개발자는 각각의 분야에서 강점을 가지고 있으며, 전체 웹 개발 프로젝트에서 중요한 역할을 수행합니다.&lt;/p&gt;</description>
      <category>일상</category>
      <category>ChatGPT</category>
      <category>백엔드</category>
      <category>프론트엔드</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/153</guid>
      <comments>https://okdohyuk.tistory.com/153#entry153comment</comments>
      <pubDate>Sun, 2 Apr 2023 14:53:22 +0900</pubDate>
    </item>
    <item>
      <title>SSR 서비스에서 반응형 웹 개발 이야기(SSR이 필요한 이유)</title>
      <link>https://okdohyuk.tistory.com/152</link>
      <description>&lt;h1&gt;개요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;useWindow&lt;/code&gt;라는 커스텀 훅을 만들어 &lt;code&gt;isMobile&lt;/code&gt;, &lt;code&gt;isTablet&lt;/code&gt;, &lt;code&gt;isDesktop&lt;/code&gt; 으로 컴포넌트나 props를 분기시키며 개발하고 있었다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// useWindow.ts

const useWindow = () =&amp;gt; {
  const isMobile = useRef&amp;lt;boolean&amp;gt;(false);
  const isTablet = useRef&amp;lt;boolean&amp;gt;(false);
  const isDesktop = useRef&amp;lt;boolean&amp;gt;(false);
  const isClient = useRef&amp;lt;boolean&amp;gt;(false);

  const [windowSize, setWindowSize] = useState&amp;lt;{ width: number; height: number }&amp;gt;({
    width: 0,
    height: 0,
  });

  useEffect(() =&amp;gt; {
    if (typeof window !== 'undefined') {
      isClient.current = true;
      const handleResize = () =&amp;gt; {
        setWindowSize({
          width: window.innerWidth,
          height: window.innerHeight,
        });

        isMobile.current = window.innerWidth &amp;lt; deviceWidthNumber.tablet;
        isTablet.current =
          window.innerWidth &amp;gt; deviceWidthNumber.tablet &amp;amp;&amp;amp;
          window.innerWidth &amp;lt; deviceWidthNumber.desktop;
        isDesktop.current = window.innerWidth &amp;gt; deviceWidthNumber.desktop;
      };

      window.addEventListener('resize', handleResize);

      handleResize();

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

  return {
    ...
  };
};

export default useWindow;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M6Wlu/btrNv9CvP9m/LdYKnDw7bjstod4EaYzV2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M6Wlu/btrNv9CvP9m/LdYKnDw7bjstod4EaYzV2K/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2926&quot; data-origin-height=&quot;1646&quot; data-filename=&quot;CleanShot_2022-09-27_at_01.16.012x.png&quot; style=&quot;width: 73.0411%; margin-right: 10px;&quot; data-widthpercent=&quot;73.9&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M6Wlu/btrNv9CvP9m/LdYKnDw7bjstod4EaYzV2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM6Wlu%2FbtrNv9CvP9m%2FLdYKnDw7bjstod4EaYzV2K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2926&quot; height=&quot;1646&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw1XpO/btrNvhuwUWG/nsuJLdbtWg1eIOBskdAZO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw1XpO/btrNvhuwUWG/nsuJLdbtWg1eIOBskdAZO0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1562&quot; data-origin-height=&quot;2488&quot; data-filename=&quot;CleanShot_2022-09-27_at_01.44.552x.png&quot; style=&quot;width: 25.7961%;&quot; data-widthpercent=&quot;26.1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw1XpO/btrNvhuwUWG/nsuJLdbtWg1eIOBskdAZO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw1XpO%2FbtrNvhuwUWG%2FnsuJLdbtWg1eIOBskdAZO0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1562&quot; height=&quot;2488&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 위 사진처럼, PC 화면에서는 aside 화면이 노출되는 상태이고, 모바일 화면에서는 햄버거 메뉴가 추가되어 이를 클릭하였을 때 aside 화면을 확인할 수 있는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;useWindow&lt;/code&gt;를 사용하였을 경우 브라우저 화면을 늘리고 줄일 때마다 UI가 즉시 반영된다. 하지만 이는 모든 초기 렌더링이 끝났을 경우의 이야기이고, 아래의 각 서비스 환경에 따라 초기 렌더링 시의 보이는 화면에 대한 차이를 이야기해보려고 한다.&lt;/p&gt;
&lt;h1&gt;각 초기 렌더링&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CSR과 useEffect();&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.10.282x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8cqtB/btrNyCRdjfj/2HtPmzeMLeKZ6P5Lzpzz11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8cqtB/btrNyCRdjfj/2HtPmzeMLeKZ6P5Lzpzz11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8cqtB/btrNyCRdjfj/2HtPmzeMLeKZ6P5Lzpzz11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8cqtB%2FbtrNyCRdjfj%2F2HtPmzeMLeKZ6P5Lzpzz11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3356&quot; height=&quot;1310&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.10.282x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 CSR의 경우 첫 화면을 불러올시, 깡통 &lt;code&gt;index.html&lt;/code&gt;파일과 함께 해당 페이지에대한 번들링된 &lt;code&gt;bundle.js&lt;/code&gt; 파일을 클라이언트에 던져주게 된다. 이는 &lt;code&gt;JavaScript&lt;/code&gt; 파일이 동작하기 이전까지 사용자는 빈 화면을 볼 것이고, 크롤링 봇의 경우는 유용한 페이지가 아니라고 인식하고 지나갈 것이다. 물론 &lt;code&gt;JavaScript&lt;/code&gt;가 동작하고나면, 사용자가 어느 액션을 취한다거나 같은 서비스 내의 페이지를 이동시 화면이 빠르게 변경되는 것을 확인할 수 있을 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CSR useLayoutEffect();&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.10.422x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E29Dw/btrNu8xylTQ/tZzKcJmq2yu2PG3CkeYafK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E29Dw/btrNu8xylTQ/tZzKcJmq2yu2PG3CkeYafK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E29Dw/btrNu8xylTQ/tZzKcJmq2yu2PG3CkeYafK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE29Dw%2FbtrNu8xylTQ%2FtZzKcJmq2yu2PG3CkeYafK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3356&quot; height=&quot;1310&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.10.422x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 초급 &lt;code&gt;React&lt;/code&gt; 개발자들이 자주 하는 실수가 적절한 상황에 적절한 &lt;code&gt;hook&lt;/code&gt;을 사용하지 않는 것이다. &lt;code&gt;hook&lt;/code&gt;을 많이 알고 있는 것에 따라 서비스의 리소스 최적화에 어느 정도 영향을 끼치는 것 같다. 지금과 같은 상황에서도 &lt;code&gt;useWindow&lt;/code&gt;라는 커스텀 훅을 &lt;code&gt;useEffect&lt;/code&gt;로 사용 중이다. 이를 &lt;code&gt;useLayoutEffect&lt;/code&gt;로만 바꾸어주어도, 초기 렌더링 속도를 꽤 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  useEffect - Data측면의 상태관리를 할때 주로 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  useLayoutEffect - 화면에 영향이 있는 상태관리시 주로 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSR과 useEffect();&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.09.172x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsEmNr/btrNwGzPdAR/6YxzSkqJ0ywMT24i1JL1Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsEmNr/btrNwGzPdAR/6YxzSkqJ0ywMT24i1JL1Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsEmNr/btrNwGzPdAR/6YxzSkqJ0ywMT24i1JL1Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsEmNr%2FbtrNwGzPdAR%2F6YxzSkqJ0ywMT24i1JL1Z0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3356&quot; height=&quot;1310&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.09.172x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;SSR&lt;/code&gt;(server side rendering) 말 그대로 &lt;code&gt;client&lt;/code&gt;에서 요청을 하면, &lt;code&gt;CSR&lt;/code&gt;과는 다르게 &lt;code&gt;server&lt;/code&gt;에서 &lt;code&gt;JavaScript&lt;/code&gt;를 동작시켜, 1차적으로 렌더링을 끝낸 화면과 &lt;code&gt;bundle.js&lt;/code&gt;파일을 &lt;code&gt;client&lt;/code&gt;로 보내주게된다. 하지만, 이 과정에서 &lt;code&gt;client server&lt;/code&gt;에서는 요청한 &lt;code&gt;client&lt;/code&gt;의 &lt;code&gt;window size&lt;/code&gt;를 알 수 없기 때문에 &lt;code&gt;SSR&lt;/code&gt;이 원하는 대로 이루어지지 않고, &lt;code&gt;client&lt;/code&gt;에 렌더링 된 파일을 보내준다. 그 이후 동작은 기존 &lt;code&gt;CSR&lt;/code&gt;과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가끔 SSR이 왜 필요한지, 예전의 SSR과 다른게 뭔지 질문을 받아보았는데 이참에 필요한 이유를 정리해보려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예전의 SSR&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;server&lt;/code&gt;에서 &lt;code&gt;html&lt;/code&gt; &lt;code&gt;template&lt;/code&gt;파일을 갖고 있다가, &lt;code&gt;client&lt;/code&gt;에서 요청시 필요한 &lt;code&gt;template&lt;/code&gt;을 &lt;code&gt;response&lt;/code&gt;로 보내준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 페이지를 이동할 때 마다 비슷한 UI가 있어도 다시 모든 파일을 보내준다. 상태관리나 &lt;code&gt;api call&lt;/code&gt;의 경우 주로 jQuery나 Ajax로 하게되는데, 객체지향 프로그래밍이 어렵고, 코드가 전혀 암호화되지 않기 때문에 보안적으로 매우 취약한 상태가된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CSR&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;client&lt;/code&gt;에서 요청 시 해당 프로젝트를 갖는 저장소에서 &lt;code&gt;index.html&lt;/code&gt;과 빌드 및 번들링 된 파일을 보내준다. 초기 렌더링이 조금 느리지만, 페이지 이동 시 같은 화면은 놔두고, 필요한 파일만 받아와서 화면을 업데이트할 수 있게 되었으며, 예전의 &lt;code&gt;SSR&lt;/code&gt;과 비교해서 코드를 객체 지향적으로 작성할 수 있게 되었고, 성능 최적화에 많은 이점을 얻게 되었다. 하지만, 기본적으로 &lt;code&gt;index.html&lt;/code&gt;에 넣어둔 &lt;code&gt;opengraph&lt;/code&gt;정보 말고는 크롤링 봇이 갖고갈 수 있는 정보가 전혀 없기 때문에, 다른 사이트들보다 좋은 콘텐츠를 갖고 있다고 해도 포털사이트들의 검색 결과에 상위노출 시키기가 어렵고, 다양한 소셜서비스에서 바이럴 효과를 얻고 싶어도, 미리보기 화면이 원하는 대로 만들어지지 않기 때문에, 클릭률이 낮아질 수밖에 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CSR Pre-Rendering&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 &lt;code&gt;CSR&lt;/code&gt;의 단점을 보완하기 위해서 만들어진 기술로, 프로젝트 빌드 시점에 어느 정도의 원하는 페이지들을 미리 만들어두고, &lt;code&gt;client&lt;/code&gt; 요청 시 미리 만들어둔 파일을 전달해준다. 당연하게도 초기 렌더링 시간이 없어지고, 크롤링 봇또한 의도한 콘텐츠를 읽어 들여 포털사이트의 검색 결과의 순위가 올라갈 수도 있고, 소셜서비스의 바이럴 효과 클릭률도 올라가게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;그럼 &lt;code&gt;Pre-Rendering&lt;/code&gt; 하면 되지, 왜 &lt;code&gt;신규 SSR&lt;/code&gt;이 나와서 저장소가 아니라 서버를 써야 하냐?&amp;rdquo;&amp;rdquo;라고 묻는다면, 만약 우리 서비스에서 잘 보여주고 싶은 페이지가 1억 개를 가지고 있고, 그것을 모두 &lt;code&gt;Pre-Rendering&lt;/code&gt; 하게 된다고 한다면, 빌드 시간이 엄청나게 불어나게 되고, 어느 페이지의 콘텐츠가 업데이트되었다고 한다면, 이미 &lt;code&gt;Pre-Rendering&lt;/code&gt; 된 파일은 업데이트되지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;신규 SSR&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 모든 단점이 보완되어 즉, 객체 지향적으로 프로그래밍할 수 있고, 코드를 쉽게 암호화할 수 있으며, 기본적인 콘텐츠를 &lt;code&gt;client server&lt;/code&gt;가 서비스의 &lt;code&gt;server&lt;/code&gt;에서 불러온 다음에 렌더링하여 크롤링 봇의 인식 및 소셜 바이럴에 문제가 없고, 페이지 이동 시 필요한 파일만 줄 수 있고, &lt;code&gt;Pre-Rendering&lt;/code&gt; 기술도 사용할 수 있어서 저장된 페이지를 바로 &lt;code&gt;client&lt;/code&gt;에 건네줄 수 있으며, 한 번 요청된 페이지를 캐싱시키고 캐싱 된 페이지를 다음부턴 바로 보내줄 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 더 알아본다면, &lt;code&gt;react&lt;/code&gt;의 최신 기술인 &lt;code&gt;hydration&lt;/code&gt;이라는 기술이 있는데, 사용자가 보고 있는 컴포넌트만 &lt;code&gt;client&lt;/code&gt;에 보내주고, 스크롤 등 화면을 이동 시에 추가로 필요한 컴포넌트를 받아와서 렌더링시키는 방법이다. 이를 SSR에 적용하면, 크롤링 봇이 &lt;code&gt;hydration&lt;/code&gt; 된 컴포넌트를 인식하지 못한다고 생각할 수 있는데, 화면 렌더링은 시켜두되 해당 컴포넌트의 번들링 된 &lt;code&gt;JavaScript&lt;/code&gt; 파일은 화면상에 보였을 때 받아오는 신기술이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/advanced-features/dynamic-import&quot;&gt;https://nextjs.org/docs/advanced-features/dynamic-import&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1664696527375&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Advanced Features: Dynamic Import | Next.js&quot; data-og-description=&quot;Dynamically import JavaScript modules and React Components and split your code into manageable chunks.&quot; data-og-host=&quot;nextjs.org&quot; data-og-source-url=&quot;https://nextjs.org/docs/advanced-features/dynamic-import&quot; data-og-url=&quot;https://nextjs.org/docs/advanced-features/dynamic-import&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bkVMLj/hyPY4LobpB/8tPVTKmRI8Njs9bGSz6MjK/img.png?width=2048&amp;amp;height=1170&amp;amp;face=0_0_2048_1170&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/advanced-features/dynamic-import&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nextjs.org/docs/advanced-features/dynamic-import&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bkVMLj/hyPY4LobpB/8tPVTKmRI8Njs9bGSz6MjK/img.png?width=2048&amp;amp;height=1170&amp;amp;face=0_0_2048_1170');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Advanced Features: Dynamic Import | Next.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Dynamically import JavaScript modules and React Components and split your code into manageable chunks.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nextjs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&lt;del&gt;즉, nextjs는 무적이고 최강이다.&lt;/del&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSR과 useLayoutEffect();&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.09.232x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baEuUw/btrNwE22IRH/GFgaEfhBV5RXDJo6bdvMSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baEuUw/btrNwE22IRH/GFgaEfhBV5RXDJo6bdvMSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baEuUw/btrNwE22IRH/GFgaEfhBV5RXDJo6bdvMSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaEuUw%2FbtrNwE22IRH%2FGFgaEfhBV5RXDJo6bdvMSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3356&quot; height=&quot;1310&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.09.232x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSR과 css @media screen&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.09.302x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHp5po/btrNyGF6F8o/Lajalx3P82lJLlKps2TwyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHp5po/btrNyGF6F8o/Lajalx3P82lJLlKps2TwyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHp5po/btrNyGF6F8o/Lajalx3P82lJLlKps2TwyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHp5po%2FbtrNyGF6F8o%2FLajalx3P82lJLlKps2TwyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3356&quot; height=&quot;1310&quot; data-filename=&quot;CleanShot_2022-10-01_at_14.09.302x.png&quot; data-origin-width=&quot;3356&quot; data-origin-height=&quot;1310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 &lt;code&gt;SSR&lt;/code&gt;에서 반응형 웹 개발을 할 때 &lt;code&gt;hook&lt;/code&gt;의 의존성을 줄이고, &lt;code&gt;css&lt;/code&gt;의 &lt;code&gt;@media screen&lt;/code&gt;으로 개발하면, 더욱 나은 SSR 개발을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;
&lt;!-- &lt;script&gt;
  window.location.href = 'https://okdohyuk.dev/blog/SSR-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90%EC%84%9C-%EB%B0%98%EC%9D%91%ED%98%95-%EC%9B%B9-%EA%B0%9C%EB%B0%9C-%EC%9D%B4%EC%95%BC%EA%B8%B0-SSR%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0';
&lt;/script&gt; --&gt;
&lt;/p&gt;</description>
      <category>프론트엔드/React | Next</category>
      <category>@media screen</category>
      <category>custom hook</category>
      <category>Hook</category>
      <category>NextJS</category>
      <category>React</category>
      <category>seo</category>
      <category>useEffect</category>
      <category>useLayoutEffect</category>
      <category>반응형 웹</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/152</guid>
      <comments>https://okdohyuk.tistory.com/152#entry152comment</comments>
      <pubDate>Sat, 1 Oct 2022 16:21:58 +0900</pubDate>
    </item>
    <item>
      <title>nextjs 다국어, 로컬라이제이션(localization) 지원하기 next-i18next 사용</title>
      <link>https://okdohyuk.tistory.com/151</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존 개인 프로젝트에서 영어와 한국어를 섞어서 사용하고 있었고, 게다가 텍스트를 그냥 코드 중간에 삽입하는 형식으로 관리하고 있었다. 사실상 관리는 아닌 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;nextjs&lt;/code&gt;에서 사용하는 다국어 패키지를 찾아보았고, 그중 괜찮은 것을 사용하기로 하였다.&lt;/p&gt;
&lt;h1&gt;기본 설정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;next-i18next 설치&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;npm install next-i18next&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;npm&lt;/code&gt;과 &lt;code&gt;yarn&lt;/code&gt;등으로 쉽게 설치할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;next-i18next 경로 예시&lt;/h2&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;.
└── public
    └── locales
        ├── ko
        |   └── common.json
        |   └── index.json
        |   └── footer.json
        └── en
            └── common.json
            └── index.json
            └── footer.json&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;json&lt;/code&gt;형식으로 사용하는 텍스트를 관리할 수 있다. nextjs가 &lt;code&gt;pages&lt;/code&gt;를 &lt;code&gt;router&lt;/code&gt;형태로 관리하다보니 기본적으로 페이지 이름별로 &lt;code&gt;json&lt;/code&gt;파일을 만들고 공통으로 사용하는 &lt;code&gt;common&lt;/code&gt;이나 &lt;code&gt;footer&lt;/code&gt;을 따로 만들어서 관리하였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;next-i18next.config.js&lt;/h2&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;module.exports = {
  debug: process.env.NODE_ENV === 'development',
  i18n: {
    defaultLocale: 'ko',
    locales: ['ko', 'en'],
  },
  reloadOnPrerender: process.env.NODE_ENV === 'development',
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;next-i18next&lt;/code&gt;의 설정 파일이다. 프로젝트의 &lt;code&gt;root&lt;/code&gt;경로에 두어 관리하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자세한 설정은 &lt;a href=&quot;https://github.com/i18next/next-i18next&quot;&gt;docs&lt;/a&gt;를 참고하면 좋을 것 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;next.config.js&lt;/h2&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;const { i18n } = require('./next-i18next.config');

const nextConfig = {
  i18n,
};

module.exports = nextConfig;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 &lt;code&gt;next-i18next.config.js&lt;/code&gt;파일에서 설정을 해준 것을 적용하려면, &lt;code&gt;next.config.js&lt;/code&gt;파일에서 적용시켜주어야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;_app.tsx&lt;/h2&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import React from 'react';
import { AppProps } from 'next/app';
import { appWithTranslation } from 'next-i18next';

function MyApp({ Component, pageProps }: AppProps) {
  return (
      &amp;lt;Component {...pageProps} /&amp;gt;
  );
}

export default appWithTranslation(MyApp);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 적용시켜주려면 &lt;code&gt;_app.tsx&lt;/code&gt;에서 &lt;code&gt;appWithTranslation&lt;/code&gt;로 감싸주어야 한다.&lt;/p&gt;
&lt;h1&gt;사용 방법&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공통사항&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { serverSideTranslations } from 'next-i18next/serverSideTranslations';

export async function getStaticProps({ locale }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, ['common', 'footer'])),
      // Will be passed to the page component as props
    },
  };
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;nextjs&lt;/code&gt;에서 서버사이드로 &lt;code&gt;next-i18next&lt;/code&gt;를 적용시키려면, 위와 같이 모든 페이지에 &lt;code&gt;getStaticProps&lt;/code&gt;에서 텍스트 정보들을 가져와 주어야 한다. 이것이 귀찮다면, &lt;a href=&quot;https://github.com/i18next/next-i18next/tree/master/examples/ssg&quot;&gt;이처럼&lt;/a&gt; 사용하는 고도화 방법도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;페이지에서 사용하기&lt;/h2&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;import Link from 'next/link'
import { useRouter } from 'next/router'

import { useTranslation, Trans } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'

import { Header } from '../components/Header'
import { Footer } from '../components/Footer'

const Homepage = () =&amp;gt; {

  const router = useRouter()
  const { t } = useTranslation('common')

  return (
    &amp;lt;&amp;gt;
      &amp;lt;main&amp;gt;
        &amp;lt;Header heading={t('h1')} title={t('title')} /&amp;gt;
        &amp;lt;div style={{ display: 'inline-flex', width: '90%' }}&amp;gt;
          &amp;lt;div style={{ width: '50%' }}&amp;gt;
            &amp;lt;h3 style={{ minHeight: 70 }}&amp;gt;{t('blog.optimized.question')}&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;
              &amp;lt;Trans i18nKey='blog.optimized.answer'&amp;gt;
                Then you may have a look at &amp;lt;a href={t('blog.optimized.link')}&amp;gt;this blog post&amp;lt;/a&amp;gt;.
              &amp;lt;/Trans&amp;gt;
            &amp;lt;/p&amp;gt;
            &amp;lt;a href={t('blog.optimized.link')}&amp;gt;
              &amp;lt;img style={{ width: '50%' }} src='https://locize.com/blog/next-i18next/next-i18next.jpg' /&amp;gt;
            &amp;lt;/a&amp;gt;
          &amp;lt;/div&amp;gt;
          &amp;lt;div style={{ width: '50%' }}&amp;gt;
            &amp;lt;h3 style={{ minHeight: 70 }}&amp;gt;{t('blog.ssg.question')}&amp;lt;/h3&amp;gt;
            &amp;lt;p&amp;gt;
              &amp;lt;Trans i18nKey='blog.ssg.answer'&amp;gt;
                Then you may have a look at &amp;lt;a href={t('blog.ssg.link')}&amp;gt;this blog post&amp;lt;/a&amp;gt;.
              &amp;lt;/Trans&amp;gt;
            &amp;lt;/p&amp;gt;
            &amp;lt;a href={t('blog.ssg.link')}&amp;gt;
              &amp;lt;img style={{ width: '50%' }} src='https://locize.com/blog/next-i18n-static/title.jpg' /&amp;gt;
            &amp;lt;/a&amp;gt;
          &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;hr style={{ marginTop: 20, width: '90%' }} /&amp;gt;
        &amp;lt;div&amp;gt;
          &amp;lt;Link
            href='/'
            locale={router.locale === 'en' ? 'de' : 'en'}
          &amp;gt;
            &amp;lt;button&amp;gt;
              {t('change-locale', { changeTo: router.locale === 'en' ? 'de' : 'en' })}
            &amp;lt;/button&amp;gt;
          &amp;lt;/Link&amp;gt;
          &amp;lt;Link href='/second-page'&amp;gt;
            &amp;lt;button
              type='button'
            &amp;gt;
              {t('to-second-page')}
            &amp;lt;/button&amp;gt;
          &amp;lt;/Link&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/main&amp;gt;
      &amp;lt;Footer /&amp;gt;
    &amp;lt;/&amp;gt;
  )
}

export const getStaticProps = async ({ locale }) =&amp;gt; ({
  props: {
    ...await serverSideTranslations(locale, ['common', 'footer']),
  },
})

export default Homepage&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 &lt;code&gt;json object&lt;/code&gt;를 &lt;code&gt;object[key]&lt;/code&gt;처럼 사용하는 것이 아니라, &lt;code&gt;t(&amp;rsquo;object.key&amp;rsquo;)&lt;/code&gt;의 형식으로 접근할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;t('object.key', { returnObjects: true })&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전의 방식은 텍스트를 불러오는 방법이고, 위처럼 사용하면, object나, array형식도 불러올 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴포넌트에서의 사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시나 컴포넌트에서 사용하려면, &lt;code&gt;props&lt;/code&gt;로 전달해야 하는가 궁금하였다면, 걱정하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page단에서 이미 &lt;code&gt;getStaticProps&lt;/code&gt;로 불러왔다면, 추가 방식 없이 &lt;code&gt;t(&amp;rsquo;object.key&amp;rsquo;)&lt;/code&gt;처럼 그냥 사용할 수 있다.&lt;/p&gt;
&lt;h1&gt;결과&lt;/h1&gt;
&lt;figure id=&quot;og_1661676716784&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;할인율, 세일, 비율, 일부 값, 퍼센트 계산기 | 개발자 유도혁&quot; data-og-description=&quot;할인율, 퍼센트의 계산을 간단하게 하고 싶은 분들이 많습니다. 총 6가지의 퍼센트 계산을 활용해서 원하는 결과를 찾아보세요! | 개발자 유도혁&quot; data-og-host=&quot;okdohyuk.dev&quot; data-og-source-url=&quot;https://okdohyuk.dev/en/percent&quot; data-og-url=&quot;https://okdohyuk.dev/percent&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/kIFUb/hyPAszMU4t/17ulMPiAQkbjYdlA97r4b1/img.png?width=2456&amp;amp;height=1411&amp;amp;face=0_0_2456_1411&quot;&gt;&lt;a href=&quot;https://okdohyuk.dev/en/percent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://okdohyuk.dev/en/percent&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/kIFUb/hyPAszMU4t/17ulMPiAQkbjYdlA97r4b1/img.png?width=2456&amp;amp;height=1411&amp;amp;face=0_0_2456_1411');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;할인율, 세일, 비율, 일부 값, 퍼센트 계산기 | 개발자 유도혁&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;할인율, 퍼센트의 계산을 간단하게 하고 싶은 분들이 많습니다. 총 6가지의 퍼센트 계산을 활용해서 원하는 결과를 찾아보세요! | 개발자 유도혁&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;okdohyuk.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트에 적용해본 결과이다. &lt;code&gt;opengraph&lt;/code&gt;도 언어별로 설정하여서 &lt;code&gt;seo&lt;/code&gt;에 더 좋은 결과가 있으면 좋겠다.&lt;/p&gt;
&lt;h1&gt;참고 링크&lt;/h1&gt;
&lt;figure id=&quot;og_1661676097053&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;next-i18next&quot; data-og-description=&quot;The easiest way to translate your NextJs apps.. Latest version: 12.0.0, last published: 7 days ago. Start using next-i18next in your project by running &amp;#96;npm i next-i18next&amp;#96;. There are 74 other projects in the npm registry using next-i18next.&quot; data-og-host=&quot;www.npmjs.com&quot; data-og-source-url=&quot;https://www.npmjs.com/package/next-i18next&quot; data-og-url=&quot;https://www.npmjs.com/package/next-i18next&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cU2NIB/hyPAxHPkF3/GY6LBOR6YD6OfE5Z2mEdI1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bqBJnU/hyPAAYQ5vT/K3iwrl09Ee9VKPKkgn77Wk/img.jpg?width=1920&amp;amp;height=783&amp;amp;face=0_0_1920_783,https://scrap.kakaocdn.net/dn/bwAP2Y/hyPAyNu8by/nDWrYtc7tI4OhQqY5iC020/img.jpg?width=1040&amp;amp;height=489&amp;amp;face=0_0_1040_489&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/next-i18next&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.npmjs.com/package/next-i18next&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cU2NIB/hyPAxHPkF3/GY6LBOR6YD6OfE5Z2mEdI1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bqBJnU/hyPAAYQ5vT/K3iwrl09Ee9VKPKkgn77Wk/img.jpg?width=1920&amp;amp;height=783&amp;amp;face=0_0_1920_783,https://scrap.kakaocdn.net/dn/bwAP2Y/hyPAyNu8by/nDWrYtc7tI4OhQqY5iC020/img.jpg?width=1040&amp;amp;height=489&amp;amp;face=0_0_1040_489');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;next-i18next&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The easiest way to translate your NextJs apps.. Latest version: 12.0.0, last published: 7 days ago. Start using next-i18next in your project by running `npm i next-i18next`. There are 74 other projects in the npm registry using next-i18next.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.npmjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1661676098936&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - i18next/next-i18next: The easiest way to translate your NextJs apps.&quot; data-og-description=&quot;The easiest way to translate your NextJs apps. Contribute to i18next/next-i18next development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/i18next/next-i18next/tree/master/examples&quot; data-og-url=&quot;https://github.com/i18next/next-i18next&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cxEd8M/hyPAojO0u0/K1I3Cyhk9SUDjPxtQHV3Lk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/i18next/next-i18next/tree/master/examples&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/i18next/next-i18next/tree/master/examples&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cxEd8M/hyPAojO0u0/K1I3Cyhk9SUDjPxtQHV3Lk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - i18next/next-i18next: The easiest way to translate your NextJs apps.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The easiest way to translate your NextJs apps. Contribute to i18next/next-i18next development by creating an account on GitHub.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;</description>
      <category>프론트엔드/React | Next</category>
      <category>localization</category>
      <category>next-i18next</category>
      <category>NextJS</category>
      <category>다국어</category>
      <category>로컬라이제이션</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/151</guid>
      <comments>https://okdohyuk.tistory.com/151#entry151comment</comments>
      <pubDate>Sun, 28 Aug 2022 17:49:49 +0900</pubDate>
    </item>
    <item>
      <title>유도혁 인프콘 2022에 참가하다</title>
      <link>https://okdohyuk.tistory.com/150</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;3년 만에 오프라인 컨퍼런스에 참가하였다.&amp;nbsp; &lt;a href=&quot;https://www.datanet.co.kr/news/articleView.html?idxno=175119&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 기사&lt;/a&gt;의 내용을 보면 신청자수가 1만 명 정도인데 추첨인원은 1천 명 정도이니 대강 1/10의 확률로 합격한 샘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행사날짜가 8월 26일 금요일 이어서 연차를 사용해야 갈 수 있는 상황이었는데, 최근 생긴 매달 마지막 금요일과 명절 전날에 오후 반차를 주는 패밀리데이라고 하는 복지제도가 생겨서 반차만 쓰고 참가할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;맥북을 가져가서 정리라도 할까 싶었는데 아침에 각 세션 자리에 책상이 없는 것을 확인하고 있으면 좋지만, 별로 안 쓰고, 없으면 불편한 아이패드를 가져갔다. 굉장히 오랜만에 굿 노트에 필기를 하며 정리하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZUT0o/btrKGp9tPyz/kb22e4KEFMeNBd33syx3Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZUT0o/btrKGp9tPyz/kb22e4KEFMeNBd33syx3Bk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;1728&quot; data-filename=&quot;CleanShot 2022-08-27 at 13.29.27@2x.png&quot; style=&quot;width: 47.4329%; margin-right: 10px;&quot; data-widthpercent=&quot;47.99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZUT0o/btrKGp9tPyz/kb22e4KEFMeNBd33syx3Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZUT0o%2FbtrKGp9tPyz%2Fkb22e4KEFMeNBd33syx3Bk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2126&quot; height=&quot;1728&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ooAXo/btrKHxsc6Y4/cSgfZjigbGCCNWbOs4Kpi0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ooAXo/btrKHxsc6Y4/cSgfZjigbGCCNWbOs4Kpi0/img.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot; data-is-animation=&quot;false&quot; data-filename=&quot;IMG_4155.jpeg&quot; style=&quot;width: 51.4043%;&quot; data-widthpercent=&quot;52.01&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ooAXo/btrKHxsc6Y4/cSgfZjigbGCCNWbOs4Kpi0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FooAXo%2FbtrKHxsc6Y4%2FcSgfZjigbGCCNWbOs4Kpi0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXJFHu/btrKHefoSZm/AKK0gz33iukVjkflPFbGiK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXJFHu/btrKHefoSZm/AKK0gz33iukVjkflPFbGiK/img.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot; data-is-animation=&quot;false&quot; data-filename=&quot;IMG_4154.jpeg&quot; style=&quot;width: 63.2558%; margin-right: 10px; margin-top: 10px;&quot; data-widthpercent=&quot;64&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXJFHu/btrKHefoSZm/AKK0gz33iukVjkflPFbGiK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXJFHu%2FbtrKHefoSZm%2FAKK0gz33iukVjkflPFbGiK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N6DUu/btrKHchzifI/jt0oyVKROK2y1zepx1NMCK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N6DUu/btrKHchzifI/jt0oyVKROK2y1zepx1NMCK/img.jpg&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-is-animation=&quot;false&quot; data-filename=&quot;IMG_4218.jpeg&quot; style=&quot;width: 35.5814%; margin-top: 10px;&quot; data-widthpercent=&quot;36&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N6DUu/btrKHchzifI/jt0oyVKROK2y1zepx1NMCK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN6DUu%2FbtrKHchzifI%2Fjt0oyVKROK2y1zepx1NMCK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;그림 1: 굿노트에 필기한 내용중 일부, 그림 2: 당근마켓 기업부스 룰렛화면, 그림 3: 기업부스에 줄선사람들 일부, 그림 4: 룰렛도장 못 받은 종이&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;듣고 싶은 세션을 정리하고 다 듣고 싶었지만, 각 기업부스에서 주는 스티커와 다양한 경품들과 도장을 3개 모으면 한 번 돌릴 수 있는 룰렛이 더 갖고 싶었는데 많은 사람들도 같은 생각이다 보니 줄이 늘어서서 3가지의 세션을 정상적으로 듣지 못했다. 기업부스는 다 돌았지만, 마지막 룰렛이 하나의 줄로 운영되다 보니 분산처리가 되지 않아 결국 끝까지 룰렛 보상은 받지 못했다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_4169.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0v3Fp/btrKIrdXnAN/oLvDlSXkaLVa8GfsSrYGT1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0v3Fp/btrKIrdXnAN/oLvDlSXkaLVa8GfsSrYGT1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0v3Fp/btrKIrdXnAN/oLvDlSXkaLVa8GfsSrYGT1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0v3Fp%2FbtrKIrdXnAN%2FoLvDlSXkaLVa8GfsSrYGT1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; data-filename=&quot;IMG_4169.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;&lt;b&gt;&lt;a style=&quot;color: #009a87;&quot; href=&quot;https://www.inflearn.com/course/infcon2022/unit/126515?tab=curriculum&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #00c471;&quot;&gt;인프런 아키텍처의 과거, 현재 그리고 미래, 이동욱&lt;/span&gt;&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프런 아키텍처의 과거, 현재 그리고 미래를 들으며 공감 가는 내용이 많이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인프런은 완전 초기에 워드프레스를 사용하다가 개발자가 조금씩 들어오자 언어를 통일할 수 있는 FxJS환경의 단일 프로젝트로 개발하다가 타입 추론 및 신규 입사자들의 적응이 힘들다는 이유로 점진적 개선방식으로 프론트와 백엔드를 점차 분리하였고, 인프런에서 이벤트를 열면 매번 서버가 터져서 MSA방식으로 발전시키는 듯싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 회사에서 여러 기술을 사용하겠지만, 위와 같은 아키텍처의 변천사는 대부분의 회사에서 필연적으로 겪게 되는 것 같다. 처음부터 완벽에 가까운 아키텍처를 설계하고 개발하는 것과 같이 이상적인 그림은 있지만, 주변 환경과 서비스의 발전과 기술의 발전을 생각하였을 때 결코 점점 발전해 나가는 아키텍처도 나쁜 것만은 아니라는 것을 알게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D8kC0/btrKGx7z6FC/2Hcd6NS99cfiRyEnEWD7s0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D8kC0/btrKGx7z6FC/2Hcd6NS99cfiRyEnEWD7s0/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3264&quot; data-origin-height=&quot;2448&quot; data-filename=&quot;IMG_2159.jpeg&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D8kC0/btrKGx7z6FC/2Hcd6NS99cfiRyEnEWD7s0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD8kC0%2FbtrKGx7z6FC%2F2Hcd6NS99cfiRyEnEWD7s0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3264&quot; height=&quot;2448&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mhD82/btrKF7H2pck/DFhpPZWUDVZ4AdfmWt6sI1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mhD82/btrKF7H2pck/DFhpPZWUDVZ4AdfmWt6sI1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot; data-filename=&quot;IMG_4198.JPG&quot; style=&quot;width: 32.5581%; margin-right: 10px;&quot; data-widthpercent=&quot;33.33&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mhD82/btrKF7H2pck/DFhpPZWUDVZ4AdfmWt6sI1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmhD82%2FbtrKF7H2pck%2FDFhpPZWUDVZ4AdfmWt6sI1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2MXZj/btrKF63w1RT/r1oaREIizAtSebtyzgwdI1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2MXZj/btrKF63w1RT/r1oaREIizAtSebtyzgwdI1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot; data-filename=&quot;IMG_4195.jpeg&quot; data-widthpercent=&quot;33.34&quot; style=&quot;width: 32.5581%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2MXZj/btrKF63w1RT/r1oaREIizAtSebtyzgwdI1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2MXZj%2FbtrKF63w1RT%2Fr1oaREIizAtSebtyzgwdI1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #00c471;&quot;&gt;&lt;a style=&quot;color: #00c471;&quot; href=&quot;https://www.inflearn.com/course/infcon2022/unit/126528?tab=curriculum&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;개발바닥 공개방송 - 개발바닥 (향로, 호돌맨)&lt;/a&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #00c471;&quot;&gt;&amp;nbsp;&amp;nbsp;&lt;b&gt;&lt;a style=&quot;color: #00c471;&quot; href=&quot;https://www.inflearn.com/course/infcon2022/unit/126504?tab=curriculum&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;개발자의&amp;nbsp;셀프&amp;nbsp;브랜딩,&amp;nbsp;김민준&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #00c471;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;http://okdohyuk.dev&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;okdohyuk.dev&lt;/a&gt;를 개발하기 시작한 것도 토이 프로젝트를 만들어 배포하고 운영하는 것도 좋겠지만, 그것은 같이 할 사람을 모아서 하거나 대회에 나가서 하는 게 더 재미있을 것 같았고, 내 개인 프로젝트는 내가 사용하는 서비스를 만들어야 더 보람찰 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 도메인을 위해서 사용하고 있던 okdohyuk(오케이도혁, 유도혁) 아이디를 확정하고, 나만의 심벌을 밀고 있고, tailwindCSS에서 시스템 라이트/다크 모드를 적용하거나 MobX를 배우면서 사용하는 등 발전과 개선을 거듭해나가고 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플로우) 아이디어 -&amp;gt; 학습 -&amp;gt; 개발 -&amp;gt; 포스팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주위에는 토이 프로젝트를 운영하는 사람들이 많아서 &quot;이러한 방식이 맞나?&quot;라는 의심을 조금 해보기도 했었는데, 김민준 님의 셀프 브랜딩 세션과 개발바닥 QnA 토크쇼를 들으며, 내가 하는 방식을 확신할 수 있었고 더 열심히 하게 될 동기가 되었다. 뿐만 아니라 전부터 팬이어서 눈앞에서 그분들을 볼 수 있었다는 것만으로도 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지는 포스팅을 하더라도 공유를 잘 안 했다는 것이 많이 뜨끔하긴 했는데, 앞으로는 용기를 내서 여러 채널에 올려볼 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유튜브를 하는 것도 좋다고 하시는데 그건 자신감이 더 있을 때 해보도록 하겠다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OC5mT/btrKHtQRPLE/0BcV0JTgGKeMhK9Q3c8rrK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OC5mT/btrKHtQRPLE/0BcV0JTgGKeMhK9Q3c8rrK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot; data-filename=&quot;IMG_4160.jpeg&quot; style=&quot;width: 38.1168%; margin-right: 10px;&quot; data-widthpercent=&quot;39.02&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OC5mT/btrKHtQRPLE/0BcV0JTgGKeMhK9Q3c8rrK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOC5mT%2FbtrKHtQRPLE%2F0BcV0JTgGKeMhK9Q3c8rrK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n7a7k/btrKHw7VuCt/PBWJFbnKiQcybKHb3GzkHK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n7a7k/btrKHw7VuCt/PBWJFbnKiQcybKHb3GzkHK/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot; data-filename=&quot;IMG_4158.jpeg&quot; style=&quot;width: 38.1168%; margin-right: 10px;&quot; data-widthpercent=&quot;39.02&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n7a7k/btrKHw7VuCt/PBWJFbnKiQcybKHb3GzkHK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn7a7k%2FbtrKHw7VuCt%2FPBWJFbnKiQcybKHb3GzkHK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btXshw/btrKF8toIjN/kNKOcqZbtwlTDublK88hE1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btXshw/btrKF8toIjN/kNKOcqZbtwlTDublK88hE1/img.jpg&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot; data-filename=&quot;IMG_4173.jpeg&quot; data-widthpercent=&quot;21.96&quot; style=&quot;width: 21.4407%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btXshw/btrKF8toIjN/kNKOcqZbtwlTDublK88hE1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtXshw%2FbtrKF8toIjN%2FkNKOcqZbtwlTDublK88hE1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_4221.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;1639&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mp23p/btrKJ34Xzb5/NQp8JctNVK00brUSCE4b9K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mp23p/btrKJ34Xzb5/NQp8JctNVK00brUSCE4b9K/img.jpg&quot; data-alt=&quot;인프콘에서 받은 경품들의 대부분&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mp23p/btrKJ34Xzb5/NQp8JctNVK00brUSCE4b9K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmp23p%2FbtrKJ34Xzb5%2FNQp8JctNVK00brUSCE4b9K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;1639&quot; data-filename=&quot;IMG_4221.jpeg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;1639&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;인프콘에서 받은 경품들의 대부분&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>INFCON</category>
      <category>INFCON 2022</category>
      <category>인프콘</category>
      <category>인프콘 2022</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/150</guid>
      <comments>https://okdohyuk.tistory.com/150#entry150comment</comments>
      <pubDate>Sat, 27 Aug 2022 15:10:28 +0900</pubDate>
    </item>
    <item>
      <title>Nextjs에서 React Testing Library + Mobx 사용하기</title>
      <link>https://okdohyuk.tistory.com/149</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 운영하다보면, 요구사항이 추가되거나 변경되는 경우가 많다. 그리고 어떤 환경에서 개발을 하더라도 테스트를 하던 코드를 실수로 커밋하거나 다양한 실수를 할 수 있다. 이를 해결하는 것이 테스트 코드를 미리 작성하여, 테스팅을 하며 개발하는 것인데 TDD에 대한 짧고 명확한 개념과, Nextjs에서 hooks과 testing-library/react와 Mobx와 TypeScript를 사용해본 방법을 작성해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TDD&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FA2Xx/btrJC5rkdGx/jBjRcRA1hkQImbjUJXFORK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FA2Xx/btrJC5rkdGx/jBjRcRA1hkQImbjUJXFORK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FA2Xx/btrJC5rkdGx/jBjRcRA1hkQImbjUJXFORK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFA2Xx%2FbtrJC5rkdGx%2FjBjRcRA1hkQImbjUJXFORK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1204&quot; height=&quot;788&quot; data-origin-width=&quot;1204&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실패&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 시작 전부터 테스트코드를 먼저 작성하여,&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;성공&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트코드에 의한 테스트를 진행하면서 개발하고 성공을 이루어내고,&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;리팩토링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공 후 개선할 수 있는 부분은 최대로 개선하면서 그 도중에도 테스트를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;install and setting&lt;/h3&gt;
&lt;pre id=&quot;code_1660555272962&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1660555306007&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// jest.config.js
const nextJest = require('next/jest');
const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig.paths.json');

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: './',
});

// Add any custom config to be passed to Jest
const customJestConfig = {
  preset: 'ts-jest',
  moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '&amp;lt;rootDir&amp;gt;/src/' }),

  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['&amp;lt;rootDir&amp;gt;/jest.setup.js'],
  // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
  moduleDirectories: ['node_modules', '&amp;lt;rootDir&amp;gt;/'],
  testEnvironment: 'jest-environment-jsdom',

  collectCoverage: true,
  // on node 14.x coverage provider v8 offers good speed and more or less good report
  coverageProvider: 'v8',
  collectCoverageFrom: [
    '**/*.{js,jsx,ts,tsx}',
    '!**/*.d.ts',
    '!**/node_modules/**',
    '!&amp;lt;rootDir&amp;gt;/out/**',
    '!&amp;lt;rootDir&amp;gt;/.next/**',
    '!&amp;lt;rootDir&amp;gt;/*.config.js',
    '!&amp;lt;rootDir&amp;gt;/coverage/**',
  ],
  // Add more setup options before each test is run
  // setupFilesAfterEnv: ['&amp;lt;rootDir&amp;gt;/jest.setup.js'],
  testPathIgnorePatterns: ['&amp;lt;rootDir&amp;gt;/node_modules/', '&amp;lt;rootDir&amp;gt;/.next/'],
  transform: {
    // Use babel-jest to transpile tests with the next/babel preset
    // https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object
    '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
  },
  transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'],
};

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1660555380503&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;./tsconfig.paths.json

{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;./src&quot;,
    &quot;paths&quot;: {
      &quot;~&quot;: [&quot;./&quot;],
      &quot;~/*&quot;: [&quot;./*&quot;],
      &quot;@components&quot;: [&quot;./components&quot;],
      &quot;@components/*&quot;: [&quot;./components/*&quot;],
      ...
    }
  }}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위는 ./src 를 기본 디렉터리로 사용하고, 절대경로 별칭을 사용할 경우 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rtn + mobx&lt;/p&gt;
&lt;pre id=&quot;code_1660555585740&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Component.test.tsx
it('matches snapshot', () =&amp;gt; {
    const store = new Store();
    const utils = render(
      &amp;lt;Provider store={store}&amp;gt;
        &amp;lt;Component /&amp;gt; // 테스트할 컴포넌트
      &amp;lt;/Provider&amp;gt;,
    );
    expect(utils.container).toMatchSnapshot();
  });&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1660555701715&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// useStore.ts
import React from 'react';
import { MobXProviderContext } from 'mobx-react';
import { Store } from '@stores/type';

function useStore(store: keyof Store) {
  return React.useContext(MobXProviderContext)[store];
}

export default useStore;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1660555666180&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Components.tsx
function Component() {
  const store = useStore('store');
  
  return (
    &amp;lt;div&amp;gt;
	    {store}
    &amp;lt;/div&amp;gt;
  );
}

export default observer(Component);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드에서 inject로 store을 가져와 사용하였는데, 컴포넌트에서 사용하려니 컴포넌트의 props와 스토어의 props에 관련된 문제가 있어 useStore이라는 커스텀 훅을 만들어 가져와 사용하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고:&lt;/p&gt;
&lt;figure id=&quot;og_1660552683278&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;벨로퍼트와 함께하는 리액트 테스팅&quot; data-og-description=&quot;&quot; data-og-host=&quot;learn-react-test.vlpt.us&quot; data-og-source-url=&quot;https://learn-react-test.vlpt.us/&quot; data-og-url=&quot;https://learn-react-test.vlpt.us/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://learn-react-test.vlpt.us/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://learn-react-test.vlpt.us/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;벨로퍼트와 함께하는 리액트 테스팅&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;learn-react-test.vlpt.us&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;figure id=&quot;og_1660552375274&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Testing | Next.js&quot; data-og-description=&quot;Learn how to set up Next.js with three commonly used testing tools &amp;mdash; Cypress, Playwright, Jest, and React Testing Library.&quot; data-og-host=&quot;nextjs.org&quot; data-og-source-url=&quot;https://nextjs.org/docs/testing&quot; data-og-url=&quot;https://nextjs.org/docs/testing&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ohExK/hyPq55tVJ8/p1aDkME1lIlaWfPern8wE1/img.png?width=2048&amp;amp;height=1170&amp;amp;face=0_0_2048_1170&quot;&gt;&lt;a href=&quot;https://nextjs.org/docs/testing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nextjs.org/docs/testing&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ohExK/hyPq55tVJ8/p1aDkME1lIlaWfPern8wE1/img.png?width=2048&amp;amp;height=1170&amp;amp;face=0_0_2048_1170');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Testing | Next.js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Learn how to set up Next.js with three commonly used testing tools &amp;mdash; Cypress, Playwright, Jest, and React Testing Library.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nextjs.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=G5p3LsfLS3A&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/ppbdC/hyPsIU8BpU/gbeoJJGQxGfxWQQAPKFDQ0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/G5p3LsfLS3A&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프론트엔드/React | Next</category>
      <category>@testing-library/react</category>
      <category>hooks</category>
      <category>mobx</category>
      <category>NextJS</category>
      <category>react-testing-library</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/149</guid>
      <comments>https://okdohyuk.tistory.com/149#entry149comment</comments>
      <pubDate>Mon, 15 Aug 2022 18:45:43 +0900</pubDate>
    </item>
    <item>
      <title>nextjs에서 원하는 키워드로 구글검색 상위노출시키기</title>
      <link>https://okdohyuk.tistory.com/148</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;7월부터 계속해서 개인 프로젝트를 성장시키는 중이다. 딱 어떠한 서비스 같은 토이 프로젝트가 아니고, 확장성을 최대한으로 할 수 있는 도메인을 판 것이다. 자세한 내용은 이 전 포스트를 참고하면 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1659851029105&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;개인프로젝트에 대한 생각&quot; data-og-description=&quot;2022년 연초에 올해의 목표라던가 계획을 세웠었다. 사실 대강 몇년 후 까지도 적어보긴 했는데, 대부분 개발자로서의 성장에 관련된 내용이다. &amp;quot;개인 프로젝트를 만들어서 서비스하고, 거기서 &quot; data-og-host=&quot;blog.okdohyuk.dev&quot; data-og-source-url=&quot;https://blog.okdohyuk.dev/147&quot; data-og-url=&quot;https://blog.okdohyuk.dev/147&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bbg7yd/hyPmgdElQv/RZqgKDKPV96yRJwfG317KK/img.png?width=800&amp;amp;height=155&amp;amp;face=0_0_800_155,https://scrap.kakaocdn.net/dn/fwGX5/hyPl8GHwih/wLUzNvV341IG3hQkqktnm0/img.png?width=800&amp;amp;height=155&amp;amp;face=0_0_800_155,https://scrap.kakaocdn.net/dn/VCQaX/hyPmfsgZH8/Mwz6c07hJ90FZxrITFdiW0/img.png?width=1430&amp;amp;height=578&amp;amp;face=0_0_1430_578&quot;&gt;&lt;a href=&quot;https://blog.okdohyuk.dev/147&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.okdohyuk.dev/147&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bbg7yd/hyPmgdElQv/RZqgKDKPV96yRJwfG317KK/img.png?width=800&amp;amp;height=155&amp;amp;face=0_0_800_155,https://scrap.kakaocdn.net/dn/fwGX5/hyPl8GHwih/wLUzNvV341IG3hQkqktnm0/img.png?width=800&amp;amp;height=155&amp;amp;face=0_0_800_155,https://scrap.kakaocdn.net/dn/VCQaX/hyPmfsgZH8/Mwz6c07hJ90FZxrITFdiW0/img.png?width=1430&amp;amp;height=578&amp;amp;face=0_0_1430_578');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;개인프로젝트에 대한 생각&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2022년 연초에 올해의 목표라던가 계획을 세웠었다. 사실 대강 몇년 후 까지도 적어보긴 했는데, 대부분 개발자로서의 성장에 관련된 내용이다. &quot;개인 프로젝트를 만들어서 서비스하고, 거기서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.okdohyuk.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들었으면, 아니 만들고 있는 중이지만, 나만 알고 있는 것 보단 알리는 것이 좋을 것 같다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표로 설정한 것은 내 아이디인 &quot;okdohyuk&quot;를 구글에 검색하였을 때 1위를 차지하는 것이었다. 애초에 도메인에도 그 키워드가 들어가 있어서 구글 search-console에 robots.txt 정도만 잘 갖다 줘도 될 것이라고 생각하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;next-sitemap&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최대한 쉽게 만들 수 있고, 자주 변경될 수 있으면 좋을 것 같아서, 빌드시 자동으로 생성될 수 있게 하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://www.npmjs.com/package/next-sitemap&quot;&gt;next-sitemap&lt;/a&gt;을 발견하여 추가하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 방법은 아래와 같고, 특정 페이지는 노출시키지 않음과 같은 내용은 해당 패키지를 참고하면 좋을 것 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1659851651327&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install next-sitemap&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1659851827444&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package.json

&quot;scripts&quot;: {
    &quot;postbuild&quot;: &quot;next-sitemap&quot;, // 추가
  },&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1659851670474&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;next-sitemap.config.js

/** @type {import('next-sitemap').IConfig} */

module.exports = {
  siteUrl: 'https://okdohyuk.dev',
  changefreq: 'daily',
  generateRobotsTxt: true,
  robotsTxtOptions: {
    policies: [
      {
        userAgent: '*',
        allow: '/',
      },
    ],
  },
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 /robots.txt 와 /sitemap.xml을 구글 서치 콘솔에 업로드하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;analytics&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 같은 구글이니까 애널리틱스를 추가하면 도움이 되지 않을까 싶기도 하였고, 그냥 달아두고 나중에 지표라도 보고 싶어서 추가하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 nextjs에서 analytics를 추가하는 코드이다. 클라쪽에서만 실행되어야 하는 것과, nextjs에서 페이지가 이동되었을 때 그것을 인식할 수 있게 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1659852429286&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;lib/gtag.ts
export const GA_TRACKING_ID = 'analytics 개인 ID';

// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
export const pageview = (url: URL) =&amp;gt; {
  window.gtag('config', GA_TRACKING_ID, {
    page_path: url,
  });
};

type GTagEvent = {
  action: string;
  category: string;
  label: string;
  value: number;
};

// https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const event = ({ action, category, label, value }: GTagEvent) =&amp;gt; {
  window.gtag('event', action, {
    event_category: category,
    event_label: label,
    value: value,
  });
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1659852511305&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;_document.tsx

&amp;lt;Head&amp;gt;
	&amp;lt;Script
          src={'https://www.googletagmanager.com/gtag/js?id=' + GA_TRACKING_ID}
          strategy=&quot;afterInteractive&quot;
        /&amp;gt;
        &amp;lt;Script id=&quot;google-analytics&quot; strategy=&quot;afterInteractive&quot;&amp;gt;
          {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){window.dataLayer.push(arguments);}
          gtag('js', new Date());

          gtag('config', '${GA_TRACKING_ID}', {page_path: window.location.pathname});
        `}
        &amp;lt;/Script&amp;gt;
&amp;lt;/Head&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1659852565316&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; _app.tsx
 
 
  const router = useRouter();

  useEffect(() =&amp;gt; {
    const handleRouteChange = (url: URL) =&amp;gt; {
      gtag.pageview(url);
    };
    router.events.on('routeChangeComplete', handleRouteChange);
    return () =&amp;gt; {
      router.events.off('routeChangeComplete', handleRouteChange);
    };
  }, [router.events]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 &quot;okdohyuk&quot; 키워드로 okdohyuk.dev 사이트가 뜨긴 하였다. 하지만, 순위는 아래쪽에 있었고, 깃헙이라는 큰 산이 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2022-08-07 at 15.16.16@2x.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;214&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KeneC/btrI2dXxcBX/ciiCypbTVVGHoW7Ck5yhT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KeneC/btrI2dXxcBX/ciiCypbTVVGHoW7Ck5yhT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KeneC/btrI2dXxcBX/ciiCypbTVVGHoW7Ck5yhT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKeneC%2FbtrI2dXxcBX%2FciiCypbTVVGHoW7Ck5yhT1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1326&quot; height=&quot;214&quot; data-filename=&quot;CleanShot 2022-08-07 at 15.16.16@2x.png&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;214&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;openGraph&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적어도 카톡에 링크를 공유했을 때 링크 한 줄 나오는 것보다 이미지와 설명이 있으면 멋질 것 같고, 결국 SEO에서 인식률을 높일 수 있겠다 싶어 빠르게 작업했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 작업에서는 딱히 다른 정보들은 찾아보지 않았고, openGraph에 관련된 meta 태그만 검색하였다.&lt;/p&gt;
&lt;figure id=&quot;og_1659853446783&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;OpenGraph - Preview Social Media Share and Generate Metatags&quot; data-og-description=&quot;OpenGraph is the easiest way to preview and generate open graph meta tags for any website.&quot; data-og-host=&quot;www.opengraph.xyz&quot; data-og-source-url=&quot;https://www.opengraph.xyz/&quot; data-og-url=&quot;https://www.opengraph.xyz&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cAkgmy/hyPkBDLg2Y/Q5aqljtSbO6oPx6pzdfHP0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.opengraph.xyz/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.opengraph.xyz/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cAkgmy/hyPkBDLg2Y/Q5aqljtSbO6oPx6pzdfHP0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;OpenGraph - Preview Social Media Share and Generate Metatags&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;OpenGraph is the easiest way to preview and generate open graph meta tags for any website.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.opengraph.xyz&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발하면서 3000포트를 ngrok에 띄어 위 오픈그래프를 테스트할 수 있는 사이트에 테스팅을 하면서 개발했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2022-08-07 at 15.26.27@2x.png&quot; data-origin-width=&quot;2052&quot; data-origin-height=&quot;1972&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/81Bpi/btrI2wPY1Iw/GB0oLITQRERSEh6x1OM8Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/81Bpi/btrI2wPY1Iw/GB0oLITQRERSEh6x1OM8Kk/img.png&quot; data-alt=&quot;opengraph preview&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/81Bpi/btrI2wPY1Iw/GB0oLITQRERSEh6x1OM8Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F81Bpi%2FbtrI2wPY1Iw%2FGB0oLITQRERSEh6x1OM8Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;478&quot; height=&quot;459&quot; data-filename=&quot;CleanShot 2022-08-07 at 15.26.27@2x.png&quot; data-origin-width=&quot;2052&quot; data-origin-height=&quot;1972&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;opengraph preview&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단히 페이지별로 제목과 설명을 추가할 수 있게 하였고, 아직 썸네일 이미지를 바꿀만한 페이지는 없어서 그 부분은 통일시켰다.&lt;/p&gt;
&lt;pre id=&quot;code_1659853264477&quot; class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;opengraph.tsx

import React from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';

type Opengraph = {
  isMainPage?: boolean;
  title: string;
  ogTitle: string;
  description: string;
};

function Opengraph({ title, ogTitle, description, isMainPage = false }: Opengraph) {
  const router = useRouter();
  const withMe = ' with okdohyuk';
  const URL = 'https://okdohyuk.dev';

  return (
    &amp;lt;Head&amp;gt;
      &amp;lt;title&amp;gt;{isMainPage ? title : title + withMe}&amp;lt;/title&amp;gt;
      &amp;lt;meta name=&quot;description&quot; content={isMainPage ? description : description + withMe} /&amp;gt;

      &amp;lt;meta property=&quot;og:url&quot; content={URL + router.asPath} /&amp;gt;
      &amp;lt;meta property=&quot;og:type&quot; content=&quot;website&quot; /&amp;gt;
      &amp;lt;meta property=&quot;og:title&quot; content={isMainPage ? ogTitle : ogTitle + withMe} /&amp;gt;
      &amp;lt;meta property=&quot;og:description&quot; content={isMainPage ? description : description + withMe} /&amp;gt;
      &amp;lt;meta property=&quot;og:image&quot; content=&quot;/opengraph_image.png&quot; /&amp;gt;
      {...}
    &amp;lt;/Head&amp;gt;
  );
}

export default Opengraph;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2022-08-07 at 15.33.28@2x.png&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;992&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tlElm/btrI7LynDuA/ObYbY9WdCtVMNcdyfLfXF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tlElm/btrI7LynDuA/ObYbY9WdCtVMNcdyfLfXF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tlElm/btrI7LynDuA/ObYbY9WdCtVMNcdyfLfXF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtlElm%2FbtrI7LynDuA%2FObYbY9WdCtVMNcdyfLfXF1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1990&quot; height=&quot;992&quot; data-filename=&quot;CleanShot 2022-08-07 at 15.33.28@2x.png&quot; data-origin-width=&quot;1990&quot; data-origin-height=&quot;992&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈그래프를 설정 후 주기적으로 해당 키워드를 검색해보며 변화하는 검색 결과를 느낄 수 있었다. 그때마다 사진을 찍어두지 않은 것이 아쉽지만, 처음엔 제목으로 설정해둔 developer okdohyuk가 떴지만, 점점 제목이 바뀌고, 썸네일이 추가되고, 깃허브를 넘어스며 1착을 차지하였다. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 아쉬운 점이 있다면, 내 이름 &quot;유도혁&quot;을 적어 둔 곳이 없어서 그런지 한글로 검색하면, 나오질 않는다. 그렇기 때문에 다음 목표는 이름으로 검색하였을 때 상위 노출시키는 것이고, 다른 서비스를 추가하며 발전시킬 생각이다.&lt;/p&gt;</description>
      <category>프론트엔드/React | Next</category>
      <category>Analytics</category>
      <category>Google</category>
      <category>NextJS</category>
      <category>opengraph</category>
      <category>Robots</category>
      <category>search</category>
      <category>seo</category>
      <category>sitemap</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/148</guid>
      <comments>https://okdohyuk.tistory.com/148#entry148comment</comments>
      <pubDate>Sun, 7 Aug 2022 15:40:04 +0900</pubDate>
    </item>
    <item>
      <title>개인프로젝트에 대한 생각</title>
      <link>https://okdohyuk.tistory.com/147</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 연초에 올해의 목표라던가 계획을 세웠었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2022-07-09 at 19.26.27@2x.png&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cllp6/btrGPDYtIvU/v6cLxN3Din7naANLlssvY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cllp6/btrGPDYtIvU/v6cLxN3Din7naANLlssvY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cllp6/btrGPDYtIvU/v6cLxN3Din7naANLlssvY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCllp6%2FbtrGPDYtIvU%2Fv6cLxN3Din7naANLlssvY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1430&quot; height=&quot;578&quot; data-filename=&quot;CleanShot 2022-07-09 at 19.26.27@2x.png&quot; data-origin-width=&quot;1430&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 대강 몇년 후 까지도 적어보긴 했는데, 대부분 개발자로서의 성장에 관련된 내용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;개인 프로젝트를 만들어서 서비스하고, 거기서 수익을 낸다&quot;, &quot;해커톤에 참가한다&quot;, &quot;자격증을 딴다&quot;, &quot;오프라인 컨퍼런스에 참가한다&quot; 등등&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그중에 역시 가장 비중이 큰 것은 개인 프로젝트라고 생각한다. 다른 것들은 집단지성으로 문제를 해결하거나 확실하게 해야 하는 것이 정해져 있지만, 이것을 그렇지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획을 하기 힘들어서 클론코딩을 하는 사람들도 있는 것 같다. 하지만 나는 정말 나의 서비스를 만들어보고 싶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 기획을 해보았지만 쉽지 않았다. 생각한 아이디어 자체는 여러 개지만, 개발까지 가기가 어려웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애초에 디자인을 해본 경험도 없어서 저번에 피그마로 디자인해보려다가 망했다. 심지어 3시간 동안 했던가...&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2022-07-09 at 19.36.18@2x.png&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAfsnn/btrGQpMIej3/2QonLXGJHucCXoV5q5vnGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAfsnn/btrGQpMIej3/2QonLXGJHucCXoV5q5vnGk/img.png&quot; data-alt=&quot;망한 디자인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAfsnn/btrGQpMIej3/2QonLXGJHucCXoV5q5vnGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAfsnn%2FbtrGQpMIej3%2F2QonLXGJHucCXoV5q5vnGk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;613&quot; height=&quot;502&quot; data-filename=&quot;CleanShot 2022-07-09 at 19.36.18@2x.png&quot; data-origin-width=&quot;2400&quot; data-origin-height=&quot;1964&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;망한 디자인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각을 바꾸기로 했다. 지금까지는 &quot;어떤 서비스를 만들지?&quot; 부터 시작했다면, 앞으로는 &quot;일단 만들자!&quot;로 바꾸기로 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 구글I/O 온라인 컨퍼런스에 참가했더니, .dev 도메인을 1년 치 무료로 받게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술 스택은 &lt;b&gt;Nextjs typescript mobx tailwindCSS&amp;nbsp;&lt;/b&gt;로 정하고 &lt;b&gt;vercel로&lt;/b&gt; CI/CD 배포를 단번에 해결하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발, 배포, 도메인까지 단 1원도 들지 않았으니 이 얼마나 좋은 세상인가 ㅋㅋ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 개발을 못해도 Lighthouse점수를 든든히 챙길 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2022-07-09 at 19.39.44@2x.png&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lnRvg/btrGTZrGTno/Jdvl80bZ7pjacDRffCkUik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lnRvg/btrGTZrGTno/Jdvl80bZ7pjacDRffCkUik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lnRvg/btrGTZrGTno/Jdvl80bZ7pjacDRffCkUik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlnRvg%2FbtrGTZrGTno%2FJdvl80bZ7pjacDRffCkUik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1892&quot; height=&quot;368&quot; data-filename=&quot;CleanShot 2022-07-09 at 19.39.44@2x.png&quot; data-origin-width=&quot;1892&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1657362225521&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;퍼센트 계산기 with okdohyuk&quot; data-og-description=&quot;퍼센트 계산기를 이용해보세요 with okdohyuk&quot; data-og-host=&quot;okdohyuk.dev&quot; data-og-source-url=&quot;https://okdohyuk.dev/percent&quot; data-og-url=&quot;https://okdohyuk.dev/percent&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/h29rD/hyO18Oz1vV/2KlaY4YnHfsfX6ODlVo9YK/img.png?width=2456&amp;amp;height=1411&amp;amp;face=0_0_2456_1411&quot;&gt;&lt;a href=&quot;https://okdohyuk.dev/percent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://okdohyuk.dev/percent&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/h29rD/hyO18Oz1vV/2KlaY4YnHfsfX6ODlVo9YK/img.png?width=2456&amp;amp;height=1411&amp;amp;face=0_0_2456_1411');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;퍼센트 계산기 with okdohyuk&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;퍼센트 계산기를 이용해보세요 with okdohyuk&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;okdohyuk.dev&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이것이 오늘 만든 서비스이다. 최근 가장 많이 사용했던 것이 퍼센트 계산기였다. 비록 기능은 어느 사이트를 따라한 것이지만, 내가 만든 것이 더 이쁘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로도 이 도메인을 키워나가면서 개인 프로젝트를 기획하는 시간을 줄일 것이고, 블로그 이전, 분석 툴이나 광고도 붙여볼 생각이다.&lt;/p&gt;</description>
      <category>T자형 개발</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/147</guid>
      <comments>https://okdohyuk.tistory.com/147#entry147comment</comments>
      <pubDate>Sat, 9 Jul 2022 19:51:54 +0900</pubDate>
    </item>
    <item>
      <title>PWA(Progressive Web Apps) in NEXT JS</title>
      <link>https://okdohyuk.tistory.com/146</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  사용&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;install&lt;/p&gt;
&lt;pre id=&quot;code_1656778178750&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install next-pwa&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next js로 만들어진 프로젝트가 있다는 가정하에 설치해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next.config.js&lt;/p&gt;
&lt;pre id=&quot;code_1656778274351&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const withPWA = require('next-pwa');

const nextConfig = {
  pwa: {
    dest: 'public',
  },
  ...
};

module.exports = withPWA(nextConfig);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 파일에 pwa에 관한 설정을 추가해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;manifest.json&lt;/p&gt;
&lt;figure id=&quot;og_1656778492339&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;PWA Manifest Generator | SimiCart&quot; data-og-description=&quot;PWA Manifest Generator Automatically generate a fully functional web app manifest along with size-optimized icons for your PWA Web app manifest &amp;nbsp; (i.e. manifest.json) is a JSON file that provides the necessary metadata for your Progressive Web App. With a&quot; data-og-host=&quot;www.simicart.com&quot; data-og-source-url=&quot;https://www.simicart.com/manifest-generator.html/&quot; data-og-url=&quot;https://www.simicart.com/manifest-generator.html/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.simicart.com/manifest-generator.html/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.simicart.com/manifest-generator.html/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;PWA Manifest Generator | SimiCart&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;PWA Manifest Generator Automatically generate a fully functional web app manifest along with size-optimized icons for your PWA Web app manifest &amp;nbsp; (i.e. manifest.json) is a JSON file that provides the necessary metadata for your Progressive Web App. With a&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.simicart.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1656779544422&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Favicon &amp;amp; App Icon Generator&quot; data-og-description=&quot;Upload an image (PNG to ICO, JPG to ICO, GIF to ICO) and convert it to a Windows favicon (.ico) and App Icons. Learn more about favicons.&quot; data-og-host=&quot;www.favicon-generator.org&quot; data-og-source-url=&quot;https://www.favicon-generator.org/&quot; data-og-url=&quot;https://www.favicon-generator.org/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.favicon-generator.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.favicon-generator.org/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Favicon &amp;amp; App Icon Generator&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Upload an image (PNG to ICO, JPG to ICO, GIF to ICO) and convert it to a Windows favicon (.ico) and App Icons. Learn more about favicons.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.favicon-generator.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2022-07-03 at 03.50.54@2x.png&quot; data-origin-width=&quot;2488&quot; data-origin-height=&quot;2590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceGY2O/btrGg2xG79A/9FVJ1Twa2htI9XROZ881Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceGY2O/btrGg2xG79A/9FVJ1Twa2htI9XROZ881Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceGY2O/btrGg2xG79A/9FVJ1Twa2htI9XROZ881Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceGY2O%2FbtrGg2xG79A%2F9FVJ1Twa2htI9XROZ881Wk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2488&quot; height=&quot;2590&quot; data-filename=&quot;CleanShot 2022-07-03 at 03.50.54@2x.png&quot; data-origin-width=&quot;2488&quot; data-origin-height=&quot;2590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 설치 시 보이게 될 아이콘과 제목 등을 표시할 정의 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next head&lt;/p&gt;
&lt;pre id=&quot;code_1656787578732&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;Head&amp;gt;
	&amp;lt;meta
    	name=&quot;viewport&quot;
        content=&quot;width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no&quot;
    /&amp;gt;
    &amp;lt;meta name=&quot;description&quot; content=&quot;Description&quot; /&amp;gt;
    &amp;lt;meta name=&quot;keywords&quot; content=&quot;Keywords&quot; /&amp;gt;
    &amp;lt;link rel=&quot;manifest&quot; href=&quot;/manifest.json&quot; /&amp;gt;
    &amp;lt;meta name=&quot;theme-color&quot; content=&quot;#317EFB&quot; /&amp;gt;
&amp;lt;/Head&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 만들어둔 manifest 파일을 불러와줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;CleanShot 2022-07-03 at 03.55.41@2x.png&quot; data-origin-width=&quot;188&quot; data-origin-height=&quot;62&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sAcjV/btrGg3QYALp/gYbe3gODhnlc8zRlQkORZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sAcjV/btrGg3QYALp/gYbe3gODhnlc8zRlQkORZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sAcjV/btrGg3QYALp/gYbe3gODhnlc8zRlQkORZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsAcjV%2FbtrGg3QYALp%2FgYbe3gODhnlc8zRlQkORZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;188&quot; height=&quot;62&quot; data-filename=&quot;CleanShot 2022-07-03 at 03.55.41@2x.png&quot; data-origin-width=&quot;188&quot; data-origin-height=&quot;62&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롬에서 공유하기 왼쪽에 새로운 아이콘이 생기면 성공입니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;  생각&lt;/b&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근엔 브라우저에서 마이크, 카메라, 파일, 블루투스 등등 이 접근 가능한 상황에서 웹을 앱으로 설치하여 사용한다는 것은 앱이 없는 서비스를 자주 이용하는 상황에 적합할 것 같다는 생각이 든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 구글과 애플의 인앱 결제 강제가 요구되는 상황에서 유리한 건가 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 웹이긴 웹이라서 캐싱 이외에 미리 데이터를 설치해둔 상태에서 사용하지는 못하니 단점도 분명한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;딱 앱 서비스가 없으면서 MAU가 높은 서비스에서 사용하면 큰 이익이 될 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://github.com/vercel/next.js/tree/canary/examples/progressive-web-app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/vercel/next.js/tree/canary/examples/progressive-web-app&lt;/a&gt;&lt;/p&gt;</description>
      <category>프론트엔드</category>
      <category>NextJS</category>
      <category>pwa</category>
      <category>설치</category>
      <category>앱</category>
      <category>웹에서</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/146</guid>
      <comments>https://okdohyuk.tistory.com/146#entry146comment</comments>
      <pubDate>Sun, 3 Jul 2022 04:05:25 +0900</pubDate>
    </item>
    <item>
      <title>블로그 다크모드 적용기</title>
      <link>https://okdohyuk.tistory.com/144</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;✍️ 개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 사용하는 휴대폰, 컴퓨터, 각종 프로그램 등 다크 모드를 지원하는 서비스에서는 모두 다크 모드를 사용 중이다. 처음 사용할 당시만 해도 멋져서 사용했다고 한다면, 지금은 라이트 모드가 익숙하지 않은 수준까지 왔다. 그런데 내가 운영하는 블로그가 다크 모드가 없다는 것을 생각해보니 화가 났다. 그래서 예전부터 다크 모드를 적용하려는 생각이 있었지만, 쉽게 시작하지 못하였고, 티스토리의 다른 테마들 중 다크 모드가 적용되어 있는 것들을 둘러보았는데, 남들이 만든 것 중에서는 맘에 드는 것이 없었다. 지금까지 티스토리의 &lt;b&gt;Odyssey&lt;/b&gt; 스킨을 사용중이었는데 잡스러운 것이 없고 깔끔해서 사용 중이었다. 그래서 어느 순간 필이와서 작업을 시작하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  고민&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다고 바로 작업을 시작한 것은 아니고, 가장 쉽게 할 수 있는 방법에 대해 찾아보았다. 그깟 css 요즘은 html to react component 이런 기술도 있는데 대충 &lt;b&gt;css dark theme generator &lt;/b&gt;라고 검색하면 쉽게 나올 줄 알았다.&lt;/p&gt;
&lt;figure id=&quot;og_1653764929678&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Dark CSS Theme Generator | Night Eye&quot; data-og-description=&quot;Dark CSS theme for your website Dark themes and dark mode in general have been gaining popularity in the past couple of years. We've reached a point where dark UI is not only for the geeks, but actually is a mainstream standard expected by the user. Arguab&quot; data-og-host=&quot;nighteye.app&quot; data-og-source-url=&quot;https://nighteye.app/dark-css-generator/&quot; data-og-url=&quot;https://nighteye.app/dark-css-generator/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bwam8t/hyOzR6C5Uv/0l269jkO2xkiFkT2MmNg90/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/bzdnF8/hyOyBRUsJD/kqRaNo52UR352uWmoa3IcK/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628&quot;&gt;&lt;a href=&quot;https://nighteye.app/dark-css-generator/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://nighteye.app/dark-css-generator/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bwam8t/hyOzR6C5Uv/0l269jkO2xkiFkT2MmNg90/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628,https://scrap.kakaocdn.net/dn/bzdnF8/hyOyBRUsJD/kqRaNo52UR352uWmoa3IcK/img.jpg?width=1200&amp;amp;height=628&amp;amp;face=0_0_1200_628');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Dark CSS Theme Generator | Night Eye&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Dark CSS theme for your website Dark themes and dark mode in general have been gaining popularity in the past couple of years. We've reached a point where dark UI is not only for the geeks, but actually is a mainstream standard expected by the user. Arguab&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;nighteye.app&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 찾은 것이 이 사이트인데 생각과는 다르게 맘대로 되지 않았고, !importent로 css를 강제로 씌우는 방식이라 매우 별로였다. 결국 css 컬러 시스템을 만드는 방법이 지금으로도, 나중으로도 도움이 될 것 같아서 시작하였다. &lt;s&gt;노가다를&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹스톰에서 rgb로 검색하는 것들을 모두 찾고, #도 검색해서 HEX값인 녀석들을 모두 정리했다. 거의 3시간가량의 중노동으로 마침내 모든 색상값들을 var()로 바꾸었다. 네이밍이 매우 별로이지만, 일단 어느 정도 볼 수준은 되어서 스탑 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXFzzY/btrDoVmM8Td/fbixuk6EYV1Zgfa9nEE311/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXFzzY/btrDoVmM8Td/fbixuk6EYV1Zgfa9nEE311/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1880&quot; data-filename=&quot;CleanShot 2022-05-29 at 14.54.45@2x.png&quot; style=&quot;width: 51.6044%; margin-right: 10px;&quot; data-widthpercent=&quot;52.21&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXFzzY/btrDoVmM8Td/fbixuk6EYV1Zgfa9nEE311/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXFzzY%2FbtrDoVmM8Td%2Ffbixuk6EYV1Zgfa9nEE311%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1992&quot; height=&quot;1880&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biizFA/btrDrPsAwg4/FC5PXa7rqbzkBUbd1M1Ts0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biizFA/btrDrPsAwg4/FC5PXa7rqbzkBUbd1M1Ts0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;2054&quot; data-filename=&quot;CleanShot 2022-05-29 at 14.55.15@2x.png&quot; style=&quot;width: 47.2328%;&quot; data-widthpercent=&quot;47.79&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biizFA/btrDrPsAwg4/FC5PXa7rqbzkBUbd1M1Ts0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiizFA%2FbtrDrPsAwg4%2FFC5PXa7rqbzkBUbd1M1Ts0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1992&quot; height=&quot;2054&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;style.css&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/80P4F/btrDoUg7n4f/IoSz3wcHhjxnVQ9uj4bnRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/80P4F/btrDoUg7n4f/IoSz3wcHhjxnVQ9uj4bnRk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;2103&quot; data-filename=&quot;CleanShot 2022-05-29 at 14.56.28@2x.png&quot; style=&quot;width: 47.717%; margin-right: 10px;&quot; data-widthpercent=&quot;48.28&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/80P4F/btrDoUg7n4f/IoSz3wcHhjxnVQ9uj4bnRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F80P4F%2FbtrDoUg7n4f%2FIoSz3wcHhjxnVQ9uj4bnRk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1992&quot; height=&quot;2103&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qyjEc/btrDrPsABl2/MY35wfSXITHOeLCaXEkaR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qyjEc/btrDrPsABl2/MY35wfSXITHOeLCaXEkaR1/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1992&quot; data-origin-height=&quot;1963&quot; data-filename=&quot;CleanShot 2022-05-29 at 14.56.17@2x.png&quot; style=&quot;width: 51.1202%;&quot; data-widthpercent=&quot;51.72&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qyjEc/btrDrPsABl2/MY35wfSXITHOeLCaXEkaR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqyjEc%2FbtrDrPsABl2%2FMY35wfSXITHOeLCaXEkaR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1992&quot; height=&quot;1963&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;markdown.css&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 이름은 안 겹치게 만들어는 두었으니 나중에 색의 수를 줄이거나 색깔놀이를 할 수 있을 수준이긴 하다. 과연 업데이트를 할 날이 올진 모르겠지만, 일단 현재는 이것으로 만족하는 것으로 마무리지었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 몇몇 글에 텍스트 색상을 #000으로 설정하는 바람에 보이는 선에서 기본 색으로 변경하여 문제를 해결하였다. 이런 문제들 때문이라도 .mdx로 작성하는 깃헙 블로그가 개발자 친화적이라는 점을 더욱 실감하였다. 하지만, 그만큼의 인사이트를 갖는 글을 작성할 수 없는 나는 티스토리를 주류로 계속 운영할 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>일상</category>
      <category>다크모드</category>
      <category>티스토리</category>
      <author>유도혁</author>
      <guid isPermaLink="true">https://okdohyuk.tistory.com/144</guid>
      <comments>https://okdohyuk.tistory.com/144#entry144comment</comments>
      <pubDate>Sun, 29 May 2022 04:13:15 +0900</pubDate>
    </item>
  </channel>
</rss>