<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>디지털 노마드를 꿈꾸며</title>
    <link>https://9ummy.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 4 Jul 2026 18:42:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>구미구미</managingEditor>
    <image>
      <title>디지털 노마드를 꿈꾸며</title>
      <url>https://tistory1.daumcdn.net/tistory/4986165/attach/d1659a40840d441d9410a7b3e8beec60</url>
      <link>https://9ummy.tistory.com</link>
    </image>
    <item>
      <title>넘블 딥다이브 &amp;lt;흔들리지 않는 단단한 프론트 개발하기 - 패션 이커머스 편&amp;gt; 회고록</title>
      <link>https://9ummy.tistory.com/29</link>
      <description>&lt;figure id=&quot;og_1694948550746&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;www.numble.it&quot; data-og-source-url=&quot;https://www.numble.it/deepdive/7b3932dc-9caa-4c77-a2b5-8ba8dabbd136&quot; data-og-url=&quot;https://www.numble.it/deepdive/7b3932dc-9caa-4c77-a2b5-8ba8dabbd136&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/uQeB4/hyTVOLyPay/69c0XwkhP49ZHMsTGqY7L1/img.png?width=2561&amp;amp;height=1720&amp;amp;face=0_0_2561_1720,https://scrap.kakaocdn.net/dn/cGqzEb/hyTYZq9Q3n/hKi2QjS7oQt08PZqrmNQfk/img.png?width=2561&amp;amp;height=1720&amp;amp;face=0_0_2561_1720,https://scrap.kakaocdn.net/dn/b4Vg4e/hyTV2bY0Eq/qaB3PLCY6Un0GKif5j4Dt0/img.png?width=2000&amp;amp;height=6507&amp;amp;face=0_0_2000_6507&quot;&gt;&lt;a href=&quot;https://www.numble.it/deepdive/7b3932dc-9caa-4c77-a2b5-8ba8dabbd136&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.numble.it/deepdive/7b3932dc-9caa-4c77-a2b5-8ba8dabbd136&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/uQeB4/hyTVOLyPay/69c0XwkhP49ZHMsTGqY7L1/img.png?width=2561&amp;amp;height=1720&amp;amp;face=0_0_2561_1720,https://scrap.kakaocdn.net/dn/cGqzEb/hyTYZq9Q3n/hKi2QjS7oQt08PZqrmNQfk/img.png?width=2561&amp;amp;height=1720&amp;amp;face=0_0_2561_1720,https://scrap.kakaocdn.net/dn/b4Vg4e/hyTV2bY0Eq/qaB3PLCY6Un0GKif5j4Dt0/img.png?width=2000&amp;amp;height=6507&amp;amp;face=0_0_2000_6507');&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;이제는 진짜 기본기에 충실한 개발자로 거듭날 때&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.numble.it&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&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;h4 data-ke-size=&quot;size20&quot;&gt;아쉬운 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 부실한 요구사항 기획&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 소개 페이지를 보고 예상했던 것과 다른 구현 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지네이션, 무한스크롤 등의 구현 방식이 언급되어 있어서 API를 통한 비동기 통신과 복잡한 상태 관리를 경험해볼 수 있겠구나 기대하고 신청했는데 API가 제공되지 않는다고 해서 좀 당황했다. 나만 당황한 건 아니었는지 챌린지 오픈 첫 날 API 관련 문의로 관계자분들이 공지를 올려주셨고 호스트님도 이 부분에 대해 추가적인 의견을 내주셨는데 진행 이전에 호스트님의 기획 의도와 챌린지 진행에 대한 구체적인 방식을 명확히 공유받을 수 있었다면 더 좋았을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3. 기획 및 디자인을 직접 해야 함&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;size18&quot;&gt;4. 관리가 부실(?)하다는 느낌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 개인으로 참가해서 더 그런 건지는 모르겠는데 제출 방식이나 마감일 등에 대한 공지가 좀 부족하다는 생각이 들었다. 디스코드 방이 있긴 한데 챌린지를 진행하는 3주 내내 한 두 번의 Q&amp;amp;A를 제외하고는 그 어떤 공지사항도 올라오지 않았고... 넘블에서 런칭하는 다른 서비스 홍보만 올라오길래 딥다이브는 그냥 고객 풀 넓히는 용으로 진행하는 거고 크게 관심이 없는 걸까...? 하는 생각이 들었다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;진행 사항&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여튼 ... 그럼에도 진행한 것들을 살짝 공유해보겠다. 비동기 통신이 필요하면 이번에야말로 react-query를 써보고 공부해야겠다 생각했는데 아니어서 ... 이번에도 그냥 익숙한 Recoil을 사용했고 app 디렉토리 사용해보려고 Next.js에 TypeScript 사용해서 구현했다.&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;Screenshot 2023-09-17 at 7.59.22 PM.png&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;1082&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZ6V0C/btsugTcXpIG/qrPGHL1KoKtUMsiYvsykv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZ6V0C/btsugTcXpIG/qrPGHL1KoKtUMsiYvsykv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZ6V0C/btsugTcXpIG/qrPGHL1KoKtUMsiYvsykv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZ6V0C%2FbtsugTcXpIG%2FqrPGHL1KoKtUMsiYvsykv0%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;675&quot; height=&quot;483&quot; data-filename=&quot;Screenshot 2023-09-17 at 7.59.22 PM.png&quot; data-origin-width=&quot;1512&quot; data-origin-height=&quot;1082&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-09-17 at 8.00.52 PM.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;1086&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPBy7U/btst7xP6lrx/x4ueD9RLlZIJOajaFgH2MK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPBy7U/btst7xP6lrx/x4ueD9RLlZIJOajaFgH2MK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPBy7U/btst7xP6lrx/x4ueD9RLlZIJOajaFgH2MK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPBy7U%2Fbtst7xP6lrx%2Fx4ueD9RLlZIJOajaFgH2MK%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;648&quot; height=&quot;465&quot; data-filename=&quot;Screenshot 2023-09-17 at 8.00.52 PM.png&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;1086&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;이런 느낌의 페이지들을 기획 + 디자인 + 구현했다. 사실 유저 페이지는 진짜... 왜... 만들어야 하는 건지 사실 잘 모르겠다 (제공 받은 유저 정보가 없어서...) 그래서 디자인도 하다 말았고 구현도... 얼레벌레 해서 제출할 것 같다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>웹 개발</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/29</guid>
      <comments>https://9ummy.tistory.com/29#entry29comment</comments>
      <pubDate>Sun, 17 Sep 2023 20:07:49 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 서버 사이드 자바스크립트에 필요한 것</title>
      <link>https://9ummy.tistory.com/28</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;본 포스팅은 &lt;a href=&quot;https://www.blueskyonmars.com/2009/01/29/what-server-side-javascript-needs/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;What Server Side JavaScript needs&lt;/a&gt; 글을 번역한 것입니다.&lt;br /&gt;오역, 잘못된 내용이 있을 경우 댓글로 알려주시면 감사하겠습니다  &lt;/blockquote&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;background-color: #e6f5ff; color: #0070d1; text-align: start;&quot; href=&quot;https://en.wikipedia.org/wiki/JavaScript#Server-side_JavaScript&quot;&gt;서버 사이드 자바스크립트&lt;/a&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기술은 &lt;/span&gt;오랫동안 존재해 왔습니다. 1996년, Netscape는 서버 소프트웨어에서 서버 사이드 자바스크립트를 제공했으며, Helma도 상당한 기간동안 존재해 왔습니다.&lt;b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;[1]&lt;/span&gt;&lt;/b&gt; 그러나 지난 몇 년 동안 서버 사이드 개발은 크게 변화했습니다.&lt;br /&gt;&lt;br /&gt;Aptana의&amp;nbsp;&lt;a href=&quot;https://www.axway.com/en/aptana&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Jaxer&lt;/a&gt;는 클라이언트와 서버 양쪽에서 실행되는 자바스크립트 환경이라는 혁신적인 관점을 제시합니다. 매우 편리한 커뮤니케이션과 클라이언트와 서버 사이에서 편하게 코드를 공유할 수 있는 점은 서버에서 자바스크립트를 실행하는 것의 큰 이점입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;Jaxer와 Helma는 흥미로운 프로젝트입니다. 하지만 제가 생각하는 서버 사이드 자바스크립트에 부족한 것은 흥미로운 프로젝트가 아닌 유용한 생태계입니다. 파이썬으로 작업하기 좋아하는 사람들은 웹 프레임워크의 단편화(fragmentation) 등을 이야기하기도 하지만, 이것은 자바스크립트의 단편화에 비하면 아무것도 아닙니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;예를 들어, 자바스크립트는 &lt;b&gt;교차 인터프리터 표준 라이브러리&lt;/b&gt;가 필요합니다. 다행히 일부 표준 라이브러리가 존재합니다(브라우저로부터 상속된 부분). 따라서 정규 표현식과 날짜를 사용할 수 있습니다. 그러나 파일과 디렉토리는 어떨까요? Rhino, Spidermonkey, V8, JSCore에서 같은 API를 동작시킬 수는 없을까요?&lt;b&gt;[2]&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;일부&amp;nbsp;표준&amp;nbsp;인터페이스&lt;/b&gt;가 필요합니다. 데이터베이스에 연결하고 쿼리를 실행하는 것은 이해되기 쉽고 흔한 문제입니다. Rhino에서는 JDBC를 사용합니다. 하지만 자바스크립트는 Python의 DBAPI와 같은 교차 인터프리터 표준을 가질 필요가 있습니다. 또한 &lt;a href=&quot;http://jackjs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;웹 서버/웹 앱 인터페이스의 표준&lt;/a&gt;을 통해 Apache 모듈 뒤에서 Spidermonkey로 실행되던 웹앱을 Jetty로 옮길 수 있어야 합니다.&lt;br /&gt;&lt;br /&gt;자바스크립트에는 &lt;b&gt;다른 모듈을 포함하는 표준적인 방법&lt;/b&gt;이 필요하며, 이러한 모듈은 개별적인 네임스페이스에서 존재해야 합니다. 네임스페이스를 사용하는 쉬운 방법은 있지만, 모듈을 (한 번만!) 로드하는 표준 프로그래밍 방법은 없습니다. 서버 사이드 앱들은 많은 코드를 포함할 수 있고, 표준 인터페이스를 충족하는 부분들을 혼합할 가능성이 높기 때문에 이것은 매우 중요합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배포&amp;nbsp;및&amp;nbsp;배포용으로&amp;nbsp;코드를&amp;nbsp;패키징하고&amp;nbsp;설치하는&amp;nbsp;방법&lt;/b&gt;이 필요합니다. Linux 사용자들은 &quot;apt get&quot; (또는 yum 또는 기타)를 입력하기만 하면 작업이 끝납니다. 그러나 &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;맥과 윈도우를 사용하는 많은 사람들에게는 개발 환경을 편리하게 설정하고 작성한 코드를 패키징하고 다른 사람들이 사용할 수 있게 하는 방법이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;배포&amp;nbsp;및&amp;nbsp;설치&amp;nbsp;문제&amp;nbsp;중&amp;nbsp;일부는&amp;nbsp;&lt;b&gt;패키지&amp;nbsp;저장소&lt;/b&gt;입니다.&amp;nbsp;&lt;a href=&quot;http://openjsan.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;JSAN&lt;/a&gt;이&amp;nbsp;그&amp;nbsp;답인지는&amp;nbsp;모르겠지만,&amp;nbsp;패키지와&amp;nbsp;&lt;b&gt;해당 종속성(dependencies)&lt;/b&gt;을 쉽게 설치하는 방법이 사람들이 앱에 얼마나 많은 라이브러리를 사용할 것인지에 있어 큰 차이를 만든다는 것은 알고 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그리고 이 모든 것들 위에 템플릿 엔진, 객체 관계 매퍼, 미들웨어, 패키지화된 앱 등이 있을 것입니다. 사실, 이것들 중 많은 것들이 이미 존재합니다. 그러나 문제는 그들이 서로 공통적인 기초를 가지고 있지 않다는 것입니다. 그리고 바로 이 점이 생태계의 성장을 방해하는 지점입니다.&lt;br /&gt;&lt;br /&gt;만약 파이썬 패키지 인덱스에서 WSGI(웹 앱과 웹 서버를 연결하는 파이썬 표준)를 검색하면 오늘날 180개의 패키지가 나옵니다... 서버, 미들웨어, 완전한 애플리케이션들이 말이죠. 그리고 그것들은 단지 &quot;WSGI&quot;를 리스트에 포함시킨 패키지들뿐입니다. 이것이 생태계가 어떻게 보이는지입니다. 자바에도 있고, 루비에도 있고, 자바스크립트에도 필요한 것입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;그리고&amp;nbsp;이&amp;nbsp;모든&amp;nbsp;이점&amp;nbsp;위에&amp;nbsp;템플릿&amp;nbsp;엔진,&amp;nbsp;객체&amp;nbsp;관계&amp;nbsp;매퍼,&amp;nbsp;미들웨어,&amp;nbsp;패키지화된&amp;nbsp;앱&amp;nbsp;등이&amp;nbsp;추가될&amp;nbsp;것입니다.&amp;nbsp;실제로,&amp;nbsp;이러한&amp;nbsp;많은&amp;nbsp;기능들은&amp;nbsp;이미&amp;nbsp;존재합니다.&amp;nbsp;그러나&amp;nbsp;문제는&amp;nbsp;이러한&amp;nbsp;기능들이&amp;nbsp;공통적인&amp;nbsp;기반을&amp;nbsp;갖지&amp;nbsp;않는다는&amp;nbsp;것입니다.&amp;nbsp;이것이&amp;nbsp;생태계가&amp;nbsp;성장하는&amp;nbsp;것을&amp;nbsp;막는&amp;nbsp;요인입니다.&lt;br /&gt;&lt;br /&gt;만약 파이썬 패키지 인덱스에서 WSGI(웹 앱과 웹 서버를 연결하는 파이썬 표준)를 검색하면 180개의 패키지를 볼 수 있을 것입니다. 서버, 미들웨어, 완전한 애플리케이션들을 말이죠. 그리고 그것들은 단지 &quot;WSGI&quot;를 리스트에 포함시킨 패키지일 뿐입니다. 이것이 생태계가 돌아가는 방식입니다. 자바도 루비도 그 생태계를 가지고 있고, 자바스크립트도 이러한 생태계를 필요로 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;또한, 그 WSGI 구성 요소들 중 많은 것들은 공통 표준 라이브러리 덕분에 CPython, Jython, IronPython에서 그대로 실행될 수 있습니다. 자바스크립트는 C, 자바, .Net에서 구현된 모음이 있고, 일부 인터페이스에 대해 약간의 합의가 필요합니다. 이러한 상황에서 실행되는 라이브러리는 더 많은 사용자와, 더 많은 기여자를 모을 수 있을 것이고 희망적으로는 이러한 라이브러리들이 성장하는 데 도움이 될 것입니다.&lt;br /&gt;&lt;br /&gt;이 글에서 설명하는 것은 기술적인 문제가 아닙니다. 사람들이 모여서 앞으로 나아가고, 함께 더 크고 더 멋진 것을 만들기로 결정하는 문제입니다.&lt;br /&gt;&lt;br /&gt;이를&amp;nbsp;위해,&amp;nbsp;나는&amp;nbsp;관심&amp;nbsp;있는&amp;nbsp;사람들이&amp;nbsp;대화를&amp;nbsp;나누고,&amp;nbsp;얼굴을&amp;nbsp;맞대고&amp;nbsp;코드를&amp;nbsp;만들고&amp;nbsp;인터페이스를&amp;nbsp;결정하기&amp;nbsp;위해&amp;nbsp;새로운&amp;nbsp;&lt;a href=&quot;https://groups.google.com/g/commonjs?pli=1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ServerJS&amp;nbsp;그룹&lt;/a&gt;을 설립했습니다. 이미 많은 양의 자바스크립트 코드가 모였고, 우리가 이 모든 코드를 훨씬 더 가치 있게 만들 수 있는지 확인해봅시다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Mozilla의 웹 개발자 도구 그룹에서는 오픈 웹을 최대한 활용하는 데 도움이 되도록 하는 넓은 토대를 가지고 있습니다. 서버 사이드 자바스크립트 커뮤니티가 성장하고 번영하도록 하는 것도 이 일부가 될 수 있을 것입니다.&lt;br /&gt;&lt;br /&gt;(&quot;왜&amp;nbsp;루비/파이썬/자바/C#을&amp;nbsp;사용하지&amp;nbsp;않을까?&quot;라는&amp;nbsp;질문에&amp;nbsp;대해서는&amp;nbsp;이&amp;nbsp;글에서&amp;nbsp;다루지&amp;nbsp;않겠습니다.)&lt;br /&gt;&lt;br /&gt;업데이트:&amp;nbsp;그&amp;nbsp;그룹은&amp;nbsp;이제&amp;nbsp;&lt;a href=&quot;http://groups.google.com/group/commonjs&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CommonJS&lt;/a&gt;로&amp;nbsp;불리고&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;옮긴이 주&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;[1] Netscape는 웹 브라우저와 서버 소프트웨어를 개발한 회사로서, 초기 인터넷에서 중요한 역할을 한 기업이다. Helma는 웹 애플리케이션 프레임워크로서 서버 사이드 자바스크립트를 활용하여 웹 애플리케이션을 개발할 수 있게 해주는 플랫폼이다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;[2] Rhino는 Mozila Foundation에서 개발한 오픈소스 자바스크립트 엔진으로 자바 플랫폼에서 자바스크립트 코드를 실행하는 데 사용된다. Spidermonkey는 Mozilla Firefox 웹 브라우저의 자바스크립트 엔진으로 개발된 것으로 초기 버전의 자바스크립트 엔진 중 하나이다. V8은 Google에서 개발한 오픈소스 자바스크립트 엔진으로 Google Chrome 브라우저에서 사용되고 있다. JSCore는 Apple에서 개발한 오픈소스 자바스크립트 엔진으로 macOS와 iOS에서 사용된다. 웹 브라우저가 아닌 Apple의 운영 체제와 앱에서 자바스크립트를 실행하기 위해 사용된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@eunbinn/commonjs-is-hurting-javascript?utm_source=substack&amp;amp;utm_medium=email&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CommonJS가 자바스크립트를 해치고 있습니다&lt;/a&gt; 글을 읽으며 같이 읽어보게 된 글이다. 2009년에 쓰여진 글인 만큼 현재는 많이 개선된 부분들에 대한 이야기도 있지만 자바스크립트의 발전 과정(?)을 이해하는 데 도움이 된 것 같다. &lt;a href=&quot;https://velog.io/@eunbinn/commonjs-is-hurting-javascript?utm_source=substack&amp;amp;utm_medium=email&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CommonJS가 자바스크립트를 해치고 있습니다&lt;/a&gt; 포스팅을 같이 읽으며 CommonJS와 ESM을 이해하는 데 많은 도움을 받았다.&lt;/p&gt;</description>
      <category>웹 개발/JS &amp;middot; TS</category>
      <category>javascript</category>
      <category>개발</category>
      <category>자바스크립트</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/28</guid>
      <comments>https://9ummy.tistory.com/28#entry28comment</comments>
      <pubDate>Mon, 31 Jul 2023 22:21:16 +0900</pubDate>
    </item>
    <item>
      <title>[번역] 웹의 과거와 미래, 서버 사이드 렌더링</title>
      <link>https://9ummy.tistory.com/27</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;본 포스팅은 &lt;a href=&quot;https://deno.com/blog/the-future-and-past-is-server-side-rendering&quot;&gt;The Future (and the Past) of the Web is Server Side Rendering&lt;/a&gt; 글을 번역한 것입니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;오역, 잘못된 내용이 있을 경우 댓글로 알려주시면 감사하겠습니다  &lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&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;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #333333; text-align: left;&quot;&gt;스위스 지하실에 서버가 있던 시절에 서버가 제공할 수 있는 것은 정적인 HTML뿐이었다. 운이 좋다면 이미지를 볼 수도 있었다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR'; color: #666666; text-align: left;&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;현재는 웹 페이지가 데이터를 여러 소스로부터 가져와서 실시간으로 데이터를 가공하고 유저가 상호작용할 수 있게끔 하면서 웹 페이지가 온전한 앱이 될 수 있다. 이것은 웹의 사용성을 크게 향상시켰지만 용량, 대역폭, 속도 등에서 비용이 발생하게 되었다. 지난 10년간 데스크톱 웹 페이지 용량의 중앙값은 468KB에서 2284KB로 늘어났고, 이는 388.3% 증가한 것이다. 모바일의 경우 더 놀라운데, 145KB에서 2010KB로 1288.1% 엄청나게 증가했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 모바일에서 이러한 큰 용량은 네트워크를 통해 전송하기에 부담이 된다. 이에 따라 유저들은 긴 로딩 시간을 기다려야 하고, 모든 것이 렌더링 되기 이전까지 웹과 상호작용을 할 수 없는 등 끔찍한 UX를 경험하게 된다. 하지만 우리의 웹사이트가 우리가 원하는 대로 동작하게 하기 위해서는 모든 코드가 필요하다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘날 프론트엔드 개발자가 겪는 문제가 바로 이것이다. 모든 멋진 기능을 갖춘 웹사이트를 만드는 것은 프론트엔드 개발자들에게 재미있는 일이었지만 이제는 재미없는 일이 되어버렸다. 우리는 이제 각기 다른 브라우저를 지원하기 위해, 느린 네트워크와 불안정한 모바일 환경에서 코드를 전송하기 위해 애쓰고 있다. 이러한 모든 경우의 수를 지원하는 것은 굉장한 고통이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 어떻게 해결하면 좋을까? 서버로 돌아가는 것이 해결책이다 (스위스 지하실은 필요하지 않다).&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; 어떻게 여기까지 오게 되었는가&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 PHP가 있었고, PHP는 대단했다. 단지 여러분이 물음표를 좋아한다면 말이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹은 정적인 HTML의 네트워크로 시작했다. 하지만 Perl과 PHP 같은 CGI(Common Gateway Interface) 스크립트 언어들은 개발자들이 백엔드 데이터 소스를 HTML에 렌더링할 수 있도록 해주면서 방문자에 따라 동적으로 변하는 웹사이트의 아이디어를 불러왔다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 개발자들이 동적인 웹사이트를 구축하고 실시간 데이터나 데이터베이스의 데이터를 유저에게 제공할 수 있게 되었다는 의미이다 (물론 #, !, $, ? 키가 고장나지 않았다면 말이다).&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;pooh-meme.jpg&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m5j8b/btsiNwA32gU/56vaIwk63ELk0jKSMrFtIK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m5j8b/btsiNwA32gU/56vaIwk63ELk0jKSMrFtIK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m5j8b/btsiNwA32gU/56vaIwk63ELk0jKSMrFtIK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm5j8b%2FbtsiNwA32gU%2F56vaIwk63ELk0jKSMrFtIK%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;617&quot; height=&quot;451&quot; data-filename=&quot;pooh-meme.jpg&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP는 네트워크에서 동작했다. 서버가 네트워크에서 강력한 부분이기 때문이다. 여러분은 서버에서 여러분의 데이터를 찾아서 HTML을 렌더링한 다음 그것들을 브라우저로 전송할 수 있었다. 브라우저의 역할은 제한적이었다. 단순히 도큐먼트를 해석하고 페이지를 보여주기만 하면 됐다. 이러한 방식은 잘 동작했지만 이것은 정보를 보여주는 것이지 정보와 상호작용하는 것은 아니었다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에 두 가지 일이 발생했다. 자바스크립트가 좋아지고 브라우저가 강력해졌다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 클라이언트 사이드에서 직접적으로 많은 재미있는 일을 할 수 있게 되었다. 기본적인 HTML 페이지와 일부 JS를 브라우저에 보내고 클라이언트가 그것들을 처리하도록 할 수 있다면 왜 귀찮게 모든 것을 서버에서 렌더링해야 할까?&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 생각으로부터 싱글 페이지 애플리케이션(SPA, Single Page Application)과 클라이언트 사이드 렌더링(CSR, Client-Side Rendering)이 탄생하게 되었다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;클라이언트 사이드 렌더링&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CSR, 동적 렌더링에서 코드는 주로 클라이언트 사이드, 즉 유저의 브라우저에서 동작한다. 브라우저에서는 필요한 HTML, 자바스크립트 그리고 다른 애셋들을 다운로드한 후 UI를 렌더링하기 위해 코드를 실행한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;walmart-csr-diagram.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LKs0r/btsiOI8LBPv/NMsFOvyBkC6dPybZAZzOFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LKs0r/btsiOI8LBPv/NMsFOvyBkC6dPybZAZzOFk/img.png&quot; data-alt=&quot;https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LKs0r/btsiOI8LBPv/NMsFOvyBkC6dPybZAZzOFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLKs0r%2FbtsiOI8LBPv%2FNMsFOvyBkC6dPybZAZzOFk%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;715&quot; height=&quot;988&quot; data-filename=&quot;walmart-csr-diagram.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;988&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 접근 방식의 이점은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;뛰어난 사용자 경험&lt;/b&gt;: 만약 빠른 네트워크 환경에서 번들과 데이터를 빠르게 다운로드 받을 수 있다면, 한 번 모든 것을 다운로드 받은 이후에는 매우 빠른 웹사이트를 경험할 수 있게 된다. 추가적인 요청을 위해 서버로 돌아가지 않아도 되기 때문에 모든 페이지 변화 또는 데이터 변화는 즉각적으로 이루어지게 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;캐싱&lt;/b&gt;: 서버를 사용하지 않기 때문에 핵심적인 HTML과 JS 번들을 CDN에 캐싱할 수 있게 된다. 이럴 경우 유저는 빠르게 접속할 수 있고 회사 입장에서는 비용을 절약할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트와 브라우저 덕분에 웹에서는 더 많은 상호작용이 가능하게 되었고, 이에 따라 클라이언트 사이드 렌더링과 SPA는 기본값이 되었다. 웹은 빠르고 격렬하게 느껴졌다. 특히 데스크톱 환경에서 유명한 브라우저와 유선 네트워크를 사용할 경우에 말이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 다른 사용자들에게 웹은 걸음마 수준으로 느려졌다. 웹이 성장함에 따라 더 많은 장치와 다양한 연결 속에서 웹을 사용하는 것이 가능해졌다. 일관적인 UX를 제공하기 위해 SPA를 관리하는 것은 점점 어려워졌다. 개발자들은 웹사이트가 크롬과 IE에서 동일하게 렌더링되게 하는 것뿐만 아니라 번잡한 도시 속 버스 안의 휴대폰에서 웹사이트가 어떻게 렌더링 될지도 고려해야 했다. 여러분이 캐시로부터 JS 번들을 다운로드할 수 없는 경우, 여러분은 웹사이트를 이용할 수 없을 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 기기와 대역폭에서 &lt;i&gt;쉽게&lt;/i&gt; 일관성을 유지하는 방법은 무엇일까? 답은 바로 서버로 돌아가는 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서버 사이드 렌더링&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹사이트 렌더링을 브라우저가 아닌 서버에서 하게 되는 경우 여러 이점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;더 좋은 성능&lt;/b&gt;: HTML이 이미 생성되어 페이지가 로드될 경우 화면에 보여질 준비가 되어있기 때문이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;더 좋은 호환성&lt;/b&gt;: 서버 사이드 렌더링을 이용하면 HTML이 서버에서 생성되기 때문에 브라우저에 의존하지 않게 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;더 낮은 복잡성&lt;/b&gt;: 서버가 HTML 생성 작업의 대부분을 처리하게 되면서 더 작고 간단한 코드로 구현할 수 있게 된다.&lt;/li&gt;
&lt;/ul&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;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;walmart-ssr-diagram.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;999&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQHXzA/btsiU52TUgW/BwkgOaWgDVkFwkQCUXjRT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQHXzA/btsiU52TUgW/BwkgOaWgDVkFwkQCUXjRT0/img.png&quot; data-alt=&quot;https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQHXzA/btsiU52TUgW/BwkgOaWgDVkFwkQCUXjRT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQHXzA%2FbtsiU52TUgW%2FBwkgOaWgDVkFwkQCUXjRT0%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;726&quot; height=&quot;518&quot; data-filename=&quot;walmart-ssr-diagram.png&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;999&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 사이드 렌더링을 지원하는 다양한 동형 자바스크립트 프레임워크들이 있다. 이것들은 자바스크립트를 이용해 서버에서 HTML을 렌더링하고 이를 자바스크립트와 함께 클라이언트에 전달하여 상호작용할 수 있게 한다. 동형 자바스크립트를 사용한다는 것은 생각하기 쉬운 작은 코드베이스를 뜻한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;a title=&quot; &quot; href=&quot;https://en.wikipedia.org/wiki/Isomorphic_JavaScript&quot;&gt;동형 자바스크립트 (Isomorphic JavaScript)&lt;/a&gt;&lt;br /&gt;동형 자바스크립트, 유니버셜 자바스크립트는 클라이언트와 서버 사이드 모두에서 동작하는 애플리케이션을 의미한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJS, Remix와 같은 일부 프레임워크들은 React를 기반으로 만들어졌다. 기본적으로 React는 클라이언트 사이드 렌더링 프레임워크이지만 &lt;code&gt;renderToString&lt;/code&gt; (그리고 &lt;code&gt;renderToPipeableStream&lt;/code&gt;, &lt;code&gt;renderToReadableStream&lt;/code&gt;과 같은 다른 권장되는 메서드)을 사용하여 SSR 기능을 제공한다. NextJS와 Remix는 &lt;code&gt;renderToString&lt;/code&gt;을 높은 추상화 수준으로 제공하여 SSR 사이트를 쉽게 만들 수 있게 해준다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR에도 트레이드오프가 존재한다. 더 많이 제어하고 빠르게 전송할 수 있지만 SSR의 제한점은 상호작용이 가능한 웹사이트를 위해서는 여전히 JS를 전송해야 한다는 점이다. 전송된 JS는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Hydration_(web_development)&quot;&gt;&quot;hydration&quot;&lt;/a&gt;이라고 불리는 과정에서 정적인 HTML과 결합하게 된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hydration을 위해 JS를 전송하는 것은 몇 가지 복잡성 문제를 일으킨다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;매 요청마다 모든 JS를 보낼 것인가? 아니면 경로에 따라 다르게 할 것인가?&lt;/li&gt;
&lt;li&gt;hydration은 하향식으로 이루어지는가? 이에 따른 비용이 얼마나 드는가?&lt;/li&gt;
&lt;li&gt;개발자는 코드베이스를 어떻게 구성할 것인가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 사이드에서는 큰 번들이 메모리 문제를 일으킬 수 있고, 유저는 HTML이 존재하지만 hydrate되기 전까지는 사용하지 못하기 때문에 &quot;아무 일도 일어나지 않는다&quot;는 느낌을 받을 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deno에서 선호하는 방법 하나는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Hydration_(web_development)&quot;&gt;아일랜드 아키텍쳐로&lt;/a&gt;, 정적인 SSR HTML의 바다 속에 상호작용의 섬들을 두는 구조이다. (여러분은 아마 클라이언트에게 JavaScript를 기본적으로 전송하지 않는 우리의 모던 웹 프레임워크 Fresh가 이러한 아일랜드를 사용하는 것을 알아차렸을 것이다.)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR을 사용할 때 기대하는 것은 HTML이 빠르게 제공되고 렌더링된 후에 개별 컴포넌트가 독립적으로 제공되고 렌더링되는 것이다. 이런 방식을 통해 작은 단위의 자바스크립트를 전송하고 클라이언트에서 작은 단위의 렌더링을 처리할 수 있게 된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아일랜드는 다음과 같이 동작한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;merch-islands-example.png&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Eh7Po/btsiFMYNL80/bxCmpmdlxKidCzqn6k3kX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Eh7Po/btsiFMYNL80/bxCmpmdlxKidCzqn6k3kX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Eh7Po/btsiFMYNL80/bxCmpmdlxKidCzqn6k3kX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEh7Po%2FbtsiFMYNL80%2FbxCmpmdlxKidCzqn6k3kX0%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;631&quot; height=&quot;554&quot; data-filename=&quot;merch-islands-example.png&quot; data-origin-width=&quot;943&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아일랜드는 점진적으로 렌더링되는 것이 아니라 개별적으로 렌더링된다. 아일랜드의 렌더링은 다른 컴포넌트의 렌더링에 의존적이지 않고 다른 가상 DOM의 일부분이 업데이트 된다고 해도 개별 아일랜드의 리렌더링을 유발하지 않는다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아일랜드는 큰 단위의 hydration이라는 트레이드오프 없이도 전반적인 SSR의 장점을 지킬 수 있게 해준다. 큰 성공이라고 할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;서버에서 렌더링하는 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 서버 사이드 렌더링이 동일한 것은 아니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Deno에서 사용하는 서버에서의 렌더링 방식을 몇 가지 소개하려고 한다. Jonas Galvez이 소개한 훌륭한 &lt;a href=&quot;https://hire.jonasgalvez.com.br/2022/apr/30/a-gentle-introduction-to-ssr/&quot;&gt;서버 렌더링 개념&lt;/a&gt;을 Deno, &lt;a href=&quot;https://deno.land/x/oak&quot;&gt;Oak&lt;/a&gt;, &lt;a href=&quot;https://handlebarsjs.com/&quot;&gt;Handlebars&lt;/a&gt;로 가져와 동일한 앱 의 세 가지 변형을 보여주려고 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트 측 상호작용이 없는 간단한 템플릿을 사용한 서버 렌더링된 HTML 예제 (&lt;a href=&quot;https://github.com/denoland/deno-ssr-examples/tree/main/ssr-example-01&quot;&gt;소스&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;처음에는 서버 사이드에서 렌더링되고 이후에 클라이언트 사이드에서 업데이트 되는 예제 (&lt;a href=&quot;https://github.com/denoland/deno-ssr-examples/tree/main/ssr-example-02&quot;&gt;소스&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;동형 JS와 공유 데이터 모델을 이용한 전체가 서버 사이드에서 렌더링되는 예제 (&lt;a href=&quot;https://github.com/denoland/deno-ssr-examples/tree/main/ssr-example-03&quot;&gt;소스&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/denoland/deno-ssr-examples&quot;&gt;모든 예제의 소스는 여기서 확인할 수 있다.&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진정한 의미의 SSR은 세 번째 예제이다. 우리는 서버와 클라이언트 양쪽에서 사용되는 하나의 자바스크립트 파일을 가지고 있고, 목록에 대한 모든 업데이트는 데이터 모델을 업데이트함으로써 수행된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 템플릿 작업을 수행해보자. 첫 번째 예제에서 우리가 할 것은 리스트를 렌더링하는 것이다. 다음 &lt;code&gt;server.ts&lt;/code&gt; 내용을 보자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;import { Application, Router } from &quot;https://deno.land/x/oak@v11.1.0/mod.ts&quot;;
import { Handlebars } from &quot;https://deno.land/x/handlebars@v0.9.0/mod.ts&quot;;

const dinos = [&quot;Allosaur&quot;, &quot;T-Rex&quot;, &quot;Deno&quot;];
const handle = new Handlebars();
const router = new Router();

router.get(&quot;/&quot;, async (context) =&amp;gt; {
  context.response.body = await handle.renderView(&quot;index&quot;, { dinos: dinos });
});

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });&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;&lt;code&gt;client.html&lt;/code&gt; 파일이 필요하지 않다는 것에 주목하자. 대신 우리는 Handlebars를 이용해 다음과 같은 파일 구조를 만들 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;|--Views
|    |--Layouts
|    |     |
|    |     |--main.hbs
|    |
|    |--Partials
|    |
|    |--index.hbs&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;&lt;code&gt;main.hbs&lt;/code&gt;는 우리의 &lt;code&gt;{{{body}}}&lt;/code&gt;를 위한 placeholder를 가진 메인 HTML 레이아웃을 포함하고 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;html lang=&quot;en&quot;&amp;gt;
 &amp;lt;head&amp;gt;
   &amp;lt;meta charset=&quot;UTF-8&quot; /&amp;gt;
   &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&amp;gt;
   &amp;lt;title&amp;gt;Dinosaurs&amp;lt;/title&amp;gt;
 &amp;lt;/head&amp;gt;
 &amp;lt;body&amp;gt;
   &amp;lt;div&amp;gt;
     &amp;lt;!--content--&amp;gt;
     {{{body}}}
   &amp;lt;/div&amp;gt;
 &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&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;&lt;code&gt;{{{body}}}&lt;/code&gt;는 &lt;code&gt;index.hbs&lt;/code&gt;로부터 나온다. 이 경우 우리는 Handlebars 문법을 사용해 목록을 순회한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;handlebars&quot;&gt;&lt;code&gt;&amp;lt;ul&amp;gt;
 {{#each dinos}}
   &amp;lt;li&amp;gt;{{this}}&amp;lt;/li&amp;gt;
 {{/each}}
&amp;lt;/ul&amp;gt;&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;다음과 같은 일이 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트에서 루트가 호출된다.&lt;/li&gt;
&lt;li&gt;서버는 Handlebars 렌더러에게 공룡 목록을 전달한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;index.hbs&lt;/code&gt;에서 각 요소가 목록에 렌더링된다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main.hbs&lt;/code&gt;에서 &lt;code&gt;index.hbs&lt;/code&gt;로부터 온 모든 목록이 렌더링된다.&lt;/li&gt;
&lt;li&gt;모든 HTML이 응답 body에 담겨 클라이언트로 전송된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 사이드 렌더링이다! 어느 정도는 말이다. 서버에서 렌더링이 되긴 했지만 상호작용이 불가능하다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록에 아이템을 추가할 수 있도록 상호작용을 추가해보자. 이는 전형적인 클라이언트 사이드 렌더링 사용 사례로, 기본적으로 SPA이다. 서버는 목록에 아이템으르 추가하기 위해 /add 엔드포인트를 추가하는 것을 제외하고는 변경사항이 많지 않다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;import { Application, Router } from &quot;https://deno.land/x/oak@v11.1.0/mod.ts&quot;;
import { Handlebars } from &quot;https://deno.land/x/handlebars@v0.9.0/mod.ts&quot;;

const dinos = [&quot;Allosaur&quot;, &quot;T-Rex&quot;, &quot;Deno&quot;];
const handle = new Handlebars();
const router = new Router();

router.get(&quot;/&quot;, async (context) =&amp;gt; {
  context.response.body = await handle.renderView(&quot;index&quot;, { dinos: dinos });
});

router.post(&quot;/add&quot;, async (context) =&amp;gt; {
  const { value } = await context.request.body({ type: &quot;json&quot; });
  const { item } = await value;
  dinos.push(item);
  context.response.status = 200;
});

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });&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;Handlebars 코드는 상당 부분 변경되었다. HTML 목록을 생성하기 위한 Handlbars 템플릿은 여전히 존재하지만 &lt;code&gt;main.hbs&lt;/code&gt;에 &lt;code&gt;Add&lt;/code&gt; 버튼으르 동작하기 위한 자바스크립트가 추가되었다. 버튼에 바운딩 된 &lt;code&gt;EventListener&lt;/code&gt; 이벤트는 다음 동작을 수행할 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로운 아이템을 &lt;code&gt;/add&lt;/code&gt; 엔드포인트로 &lt;code&gt;POST&lt;/code&gt; 한다.&lt;/li&gt;
&lt;li&gt;새로운 아이템을 HTML 리스트에 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;[...]
  &amp;lt;input /&amp;gt;
  &amp;lt;button&amp;gt;Add&amp;lt;/button&amp;gt;
 &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&amp;lt;script&amp;gt;
 document.querySelector(&quot;button&quot;).addEventListener(&quot;click&quot;, async () =&amp;gt; {
   const item = document.querySelector(&quot;input&quot;).value;
   const response = await fetch(&quot;/add&quot;, {
     method: &quot;POST&quot;,
     headers: {
       &quot;Content-Type&quot;: &quot;application/json&quot;,
     },
     body: JSON.stringify({ item }),
   });
   const status = await response.status;
   if (status === 200) {
     const li = document.createElement(&quot;li&quot;);
     li.innerText = item;
     document.querySelector(&quot;ul&quot;).appendChild(li);
     document.querySelector(&quot;input&quot;).value = &quot;&quot;;
   }
 });
&amp;lt;/script&amp;gt;&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;이것은 진정한 의미의 서버 사이드 렌더링이 아니다. SSR에서는 동일한 동형 JS를 클라이언트와 서버 사이드에서 실행해야 하는데, 이것은 코드가 어디에서 실행되는지에 따라 다르게 동작하기 때문이다. 위의 예제에서 JS는 서버와 클라이언트 사이드에서 실행되긴 하지만, 독립적으로 동작한다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 진정한 SSR로 가보자. Handlebars와 템플릿을 사용하는 대신 목록을 업데이트할 DOM을 생성할 것이다. 세 개의 파일 중 &lt;code&gt;server.ts&lt;/code&gt;를 먼저 보도록 하자.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { Application } from &quot;https://deno.land/x/oak@v11.1.0/mod.ts&quot;;
import { Router } from &quot;https://deno.land/x/oak@v11.1.0/mod.ts&quot;;
import { DOMParser } from &quot;https://deno.land/x/deno_dom@v0.1.36-alpha/deno-dom-wasm.ts&quot;;
import { render } from &quot;./client.js&quot;;

const html = await Deno.readTextFile(&quot;./client.html&quot;);
const dinos = [&quot;Allosaur&quot;, &quot;T-Rex&quot;, &quot;Deno&quot;];
const router = new Router();

router.get(&quot;/client.js&quot;, async (context) =&amp;gt; {
  await context.send({
    root: Deno.cwd(),
    index: &quot;client.js&quot;,
  });
});

router.get(&quot;/&quot;, (context) =&amp;gt; {
  const document = new DOMParser().parseFromString(
    &quot;&amp;lt;!DOCTYPE html&amp;gt;&quot;,
    &quot;text/html&quot;,
  );
  render(document, { dinos });
  context.response.type = &quot;text/html&quot;;
  context.response.body = `${document.body.innerHTML}${html}`;
});

router.get(&quot;/data&quot;, (context) =&amp;gt; {
  context.response.body = dinos;
});

router.post(&quot;/add&quot;, async (context) =&amp;gt; {
  const { value } = await context.request.body({ type: &quot;json&quot; });
  const { item } = await value;
  dinos.push(item);
  context.response.status = 200;
});

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });&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;이번에는 많은 것이 바뀌었다. 먼저 새로운 엔드포인트가 추가되었다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;client.js&lt;/code&gt; 파일을 서비스할 GET 엔드포인트&lt;/li&gt;
&lt;li&gt;데이터를 서비스할 GET 엔드포인트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트 엔드포인트에도 큰 변경사항이 생겼다. &lt;code&gt;deno_dom&lt;/code&gt;의 &lt;code&gt;DOMParser&lt;/code&gt;를 사용해서 DOM 도큐먼트 오브젝트를 만들 것이다. &lt;code&gt;DOMParser&lt;/code&gt; 모듈은 &lt;code&gt;ReactDOM&lt;/code&gt;과 같이 동작해서 서버에서 DOM을 다시 만들 수 있게 해준다. 그 후 우리는 handlebars 템플릿을 이용하는 대신 &lt;code&gt;client.js&lt;/code&gt;라는 새 파일의 렌더링 함수를 이용해서 목록을 렌더링 하기 위해 생성된 도큐먼트를 사용할 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;let isFirstRender = true;

// Simple HTML sanitization to prevent XSS vulnerabilities.
function sanitizeHtml(text) {
  return text
    .replace(/&amp;amp;/g, &quot;&amp;amp;amp;&quot;)
    .replace(/&amp;lt;/g, &quot;&amp;amp;lt;&quot;)
    .replace(/&amp;gt;/g, &quot;&amp;amp;gt;&quot;)
    .replace(/&quot;/g, &quot;&amp;amp;quot;&quot;)
    .replace(/'/g, &quot;&amp;amp;#039;&quot;);
}

export async function render(document, dinos) {
  if (isFirstRender) {
    const jsonResponse = await fetch(&quot;http://localhost:8000/data&quot;);
    if (jsonResponse.ok) {
      const jsonData = await jsonResponse.json();
      const dinos = jsonData;
      let html = &quot;&amp;lt;html&amp;gt;&amp;lt;ul&amp;gt;&quot;;
      for (const item of dinos) {
        html += `&amp;lt;li&amp;gt;${sanitizeHtml(item)}&amp;lt;/li&amp;gt;`;
      }
      html += &quot;&amp;lt;/ul&amp;gt;&amp;lt;input&amp;gt;&quot;;
      html += &quot;&amp;lt;button&amp;gt;Add&amp;lt;/button&amp;gt;&amp;lt;/html&amp;gt;&quot;;
      document.body.innerHTML = html;
      isFirstRender = false;
    } else {
      document.body.innerHTML = &quot;&amp;lt;html&amp;gt;&amp;lt;p&amp;gt;Something went wrong.&amp;lt;/p&amp;gt;&amp;lt;/html&amp;gt;&quot;;
    }
  } else {
    let html = &quot;&amp;lt;ul&amp;gt;&quot;;
    for (const item of dinos) {
      html += `&amp;lt;li&amp;gt;${sanitizeHtml(item)}&amp;lt;/li&amp;gt;`;
    }
    html += &quot;&amp;lt;/ul&amp;gt;&quot;;
    document.querySelector(&quot;ul&quot;).outerHTML = html;
  }
}

export function addEventListeners() {
  document.querySelector(&quot;button&quot;).addEventListener(&quot;click&quot;, async () =&amp;gt; {
    const item = document.querySelector(&quot;input&quot;).value;
    const dinos = Array.from(
      document.querySelectorAll(&quot;li&quot;),
      (e) =&amp;gt; e.innerText,
    );
    dinos.push(item);
    const response = await fetch(&quot;/add&quot;, {
      method: &quot;POST&quot;,
      headers: {
        &quot;Content-Type&quot;: &quot;application/json&quot;,
      },
      body: JSON.stringify({ item }),
    });
    if (response.ok) {
      render(document, dinos);
    } else {
      // In a real app, you'd want better error handling.
      console.error(&quot;Something went wrong.&quot;);
    }
  });
}&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;&lt;code&gt;client.js&lt;/code&gt; 파일은 서버와 클라이언트 양쪽에서 이용 가능하다. 진정한 SSR을 위한 동형 자바스크립트인 것이다. 처음에는 서버에서 HTML을 렌더링하기 위해 &lt;code&gt;render&lt;/code&gt; 함수를 사용하고, 이후에는 업데이트 사항을 렌더링하기 위해 클라이언트 사이드에서 &lt;code&gt;render&lt;/code&gt; 함수를 사용할 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 매 요청에서 데이터는 서버로부터 직접 제공된다. 데이터는 &lt;code&gt;/add&lt;/code&gt; 엔드포인트를 사용해 데이터 모델에 추가된다. 클라이언트에서 직접 HTML에 아이템을 추가했던 두 번째 예제와 비교했을 때, 이번 예제에서는 모든 데이터가 서버를 통해 라우팅 된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;client.js&lt;/code&gt;의 자바스크립트는 &lt;code&gt;client.html&lt;/code&gt;에서 사용되어 클라이언트 측에서도 직접적으로 사용된다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;script type=&quot;module&quot;&amp;gt;
  import { render, addEventListeners } from &quot;./client.js&quot;;
  await render(document);
  addEventListeners();
&amp;lt;/script&amp;gt;&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;클라이언트에서 처음으로 &lt;code&gt;client.js&lt;/code&gt;를 호출할 때 HTML이 hydrated 되는데, 이때 &lt;code&gt;client.js&lt;/code&gt;에서 &lt;code&gt;/data&lt;/code&gt; 엔드포인트를 호출해 이후의 렌더링에 필요한 데이터를 가져온다. 큰 SSR 페이지에서 hydration은 느리고 복잡해질 수 있다 (이때 &lt;a href=&quot;https://fresh.deno.dev/docs/concepts/islands&quot;&gt;아일랜드&lt;/a&gt;가 도움이 될 수 있다).&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 SSR이 동작하는 방식이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 DOM이 재생성된다.&lt;/li&gt;
&lt;li&gt;클라이언트와 서버 사이드 양쪽에서 동형 JS를 사용해 데이터를 렌더링할 수 있고, 클라이언트의 초기 로드에서는 모든 데이터를 가져와 앱을 상호작용 가능하게 만드는 hydration 과정이 설정된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;복잡한 웹을 SSR로 간단하게 만들기&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리는 모든 화면 사이즈와 모든 대역폭을 위한 복잡한 앱을 만들고 있다. 사람들은 여러분의 웹사이트를 터널을 지나는 기차 안에서 사용할 수도 있다. 이 모든 상황 속에서 일관성 있는 경험을 제공하면서도 여러분의 코드베이스를 작고 이해하기 쉽게 유지하는 가장 좋은 방법은 SSR을 사용하는 것이다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 경험을 중요하게 생각하는 뛰어난 성능의 프레임워크들은 클라이언트에게 필요한 것만을 정확하게 전송할 것이다. 지연시간을 더 최소화하기 위해서는 SSR 앱을 사용자 근처의 엣지에 배포해라. &lt;a href=&quot;https://fresh.deno.dev/&quot;&gt;Fresh&lt;/a&gt;와 &lt;a href=&quot;https://deno.com/deploy&quot;&gt;Deno Deploy&lt;/a&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NextJS 13 버전과 SSR의 과거와 미래 등에 관한 커리어리 게시글을 보다가 읽어보라고 추천한 포스팅이 있길래 읽으면서 번역해봤다. 커리어리 게시글도 링크하려니까 못 찾아서 나중에 찾으면 글을 첨부하겠다. 과거 서버에서 HTML을 서비스하던 방식과 현재의 SSR이 어떻게 다른지 이해하는데 도움이 되었다. 뒷 부분은 Deno 서비스 홍보 같아서 번역을 할까 말까 고민했는데 대강 말하는 요지 자체는 개념 이해에 도움이 되는 듯하여 포함했다.&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;</description>
      <category>웹 개발</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/27</guid>
      <comments>https://9ummy.tistory.com/27#entry27comment</comments>
      <pubDate>Tue, 6 Jun 2023 23:15:33 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트와 프로토타입 알아보기</title>
      <link>https://9ummy.tistory.com/26</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘 문제를 풀면서 어떤 객체가 특정 key를 가지는지 확인해야 할 상황이 있었다. 평소 객체가 특정 key를 가지는지 확인하기 위해 &lt;code&gt;in&lt;/code&gt; 연산자를 주로 사용했기 때문에 &lt;code&gt;in&lt;/code&gt; 연산자를 사용해 문제를 풀었는데, 다른 답안을 참고하던 중 &lt;code&gt;Object.prototype.hasOwnProperty()&lt;/code&gt; 메소드를 접하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;MDN의 &lt;code&gt;in&lt;/code&gt; 연산자에 대한 설명을 보면 &lt;code&gt;in 연산자는 명시된 속성이 명시된 객체에 존재하면 true를 반환합니다.&lt;/code&gt;라고 되어있다. &lt;code&gt;hasOwnProperty()&lt;/code&gt; 메소드 역시 &lt;code&gt;hasOwnProperty() 메소드는 객체가 특정 프로퍼티를 가지고 있는지를 나타내는 불리언 값을 반환한다.&lt;/code&gt;라고 되어 있는데 둘의 차이가 무엇일지 궁금해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;검색해보니 &lt;code&gt;in&lt;/code&gt;의 경우 해당 객체의 프로토타입 체인까지 포함한 모든 객체 키를 조회한다고 하고 &lt;code&gt;hasOwnProperty&lt;/code&gt; 메소드의 경우 해당 객체가 해당 키를 직접적으로 가지고 있을 경우에만 &lt;code&gt;true&lt;/code&gt; 값을 반환한다고 한다. &lt;a href=&quot;https://velog.io/@haebin/Javascript-%EA%B0%9D%EC%B2%B4%EC%9D%98-key-%EC%A1%B4%EC%9E%AC-%EC%97%AC%EB%B6%80-%ED%99%95%EC%9D%B8-in-vs-hasOwnProperty&quot;&gt;참고한 글&lt;/a&gt;에 의하면 객체의 &lt;code&gt;hasOwnProperty&lt;/code&gt; 메소드를 그냥 사용하기보다는 &lt;code&gt;Object.prototype.hasOwnProperty&lt;/code&gt; 메소드를 사용할 것을 권장하고 있는데, 해당 객체의 &lt;code&gt;hasOwnProperty&lt;/code&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;본 포스팅은 Evan Moon님의 &lt;a href=&quot;https://evan-moon.github.io/2019/10/23/js-prototype/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;[JS 프로토타입] 자바스크립트의 프로토타입 훑어보기&lt;/a&gt;와 Toast UI의 &lt;a href=&quot;https://ui.toast.com/weekly-pick/ko_20160603&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프로토타입 기반 언어, 자바스크립트&lt;/a&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;h3 data-ke-size=&quot;size23&quot;&gt;프로토타입과 프로토타입 기반 언어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;자바스크립트는 프로토타입 기반의 언어이다.&lt;/code&gt;라는 문장을 보면서 고개를 끄덕끄덕하면서도 프로토타입이 정확히 무엇인지는 잘 모르고 있었던 것 같다. 프로토타입을 이해하는 데는 &lt;a href=&quot;https://evan-moon.github.io/2019/10/23/js-prototype/&quot;&gt;Evan Moon님의 해당 포스팅&lt;/a&gt;의 도움을 많이 받았다.&lt;br /&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85_%EA%B8%B0%EB%B0%98_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot;&gt;위키피디아&lt;/a&gt;에서는 프로토타입 기반 언어를 다음과 같이 정의하고 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토타입 기반 언어는 클래스 기반 언어에서 상속을 사용하는 것과는 다르게,&lt;br /&gt;객체를 원형(프로토타입)으로 하는 복제 과정을 통해 객체의 동작 방식을 재사용 할 수 있게 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Evan Moon님 포스팅의 프로토타입 패턴 Java 코드 예시를 보면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;// 이건 너무 객체 생성 비용이 높으니까...
Player evan = new Player();
Player john = new Player();
Player wilson = new Player();

// 이런 방법으로 접근해보는 것은 어떨까?
Player player = new Player();
Player evan = player.clone();
Player john = player.clone();
Player wilson = player.clone();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Player&lt;/code&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;h3 data-ke-size=&quot;size23&quot;&gt;프로토타입 링크&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트의 경우 &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;bigint&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt;, &lt;code&gt;undefined&lt;/code&gt;, &lt;code&gt;symbol&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;의 7가지 원시 타입(Primitive Type)을 제외한 모든 타입은 객체이고, 프로토타입 링크를 통해 원형을 참조하는 식으로 동작한다.&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;pre class=&quot;routeros&quot;&gt;&lt;code&gt;function User () {}

const gummy = new User();

console.log(gummy);
console.log(typeof gummy);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;User { __proto__: Object }
object&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;&lt;code&gt;gummy&lt;/code&gt; 객체를 생성하면서 &lt;code&gt;User&lt;/code&gt; 함수를 복제한 것이 아니라 &lt;code&gt;User 함수의 프로토타입 객체&lt;/code&gt;를 복제했기 때문에 &lt;code&gt;gummy&lt;/code&gt; 객체는 &lt;code&gt;function&lt;/code&gt; 타입이 아니라 &lt;code&gt;object&lt;/code&gt; 타입을 가지게 된다. 이처럼 자바스크립트는 함수를 생성할 때 (위 예에서 &lt;code&gt;User&lt;/code&gt; 함수를 생성할 때처럼) 그 함수의 프로토타입 객체를 함께 생성하고 해당 함수의 &lt;code&gt;prototype&lt;/code&gt; 프로퍼티에 연결해둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;function User () {}

console.log(User.prototype);
console.log(typeof User.prototype);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;{ constructor: f User(), __proto__: Object }
object&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;브라우저 콘솔에서 실행해보면 다음과 같이 나온다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-05-07 at 2.42.34 PM.png&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vSDV4/btsd0uA62Gs/KkXLHnaggw8TKWWjsuWhd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vSDV4/btsd0uA62Gs/KkXLHnaggw8TKWWjsuWhd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vSDV4/btsd0uA62Gs/KkXLHnaggw8TKWWjsuWhd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvSDV4%2Fbtsd0uA62Gs%2FKkXLHnaggw8TKWWjsuWhd1%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;307&quot; height=&quot;105&quot; data-filename=&quot;Screenshot 2023-05-07 at 2.42.34 PM.png&quot; data-origin-width=&quot;432&quot; data-origin-height=&quot;148&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;중요한 것은 &lt;code&gt;new User()&lt;/code&gt;라는 문법을 사용해 새로운 객체를 만들게 되면 &lt;code&gt;User&lt;/code&gt; 함수 자체가 아니라 &lt;code&gt;User&lt;/code&gt; 함수가 생성될 때 함께 생성된 &lt;code&gt;User 함수의 프로토타입 객체&lt;/code&gt;를 복제해서 새로운 객체를 만든다는 것! 이때 복제되는 프로토타입 객체를 &lt;code&gt;프로토타입 프로퍼티(Prototype Property)&lt;/code&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;constructor&lt;/code&gt; 와 &lt;code&gt;__proto__&lt;/code&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 프로토타입 객체를 콘솔에 찍었을 때 해당 객체가 가지고 있던 프로퍼티인 &lt;code&gt;constructor&lt;/code&gt;와 &lt;code&gt;__proto__&lt;/code&gt;를 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;constructor&lt;/code&gt; 프로퍼티에는 해당 프로토타입 객체가 생성될 때 선언했던 함수(&lt;code&gt;f User()&lt;/code&gt;)가 들어있다. 함수를 선언하게 될 때 해당 함수의 프로토타입 객체가 함께 생성되며 이 둘이 연결되게 되는데, 함수는 프로토타입 객체의 &lt;code&gt;constructor&lt;/code&gt; 프로퍼티로 연결되고, 프로토타입 객체는 함수의 &lt;code&gt;prototpye&lt;/code&gt; 프로퍼티로 연결된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;prototype_connection.png&quot; data-origin-width=&quot;2164&quot; data-origin-height=&quot;340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q2KnD/btsegf908qi/b3mAlNeyYvbp32AsV465uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q2KnD/btsegf908qi/b3mAlNeyYvbp32AsV465uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q2KnD/btsegf908qi/b3mAlNeyYvbp32AsV465uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq2KnD%2Fbtsegf908qi%2Fb3mAlNeyYvbp32AsV465uk%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;605&quot; height=&quot;95&quot; data-filename=&quot;prototype_connection.png&quot; data-origin-width=&quot;2164&quot; data-origin-height=&quot;340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 내용을 보면 프로토타입 객체와 &lt;code&gt;constructor&lt;/code&gt;, &lt;code&gt;__proto__&lt;/code&gt; 프로퍼티의 관계를 이해에 도움이 될 것 같다.&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;console.log(User.prototype.constructor === User); // true&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;const gummy = new User();
console.log(gummy.__proto__.constructor === User); // true&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;gummy.__proto__&lt;/code&gt;처럼 생성된 객체가 원본 객체와 &lt;code&gt;__proto__&lt;/code&gt; 프로퍼티를 통해 연결되는 것을 &lt;code&gt;프로토타입 링크(Prototype Link)&lt;/code&gt;라고 한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function User() {};
const gummy = new User();
console.log(gummy.__proto__ === User.prototype); // true&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로토타입 체인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트 내에서 사용되는 모든 객체들은 위와 같은 프로토타입 기반 방식으로 정의되고 생성된다. 우리가 일반적으로 사용하는 빌트인 객체들 역시 같은 방식으로 만들어졌다. 그리고 이 모든 객체들은 &lt;code&gt;Object&lt;/code&gt; 함수의 프로토타입인 &lt;code&gt;Object.prototype&lt;/code&gt;을 시작으로 복제된다. &lt;code&gt;Object.prototype&lt;/code&gt; 객체가 모든 객체의 조상이라는 뜻이다. 따라서 &lt;code&gt;Object.prototype.__proto__&lt;/code&gt;을 콘솔에 찍어보면 &lt;code&gt;null&lt;/code&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;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;prototype-chain.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ey7tTD/btsd3eR8z3P/7Kbg6OIkxtkLDVpXRdBnGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ey7tTD/btsd3eR8z3P/7Kbg6OIkxtkLDVpXRdBnGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ey7tTD/btsd3eR8z3P/7Kbg6OIkxtkLDVpXRdBnGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fey7tTD%2Fbtsd3eR8z3P%2F7Kbg6OIkxtkLDVpXRdBnGK%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;606&quot; height=&quot;312&quot; data-filename=&quot;prototype-chain.png&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 프로토타입으로 이루어진 객체들의 관계를 `프로토타입 체인(Prototype Chain)`이라고 한다.&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;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언급했던 &lt;a href=&quot;https://evan-moon.github.io/2019/10/23/js-prototype/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Evan Moon님&lt;/a&gt;과 &lt;a href=&quot;https://ui.toast.com/weekly-pick/ko_20160603&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Toast UI&lt;/a&gt;의 포스팅을 꼭 한 번 읽어보시기 바란다. 잘 작성해주신 두 글을 이해하기 쉽게 정리해보고 싶었으나 잘 되었는지는 모르겠다... 그래도 콘솔 찍을 때마다 보이던 `prototype`, `__proto__`와 조금은 친숙해진 기분이 든다.&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;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/in&quot;&gt;MDN in 연산자 관련 도큐먼트&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty&quot;&gt;MDN Object.prototype.hasOwnProperty() 관련 도큐먼트&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%ED%86%A0%ED%83%80%EC%9E%85_%EA%B8%B0%EB%B0%98_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위키피디아 프로토타입 기반 프로그래밍&lt;/a&gt;&lt;/p&gt;</description>
      <category>웹 개발/JS &amp;middot; TS</category>
      <category>javascript</category>
      <category>JS</category>
      <category>개발</category>
      <category>자바스크립트</category>
      <category>프로그래밍</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/26</guid>
      <comments>https://9ummy.tistory.com/26#entry26comment</comments>
      <pubDate>Sun, 7 May 2023 15:26:59 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js] 국제화(i18n) 자동화 시스템 구축하기</title>
      <link>https://9ummy.tistory.com/25</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;올해 초 OKR을 작성하면서 빠르게 이루고 싶었던 목표 중 하나가 사내 서비스 국제화(i18n) 자동화 시스템을 구축하는 것이었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 번 프로덕트 런칭을 준비하면서 i18n 관련 노가다 작업(복붙이나 json 컨플릭트 해결 등...)이 너무 비효율적이라는 생각이 들었기 때문이다. 자동화 시스템을 구축하면서는 &lt;a href=&quot;https://ui.toast.com/weekly-pick/ko_20210303&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Toast UI 유동식님의 자동화 가이드 포스트&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;package.json &amp;amp; Dockerfile 업데이트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;scan:i18n&lt;/b&gt; - 번역할 키 값을 스캔해서 스캔한 키 값을 바탕으로 json 파일 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;uplaod:i18n&lt;/b&gt; - 생성된 json 파일을 토대로 새롭게 추가된 키 값들을 스프레드시트에 업데이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;download:i18n&lt;/b&gt; - 번역된 키 값들을 읽어 언어별 json 파일을 생성&lt;/p&gt;
&lt;pre id=&quot;code_1682768577732&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&quot;scripts&quot;: {
  ...,
  &quot;scan:i18n&quot;: &quot;i18next-scanner --config i18next-scanner.config.js&quot;,
  &quot;upload:i18n&quot;: &quot;npm run scan:i18n &amp;amp;&amp;amp; node src/utils/translate/upload.js&quot;,
  &quot;download:i18n&quot;: &quot;node src/utils/translate/download.js&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Dockerfile 등에서 build 실행 전에 download:i18n 명령어를 실행해주면 스프레드시트를 토대로 번역본 json 파일이 생성되기 때문에 항상 최신화 된 번역본을  프로젝트에 반영할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1682768630339&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

RUN npm run download:i18n
RUN npm run build:dev

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 테스트할 때는 &lt;b&gt;npm run download:i18n&lt;/b&gt; 만 실행해서 테스트해보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;download.js 파일 수정사항&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 가이드에 작성된 대로 download 파일을 작성하면 스프레드시트에 번역값이 누락되어 있을 경우 에러가 발생할 수 있다. updateJsonFromSheet 함수를 보면 스프레드시트의 모든 행을 읽어 json 파일을 생성하는데 번역값이 누락되어 있을 경우 키에 빈 스트링이 매핑되어 들어가기 때문이다. 따라서 최종적으로 서비스에 사용할 json 파일을 업데이트하기 전에, 스프레드시트를 읽어 생성한 json 파일에서 값이 비어있는 키를 제외시키는 과정을 추가해주었다. &lt;u&gt;키에 빈 스트링이 매핑되어 있는 경우 아예 해당 문자열이 화면에 나오지 않게 되지만 키 자체가 존재하지 않는 경우에는 키값 그대로 화면에 표시되게 된다.&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1682836169998&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;async function updateJsonFromSheet() {
  await checkAndMakeLocaleDir(localesPath, lngs);

  const doc = await loadSpreadsheet();
  const lngsMap = await fetchTranslationsFromSheetToJson(doc);

  fs.readdir(localesPath, (error, lngs) =&amp;gt; {
    if (error) {
      throw error;
    }

    lngs.forEach((lng) =&amp;gt; {
      const localeJsonFilePath = `${localesPath}/${lng}/${ns}.json`;
      
      // 아래 부분 추가
      const filteredMap = Object.fromEntries(
        Object.entries(lngsMap[lng]).filter(([_, v]) =&amp;gt; v)
      );

      const jsonString = JSON.stringify(filteredMap, null, 2);

      fs.writeFile(localeJsonFilePath, jsonString, 'utf8', (err) =&amp;gt; {
        if (err) {
          throw err;
        }
      });
    });
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;1. 파일이 마이크로소프트 엑셀 파일(.xlsx)이 아니라 &lt;b&gt;구글 스프레드시트&lt;/b&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;b&gt;파일 &amp;gt; Google Sheets로 저장&lt;/b&gt;을 클릭해 구글 스프레드시트 형식으로 변경할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Screenshot 2023-04-30 at 3.45.38 PM.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZFa1P/btsdpx4IKz4/ZBlEDXw2ZfJoh0ttkifSK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZFa1P/btsdpx4IKz4/ZBlEDXw2ZfJoh0ttkifSK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZFa1P/btsdpx4IKz4/ZBlEDXw2ZfJoh0ttkifSK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZFa1P%2Fbtsdpx4IKz4%2FZBlEDXw2ZfJoh0ttkifSK1%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;501&quot; height=&quot;292&quot; data-filename=&quot;Screenshot 2023-04-30 at 3.45.38 PM.png&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;416&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;2. 서비스 어카운트 계정을 공유 권한에 추가한다.&lt;/p&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;Group 3.png&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;1380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HQmo0/btsdefEnd16/ziKRkUncHEhBkkbKhWUTSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HQmo0/btsdefEnd16/ziKRkUncHEhBkkbKhWUTSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HQmo0/btsdefEnd16/ziKRkUncHEhBkkbKhWUTSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHQmo0%2FbtsdefEnd16%2FziKRkUncHEhBkkbKhWUTSk%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;415&quot; data-filename=&quot;Group 3.png&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;1380&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;h4 data-ke-size=&quot;size20&quot;&gt;덧) 고민해야 했던 부분들  &lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 이메일 템플릿&lt;br /&gt;&lt;br /&gt;유저에게 발송하는 이메일 템플릿을 컴포넌트로 구현한 뒤 ReactDOM.renderToStaticMarkup 메서드를 이용해 이메일용 html을 구성하고 있었다. 이 경우 다른 페이지들에서 이용하는 next-i18next의 useTranslation을 사용할 수 없기 때문에 &lt;b&gt;이메일 템플릿에 들어가는 메세지들의 키 값이 i18next-scanner에 인식되도록&lt;/b&gt; 하면서, &lt;b&gt;스프레드시트에 업데이트 된 번역된 값들을 이메일 템플릿에서도 사용할 수 있도록 직접 구현&lt;/b&gt;하는 과정이 필요했다.&lt;br /&gt;&lt;br /&gt;말이 거창하다뿐이지 next-i18next의 t 함수와 똑같이 동작하는 함수를 만들어주는 것으로 이 문제는 쉽게 해결할 수 있었다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1682834360243&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import enTranslation from 'public/locales/en/common.json';
import koTranslation from 'public/locales/ko/common.json';
import zhTranslation from 'public/locales/zh/common.json';

export const translationMap = {
  en: enTranslation,
  ko: koTranslation,
  zh: zhTranslation,
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1682834231318&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  const t = (value, obj) =&amp;gt; {
    let translation = translationMap[locale][value] || value;
    if (obj) {
      Object.keys(obj).forEach((key) =&amp;gt; {
        translation = translation.replaceAll(`{{${key}}}`, obj[key]);
      });
    }
    return translation;
  };&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;2. 텍스트 중간에 볼드체를 사용하거나 색상을 변경해야 하는 경우&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Frame 1.png&quot; data-origin-width=&quot;1233&quot; data-origin-height=&quot;402&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctWblO/btsdo9JzkYq/ZUVzjfmGjS0K2Dk04oUdv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctWblO/btsdo9JzkYq/ZUVzjfmGjS0K2Dk04oUdv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctWblO/btsdo9JzkYq/ZUVzjfmGjS0K2Dk04oUdv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctWblO%2Fbtsdo9JzkYq%2FZUVzjfmGjS0K2Dk04oUdv0%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;743&quot; height=&quot;242&quot; data-filename=&quot;Frame 1.png&quot; data-origin-width=&quot;1233&quot; data-origin-height=&quot;402&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 컴포넌트를 구현할 경우 보통 아래처럼 코드를 작성할 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1682835017166&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;p&amp;gt;&amp;lt;b&amp;gt;The cat&amp;lt;/b&amp;gt; jumped over &amp;lt;u&amp;gt;the fence&amp;lt;/u&amp;gt;.&amp;lt;p&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 경우 텍스트 전체를 키로 등록할 수 없기 때문에 문제가 발생한다. The cat, jumped over, the fence 처럼 문장을 쪼개서 키로 등록하려고 해도 언어에 따라 단어의 순서나 조사 등을 고려해야 하기 때문에 쪼갠 단어를 그대로 번역해서 사용할 수도 없다.&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;pre id=&quot;code_1682835515547&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const sampleSentence = {
  en: (
    &amp;lt;p&amp;gt;
      &amp;lt;b&amp;gt;The cat&amp;lt;/b&amp;gt; jumped over &amp;lt;u&amp;gt;the fence&amp;lt;/u&amp;gt;.
    &amp;lt;/p&amp;gt;
  ),
  ko: (
    &amp;lt;p&amp;gt;
      &amp;lt;b&amp;gt;고양이&amp;lt;/b&amp;gt;가 &amp;lt;u&amp;gt;울타리&amp;lt;/u&amp;gt;를 뛰어넘었습니다.
    &amp;lt;/p&amp;gt;
  ),
};

...


// 사용할 때는 이런 느낌 ...
&amp;lt;div&amp;gt;
  {sampleSentence[locale]}
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&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;글이 좀 뒤죽박죽인 것 같지만 결론은 반나절 정도만 투입하면 i18n 관련 작업에서 많은 편의를 얻을 수 있으니 필요한 분들은 꼭 시도해보시길 바란다.&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;h4 data-ke-size=&quot;size20&quot;&gt;참고한 글들&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ui.toast.com/weekly-pick/ko_20210303&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ui.toast.com/weekly-pick/ko_20210303&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hjleee93.github.io/frontend/javascript/autoLocalization/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hjleee93.github.io/frontend/javascript/autoLocalization/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/62433283/this-operation-is-not-supported-for-this-document-at-gaxios-nodejs-spreadshe&quot;&gt;https://stackoverflow.com/questions/62433283/this-operation-is-not-supported-for-this-document-at-gaxios-nodejs-spreadshe&lt;/a&gt;&lt;/p&gt;</description>
      <category>웹 개발/React &amp;middot; Next.js</category>
      <category>I18N</category>
      <category>nextjs</category>
      <category>react</category>
      <category>국제화</category>
      <category>자동화</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/25</guid>
      <comments>https://9ummy.tistory.com/25#entry25comment</comments>
      <pubDate>Sun, 30 Apr 2023 15:55:39 +0900</pubDate>
    </item>
    <item>
      <title>[React] Web API를 활용한 영상 녹화 구현하기</title>
      <link>https://9ummy.tistory.com/24</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt; &amp;zwj;♀️ 들어가며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 웹 사이트 상에서의 영상 녹화를 구현할 일이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 사용자가 웹 사이트 상에서 &lt;b&gt;녹화되는 영상을 실시간으로 확인&lt;/b&gt;할 수 있어야 하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 녹화된 영상을 &lt;b&gt;서버에 업로드&lt;/b&gt;하거나 &lt;b&gt;다운로드&lt;/b&gt;할 수 있어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'웹캠 녹화'라는 키워드로 검색하면 웹캠으로 영상을 녹화할 수 있는 사이트들이 몇 개 나오는데 Web API를 활용하면 정말 쉽고 간단하게 그런 사이트들에서 제공하는 기능을 거의 그대로 구현할 수 있다. 이번 글에서는 &lt;a href=&quot;https://webcamera.io/ko/&quot;&gt;이 사이트&lt;/a&gt;에서의 영상 녹화 기능을 비슷하게 따라 만들어 보면서 영상 녹화를 어떻게 구현했는지 기록해두려고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt; ️ 구현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API(Application Programming Interface)란 개발자가 복잡한 기능을 더 쉽게 만들 수 있도록 프로그래밍 언어로 제공되는 구조체로, 복잡한 코드 대신 개발자가 사용하기 쉬운 문법으로 기능을 구현할 수 있도록 해준다. 사용 가능한 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API&quot;&gt;Web API 목록&lt;/a&gt;을 보면 정말 다양한 API들이 있는데, 영상 녹화를 위해 &lt;b&gt;MediaRecorder 인터페이스&lt;/b&gt;를 사용해보려고 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. MediaStream 생성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MediaRecorder를 사용하려면 녹화할 MediaStream을 전달해줘야 한다. &lt;code&gt;navigator.mediaDevices.getUserMedia()&lt;/code&gt;를 통해 웹캠 등 이용 가능한 비디오, 오디오 인풋을 사용한 MediaStream을 생성해준다. 영상 피드백을 실시간으로 보여줄 때 오디오는 나오지 않아도 되기 때문에 비디오 스트림과 오디오 스트림을 따로 생성해서 사용했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;videoStream: 실시간 영상 피드백에 사용&lt;/li&gt;
&lt;li&gt;videoStream + audioStream: 영상 녹화를 위한 MediaRecorder에 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const VideoRecorder = () =&amp;gt; {
  const videoRef = useRef&amp;lt;HTMLVideoElement&amp;gt;(null);

  const getMediaPermission = useCallback(async () =&amp;gt; {
    try {
      const audioConstraints = { audio: true };
      const videoConstraints = {
        audio: false,
        video: true,
      };

      const audioStream = await navigator.mediaDevices.getUserMedia(
          audioConstraints
      );
      const videoStream = await navigator.mediaDevices.getUserMedia(
          videoConstraints
      );

      if (videoRef.current) {
          videoRef.current.srcObject = videoStream;
      }
    } catch (err) {
      console.log(err);
    }
  }, []);

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

  return (
    &amp;lt;video ref={videoRef} autoPlay /&amp;gt;
  );
};

export default VideoRecorder;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;VideoRecorder&lt;/code&gt; 컴포넌트를 만들고 &lt;code&gt;pages/index.tsx&lt;/code&gt;에서 해당 컴포넌트를 불러주면 아래와 같이 미디어 사용 허가를 묻는 프롬프트가 나오게 된다. Allow를 선택할 경우 video 태그를 통해 실시간 비디오 피드백을 볼 수 있고, Block을 선택할 경우 미디어를 사용할 수 없기 때문에 &lt;code&gt;catch&lt;/code&gt;절 안의 구문이 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clgeaR/btr5PRqxBqB/d2r6N2RY9w03pWnIQMJfa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clgeaR/btr5PRqxBqB/d2r6N2RY9w03pWnIQMJfa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clgeaR/btr5PRqxBqB/d2r6N2RY9w03pWnIQMJfa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclgeaR%2Fbtr5PRqxBqB%2Fd2r6N2RY9w03pWnIQMJfa1%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;932&quot; height=&quot;386&quot; data-origin-width=&quot;932&quot; data-origin-height=&quot;386&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;924&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1IuBt/btr5Zj7FvSD/tT1nkUFbLMVj8wsM6X2Mu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1IuBt/btr5Zj7FvSD/tT1nkUFbLMVj8wsM6X2Mu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1IuBt/btr5Zj7FvSD/tT1nkUFbLMVj8wsM6X2Mu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1IuBt%2Fbtr5Zj7FvSD%2FtT1nkUFbLMVj8wsM6X2Mu0%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;1082&quot; height=&quot;924&quot; data-origin-width=&quot;1082&quot; data-origin-height=&quot;924&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. MediaRecorder 생성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 MediaStream을 가지고 MediaRecorder를 만들어준다. MediaRecorder와 영상 데이터를 담기 위한 변수도 선언해준다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const VideoRecorder = () =&amp;gt; {
  const videoRef = useRef&amp;lt;HTMLVideoElement&amp;gt;(null);
  const mediaRecorder = useRef&amp;lt;MediaRecorder | null&amp;gt;(null);
  const videoChunks = useRef&amp;lt;Blob[]&amp;gt;([]);

  const getMediaPermission = useCallback(async () =&amp;gt; {
    try {
      const audioConstraints = { audio: true };
      const videoConstraints = {
        audio: false,
        video: true,
      };

      const audioStream = await navigator.mediaDevices.getUserMedia(
          audioConstraints
      );
      const videoStream = await navigator.mediaDevices.getUserMedia(
          videoConstraints
      );

      if (videoRef.current) {
          videoRef.current.srcObject = videoStream;
      }

      // MediaRecorder 추가
      const combinedStream = new MediaStream([
          ...videoStream.getVideoTracks(),
        ...audioStream.getAudioTracks(),
      ]);

      const recorder = new MediaRecorder(combinedStream, {
          mimeType: 'video/webm',
      });

      recorder.ondataavailable = (e) =&amp;gt; {
          if (typeof e.data === 'undefined') return;
        if (e.data.size === 0) return;
        videoChunks.current.push(e.data);
      };

      mediaRecorder.current = recorder;
    } catch (err) {
      console.log(err);
    }
  }, []);

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

  return (
    &amp;lt;video ref={videoRef} autoPlay /&amp;gt;
  );
};

export default VideoRecorder;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 컨트롤 버튼 추가하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상 녹화를 시작하고 멈출 수 있는 버튼을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const VideoRecorder = () =&amp;gt; {
  const videoRef = useRef&amp;lt;HTMLVideoElement&amp;gt;(null);
  const mediaRecorder = useRef&amp;lt;MediaRecorder | null&amp;gt;(null);
  const videoChunks = useRef&amp;lt;Blob[]&amp;gt;([]);

  const getMediaPermission = useCallback(async () =&amp;gt; {
    try {
      const audioConstraints = { audio: true };
      const videoConstraints = {
        audio: false,
        video: true,
      };

      const audioStream = await navigator.mediaDevices.getUserMedia(
          audioConstraints
      );
      const videoStream = await navigator.mediaDevices.getUserMedia(
          videoConstraints
      );

      if (videoRef.current) {
          videoRef.current.srcObject = videoStream;
      }

      // MediaRecorder 추가
      const combinedStream = new MediaStream([
          ...videoStream.getVideoTracks(),
        ...audioStream.getAudioTracks(),
      ]);

      const recorder = new MediaRecorder(combinedStream, {
          mimeType: 'video/webm',
      });

      recorder.ondataavailable = (e) =&amp;gt; {
          if (typeof e.data === 'undefined') return;
        if (e.data.size === 0) return;
        videoChunks.current.push(e.data);
      };

      mediaRecorder.current = recorder;
    } catch (err) {
      console.log(err);
    }
  }, []);

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

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;video ref={videoRef} className={styles.video} autoPlay /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; mediaRecorder.current?.start()}
      &amp;gt;
        Start Recording
      &amp;lt;/button&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; mediaRecorder.current?.stop()}
      &amp;gt;
        Stop Recording
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default VideoRecorder;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Stop Recording&lt;/code&gt; 버튼을 클릭할 경우 &lt;code&gt;ondataavailable&lt;/code&gt; 이벤트 핸들러가 실행되어 &lt;code&gt;videoChunks.current&lt;/code&gt; 배열에 Blob 데이터가 담기게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 다운로드 버튼 추가하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;createObjectURL&lt;/code&gt; 함수를 이용해 &lt;code&gt;videoChunks.current&lt;/code&gt;에 담아준 Blob 데이터를 가리키는 URL을 생성하고 영상을 다운로드할 수 있게 해준다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const VideoRecorder = () =&amp;gt; {
  const videoRef = useRef&amp;lt;HTMLVideoElement&amp;gt;(null);
  const mediaRecorder = useRef&amp;lt;MediaRecorder | null&amp;gt;(null);
  const videoChunks = useRef&amp;lt;Blob[]&amp;gt;([]);

  const getMediaPermission = useCallback(async () =&amp;gt; {
    try {
      const audioConstraints = { audio: true };
      const videoConstraints = {
        audio: false,
        video: true,
      };

      const audioStream = await navigator.mediaDevices.getUserMedia(
          audioConstraints
      );
      const videoStream = await navigator.mediaDevices.getUserMedia(
          videoConstraints
      );

      if (videoRef.current) {
          videoRef.current.srcObject = videoStream;
      }

      // MediaRecorder 추가
      const combinedStream = new MediaStream([
          ...videoStream.getVideoTracks(),
        ...audioStream.getAudioTracks(),
      ]);

      const recorder = new MediaRecorder(combinedStream, {
          mimeType: 'video/webm',
      });

      recorder.ondataavailable = (e) =&amp;gt; {
          if (typeof e.data === 'undefined') return;
        if (e.data.size === 0) return;
        videoChunks.current.push(e.data);
      };

      mediaRecorder.current = recorder;
    } catch (err) {
      console.log(err);
    }
  }, []);

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

  const downloadVideo = () =&amp;gt; {
    const videoBlob = new Blob(videoChunks.current, { type: mimeType });
    const videoUrl = URL.createObjectURL(videoBlob);
    const link = document.createElement('a');
    link.download = `My video - ${dayjs().format('YYYYMMDD')}.webm`;
    link.href = videoUrl;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
  };

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;video ref={videoRef} className={styles.video} autoPlay /&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; mediaRecorder.current?.start()}
      &amp;gt;
        Start Recording
      &amp;lt;/button&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; mediaRecorder.current?.stop()}
      &amp;gt;
        Stop Recording
      &amp;lt;/button&amp;gt;
      &amp;lt;button onClick={downloadVideo}&amp;gt;Download&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default VideoRecorder;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 영상 데이터를 서버에 전송하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영상 데이터를 서버에 전달해 처리하기 위해서는 Blob 데이터를 &lt;code&gt;multipart/form-data&lt;/code&gt; 타입으로 전달하면 된다. 필자는 formidable과 같은 모듈을 이용해 파일을 처리해줬다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const videoBlob = new Blob(videoChunks.current, { type: 'video/webm' });
const formData = new FormData();
formData.append('video', videoBlob);&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;h3 data-ke-size=&quot;size23&quot;&gt;  참고자료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Introduction&quot;&gt;MDN Web API 문서&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Blob&quot;&gt;MDN Blob 문서&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://bradheo.tistory.com/entry/HTTP-multipartform-data&quot;&gt;https://bradheo.tistory.com/entry/HTTP-multipartform-data&lt;/a&gt;&lt;/p&gt;</description>
      <category>웹 개발/React &amp;middot; Next.js</category>
      <category>javascript</category>
      <category>next.js</category>
      <category>react</category>
      <category>typescript</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/24</guid>
      <comments>https://9ummy.tistory.com/24#entry24comment</comments>
      <pubDate>Fri, 31 Mar 2023 23:23:19 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 최대공약수, 최소공배수, 유클리드 호제법</title>
      <link>https://9ummy.tistory.com/23</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;✅ 최대공약수&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 완전 탐색(Brute Force)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 자연수 a, b (a &amp;lt;= b)의 최대공약수를 구할 때 a부터 1까지의 모든 자연수로 두 수를 나누면서 두 수가 나누어 떨어지는 가장 큰 수를 찾는 방법.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678513639738&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const gcd = (num1, num2) =&amp;gt; {
  const min = num1 &amp;lt; num2 ? num1 : num2;
  for (let i = min; i &amp;gt; 0; i--) {
    if (num1 % i === 0 &amp;amp;&amp;amp; num2 % i === 0) {
      return i;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 유클리드 호제법&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;pre id=&quot;code_1678513984379&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const gcd = (num1, num2) =&amp;gt; {
  if (num2 === 0) return num1;
  return gcd(num2, num1 % num2);
}&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 data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 자연수 a, b의 최대공배수&lt;/b&gt;는 &lt;b&gt;a * b / (a, b의 최대공약수)&lt;/b&gt;라는 공식으로 쉽게 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1678514190260&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const lcm = (num1, num2) =&amp;gt; {
  return num1 * num2 / gcd(num1, num2);
}&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;h3 data-ke-size=&quot;size23&quot;&gt;✏️ 관련 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/120808&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/120808&lt;/a&gt;&lt;/p&gt;</description>
      <category>알고리즘/개념정리</category>
      <category>알고리즘</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/23</guid>
      <comments>https://9ummy.tistory.com/23#entry23comment</comments>
      <pubDate>Sat, 11 Mar 2023 14:58:42 +0900</pubDate>
    </item>
    <item>
      <title>[번역] useMemo와 useCallback 제대로 알고 사용하기</title>
      <link>https://9ummy.tistory.com/22</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 글은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.developerway.com/posts/how-to-use-memo-use-callback&quot;&gt;How to useMemo and useCallback: you can remove most of them&lt;/a&gt;&lt;span&gt;&amp;nbsp;를&amp;nbsp;&lt;/span&gt;번역한 글입니다.&lt;br /&gt;의역이 다수 포함되어 있고, 잘못된 내용은 지적해주시면 감사하겠습니다  &lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트를 처음 접해보신 게 아니라면, 여러분은 아마 useMemo와 useCallback 훅을 이미 알고 계실 것입니다. 그리고 여러분이 중간 규모나 큰 규모의 애플리케이션을 만들고 있다면, 여러분의 앱에는 &quot;이해할 수 없는 useMemo와 useCallback의 체인으로 이루어져 있어 코드를 읽거나 디버깅할 수 없는&quot; 몇몇 부분들이 존재할 것입니다. 이 훅들은 어느새 모든 곳으로 퍼져 나가서 여러분은 단지 이 훅들이 어디에나 있고, 여러분 주변의 모든 사람들이 이 훅들을 사용한다는 이유만으로 이 훅들을 사용하게 될 것입니다.&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;이 모든 것이 완전히 불필요한 작업&lt;/b&gt;이라는 것입니다. 아마 90% 정도의 useMemo와 useCallback 훅을 당장 지워도 애플리케이션은 잘 동작할 것이고 심지어 조금 빨라질 수도 있습니다. useMemo와 useCallback 훅이 쓸모없다고 말하려는 것은 아닙니다. 이 훅들이 아주 구체적이고 특정한 몇몇 상황에서만 제한적으로 유용하다는 말입니다.&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;이 글에서 말하려고 하는 것은 이런 것들입니다. 개발자가 useMemo와 useCallback 훅을 사용할 때 저지르는 실수는 어떤 것들이 있는지, 이 훅들의 실제 목적은 무엇이고 이를 적절히 사용하는 방법은 무엇인지 알아보겠습니다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리렌더링을 막기 위해 props를 메모이제이션(memoize)하기&lt;/li&gt;
&lt;li&gt;리렌더링이 될 때마다 복잡한 계산이 발생하는 것을 피하기 위해 값을 메모이제이션하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것들은 글의 뒷부분에서 다루도록 하고, 우선 useMemo와 useCallback 훅의 목적이 무엇인지 알아보도록 하겠습니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;왜 useMemo와 useCallback 훅이 필요한가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 간단합니다. &lt;b&gt;리렌더링 사이에&lt;/b&gt; 메모이제이션을 하기 위해서입니다. 어떤 값이나 어떤 함수가 이 훅으로 감싸져 있다면, 리액트는 최초의 렌더에 이 값을 캐시하고, 이후의 렌더링 시점에 저장된 값으로의 레퍼런스를 반환하게 됩니다. 메모이제이션이 없다면 배열, 객체, 함수와 같이 원시값이 아닌 값들은 리렌더링 될 때마다 값이 새로 만들어지게 됩니다. 메모이제이션은 이러한 값들이 비교되는 상황일 때 유용합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;메모이제이션&lt;/b&gt;(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. (&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위키피디아&lt;/a&gt;)&lt;/blockquote&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;pre id=&quot;code_1677496130235&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const a = { &quot;test&quot;: 1 };
const b = { &quot;test&quot;: 1 };

console.log(a === b); // false

const c = a; // &quot;c&quot; 는 &quot;a&quot;의 참조값

console.log(a === c); // true&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;리액트에 적용하면 이런 식입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677496170407&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Component = () =&amp;gt; {
  const a = { test: 1 };

  useEffect(() =&amp;gt; {
    // &quot;a&quot;의 값은 리렌더링 될 때마다 비교됨
  }, [a]);

  // ...
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a 값은 useEffect의 의존성(dependency) 값입니다. 컴포넌트가 리렌더링 될 때마다 리액트는 의존성 값을 이전 값과 비교할 것입니다. a는 Component 컴포넌트 내부에 정의된 객체이므로, 컴포넌트가 리렌더링 될 때마다 다시 선언될 것입니다. 따라서 리렌더링 전의 a 값과 리렌더링 후의 a 값은 달라지게 되므로 false를 반환하게 될 것이고 useEffect는 리렌더링 될 때마다 다시 실행될 것입니다.&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;이러한 상황을 피하기 위해 a 값을 useMemo 훅을 사용해 감싸줄 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677497958161&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Component = () =&amp;gt; {
  // a 참조값을 보존
  const a = useMemo(() =&amp;gt; ({ test: 1 }), []);

  useEffect(() =&amp;gt; {
    // &quot;a&quot; 값이 실제로 바뀌었을 때만 실행됨
  }, [a]);

  // ...
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 useEffect는 a 값이 실제로 바뀌었을 때만 실행됩니다. 위 예제에서는 실행되지 않겠죠.&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;useCallback은 useMemo와 동일하게 동작하지만 함수를 메모이제이션 할 때 더 유용합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677498131721&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Component = () =&amp;gt; {
  // 리렌더링 사이에 onClick 함수를 보존
  const fetch = useCallback(() =&amp;gt; {
    console.log('fetch some data here');
  }, []);

  useEffect(() =&amp;gt; {
    // fetch 값이 실제로 바뀌었을 때만 실행됨
    fetch();
  }, [fetch]);

  // ...
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMemo와 useCallback 훅은 &lt;b&gt;리렌더링 시점에서만 유용&lt;/b&gt;하다는 점을 기억해야 합니다. 초기 렌더링 시에 이 훅들은 유용하지 않을 뿐더러 없는 게 더 나을 수도 있습니다. 리액트가 추가로 더 많은 일을 하게 만들기 때문입니다. 그렇다는 것은 여러분의 앱이 초기 렌더링 시에 &lt;b&gt;살짝 더 느려질 수 있다&lt;/b&gt;는 뜻입니다. 이러한 훅들이 곳곳에 수백 개씩 존재한다면, 이러한 성능 저하가 눈에 띄게 나타날 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;리렌더링을 막기 위해 props를 메모이제이션 하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 훅들의 목적을 알았으니, 이것들의 실용적인 사용법을 알아봅시다. 가장 중요하면서 가장 많이 사용되는 방법 중 하나는 리렌더링을 막기 위해 props를 메모이제이션 하는 것입니다. 여러분의 앱에서 아래와 같은 코드를 본 적 있다면 소리 질러~&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; &amp;nbsp; 1. 리렌더링을 막기 위해 onClick 함수를 useCallback 훅으로 감싸기&lt;/p&gt;
&lt;pre id=&quot;code_1677498581806&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Component = () =&amp;gt; {
  const onClick = useCallback(() =&amp;gt; {
    /* do something */
  }, []);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={onClick}&amp;gt;Click me&amp;lt;/button&amp;gt;
      ... // some other components
    &amp;lt;/&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 2. 리렌더링을 막기 위해 onClick 함수를 useCallback 훅으로 감싸기&lt;/p&gt;
&lt;pre id=&quot;code_1677498638102&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Item = ({ item, onClick, value }) =&amp;gt; &amp;lt;button onClick={onClick}&amp;gt;{item.name}&amp;lt;/button&amp;gt;;

const Component = ({ data }) =&amp;gt; {
  const value = { a: someStateValue };

  const onClick = useCallback(() =&amp;gt; {
    /* do something on click */
  }, []);

  return (
    &amp;lt;&amp;gt;
      {data.map((d) =&amp;gt; (
        &amp;lt;Item item={d} onClick={onClick} value={value} /&amp;gt;
      ))}
    &amp;lt;/&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 3. 메모이제이션 된 onClick 함수의 의존성 값인 value를 useMemo로 감싸기&lt;/p&gt;
&lt;pre id=&quot;code_1677498686249&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Item = ({ item, onClick }) =&amp;gt; &amp;lt;button onClick={onClick}&amp;gt;{item.name}&amp;lt;/button&amp;gt;;

const Component = ({ data }) =&amp;gt; {
  const value = useMemo(() =&amp;gt; ({ a: someStateValue }), [someStateValue]);
  const onClick = useCallback(() =&amp;gt; {
    console.log(value);
  }, [value]);

  return (
    &amp;lt;&amp;gt;
      {data.map((d) =&amp;gt; (
        &amp;lt;Item item={d} onClick={onClick} /&amp;gt;
      ))}
    &amp;lt;/&amp;gt;
  );
};&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;주변 사람들이 위와 같은 코드를 쓰는 것을 본 적이 있거나, 여러분이 이런 코드를 써본 적 있나요? 위와 같은 상황을 훅이 해결해주고, 이런 상황에 훅을 사용하는 것이 적절하다고 생각하시나요? 만약 '그렇다'고 답변하셨다면, 여러분은 useMemo와 useCallback에 사로잡혀 계신 겁니다. 위 모든 예제에서 이 훅들은 불필요하고 쓸데없이 코드를 복잡하게 만들며, 초기 렌더링을 느리게 하면서도 아무것도 방지해주지 못합니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;컴포넌트는 어떻게 리렌더링 할 수 있을까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;컴포넌트는 state나 prop 값이 바뀌면 스스로 리렌더링 한다&quot;는 것은 일반적인 상식입니다. &lt;a href=&quot;https://reactjs.org/docs/react-component.html#updating&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;리액트 공식문서&lt;/a&gt;에서도 이러한 내용이 적혀있습니다. 저는 이 문장으로부터 &quot;(메모이제이션 등을 통해서) props 값이 바뀌지 않으면, 컴포넌트가 리렌더링 되는 것을 막을 수 있다&quot;는 틀린 결론이 도출된다고 생각합니다.&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;pre id=&quot;code_1677499635572&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const App = () =&amp;gt; {
  const [state, setState] = useState(1);

  return (
    &amp;lt;div className=&quot;App&quot;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; setState(state + 1)}&amp;gt; click to re-render {state}&amp;lt;/button&amp;gt;
      &amp;lt;br /&amp;gt;
      &amp;lt;Page /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App 컴포넌트는 몇 개의 state와 자식 컴포넌트를 가집니다. 버튼이 클릭되면 어떤 일이 발생할까요? state가 변경되고, App 컴포넌트의 리렌더링을 유발되어, Page 컴포넌트를 포함한 모든 자식 컴포넌트가 리렌더링 됩니다. Page 컴포넌트는 props를 가지지도 않는데 말입니다.&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;Page 컴포넌트도 자식 컴포넌트를 가진다고 생각해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1677499794508&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Page = () =&amp;gt; &amp;lt;Item /&amp;gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;state나 prop 없이 텅 비어있습니다. 하지만 App 컴포넌트가 리렌더링 될 때 Page 컴포넌트도 리렌더링 되며, 자식 컴포넌트인 Item 컴포넌트도 리렌더링 될 것입니다. App 컴포넌트의 state가 변경되면 앱 전반에 걸쳐 리렌더링이 유발되는 것입니다. &lt;a href=&quot;https://codesandbox.io/s/parent-children-re-renders-gihtcw?file=/src/App.tsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;코드 샌드박스에서 전체 예제를 참고해보세요.&lt;/a&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;이 연속적인 리렌더링을 막는 유일한 방법은 앱 내부의 컴포넌트를 메모이제이션 하는 것입니다. useMemo 훅을 사용할 수도 있지만 &lt;a href=&quot;https://reactjs.org/docs/react-api.html#reactmemo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;React.memo&lt;/a&gt; 유틸을 사용하는 것이 더 좋습니다. React.memo로 컴포넌트를 감싼 경우에만 리액트가 리렌더링 전에 멈춰서 props 값이 바뀌었는지 체크할 것입니다.&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;pre id=&quot;code_1677500271192&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const Page = () =&amp;gt; &amp;lt;Item /&amp;gt;;
const PageMemoized = React.memo(Page);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모이제이션 한 컴포넌트를 state가 바뀌는 App에 사용하기&lt;/p&gt;
&lt;pre id=&quot;code_1677500311915&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const App = () =&amp;gt; {
  const [state, setState] = useState(1);

  return (
    ... // same code as before
      &amp;lt;PageMemoized /&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면, 그리고 &lt;b&gt;이렇게 했을 때에만&lt;/b&gt; props가 메모이제이션 됐는지가 중요해집니다.&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;Page 컴포넌트가 onClick이라는 함수 prop을 가진다고 가정해봅시다. 이것을 메모이제이션 하지 않고 Page 컴포넌트에 넘겨준다면 어떻게 될까요?&lt;/p&gt;
&lt;pre id=&quot;code_1677500511609&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const App = () =&amp;gt; {
  const [state, setState] = useState(1);
  const onClick = () =&amp;gt; {
    console.log('Do something on click');
  };
  return (
    // onClick의 메모이제이션 여부와 관계없이 페이지는 리렌더링 됨
    &amp;lt;Page onClick={onClick} /&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App은 리렌더링 될 것이고 리액트는 자식 컴포넌트인 Page 컴포넌트를 리렌더링 할 것입니다. onClick 변수가 useCallback으로 감싸져있는지와는 상관 없이 말이죠.&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;그렇다면 Page 컴포넌트를 메모이제이션 한다면 어떨까요?&lt;/p&gt;
&lt;pre id=&quot;code_1677511645135&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const PageMemoized = React.memo(Page);

const App = () =&amp;gt; {
  const [state, setState] = useState(1);
  const onClick = () =&amp;gt; {
    console.log('Do something on click');
  };
  return (
    // onClick이 메모이제이션 되지 않았기 때문에 PageMemoized 컴포넌트는 리렌더링 됨
    &amp;lt;PageMemoized onClick={onClick} /&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App 컴포넌트는 리렌더링 될 것이고, 자식 컴포넌트인 PageMemoized 컴포넌트는 React.memo로 감싸져 있기 때문에 리렌더링을 멈추고 이 컴포넌트의 props 값이 변경되었는지 확인할 것입니다. 이 경우에는 onClick 함수가 메모이제이션 되지 않았기 때문에 prop이 변경되었다고 판단되어 PageMemoized 컴포넌트는 리렌더링 될 것입니다. 마침내 useCallback 훅을 사용할 때가 된 것이죠!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1677511827174&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const PageMemoized = React.memo(Page);

const App = () =&amp;gt; {
  const [state, setState] = useState(1);
  const onClick = useCallback(() =&amp;gt; {
    console.log('Do something on click');
  }, []);

  return (
    // onClick 함수가 메모이제이션 되었기 때문에 PageMemoized 컴포넌트는 리렌더링 되지 않음
    &amp;lt;PageMemoized onClick={onClick} /&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 리액트가 PageMemoized 컴포넌트의 prop을 비교할 때 onClick 함수는 변경되지 않았기 때문에 PageMemoized 컴포넌트는 리렌더링 되지 않을 것입니다.&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;PageMemoized 컴포넌트에 메모이제이션 되지 않은 다른 값을 추가하면 어떻게 될까요? 똑같은 일이 발생할 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677512302047&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const PageMemoized = React.memo(Page);

const App = () =&amp;gt; {
  const [state, setState] = useState(1);
  const onClick = useCallback(() =&amp;gt; {
    console.log('Do something on click');
  }, []);

  return (
    // 값이 메모이제이션 되지 않았기 때문에 페이지는 리렌더링 됨
    &amp;lt;PageMemoized onClick={onClick} value={[1, 2, 3]} /&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트는 PageMemoized 컴포넌트의 prop을 확인하기 위해 멈춥니다. onClick 함수는 그대로 유지되지만 value 값은 변하기 때문에 PageMemoized 컴포넌트는 리렌더링 될 것입니다. &lt;a href=&quot;https://codesandbox.io/s/everything-memoized-8oltqg?file=/src/App.tsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;링크&lt;/a&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;위 내용을 고려했을 때, 컴포넌트의 prop을 메모이제이션 해야 하는 경우는 단 한 가지 경우가 있습니다. &lt;b&gt;컴포넌트의 모든 prop과 컴포넌트 자체가 메모이제이션 되었을 경우&lt;/b&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;다음과 같은 경우 맘 편히 useMemo와 useCallback을 지워버리세요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값들이 직접적으로, 또는 연속된 의존성을 거쳐 DOM 엘리먼트의 속성으로서 전달되는 경우&lt;/li&gt;
&lt;li&gt;값들이 직접적으로, 또는 연속된 의존성을 거쳐 메모이제이션 되지 않은 컴포넌트의 prop으로서 전달되는 경우&lt;/li&gt;
&lt;li&gt;값들이 직접적으로, 또는 연속된 의존성을 거쳐 메모이제이션 되지 않은 prop을 가지는 컴포넌트의 prop으로서 전달되는 경우&lt;/li&gt;
&lt;/ul&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;메모이제이션을 고치는 게 아니라 왜 삭제하냐구요?&lt;/b&gt; 리렌더링으로 인한 퍼포먼스 문제가 발생했다면 진작에 이 문제를 발견하고 고쳤겠죠?   그리고 퍼포먼스 문제가 없다면 메모이제이션을 고쳐서 사용할 필요가 없습니다. 불필요한 useMemo와 useCallback을 삭제하면 기존의 리렌더링 퍼포먼스를 해치지 않으면서도 코드를 간소화하고 초기 렌더링을 조금 빠르게 할 수 있을 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;a href=&quot;https://reactjs.org/docs/hooks-reference.html#usememo&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;리액트 공식문서&lt;/a&gt;에 의하면 useMemo의 주요한 목표는 렌더링 때마다 복잡한 계산이 일어나는 것을 피하는 것이라고 합니다. 하지만 무엇이 &quot;복잡한(expensive)&quot; 계산을 이루는 것인지에 대한 힌트는 나와있지 않습니다. 때문에 개발자들은 useMemo에 꽤나 대부분의 연산을 넣고는 한다. 새 날짜를 만들 때, 배열을 map 하거나 sort 할 때, 객체를 만들 때, 이 모든 경우에 useMemo를 쓰고 있습니다.&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;숫자 몇 개를 봅시다. 최대 250개 정도의 국가들의 리스트가 있고, 우리는 그 배열을 화면에 보여주고 사용자가 정렬할 수 있도록 해준다고 생각해봅시다.&lt;/p&gt;
&lt;pre id=&quot;code_1677514789901&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const List = ({ countries }) =&amp;gt; {
  const sortedCountries = orderBy(countries, 'name', sort);

  return (
    &amp;lt;&amp;gt;
      {sortedCountries.map((country) =&amp;gt; (
        &amp;lt;Item country={country} key={country.id} /&amp;gt;
      ))}
    &amp;lt;/&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;궁금한 점은 250개의 엘리먼트를 가진 배열을 정렬하는 것은 복잡한 계산일까요? 그럴 것 같지 않나요? 리렌더링 될 때마다 값을 다시 계산하는 것을 막기 위해 useMemo를 사용해야 할 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1677514979125&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const List = ({ countries }) =&amp;gt; {
  const before = performance.now();

  const sortedCountries = orderBy(countries, 'name', sort);

  // this is the number we're after
  const after = performance.now() - before;

  return (
    // same
  )
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 어떻게 되었을까요? 메모이제이션 없이 CPU가 6배 느려진 상태에서, 약 250개 항목이 있는 이 배열을 정렬하는 데에는 2밀리세컨드도 걸리지 않았습니다. 비교해보면, 그냥 버튼과 텍스트로 이 리스트를 렌더링하는 것은 20밀리세컨드 이상이 걸렸습니다. 10배 이상이죠! &lt;a href=&quot;https://codesandbox.io/s/measure-without-memo-tnhggk?file=/src/page.tsx&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;코드 샌드박스를 참고해보세요.&lt;/a&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;span style=&quot;background-color: #ffffff; color: #292c32;&quot;&gt;실제로는 배열이 훨씬 더 작아지고 렌더링되는 것이 훨씬 더 복잡해져 속도가 느려질 가능성이 높습니다. 따라서 성능 차이는 10배 그 이상으로 커질 것입니다.&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;span style=&quot;background-color: #ffffff; color: #292c32;&quot;&gt;배열 연산을 메모이제이션 하는 것 대신 우리는 여기에서 가장 복잡한 연산을 메모이제이션 해야 합니다. 컴포넌트를 리렌더링 하고 업데이트하는 것이죠. 이런 식으로 말입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1677515412457&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const List = ({ countries }) =&amp;gt; {
  const content = useMemo(() =&amp;gt; {
    const sortedCountries = orderBy(countries, 'name', sort);

    return sortedCountries.map((country) =&amp;gt; &amp;lt;Item country={country} key={country.id} /&amp;gt;);
  }, [countries, sort]);

  return content;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useMemo는 불필요한 리렌더링을 줄여 20밀리세컨드의 리렌더링 시간을 2밀리세컨드보다 작게 해줍니다.&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; 계산을 메모이제이션 하는 것에 대한 규칙입니다. 실제로 큰 수들의 팩토리얼을 계산하는 게 아니라면 순수한 자바스크립트 함수의 useMemo는 제거하는 것입니다. 자식 컴포넌트를 리렌더링 하는 것은 항상 병목을 일으킬 것입니다. 렌더링 트리의 무거운 부분에만 useMemo를 사용합시다.&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;왜 삭제까지 해야 하나요?&lt;/b&gt; 그냥 모든 것을 메모이제이션 하는 게 낫지 않나요? 훅을 전부 지웠을 때 퍼포먼스를 떨어뜨리는 복합적인 영향이 있지 않을까요? 여기서 1밀리세컨드, 저기서 2밀리세컨드, 이런 식이면 전체 앱이 느려지지 않을까요?&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;합당한 의문입니다. 그리고 이러한 생각은 한 가지 사실을 고려하지 않는다면 100% 유효한 의견입니다. 메모이제이션은 공짜가 아니라는 사실 말입니다. 우리가 useMemo를 사용한다면 초기 렌더링 동안 리액트는 결과값을 캐시해야 하고, 이 과정에 시간이 소요됩니다. 물론 이 시간은 적을 수 있습니다. 위 예제에서도 정렬된 국가들을 메모이제이션 하는 데에는 1밀리세컨드가 안 되는 시간이 걸렸습니다. 하지만! 이런 것들이야말로 복합적인 영향을 미치게 될 것입니다. 초기 렌더링은 여러분의 앱이 화면에 가장 먼저 등장했을 때 일어납니다. &lt;b&gt;모든&lt;/b&gt; 컴포넌트가 초기 렌더링을 거쳐 화면에 나타나게 됩니다. 수백 개의 컴포넌트가 존재하는 큰 앱의 경우, 컴포넌트들 중 1/3이 무언가를 메모이제이션 했다면 초기 렌더링 시 추가적으로 10, 20 최악의 경우 100 밀리세컨드가 추가적으로 필요할 것입니다.&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;들이 이 변경된 파트 내에서 위에서 설명한 경우에 해당될까요? 두세 개? 다섯 개라고 해봅시다. 각각의 메모이제이션을 통해 2밀리세컨드가 조금 안 되게 아낄 수 있을 것이고, 전체적으로 10밀리세컨드 이하의 시간을 아낄 수 있을 것입니다. 우리는 이 10밀리세컨드를 아낄 수도 있고, 아낄 만한 상황이 발생하지 않은 경우 아끼지 못할 수도 있습니다. 이 정도의 시간은 맨눈으로는 체감하기 어려운 시간이고, 자식 컴포넌트의 리렌더링은 이것보다 10배 이상의 시간이 걸리기 때문에 줄어든 시간은 큰 의미가 없어지게 될 것입니다. 항상 발생하는 초기 렌더링을 느리게 만드는 부담도 있는데 말이죠  .&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;옮기면서&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 useMemo와 useCallback이 필요한 상황에 대한 판단이 팀원들끼리 일치하지 않는다는 생각이 들었다. 예를 들어 나는 다른 팀원이 useMemo, useCallback을 사용한 코드를 보면서 '여기서 이게 꼭 필요한 건가?'라는 생각을 종종 했었고, 그 팀원은 내가 훅을 사용하지 않고 작성한 코드를 보면서 비슷한 생각을 했던 것 같다. 슬프게도 사내에서 이런 상황에 대한 생각을 나누고 의견을 통일시킬 수 있는 코드리뷰 시간 등이 없었기 때문에 여러 글을 읽어보며 나의 판단기준이라도 확실히 해두자고 생각했다.&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;그러던 중 발견한 이 글의 제목이 눈에 띄었다. 'you can remove most of them'이라는 부제가 내가 생각하던 '굳이, 꼭 필요한 상황인가?'라는 의문점에 대한 대답인 것처럼 느껴졌기 때문이다. 원문의 글쓴이가 글의 말미에 적어둔 것처럼 우리 프로젝트에서 불필요하게 훅이 사용되고 있는 부분들을 찾아서 없애거나 혹은 제대로 사용하고 싶다는 결심을 하며 글을 마친다.&lt;/p&gt;</description>
      <category>웹 개발/React &amp;middot; Next.js</category>
      <category>개발</category>
      <category>리액트</category>
      <category>웹개발</category>
      <category>프론트엔드</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/22</guid>
      <comments>https://9ummy.tistory.com/22#entry22comment</comments>
      <pubDate>Tue, 28 Feb 2023 01:59:51 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] 둘만의 암호 (JavaScript)</title>
      <link>https://9ummy.tistory.com/21</link>
      <description>&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;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;제출한 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1676192038336&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function solution(s, skip, index) {
    const sArr = s.split('').map((char) =&amp;gt; char.charCodeAt(0));
    const skipArr = skip.split('').map((char) =&amp;gt; char.charCodeAt(0));
    const resultArr = sArr.map((code) =&amp;gt; {
        let resultCode = code;
        let count = 0;
        while (count &amp;lt; index) {
            resultCode = resultCode + 1;
            if (resultCode &amp;gt; 122) resultCode = resultCode - 26;
            if (!skipArr.find((skipCode) =&amp;gt; skipCode === resultCode)) count++;
        }
        return resultCode;
    })
    return String.fromCharCode(...resultArr);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고한 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 skip 문자열을 split, sort 해준 후 s 문자열의 각 알파벳과 각 알파벳 + index한 문자 사이에 skip에 해당하는 문자열이 몇 개 있는지 파악한 뒤, 해당하는 개수만큼의 index를 더해주는 방향으로 문제 풀이를 했었다. 그런데 이 경우 skip에 해당하는 개수만큼 추가로 이동하는 과정에서 다시 skip에 해당하는 알파벳을 체크하지 못하는 문제가 있어서 테스트 케이스를 제외한 케이스를 통과할 수 없었다. 때문에 알파벳을 하나씩 이동해주는 과정에서 해당 알파벳이 skip 대상인지 아닌지 판별하는 식으로 접근 방향을 수정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제 출처&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘/문제풀이</category>
      <category>알고리즘</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/21</guid>
      <comments>https://9ummy.tistory.com/21#entry21comment</comments>
      <pubDate>Sun, 12 Feb 2023 18:09:24 +0900</pubDate>
    </item>
    <item>
      <title>[React] 전역상태관리 라이브러리 Recoil</title>
      <link>https://9ummy.tistory.com/20</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;0. 도입 동기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 진행 중인 프로젝트에 전역상태관리가 필요하다고 판단하게 되어 Recoil을 도입하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 Redux를 사용해본 적이 있었는데 Redux를 사용했을 때 규칙에 맞게 작성해야 하는 코드양이 많았던 것이 생각나서 이번 기회에 &lt;b&gt;&quot;React를 위한&quot;&lt;/b&gt; 상태관리 라이브러리인 Recoil은 사용성이 어떨지 경험해보고 싶었다. 또한 Redux를 도입하게 되었을 때 Redux를 처음 접하는 팀원들의 러닝커브가 높아질 것에 대한 우려도 있었다.&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;h4 data-ke-size=&quot;size20&quot;&gt;1. Recoil이란&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Recoil은 atom과 selector라는 두 가지 주요한 개념을 토대로 애플리케이션의 상태를 관리할 수 있게 해준다.&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-origin-width=&quot;924&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcSKAD/btrXqCH93GR/KWkXoQUl7VorXXP4BYy6Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcSKAD/btrXqCH93GR/KWkXoQUl7VorXXP4BYy6Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcSKAD/btrXqCH93GR/KWkXoQUl7VorXXP4BYy6Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcSKAD%2FbtrXqCH93GR%2FKWkXoQUl7VorXXP4BYy6Bk%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;924&quot; height=&quot;554&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;554&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;위 그림은 atoms(공유 상태)가 selectors(순수 함수)를 나타내는 화살표를 거쳐 컴포넌트로 내려가는 Recoil의 데이터 플로우를 그래프로 표현한 것이다. 각 컴포넌트들은 상태의 단위를 나타내는 atoms를 구독하게 되며 selectors는 atoms 상태값을 동기 또는 비동기 방식을 통해 변환하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #1c1e21;&quot;&gt;1.1. Atoms&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1c1e21;&quot;&gt;- 상태의 단위&lt;/span&gt;&lt;span style=&quot;color: #1c1e21;&quot;&gt;- atom 업데이트 시 이를 구독하는 컴포넌트는 &lt;b&gt;새로운 값을 반영하여 다시 렌더링&lt;/b&gt; 됨&lt;/span&gt;&lt;span style=&quot;color: #1c1e21;&quot;&gt;- 동일한 atom이 여러 컴포넌트에서 사용될 경우 &lt;b&gt;모든 컴포넌트는 상태를 공유&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 모든 atom은 반드시 &lt;b&gt;고유한 키&lt;/b&gt;를 가져야 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- React 컴포넌트의 상태처럼 기본값(default)을 가질 수 있음&lt;/p&gt;
&lt;pre id=&quot;code_1675005493359&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const ageState = atom({
  key: 'ageState',
  default: 24,
});&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;컴포넌트에서 atom 값을 읽고 쓰기 위해서는 훅을 사용한다. (이런 점이 Recoil이 Redux에 비해 React스러운 지점이 아닐까 싶다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useRecoilState라는 훅에 지정해준 키 값을 인자로 넣어 사용한다. 이렇게 하면 React의 useState를 쓰는 것과 거의 동일하게 상태를 사용할 수 있다.&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;atom의 상태는 사용되는 모든 컴포넌트에서 공유되기 때문에 한 컴포넌트에서 atom 값을 변경할 경우 다른 컴포넌트에서도 변경된 상태를 공유받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675005821362&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function AgeButton() {
  const [age, setAge] = useRecoilState(ageState);
  return (
  	&amp;lt;button onClick={() =&amp;gt; setAge((value) =&amp;gt; value + 1)}&amp;gt;Add Age&amp;lt;/button&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1675006095997&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function AgeTag() {
  const [age, setAge] = useRecoilState(ageState);
  return &amp;lt;p&amp;gt;I'm {age}&amp;lt;/p&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AgeButton 버튼을 클릭할 경우 ageState atom을 사용하는 AgeTag에서의 텍스트가 함께 변경된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.2. Selectors&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;nbsp; atoms나 다른 selectors를 인자로 받는 &lt;u&gt;순수 함수&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 상위의 atoms 또는 selectors가 업데이트 되면 하위의 selector 함수도 다시 실행됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 컴포넌트는 atoms를 구독하듯 selectors를 구독할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- selectors 변경 시 컴포넌트는 다시 렌더링됨&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;❓ 순수 함수&lt;br /&gt;- 부수 효과가 없는 함수&lt;br /&gt;- 어떤 함수에 &lt;b&gt;동일한 인자&lt;/b&gt;를 주었을 때 &lt;b&gt;항상 같은 값을 리턴&lt;/b&gt;하며, &lt;b&gt;외부의 상태를 변경하지 않는&lt;/b&gt; 함수&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다. &lt;b&gt;최소한의 상태&lt;/b&gt;는&amp;nbsp;atoms에 저장하고, 이로부터 파생되는 데이터는 selectors에 명시한 함수를 통해 계산함으로써 쓸모없는 상태의 보존을 방지할 수 있다.&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;실제로 이번 프로젝트의 한 컴포넌트에서 배율 정보를 나타내기 위해 두 가지 상태값을 사용하고 있었는데, 실제로 DB에 저장되는 값을 atom으로 지정하고, 이로부터 계산되어 사용되는 값을 selector 함수를 통해 계산하는 식으로 변경할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675006705710&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function MyComponent() {
  const [size, setSize] = useState(1); // DB에 저장되는 실수값
  const [sizeInput, setSizeInput] = useState(100); // 실수값을 바탕으로 계산되는 퍼센티지 값
  ...
}&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;Recoil 도입 이전에는 위처럼 실제로 DB에 저장될 실수값을 나타내는 상태와 이 실수값을 바탕으로 퍼센티지로 변환되어 input에 보여질 값을 나타내는 상태 두 가지가 따로 관리되고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675006912004&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const sizeState = atom({
  key: 'sizeState',
  default: 1,
});

const sizeInputState = selector({
  key: 'sizeInputState',
  get: ({ get }) =&amp;gt; {
  	const size = get(sizeState);
    return size * 100;
  },
});&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1675007014019&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function MyComponent() {
  const [size, setSize] = useRecoilState(sizeState);
  const sizeInput = useRecoilValue(sizeInputState);
  ...
}&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;이를 위와 같이 atom과 selector로 분리해 필요한 최소 단위 상태인 size값만 관리를 하면서도 이로부터 파생되는 필요한 값을 적절히 계산하여 사용할 수 있었다. 또한 위처럼 atom이나 selector의 값을 읽어오기만 할 때는 useRecoilValue 훅을 사용할 수 있다.&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;또한 selector에 set 프로퍼티를 추가하여 selector를 writable한 상태로 만들 수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1675576008008&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const sizeState = atom({
  key: 'sizeState',
  default: 1,
});

const sizeInputState = selector({
  key: 'sizeInputState',
  get: ({ get }) =&amp;gt; {
  	const size = get(sizeState);
    return size * 100;
  },
  set: ({ set }, sizeInput) =&amp;gt; {
    set(sizeState, sizeInput / 100);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위처럼 setter 함수를 추가하여 복수의 atom 값을 변경할 수 있다.&lt;/p&gt;</description>
      <category>웹 개발/React &amp;middot; Next.js</category>
      <author>구미구미</author>
      <guid isPermaLink="true">https://9ummy.tistory.com/20</guid>
      <comments>https://9ummy.tistory.com/20#entry20comment</comments>
      <pubDate>Mon, 30 Jan 2023 18:17:07 +0900</pubDate>
    </item>
  </channel>
</rss>