<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Developer Calssess</title>
    <link>https://calssess.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 3 Jul 2026 17:23:04 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Calssess</managingEditor>
    <item>
      <title>@types 라이브러리란?</title>
      <link>https://calssess.tistory.com/167</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트로 만들어진 써드 파티 라이브러리(jQuery, lodash, chart 등)를 타입스크립트에서 사용하려면 각 기능에 대한 타입이 정의되어 있어야 합니다. 예를 들면 아래와 같은 코드는 타입스크립트에서 제대로 동작하지 않습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;// app.ts
import $ from 'jquery';

$(document).ready();
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그 이유는 제이쿼리 라이브러리의 내부 코드에 대한 타입이 정의되어 있지 않아 이 라이브러리를 들고 와서 사용할 때 타입스크립트 파일에서 타입 추론을 할 수 없기 때문입니다. 이런 경우에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;@types&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 라이브러리를 설치하면 됩니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;npm i -D @types/jquery
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대중적으로 흔히 사용되는 자바스크립트 라이브러리는 대부분&lt;span&gt;&amp;nbsp;&lt;/span&gt;@types라는 별칭으로 타입스크립트 추론이 가능한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0385d1;&quot; href=&quot;https://www.npmjs.com/package/@types/jquery&quot;&gt;보조 라이브러리&lt;/a&gt;를 제공합니다. 만약 이 라이브러리가 없는 경우에는 [스스로 선언하거나 다른 방법](아래 내용 작성 후 링크 제공)을 찾아보셔야 합니다  &lt;/p&gt;
&lt;h2 id=&quot;types-라이브러리의-내부-구조&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #0385d1;&quot; href=&quot;https://joshua1988.github.io/ts/config/types.html#types-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%9D%98-%EB%82%B4%EB%B6%80-%EA%B5%AC%EC%A1%B0&quot;&gt;#&lt;/a&gt;@types 라이브러리의 내부 구조&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;types 라이브러리는 일반적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;index.d.ts&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일과&lt;span&gt;&amp;nbsp;&lt;/span&gt;package.json&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일로 구성되어 있습니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;└─ @types/jquery
   ├─ index.d.ts
   ├─ package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;package.json&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일 안에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;types&lt;span&gt;&amp;nbsp;&lt;/span&gt;속성이 정의되어 있습니다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #ffffff; color: #999999; text-align: start;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Keep in mind that automatic inclusion is only important if you&amp;rsquo;re using files with global declarations (as opposed to files declared as modules).&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/167</guid>
      <comments>https://calssess.tistory.com/167#entry167comment</comments>
      <pubDate>Thu, 20 Apr 2023 09:09:36 +0900</pubDate>
    </item>
    <item>
      <title>react-query 설명 및 간단 사용기</title>
      <link>https://calssess.tistory.com/166</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;react-query는 서버의 값을 클라이언트에 가져오거나, 캐싱, 값 업데이트, 에러핸들링 등 비동기 과정을 더욱 편하게 하는데 사용됩니다.&lt;/p&gt;
&lt;h2 id=&quot;사용하는-이유&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%82%E1%85%B3%E1%86%AB-%E1%84%8B%E1%85%B5%E1%84%8B%E1%85%B2&quot;&gt;#&lt;/a&gt;사용하는 이유&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저의 경우 서버로 부터 값을 가져오거나 업데이트 하는 로직을 store 내부에 개발하는 경우가 많습니다. 그렇다보니 store는 클라이언트 state를 유지해야하는데 어느 순간부터 store에 클라이언트 데이터와 서버 데이터가 공존 하게 됩니다. 그리고 그 데이터가 서로 상호작용하면서 서버 데이터도 클라이언트 데이터도 아닌 끔찍한 혼종(?)이 탄생하게 됩니다. (예를 들면 서버에는 이미 패치된 데이터가 클라이언트에서는 패치되기 전 데이터가 유저에게 사용되고 있는 것이라고 볼 수 있습니다.)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그래서 react-query를 사용함으로 서버, 클라이언트 데이터를 분리합니다. 이 개념에 대해 동의 하지 않아도 아래의 장점을 보신다면 사용하고 싶은 생각이 드실 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;react-query-장점&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#react-query-%E1%84%8C%E1%85%A1%E1%86%BC%E1%84%8C%E1%85%A5%E1%86%B7&quot;&gt;#&lt;/a&gt;react-query 장점&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여러가지 장점이 있지만 주로 아래와 같이 프론트 개발자가 구현하기 귀찮은 일들을 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;캐싱&lt;/li&gt;
&lt;li&gt;get을 한 데이터에 대해 update를 하면 자동으로 get을 다시 수행한다. (예를 들면 게시판의 글을 가져왔을 때 게시판의 글을 생성하면 게시판 글을 get하는 api를 자동으로 실행 )&lt;/li&gt;
&lt;li&gt;데이터가 오래 되었다고 판단되면 다시 get (invalidateQueries)&lt;/li&gt;
&lt;li&gt;동일 데이터 여러번 요청하면 한번만 요청한다. (옵션에 따라 중복 호출 허용 시간 조절 가능)&lt;/li&gt;
&lt;li&gt;무한 스크롤 (&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://react-query.tanstack.com/guides/infinite-queries&quot;&gt;Infinite Queries&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;(opens new window)&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;비동기 과정을 선언적으로 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;react hook과 사용하는 구조가 비슷하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;사용하기&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5&quot;&gt;#&lt;/a&gt;사용하기&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;본격적으로 react-query에 대해 알아보겠습니다. 먼저 react 프로젝트를 만듭니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;dsconfig&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;npx create-react-app my-app
cd my-app
yarn install react-query
yarn install &amp;amp;&amp;amp; yarn start&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 react의 가장 기본이 되는 곳에 react-query를 사용하도록 세팅합니다.&lt;/p&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;// src/index.js
import React from &quot;react&quot;;
import ReactDOM from &quot;react-dom&quot;;
import App from &quot;./App&quot;;
import { QueryClient, QueryClientProvider } from &quot;react-query&quot;;
import { ReactQueryDevtools } from &quot;react-query/devtools&quot;;

const queryClient = new QueryClient();

ReactDOM.render(
  &amp;lt;React.StrictMode&amp;gt;
    &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
      {/* devtools */}
      &amp;lt;ReactQueryDevtools initialIsOpen={true} /&amp;gt;
      &amp;lt;App /&amp;gt;
    &amp;lt;/QueryClientProvider&amp;gt;
  &amp;lt;/React.StrictMode&amp;gt;,
  document.getElementById(&quot;root&quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;api&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#api&quot;&gt;#&lt;/a&gt;api&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용하는 api를 알아본 후 예시를 보도록 하겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;usequery&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#usequery&quot;&gt;#&lt;/a&gt;useQuery&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 get 하기 위한 api입니다. post, update는&lt;span&gt;&amp;nbsp;&lt;/span&gt;useMutation을 사용합니다.&lt;/li&gt;
&lt;li&gt;첫번째 파라미터로 unique Key가 들어가고, 두번째 파라미터로 비동기 함수(api호출 함수)가 들어갑니다. (당연한 말이지만 두번째 파라미터는 promise가 들어가야합니다.)&lt;/li&gt;
&lt;li&gt;첫번째 파라미터로 설정한 unique Key는 다른 컴포넌트에서도 해당 키를 사용하면 호출 가능합니다. unique Key는 string과 배열을 받습니다. 배열로 넘기면 0번 값은 string값으로 다른 컴포넌트에서 부를 값이 들어가고 두번째 값을 넣으면 query 함수 내부에 파라미터로 해당 값이 전달됩니다.&lt;/li&gt;
&lt;li&gt;return 값은 api의 성공, 실패여부, api return 값을 포함한 객체입니다.&lt;/li&gt;
&lt;li&gt;useQuery는&lt;span&gt;&amp;nbsp;&lt;/span&gt;비동기로 작동합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;즉, 한 컴포넌트에 여러개의 useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는 것이 아닌 두개의 useQuery가 동시에 실행됩니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;여러개의 비동기 query가 있다면 useQuery보다는 밑에 설명해 드릴&lt;span&gt;&amp;nbsp;&lt;/span&gt;useQueries를 권유드립니다.&lt;/li&gt;
&lt;li&gt;enabled를 사용하면 useQuery를 동기적으로 사용 가능합니다. 아래 예시로 추가 설명하겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;예시&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#%E1%84%8B%E1%85%A8%E1%84%89%E1%85%B5&quot;&gt;#&lt;/a&gt;예시&lt;/h3&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;const Todos = () =&amp;gt; {
  const { isLoading, isError, data, error } = useQuery(&quot;todos&quot;, fetchTodoList, {
    refetchOnWindowFocus: false, // react-query는 사용자가 사용하는 윈도우가 다른 곳을 갔다가 다시 화면으로 돌아오면 이 함수를 재실행합니다. 그 재실행 여부 옵션 입니다.
    retry: 0, // 실패시 재호출 몇번 할지
    onSuccess: data =&amp;gt; {
      // 성공시 호출
      console.log(data);
    },
    onError: e =&amp;gt; {
      // 실패시 호출 (401, 404 같은 error가 아니라 정말 api 호출이 실패한 경우만 호출됩니다.)
      // 강제로 에러 발생시키려면 api단에서 throw Error 날립니다. (참조: https://react-query.tanstack.com/guides/query-functions#usage-with-fetch-and-other-clients-that-do-not-throw-by-default)
      console.log(e.message);
    }
  });

  if (isLoading) {
    return &amp;lt;span&amp;gt;Loading...&amp;lt;/span&amp;gt;;
  }

  if (isError) {
    return &amp;lt;span&amp;gt;Error: {error.message}&amp;lt;/span&amp;gt;;
  }

  return (
    &amp;lt;ul&amp;gt;
      {data.map(todo =&amp;gt; (
        &amp;lt;li key={todo.id}&amp;gt;{todo.title}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;isLoading, isSuccess 말고&lt;span&gt;&amp;nbsp;&lt;/span&gt;status로 한번에 처리 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;function Todos() {
  const { status, data, error } = useQuery(&quot;todos&quot;, fetchTodoList);

  if (status === &quot;loading&quot;) {
    return &amp;lt;span&amp;gt;Loading...&amp;lt;/span&amp;gt;;
  }

  if (status === &quot;error&quot;) {
    return &amp;lt;span&amp;gt;Error: {error.message}&amp;lt;/span&amp;gt;;
  }

  return (
    &amp;lt;ul&amp;gt;
      {data.map(todo =&amp;gt; (
        &amp;lt;li key={todo.id}&amp;gt;{todo.title}&amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  );
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;usequery-동기적으로-실행&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#usequery-%E1%84%83%E1%85%A9%E1%86%BC%E1%84%80%E1%85%B5%E1%84%8C%E1%85%A5%E1%86%A8%E1%84%8B%E1%85%B3%E1%84%85%E1%85%A9-%E1%84%89%E1%85%B5%E1%86%AF%E1%84%92%E1%85%A2%E1%86%BC&quot;&gt;#&lt;/a&gt;useQuery 동기적으로 실행&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 설명드린대로&lt;span&gt;&amp;nbsp;&lt;/span&gt;enabled&lt;span&gt;&amp;nbsp;&lt;/span&gt;옵션을 사용하면 useQuery를 동기적으로 사용 가능합니다.&lt;/li&gt;
&lt;li&gt;useQuery의 3번째 인자로 옵션값이 들어가는데 그 옵션의 enabled에 값을 넣으면 그 값이 true일때 useQuery를 실행합니다. 이것을 이용하면 동기적으로 함수를 실행 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;aspectj&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;const { data: todoList, error, isFetching } = useQuery(&quot;todos&quot;, fetchTodoList);
const { data: nextTodo, error, isFetching } = useQuery(
  &quot;nextTodos&quot;,
  fetchNextTodoList,
  {
    enabled: !!todoList // true가 되면 fetchNextTodoList를 실행한다
  }
);&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;usequeries&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#usequeries&quot;&gt;#&lt;/a&gt;useQueries&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;useQuery를 비동기로 여러개 실행할 경우 여러 귀찮은 경우가 생깁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;const usersQuery = useQuery(&quot;users&quot;, fetchUsers);
const teamsQuery = useQuery(&quot;teams&quot;, fetchTeams);
const projectsQuery = useQuery(&quot;projects&quot;, fetchProjects);

// 어짜피 세 함수 모두 비동기로 실행하는데, 세 변수를 개발자는 다 기억해야하고 세 변수에 대한 로딩, 성공, 실패처리를 모두 해야한다.&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때&lt;span&gt;&amp;nbsp;&lt;/span&gt;promise.all처럼 useQuery를 하나로 묶을 수 있는데, 그것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;useQueries입니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;promise.all과 마찬가지로 하나의 배열에 각 쿼리에 대한 상태 값이 객체로 들어옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;// 아래 예시는 롤 룬과, 스펠을 받아오는 예시입니다.
const result = useQueries([
  {
    queryKey: [&quot;getRune&quot;, riot.version],
    queryFn: () =&amp;gt; api.getRunInfo(riot.version)
  },
  {
    queryKey: [&quot;getSpell&quot;, riot.version],
    queryFn: () =&amp;gt; api.getSpellInfo(riot.version)
  }
]);

useEffect(() =&amp;gt; {
  console.log(result); // [{rune 정보, data: [], isSucces: true ...}, {spell 정보, data: [], isSucces: true ...}]
  const loadingFinishAll = result.some(result =&amp;gt; result.isLoading);
  console.log(loadingFinishAll); // loadingFinishAll이 false이면 최종 완료
}, [result]);&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;unique-key-활용&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#unique-key-%E1%84%92%E1%85%AA%E1%86%AF%E1%84%8B%E1%85%AD%E1%86%BC&quot;&gt;#&lt;/a&gt;unique key 활용&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 unique key를 배열로 넣으면 query함수 내부에서 변수로 사용 가능하다고 말씀드렸습니다. 그것을 활용하면 아래와 같습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;params를 주목해주세요&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;stata&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;const riot = {
  version: &quot;12.1.1&quot;
};

const result = useQueries([
  {
    queryKey: [&quot;getRune&quot;, riot.version],
    queryFn: params =&amp;gt; {
      console.log(params); // {queryKey: ['getRune', '12.1.1'], pageParam: undefined, meta: undefined}

      return api.getRunInfo(riot.version);
    }
  },
  {
    queryKey: [&quot;getSpell&quot;, riot.version],
    queryFn: () =&amp;gt; api.getSpellInfo(riot.version)
  }
]);&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;querycache&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#querycache&quot;&gt;#&lt;/a&gt;QueryCache&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쿼리에 대해 성공, 실패 전처리를 할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) =&amp;gt; {
      console.log(error, query);
      if (query.state.data !== undefined) {
        toast.error(`에러가 났어요!!: ${error.message}`);
      },
    },
    onSuccess: data =&amp;gt; {
      console.log(data)
    }
  })
});&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;usemutation&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#usemutation&quot;&gt;#&lt;/a&gt;useMutation&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값을 바꿀때 사용하는 api입니다. return 값은 useQuery와 동일하고 간단히 예시코드로도 충분히 설명을 대신할 수 있으니 바로 코드로 들어가겠습니다.&lt;/li&gt;
&lt;li&gt;아래는 간단한 로그인 예시입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;coffeescript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;import { useState, useContext, useEffect } from &quot;react&quot;;
import loginApi from &quot;api&quot;;
import { useMutation } from &quot;react-query&quot;;

const Index = () =&amp;gt; {
  const [id, setId] = useState(&quot;&quot;);
  const [password, setPassword] = useState(&quot;&quot;);

  const loginMutation = useMutation(loginApi, {
    onMutate: variable =&amp;gt; {
      console.log(&quot;onMutate&quot;, variable);
      // variable : {loginId: 'xxx', password; 'xxx'}
    },
    onError: (error, variable, context) =&amp;gt; {
      // error
    },
    onSuccess: (data, variables, context) =&amp;gt; {
      console.log(&quot;success&quot;, data, variables, context);
    },
    onSettled: () =&amp;gt; {
      console.log(&quot;end&quot;);
    }
  });

  const handleSubmit = () =&amp;gt; {
    loginMutation.mutate({ loginId: id, password });
  };

  return (
    &amp;lt;div&amp;gt;
      {loginMutation.isSuccess ? &quot;success&quot; : &quot;pending&quot;}
      {loginMutation.isError ? &quot;error&quot; : &quot;pending&quot;}
      &amp;lt;input type=&quot;text&quot; value={id} onChange={e =&amp;gt; setId(e.target.value)} /&amp;gt;
      &amp;lt;input
        type=&quot;password&quot;
        value={password}
        onChange={e =&amp;gt; setPassword(e.target.value)}
      /&amp;gt;
      &amp;lt;button onClick={handleSubmit}&amp;gt;로그인&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

export default Index;&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;update후에-get-다시-실행&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#update%E1%84%92%E1%85%AE%E1%84%8B%E1%85%A6-get-%E1%84%83%E1%85%A1%E1%84%89%E1%85%B5-%E1%84%89%E1%85%B5%E1%86%AF%E1%84%92%E1%85%A2%E1%86%BC&quot;&gt;#&lt;/a&gt;update후에 get 다시 실행&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;react-query 장점으로 update후에 get 함수를 간단히 재실행 할 수 있다고 하였습니다.&lt;/li&gt;
&lt;li&gt;mutation 함수가 성공할 때, unique key로 맵핑된 get 함수를&lt;span&gt;&amp;nbsp;&lt;/span&gt;invalidateQueries에 넣어주면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;const mutation = useMutation(postTodo, {
  onSuccess: () =&amp;gt; {
    // postTodo가 성공하면 todos로 맵핑된 useQuery api 함수를 실행합니다.
    queryClient.invalidateQueries(&quot;todos&quot;);
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 mutation에서 return된 값을 이용해서 get 함수의 파라미터를 변경해야할 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;setQueryData를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;qml&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;const queryClient = useQueryClient();

const mutation = useMutation(editTodo, {
  onSuccess: data =&amp;gt; {
    // data가 fetchTodoById로 들어간다
    queryClient.setQueryData([&quot;todo&quot;, { id: 5 }], data);
  }
});

const { status, data, error } = useQuery([&quot;todo&quot;, { id: 5 }], fetchTodoById);

mutation.mutate({
  id: 5,
  name: &quot;nkh&quot;
});&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;react-suspense와-react-query-사용하기&quot; style=&quot;background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#react-suspense%E1%84%8B%E1%85%AA-react-query-%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5&quot;&gt;#&lt;/a&gt;react Suspense와 react-query 사용하기&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;react-query를 사용하는 또 하나의 이유입니다. 비동기를 좀 더 선언적 사용할 수 있어서 많이 사용하는 것 같습니다&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://ko.reactjs.org/docs/concurrent-mode-suspense.html&quot;&gt;Suspense&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;(opens new window)&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;를 사용하며 loading을,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #3eaf7c;&quot; href=&quot;https://ko.reactjs.org/docs/error-boundaries.html#gatsby-focus-wrapper&quot;&gt;Error buundary&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;(opens new window)&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;를 사용하여 에러 핸들링을 더욱 직관적으로 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;suspense를 사용하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;QueryClient에 옵션을 하나 추가합니다. 아래 방법은 global하게 suspense를 사용한다고 정의할 때 예시입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;// src/index.js
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 0,
      suspense: true
    }
  }
});

ReactDOM.render(
  &amp;lt;React.StrictMode&amp;gt;
    &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
      &amp;lt;App /&amp;gt;
    &amp;lt;/QueryClientProvider&amp;gt;
  &amp;lt;/React.StrictMode&amp;gt;,
  document.getElementById(&quot;root&quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래는 함수마다 suspense를 사용하는 예시입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;const { data } = useQurey(&quot;test&quot;, testApi, { suspense: true });&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #2c3e50; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위처럼 suspense 세팅을 완료했다면 react에서 제공하는 Suspense를 사용하면됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #282c34; color: #2c3e50; text-align: start;&quot;&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;color: #cccccc; text-align: left;&quot;&gt;&lt;code&gt;const { data } = useQurey(&quot;test&quot;, testApi, { suspense: true });

return (
  // isLoading이 true이면 Suspense의 fallback 내부 컴포넌트가 보여집니다.
  // isError가 true이면 ErrorBoundary의 fallback 내부 컴포넌트가 보여집니다.
  &amp;lt;Suspense fallback={&amp;lt;div&amp;gt;loading&amp;lt;/div&amp;gt;}&amp;gt;
    &amp;lt;ErrorBoundary fallback={&amp;lt;div&amp;gt;에러 발생&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;div&amp;gt;{data}&amp;lt;/div&amp;gt;
    &amp;lt;/ErrorBoundary&amp;gt;
  &amp;lt;/Supense&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;div data-v-49140617=&quot;&quot;&gt;&lt;span data-v-49140617=&quot;&quot;&gt; Copied! &lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c3e50; text-align: center;&quot;&gt;
&lt;div id=&quot;aswift_1_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://kyounghwan01.github.io/blog/React/react-query/basic/#react-suspense%E1%84%8B%E1%85%AA-react-query-%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kyounghwan01.github.io/blog/React/react-query/basic/#react-suspense%E1%84%8B%E1%85%AA-react-query-%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5&lt;/a&gt;&lt;/p&gt;</description>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/166</guid>
      <comments>https://calssess.tistory.com/166#entry166comment</comments>
      <pubDate>Mon, 13 Mar 2023 15:22:43 +0900</pubDate>
    </item>
    <item>
      <title>초보 프론트엔드 개발자들을 위한 Pub-Sub(Publish-Subscribe) 패턴을 알아보기2018/10/28</title>
      <link>https://calssess.tistory.com/165</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #282c35; color: #eeeeee; text-align: start;&quot;&gt;
&lt;h2 id=&quot;번역-머리말&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #ffffff;&quot;&gt;번역 머리말&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 글은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://softwarehut.com/blog/author/hubert-zub/&quot;&gt;Hubert Zub&lt;/a&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://itnext.io/why-every-beginner-front-end-developer-should-know-publish-subscribe-pattern-72a12cd68d44&quot;&gt;Why every beginner front-end developer should know publish-subscribe pattern?&lt;/a&gt;을 번역한 글입니다.&lt;/li&gt;
&lt;li&gt;일부 삽화의 내용은 직접 번역하여 새로 첨부했습니다. 가독성에 문제가 있다면 피드백 부탁드립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러분이 처음 프론트엔드 개발을 배우고 나서 스타일이나 그리드 시스템 등 미적인 부분에만 집중을 하다가 비지니스 로직, 프레임워크 등을 공부하면서 본격적으로 자바스크립트 코드를 작성하기 시작할 때를 떠올려 보겠습니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLrfpw/btr3eePDkji/GPKbokv2OK0SbA93ZAGAU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLrfpw/btr3eePDkji/GPKbokv2OK0SbA93ZAGAU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLrfpw/btr3eePDkji/GPKbokv2OK0SbA93ZAGAU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLrfpw%2Fbtr3eePDkji%2FGPKbokv2OK0SbA93ZAGAU0%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;1280&quot; height=&quot;534&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;[처음 시작은 이렇지만..]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서 단순히 jQuery 트릭을 조금 쓰거나 시각적 효과 일부를 JS 로 구현하는 정도를 벗어나게 됩니다. 단순히 웹페이지가 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;웹 애플리케이션&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 만들기 위한 큰 그림을 그리게 되죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS 코드를 작성하는데 많은 노력을 들이면서 상호작용, 세부 시스템이나 로직을 생각하기 시작하겠죠. 앱이 잘 동작하기 시작하면서 살아움직이는 듯한 기분이 듭니다. 완전히 새롭고 신나는 세계가 펼쳐집니다. 하지만 그러면서 새로운 문제를 마주하게 됩니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;609&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OhxZg/btr3vNJOYFQ/eSZ4IyRvk25aQYDr1IePz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OhxZg/btr3vNJOYFQ/eSZ4IyRvk25aQYDr1IePz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OhxZg/btr3vNJOYFQ/eSZ4IyRvk25aQYDr1IePz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOhxZg%2Fbtr3vNJOYFQ%2FeSZ4IyRvk25aQYDr1IePz0%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;1280&quot; height=&quot;609&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;609&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;[&amp;hellip;결국은 이렇게 끝이 납니다. 그리고 끝이 없어요!]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여러분은 실망하지 않습니다. 새로운 아이디어는 계속 떠오르고, 더욱 많은 코드를 작성합니다. 블로그 포스트에서 본 다양한 기술이나 방법론을 적용하고, 문제 해결을 위한 온갖 접근법들을 (어설프게나마) 주물럭거려봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다 갑자기 가려운 기분을 느끼기 시작합니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;647&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZnAKp/btr3efOwWLj/GzpTye4zh7VyKaIIfV5htK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZnAKp/btr3efOwWLj/GzpTye4zh7VyKaIIfV5htK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZnAKp/btr3efOwWLj/GzpTye4zh7VyKaIIfV5htK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZnAKp%2Fbtr3efOwWLj%2FGzpTye4zh7VyKaIIfV5htK%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;1140&quot; height=&quot;647&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;647&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;처음 작성한&lt;span&gt;&amp;nbsp;&lt;/span&gt;script.js&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일이 커지면서 한 시간 전에는 200 줄 정도였던 코드가 이제 500 줄이 넘어가기 시작했습니다. &amp;ldquo;흠, 별 문제는 아냐&amp;rdquo; 라고 생각합니다. 깔끔하고 유지보수가 용이한 코드에 대한 글을 읽어봤으니, 문제를 해결하기 위해 로직이나 블록, 컴포넌트별로 파일을 분리하기 시작합니다. 다시 그럴싸한 모양새의 프로젝트가 되었습니다. 모든 것은 꼼꼼하게 정리된 라이브러리처럼 보입니다. 여러 파일이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;적절하게 이름이 붙어있고 적절한 디렉토리에 있기 때문&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 모듈화하고 유지보수하기 좋게 되었는데도 갑자기 또 가려운 기분이 들기 시작합니다. 하지만 이번에는 뭐가 문제인지 잘 파악이 안됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션(웹 앱)은 선형적으로 동작하는 일이 거의 없습니다. 사실 어떤 웹 앱이든 많은 액션은 갑자기 (때로는 기대하지 않았을 때나 자발적으로) 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱은 네트워크 이벤트, 사용자의 조작, 타이밍이 설정된 동작 등 여러 종류의 비동기적인 동작에 적절하게 응답해야 합니다. 그렇게 갑자기 &amp;ldquo;비동기&amp;rdquo; 와 &amp;ldquo;경합 상태&amp;rdquo; 라 불리는 괴물들이 문을 두드립니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnUqL6/btr3eV3n4xo/Eh3NkwKkI1xwTcTNKoXqUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnUqL6/btr3eV3n4xo/Eh3NkwKkI1xwTcTNKoXqUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnUqL6/btr3eV3n4xo/Eh3NkwKkI1xwTcTNKoXqUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnUqL6%2Fbtr3eV3n4xo%2FEh3NkwKkI1xwTcTNKoXqUK%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;1280&quot; height=&quot;772&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;멋지게 모듈화된 코드가 비동기 코드라는 못난 배우자와 짝을 맺어야하는 상황이 되었습니다. 이제 가려운 기분이 어디서 오는지 명확해졌습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;ldquo;대체 이놈의 비동기 코드를 어느 부분에 두어야 하지?&amp;rdquo;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라고 어려운 질문이 고개를 들기 시작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 앱은 아름답게 블록 단위로 구성되어 있을 겁니다. 페이지 이동 및 컨텐츠를 구성하는 컴포넌트는 적절한 디렉토리에 깔끔하게 놓일 수 있고, 자그마한 헬퍼 스크립트 파일들은 코드를 반복해서 쓰지 않고 자잘한 일들을 처리할 수 있습니다. 모든 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;app.js&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 하나의 엔트리 파일로 관리되고 구동됩니다. 깔끔하죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여러분의 목표는 비동기 코드를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;앱의 한 부분에서 실행하여 처리하고 다른 부분으로 보내는 것입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 코드가 UI 컴포넌트 안에 있어야 할까요? 아니면 메인 파일에? 앱의 어떤 부분에서 반응(reaction)을 다루는 책임을 가지고 있어야 할까요? 데이터 처리는? 에러 처리는? 마음속에서 다양한 접근 방식을 시도해보지만 불편한 기분은 가시지 않습니다. 앱을 더 확장하고 싶어도 쉽지 않으리라는 사실도 알고 알고 있습니다. 가려운 기분은 여전히 사라지지 않았고, 더 이상적이고 다양한 상황에 대응 가능한 해답이 필요해졌습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #999999;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안심하세요. 여러분이 뭘 잘못한 것은 아닙니다. 사실 더 구조화될수록 생각을 할 수록 가려운 기분은 더욱 심해지게 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 위의 문제를 해결하기 위한 글을 찾아서 읽어보거나 이미 준비된 솔루션들을 찾아보게 됩니다. 처음에는 프라미스(Promise)가 콜백보다 낫다는 글을 보게 되고, 그 다음에는 RxJS 가 무엇인지 이해하려고 머리를 싸매게 됩니다(그리고 인터넷의 어떤 사람이 &amp;ldquo;RxJS 는 인류가 웹 개발을 하는데 있어 정당한 구원자&amp;rdquo; 라고 주장하는 이유를 찾기도 합니다). 더 많은 글을 읽다 보니 왜 어떤 사람은&lt;span&gt;&amp;nbsp;&lt;/span&gt;redux&lt;span&gt;&amp;nbsp;&lt;/span&gt;를&lt;span&gt;&amp;nbsp;&lt;/span&gt;redux-thunk&lt;span&gt;&amp;nbsp;&lt;/span&gt;없이 쓴다는건 말이 안된다고 이야기하고, 다른 사람은&lt;span&gt;&amp;nbsp;&lt;/span&gt;redux-saga&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 가지고 똑같은 소리를 하는지 이해하려고 아둥바둥하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국에는 혼란스러운 말들만 머리 속에 가득 차서 두통이 생길 지경이 되었습니다. 문제 해결을 위한 방대한 양의 접근 방법 때문에 멘탈은 터져나갈 것 같고요. 왜 이렇게 많은 방법이 있는걸까요? 좀 쉽게 해결할 수 없을까요? 아니면 인터넷에 있는 사람들이 하나의 좋은 패턴을 사용하는 대신에 치고박고 싸우는 것을 좋아하는 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 주제가 사소하지 않기 때문입니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #999999;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 프레임워크를 사용하더라도, 비동기 코드를 적절하게 배치하는 일은 지금도, 앞으로도 결코 간단하지 않을 것입니다. 모든 목적에 부합하는 하나의 완성된 솔루션은 존재하지 않습니다. 요구사항, 환경, 필요로 하는 결과 등 다양한 요소에 크게 달라지기 때문입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 글을 통해 모든 문제를 해결하는 완벽한 방법을 제공하는 것도 아닙니다. 하지만 여러분들이 비동기 코드를 조금 더 쉽게 생각할 수 있도록 도움이 되었으면 합니다. 왜냐면 위에 나온 모든 기술들은 아주 간단한 원칙을 기반으로 하고 있기 때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;공통적인-부분&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;공통적인 부분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정한 관점에서, 프로그래밍 언어들은 구조적으로 복잡하게 되어있지 않습니다. 어쨌든 값을 어딘가에 저장하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;if&lt;span&gt;&amp;nbsp;&lt;/span&gt;문들이나 함수 호출을 통해 흐름을 제어하여 계산을 처리해주는 단순한 기계들(dumb calculator-like machines)에 불과합니다. 명령형이면서 약간은 객체지향형인 언어(역주: 그리고 함수형)로서 자바스크립트도 저 기계들의 한 종류에 불과합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말인 즉슨 기본적으로 모든 비동기 세계에서 나온 물건들은 (redux-saga 건, RxJS 건, observable 이나 또 다른 변종이던) 반드시 같은 기본 원리에 의존한다는 뜻입니다. 이 라이브러리들의 마법같아 보이는 동작은 실제로 마법이 아닙니다. 그저 잘 알려진 기초 위에 만들어졌으며, 아주 심층적인 부분은 새로 발명된 것이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사실을 아는게 뭐가 그리 중요할까요? 예를 한번 들어보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;뭔가-만들어그리고-부숴봅시다&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;뭔가 만들어(그리고 부숴)봅시다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 간단한 애플리케이션을 생각해봅시다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;아주 간단한&lt;span&gt;&amp;nbsp;&lt;/span&gt;걸로요. 예를 들어 지도에 우리가 제일 좋아하는 장소를 표시해 두는 작은 앱이 있습니다. 특별히 대단한 구석은 없어요. 그냥 오른쪽에는 지도가 있고 왼쪽에는 단순히 사이드바가 있는 형태입니다. 지도를 클릭하면 그 위에 새로운 마커가 표시됩니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wBlzt/btr3iJBAlVK/ErRs6fmVmgynIFajxUNikk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wBlzt/btr3iJBAlVK/ErRs6fmVmgynIFajxUNikk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wBlzt/btr3iJBAlVK/ErRs6fmVmgynIFajxUNikk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwBlzt%2Fbtr3iJBAlVK%2FErRs6fmVmgynIFajxUNikk%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;1200&quot; height=&quot;675&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;물론 약간 욕심을 가지고 조금 기능을 추가할 예정입니다. 점찍어둔 장소의 리스트를 로컬 스토리지에 저장하려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 세부 사항을 기반으로 앱의 기본적인 동작 흐름을 차트로 그려보겠습니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;709&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oMRg8/btr3ky0Q1de/aqO15ErXMpY0Wawd4ja8B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oMRg8/btr3ky0Q1de/aqO15ErXMpY0Wawd4ja8B0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oMRg8/btr3ky0Q1de/aqO15ErXMpY0Wawd4ja8B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoMRg8%2Fbtr3ky0Q1de%2FaqO15ErXMpY0Wawd4ja8B0%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;1280&quot; height=&quot;709&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;709&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;보시다시피 그리 복잡하진 않을겁니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #999999;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜토리얼을 간략히 하기 위해 아래의 예는 어떠한 프레임워크나 UI 라이브러리도 사용하지 않을 예정입니다. 그저 바닐라 자바스크립트만 들어있습니다. 그리고 Google Maps API 를 일부 사용합니다. 비슷한 앱을 직접 만들고자 하시면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://cloud.google.com/maps-platform/#get-started&quot;&gt;이 링크를 통해&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;API 키를 등록하셔야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자 그러면 코딩을 좀 해서 간단한 프로토타입을 만들어보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;livescript&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;0&quot;&gt;&lt;code&gt;/* map.js */let googleMap;let myPlaces = [];function init() {  googleMap = new google.maps.Map(document.getElementById('map'), {    center: { lat: 0, lng: 0 },    zoom: 3  });  googleMap.markerList = [];  googleMap.addListener('click', addPlace);  const placesFromLocalstorage = JSON.parse(localStorage.getItem('myPlaces'));  // localStorage에 뭔가 있으면 현재 장소 리스트로 설정한다  if (Array.isArray(placesFromLocalstorage)) {    myPlaces = placesFromLocalstorage;    renderMarkers();  }}function addPlace(event) {  myPlaces.push({    position: event.latLng  });  // 마커가 추가되면 랜더링하면서 localStorage와 동기화한다  localStorage.setItem('myPlaces', JSON.stringify(myPlaces));  renderMarkers();}function renderMarkers() {  googleMap.markerList.forEach(m =&amp;gt; m.setMap(null)); // 모든 마커 제거  googleMap.markerList = [];  // myPlaces 배열의 요소를 기반으로 마커를 추가한다  myPlaces.forEach(place =&amp;gt; {    const marker = new google.maps.Marker({      position: place.position,      map: googleMap    });    googleMap.markerList.push(marker);  }):}init();&lt;/code&gt;&lt;/pre&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;init()&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수는 Google Maps API 를 사용하여 지도를 활성화하고, 지도에 클릭 액션을 설정한 뒤&lt;span&gt;&amp;nbsp;&lt;/span&gt;localStorage&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 있는 마커들을 불러오려 합니다.&lt;/li&gt;
&lt;li&gt;addPlace()&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 지도를 클릭할 때 리스트에 새로운 장소를 추가하고 마커 랜더링을 실행합니다.&lt;/li&gt;
&lt;li&gt;renderMarkers()&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 배열 안에 있는 장소들을 순회하여 지도를 정리한 다음에 그 위에 마커를 그립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 불완전해 보이는 부분은 잠시 치워두겠습니다(몽키 패칭이라거나, 에러 처리가 없다거나). 이정도면 충분히 깔끔하게 프로토타입 역할을 합니다. 이제 마크업을 좀 추가하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;html&quot; data-index=&quot;1&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;  &amp;lt;title&amp;gt;My Favorite Places&amp;lt;/title&amp;gt;  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;styles.css&quot; /&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;  &amp;lt;div class=&quot;sidebar&quot;&amp;gt;    &amp;lt;h1&amp;gt;My fav places on earth v1.0&amp;lt;/h1&amp;gt;    &amp;lt;!-- footer를 여기에 둔다 --&amp;gt;  &amp;lt;/div&amp;gt;  &amp;lt;div class=&quot;main-content-area&quot;&amp;gt;    &amp;lt;div id=&quot;map&quot;&amp;gt;&amp;lt;/div&amp;gt;  &amp;lt;/div&amp;gt;  &amp;lt;script src=&quot;https://maps.googleapis.com/maps/api/js?key=API_KEY_HERE&quot;&amp;gt;&amp;lt;/script&amp;gt;  &amp;lt;script src=&quot;map.js&quot;&amp;gt;&amp;lt;/script&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;약간의 스타일을 추가했다고 가정하고 만들었습니다. (스타일은 이번 글에 직접적인 관련은 없으므로 따로 올리지 않겠습니다.) 믿거나 말거나 이 앱은 제대로 동작합니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXi9i6/btr3uFLOtNZ/jKhzKDwp8MKgA6a4HeHmVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXi9i6/btr3uFLOtNZ/jKhzKDwp8MKgA6a4HeHmVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXi9i6/btr3uFLOtNZ/jKhzKDwp8MKgA6a4HeHmVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXi9i6%2Fbtr3uFLOtNZ%2FjKhzKDwp8MKgA6a4HeHmVK%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;1280&quot; height=&quot;778&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;778&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;좀 못생겼지만 잘 동작합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;하지만 확장할 수가 없습니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;아이고야.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 함수들의 책임이 서로 뒤섞여있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;http://bit.ly/2Ptbaiw&quot;&gt;SOLID 원칙&lt;/a&gt;에 대해 들어보신 적 있다면, 벌써부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://ko.wikipedia.org/wiki/%EB%8B%A8%EC%9D%BC_%EC%B1%85%EC%9E%84_%EC%9B%90%EC%B9%99&quot;&gt;단일 책임 원칙&lt;/a&gt;을 위배하고 있다는 것을 이미 파악하셨을 겁니다. 예제 코드 자체는 단순하지만 하나의 코드 파일이 사용자 액션과 데이터 다루기, 그리고 동기화를 모두 담당하고 있습니다. 이렇게 하면 안 됩니다. 왜냐고요?&lt;span&gt;&amp;nbsp;&lt;/span&gt;에이, 그래도 잘 동작 하잖아요&lt;span&gt;&amp;nbsp;&lt;/span&gt;라고 할 수도 있겠습니다. 하지만 다음에 추가할 기능을 생각해보면 유지보수하기 아주 어려운 형태입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 방식으로 설득해보겠습니다. 우리가 앱을 확장하여 아래와 같은 새 기능을 추가한다고 합시다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qn7cM/btr3efOwWFG/GWsmsPlzTDJFh8OOGuZyZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qn7cM/btr3efOwWFG/GWsmsPlzTDJFh8OOGuZyZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qn7cM/btr3efOwWFG/GWsmsPlzTDJFh8OOGuZyZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqn7cM%2Fbtr3efOwWFG%2FGWsmsPlzTDJFh8OOGuZyZ1%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;1200&quot; height=&quot;675&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로 우리는 사이드바에 표시 된 장소들의 리스트를 놓고, 두 번째로 Google API 를 사용하여 도시 이름을 표시하고 싶습니다. 여기서 비동기 메커니즘(동작 원리, mechanism)이 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 새로운 플로우차트는 이렇게 됩니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;925&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfuKL8/btr3dFfDp1D/Zm3rPxkun4E46twkt94YmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfuKL8/btr3dFfDp1D/Zm3rPxkun4E46twkt94YmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfuKL8/btr3dFfDp1D/Zm3rPxkun4E46twkt94YmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfuKL8%2Fbtr3dFfDp1D%2FZm3rPxkun4E46twkt94YmK%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;1280&quot; height=&quot;925&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;925&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;[참고: 도시 이름을 찾는 방법은 아주 어려운게 아닙니다. 아주 쉬운 Google Maps API 가 제공됩니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://developers.google.com/maps/documentation/geocoding/intro#ReverseGeocoding&quot;&gt;여기서 확인해보세요!&lt;/a&gt;]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google API 로 도시 이름을 가져올 때 주요한 특징이 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;즉시 가져오는게 아니라는 겁니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Google 의 자바스크립트 라이브러리에서 적절한 서비스를 호출하면 응답이 돌아올 때까지 약간의 시간이 걸립니다. 덕분에 조금 혼란스럽지만, 배우는데 확실히 도움이 되는 문제가 나타났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 이야기로 돌아가서 확실히 보이는 문제를 하나 짚어보겠습니다. 보기에는 두 개로 나뉜 인터페이스 영역이 있습니다. 사이드바와 메인 컨텐츠 영역입니다. 절대로 둘을 한꺼번에 다루는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;거대한 코드 덩어리 하나만 작성하면 안됩니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이유는 명백합니다. 만약 시간이 조금 흘러 네 개의 컴포넌트를 만들어야 한다면? 아니면 여섯 개? 백 개가 된다면? 그러므로 코드를 조각조각 나누어야 합니다. 아래의 방식으로 자바스크립트 파일을 두 개로 나눌 것입니다. 하나는 사이드바를 담당하고, 다른 하나는 지도 부분을 담당합니다. 그런데 어떤 파일이 지역을 담아놓는 배열을 다루어야 할까요?&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdqwfx/btr3pr8a2uB/RAgKKKJjXuha84hg7w8Bf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdqwfx/btr3pr8a2uB/RAgKKKJjXuha84hg7w8Bf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdqwfx/btr3pr8a2uB/RAgKKKJjXuha84hg7w8Bf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdqwfx%2Fbtr3pr8a2uB%2FRAgKKKJjXuha84hg7w8Bf1%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;1280&quot; height=&quot;1582&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 접근 방식이 더 옳을까요? 첫 번째일까요, 두 번째일까요? 사실 답은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;둘 다 아닙니다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단일 책임 원칙 기억나시죠? 깔끔하고 모듈화된 (그리고 멋진) 코드를 유지하기 위해서 어딘가 다른 곳에 관심사(concerns)를 분리하고 데이터 로직을 두어야 합니다. 이렇게요.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1330&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFndPH/btr3kxnjcVc/mnDzw9mZHTAHuWMQUMLe11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFndPH/btr3kxnjcVc/mnDzw9mZHTAHuWMQUMLe11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFndPH/btr3kxnjcVc/mnDzw9mZHTAHuWMQUMLe11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFndPH%2Fbtr3kxnjcVc%2FmnDzw9mZHTAHuWMQUMLe11%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;1280&quot; height=&quot;1330&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1330&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;코드 분리의 성배(Holy grail)가 완성되었습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;(역주: 원글 작성자가 성배라는 표현을 사용한 것은 아마&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;http://bit.ly/2RjISUZ&quot;&gt;이 용어 사용&lt;/a&gt;에서 따온 것과 유사하다고 생각합니다)&lt;span&gt;&amp;nbsp;&lt;/span&gt;데이터를 저장하고 다루는 로직을 다른 파일로 옮길 수 있게 되었습니다. 이 서비스 파일은 로컬 스토리지와 동기화를 하는 등의 매커니즘과 관심사를 다루는 책임을 가지게 됩니다. 그 반대로 컴포넌트들은 오로지 인터페이스 부분만 담당하게 됩니다. 말 그대로 단단한 구조(SOLID)를 이루었네요. 이제 설명한 패턴을 코드에 적용해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터 서비스 파일&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;lua&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;2&quot;&gt;&lt;code&gt;/* dataService.js */let myPlaces = [];const geocoder = new google.maps.Geocoder();export function addPlace(latLng) {  // Google API 를 실행하여 도시 이름을 검색한다.  // 두 번째 인자는 요청한 결과에 따른 응답이 왔을 때 처리를 담당하는 콜백 함수  geocoder.geocode({ location: latLng }, function(results) {    try {      // 콜백 안에서 결과에 따른 도시 이름을 추출한다      const cityName = results        .find(result =&amp;gt; result.types.includes('locality'))        .address_components[0]        .long_name;      // 그리고 우리가 준비해놓은 변수에 집어넣는다      myPlaces.push({ position: latLng, name: cityName });      // 그 다음 localStorage와 동기화한다      localStorage.setItem('myPlaces', JSON.stringify(myPlaces));    } catch (e) {      // 도시를 찾을 수 없을 때 콘솔에 메세지를 출력한다      console.error('No city found in this location! :(');    }  });}// 현재 가지고 있는 장소의 목록을 출력export function getPlaces() {  return myPlaces;}// localStorage에 있는 정보를 꺼내 콜렉션에 넣는 함수function initLocalStorage() {  const placesFromLocalStorage = JSON.parse(localStorage.getItem('myPlaces'));  if (Array.isArray(placesFromLocalStorage)) {    myPlaces = placesFromLocalStorage;    publish(); // 지금은 만들어지지 않은 함수. 나중에 적용될 예정  }}initLocalStorage();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;맵 컴포넌트 파일&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;3&quot;&gt;&lt;code&gt;/* map.js */let googleMap;import { addPlace, getPlaces } from './dataService.js';function init() {  googleMap = new google.maps.Map(document.getElementById('map'), {    center: { lat: 0, lng: 0 },    zoom: 3  });  googleMap.markerList = [];  googleMap.addListener('click', addMarker);}function addMarker(event) {  addPlace(event.latLng);  renderMarkers();}function renderMarkers() {  googleMap.markerList.forEach(m =&amp;gt; m.setMap(null)); // 모든 마커 제거  googleMap.markerList = [];  // myPlaces 배열의 요소를 기반으로 마커를 추가한다  getPlaces().forEach(place =&amp;gt; {    const marker = new google.maps.Marker({      position: place.position,      map: googleMap    });    googleMap.markerList.push(marker);  });}init();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;사이드바 컴포넌트 파일&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;4&quot;&gt;&lt;code&gt;/* sidebar.js */import { getPlaces } from './dataService.js';function renderCities() {  // 도시 목록을 표현하기 위한 DOM 엘리먼트를 가져온다  const cityListElement = document.getElementById('citiesList');  // 먼저 클리어 하고  cityListElement.innerHTML = '';  // forEach 함수를 써서 하나씩 다시 리스트를 그려낸다.  getPlaces().forEach(place =&amp;gt; {    const cityElement = document.createElement('div');    cityElement.innerText = place.name;    cityListElement.appendChild(cityElement);  });}renderCities();&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;hellip;이런&lt;br /&gt;어떤 액션을 실행해도 인터페이스가 반응하지 않네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 그럴까요? 음, 아직 동기화에 관련된 어떤 것도 구현하지 않았습니다. 불러온 함수를 사용하여 장소를 추가하고 장소가 추가되었다는 신호를 어디에도 보내지 않았습니다. 또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;addPlaces()&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 실행하고 바로 뒤에&lt;span&gt;&amp;nbsp;&lt;/span&gt;getPlaces()&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 실행하도록 하지도 않았습니다. 왜냐면 도시 이름을 찾는 기능은 비동기적으로 동작하여 약간 시간이 걸리기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 뒤에서 돌아가는데 인터페이스는 그 결과를 모르고 있습니다. 마커는 지도에 추가되더라도 사이드바에는 어떠한 변화도 일어나지 않고 있습니다. 어떻게 이 문제를 해결해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 간단한 방법은 주기적으로 서비스에 자료를 요청하는(poll) 겁니다. 예를 들어 모든 컴포넌트가 서비스로부터 매 초마다 자료를 가져오도록 만들 수도 있겠지요. 이렇게요.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;5&quot;&gt;&lt;code&gt;// ...setInterval(() =&amp;gt; {  renderCities();}, 1000);// ...&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;보통 여러분은 우편물이나 택배가 도착했는지 확인하려고 매 시간마다 우체국에 들르지 않습니다. 비슷하게 자동차 수리를 맡겨놓았다면 정비공에게 매 30 분마다 수리가 완료되었는지 전화를 하지도 않습니다. (최소한 여러분이 그런 사람이 아니길 바랍니다) 대신 전화가 오는 것을 기다립니다. 부탁한 일이 마무리되었을 때 정비공은 어떻게 우리에게 전화를 할 수 있을까요? 당연하게도 정비공에게 우리의 전화번호를 남겨 놓았기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 &amp;ldquo;우리의 전화번호를 남겨 둔다&amp;rdquo; 는 비유를 자바스크립트 안에서 실현해보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트는 굉장히 근사한 언어입니다. 한 가지 독특한 특징은 함수를 여느 다른 값들과 같이 취급한다는 것입니다. 전문적인 표현으로 &amp;ldquo;함수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://medium.com/@lazysoul/functional-programming-%EC%97%90%EC%84%9C-1%EA%B8%89-%EA%B0%9D%EC%B2%B4%EB%9E%80-ba1aeb048059&quot;&gt;일급 객체(first-class citizens)&lt;/a&gt;이다&amp;rdquo; 라고 합니다. 어떠한 함수도 변수에 할당할 수 있고, 다른 함수의 인자로 넘길 수 있다는 뜻입니다. 이미 이런 동작방식을 알고 있으리라 생각합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;setTimeout,&lt;span&gt;&amp;nbsp;&lt;/span&gt;setInterval&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 함수 혹은 다양한 이벤트 리스너가 콜백을 받는 것을 기억하시나요? 그런 방식이 함수를 인자로 사용하는 대표적인 예입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이 특징이 바로 비동기 시나리오 처리의 기본이 됩니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI 를 업데이트하는 함수를 정의하고 완전히 다른 부분으로 전달한 뒤에 호출되도록 만들 수 있는겁니다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvJnsJ/btr3dEVjsPb/kkA7W6rgNIOWeAVYaEawXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvJnsJ/btr3dEVjsPb/kkA7W6rgNIOWeAVYaEawXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvJnsJ/btr3dEVjsPb/kkA7W6rgNIOWeAVYaEawXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvJnsJ%2Fbtr3dEVjsPb%2FkkA7W6rgNIOWeAVYaEawXK%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;1280&quot; height=&quot;1790&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1790&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;이 매커니즘을 사용하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;renderCities&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를&lt;span&gt;&amp;nbsp;&lt;/span&gt;dataService&lt;span&gt;&amp;nbsp;&lt;/span&gt;어딘가로 전달할 수 있습니다. 그리고 필요할 때 실행되도록 만들면 되는거죠. 어쨌든 서비스는 언제 데이터가 컴포넌트로 전달되어야 할지 정확히 알고 있으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;까짓거 한 번 해보죠! 서비스 쪽에 함수를 기억할 수 있는 공간을 마련해두고 특정한 시점에 실행되도록 만들어보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;lua&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;6&quot;&gt;&lt;code&gt;/* dataService.js */// ...let changeListener = null;export function subscribe(callbackFunction) {  changeListener = callbackFunction;}export function addPlace(latLng) {  geocoder.geocode({ location: latLng }, function(results) {    try {      const cityName = results        .find(result =&amp;gt; result.types.includes('locality'))        .address_components[0]        .long_name;      myPlaces.push({ position: latLng, name: cityName });      // 추가된 부분      if (changeListener) {        changeListener();      }      localStorage.setItem('myPlaces', JSON.stringify(myPlaces));    } catch (e) {      console.error('No city found in this location! :(');    }  });}// ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 사이드바쪽 코드에 추가합니다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;7&quot;&gt;&lt;code&gt;/* sidebar.js */import { getPlaces, subscribe } from './dataService';// ...renderCities();subscribe(renderCities);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떻게 동작하는지 보이시나요? 사이드바를 다루는 코드가 실행되면서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;renderCities&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를&lt;span&gt;&amp;nbsp;&lt;/span&gt;dataService&lt;span&gt;&amp;nbsp;&lt;/span&gt;안에 등록했습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&lt;span&gt;&amp;nbsp;&lt;/span&gt;dataService&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 실행 될 필요가 있을 때 실행됩니다. 이 경우에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;데이터가 변경되었을 때&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(addPlaces()&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수가 호출되면서) 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확히 말씀드리면, 코드의 한 부분은 이벤트의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;수신자(SUBSCRIBER, 여기서는 사이드바 컴포넌트)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 되고, 다른 한 부분은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;발행자(PUBLISHER, 서비스 메서드)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 됩니다. 짠짜짠, 우리는 발행-구독 패턴(publish-subscribe pattern)의 가장 기본적인 형태를 구현했습니다. 이 패턴이 거의 모든 고급 비동기 처리 방식의 기본 개념이 되지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 살펴볼 게 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 구현된 코드로는 오로지 하나의 컴포넌트만 데이터 처리 결과를 수신할 수 있습니다. (다른 말로는 하나의 수신자만 있다는 뜻입니다) 만약 다른 함수를&lt;span&gt;&amp;nbsp;&lt;/span&gt;subscribe()&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수에 넘기게 되면 현재 설정된&lt;span&gt;&amp;nbsp;&lt;/span&gt;changeListener&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 덮어쓰게 됩니다. 이 문제를 해결하기 위해 배열로 바꾸어서 함수를 받도록 처리하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;8&quot;&gt;&lt;code&gt;/* dataService.js */// ...let changeListeners = [];export function subscribe(callbackFunction) {  changeListeners.push(callbackFunction);}// ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 코드를 좀 정리하고 모든 리스너를 실행하는 함수를 작성하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;lua&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;9&quot;&gt;&lt;code&gt;/* dataService.js */// 위에 작성한 코드 바로 아래에function publish() {  changeListeners.forEach(changeListener =&amp;gt; changeListener());}export function addPlace(latLng) {  geocoder.geocode({ location: latLng }, function(results) {    try {      const cityName = results        .find(result =&amp;gt; result.types.includes('locality'))        .address_components[0]        .long_name;      myPlaces.push({ position: latLng, name: cityName });      // 변경된 부분      publish();      localStorage.setItem('myPlaces', JSON.stringify(myPlaces));    } catch (e) {      console.error('No city found in this location! :(');    }  })}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 방식으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;map.js&lt;span&gt;&amp;nbsp;&lt;/span&gt;컴포넌트가 서비스에서 일어난 모든 액션에 반응할 수 있도록 연결할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;10&quot;&gt;&lt;code&gt;/* map.js */import { addPlace, getPlaces, subscribe } from './dataService';let googleMap;// ...init();renderMarkers();subscribe(renderMarkers);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수신자를 데이터를 전송하는데 사용하려면 어떻게 해야할까요? 이런 식으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;리스너에 직접 인자로&lt;span&gt;&amp;nbsp;&lt;/span&gt;전달해 줄 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;11&quot;&gt;&lt;code&gt;/* dataService.js */function publish(data) {  changeListeners.forEach(changeListener =&amp;gt; changeListener(data));}// ...export function addPlace(latLng) {  geocoder.geocode({location: latLng}, function(results) {    try {      const cityName = results        .find(result =&amp;gt; result.types.includes('locality'))        .address_components[0]        .long_name;      myPlaces.push({position: latLng, name: cityName});      publish(myPlaces);      // ...    }  // ...}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 쉽게 컴포넌트에 데이터를 전달할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; style=&quot;background-color: #011627; color: #d6deeb;&quot; data-language=&quot;javascript&quot; data-index=&quot;12&quot;&gt;&lt;code&gt;/* sidebar.js */import { getPlaces, subscribe } from './dataService';function renderCities(placesArray) {  const cityListElement = document.getElementById('citiesList');  cityListElement.innerHTML = '';  // getPlaces 함수 호출을 placesArray로 교체  placesArray.forEach(place =&amp;gt; {    const cityElement = document.createElement('div');    cityElement.innerText = place.name;    cityListElement.appendChild(cityElement);  });}// 초기 값으로 getPlaces() 전달renderCities(getPlaces());subscribe(renderCities);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 다양한 활용 방법이 있습니다. 다른 액션을 처리하기 위해 새로운 주제(혹은 채널)을 만들 수도 있습니다. 마찬가지로&lt;span&gt;&amp;nbsp;&lt;/span&gt;publish&lt;span&gt;&amp;nbsp;&lt;/span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;subscribe&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 전혀 다른 코드 파일로 분리하여 활용할 수도 있습니다. 하지만 지금 단계에선 그렇게 하지 않아도 충분합니다. 아래의 영상은 여태 작성한 예제로 만들어진 앱을 시연하는 영상입니다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여태까지 살펴 본 발행-구독 패턴이 원래 알고 있던 듯한 기분이 들지 않나요? 조금 더 생각을 해 보면 여태 사용해왔던&lt;span&gt;&amp;nbsp;&lt;/span&gt;element.addEventListener(action, callback)&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 형태와 상당히 유사한 작동 원리를 가지고 있습니다. 특정 이벤트에 어떤 함수를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구독하도록&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;만들고, DOM 요소에 의해 액션이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;발행되면&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;그 함수가 호출되는 거죠. 똑같네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제목을 되짚어보면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;왜 이 패턴이 오지게(bloody) 중요한걸까요?&lt;span&gt;&amp;nbsp;&lt;/span&gt;장기적으로 바라보면 바닐라 자바스크립트를 고수하면서 수동으로 DOM 을 수정하는 일은 거의 의미가 없습니다. 이벤트를 전달하고 수신하는데 수동으로 제어하는 매커니즘과 유사하죠. 다양한 프레임워크들은 이미 사용하고 있는 솔루션이 있습니다. 앵귤러는 RxJS 를 사용하고, 리액트는 state-props 기반으로 하며 Redux 를 사용하여 이 구조를 강화할 수도 있고요. 말 그대로 쓸만한 모든 라이브러리나 프레임워크들이 각자의 데이터 동기화 방법을 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 이야기하자면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;위의 모든 것들이 발행-수신 패턴(Pub-Sub Pattern)의 다양한 변종을 사용하고 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 이야기했듯이 DOM 이벤트 리스너는 UI 액션을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;발행하고&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구독하는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;정도에 불과합니다. 조금 더 나아가서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Promise&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 뭘까요? 특정 관점에서 바라보면 단순히 우리가 미뤄둔 어떠한 액션이 완료되는 것을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구독할 수 있게 하고&lt;/b&gt;, 데이터가 준비되면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;발행하는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트의 state 와 props 가 변경되는 것은 어떨까요? 컴포넌트들이 업데이트 되는 원리는 데이터 변화를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구독하는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;것입니다. 웹소켓의&lt;span&gt;&amp;nbsp;&lt;/span&gt;on()&lt;span&gt;&amp;nbsp;&lt;/span&gt;은요? Fetch API 는? 특정한 네트워크 액션을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구독하는&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;것이죠. 리덕스? 이건 스토어의 변화를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구독하도록&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;하죠. 그렇다면 RxJS 는? 말 할 것도 없이 하나의 거대한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;구독&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;패턴입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 같은 원리를 가지고 있습니다. 그 뒤에 마법의 유니콘같은게 숨어있는 것도 아니고요. 뻔한 시트콤 엔딩이나 마찬가집니다(역주: ending of the Scooby-Doo episode 라는 표현이 사용되었지만 맥락 상 뻔하다는 표현에 중점을 두었습니다).&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9VQOE/btr3dbFSO5u/azWRpUJqx7vBbfKkRuVIVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9VQOE/btr3dbFSO5u/azWRpUJqx7vBbfKkRuVIVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9VQOE/btr3dbFSO5u/azWRpUJqx7vBbfKkRuVIVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9VQOE%2Fbtr3dbFSO5u%2FazWRpUJqx7vBbfKkRuVIVk%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;1280&quot; height=&quot;548&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;대단한 발견은 아니지만 꼭 알아두면 좋습니다.&lt;/p&gt;
&lt;blockquote style=&quot;color: #999999;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떠한 비동기 처리 방법을 사용하든지, 언제나 같은 패턴의 변종일 뿐입니다. 무언가는 구독을 하고, 무언가는 발행을 하는거죠.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 이 개념이 필수요소라고 말씀드리는 겁니다. 언제나 발행과 구독에 대해 생각할 수 있습니다. 마음에 새겨두고 다양하게 학습해보세요. 다양한 비동기 처리 방법으로 더 크고 복잡한 애플리케이션을 만들어 보세요. 아무리 어려워 보일지라도 발행자와 구독자로 모든 것을 동기화하도록 노력해보세요.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;리스너가 더 이상 필요하지 않을 때 구독을 해지(unsubscribe)하는 방법&lt;/li&gt;
&lt;li&gt;다양한 주제(Multi-topic)를 구독하는 방법 (addEventListener&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 다른 이벤트에 구독을 수행하는 것 처럼)&lt;/li&gt;
&lt;li&gt;확장된 아이디어: 이벤트 버스 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 배운 지식을 확장하기 위해서 Pub-Sub 패턴을 구현한 몇 가지 자바스크립트 라이브러리를 살펴보실 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://github.com/mroderick/PubSubJS&quot;&gt;GitHub - mroderick/PubSubJS: Dependency free publish/subscribe for JavaScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://github.com/Sahadar/pubsub.js&quot;&gt;GitHub - Sahadar/pubsub.js: Dependency free JavaScript pubsub implementation with wildcards, inheritance and multisubscriptions.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://github.com/shystruk/publish-subscribe-js&quot;&gt;GitHub - shystruk/publish-subscribe-js: Publish/Subscribe UMD package&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 살펴보시고 사용해보세요. 이리저리 만져보고 디버거를 사용해서 라이브러리를 사용할 때 무슨 일이 일어나는지도 살펴보세요. 또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern&quot;&gt;이 개념을 잘 설명한&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://davidwalsh.name/pubsub-javascript&quot;&gt;여러 가지 글이 있습니다.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제로 사용 된 코드는 아래의 깃헙 저장소에 올려두었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://github.com/hzub/pubsub-demo/&quot;&gt;GitHub - hzub/pubsub-demo: Code for article: &lt;/a&gt;&lt;a href=&quot;https://medium.com/@hubert.zub/why-every-beginner-front-end-developer-should-know-publish-subscribe-pattern-72a12cd68d44&quot;&gt;https://medium.com/@hubert.zub/why-every-beginner-front-end-developer-should-know-publish-subscribe-pattern-72a12cd68d44&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 실험해보고 건드려보세요. 다양한 용어들에 겁 먹지 마세요. 어렵게 포장된 보통의 코드인 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 생각하세요. 그럼 이만!&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 id=&quot;번역-후기&quot; style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;번역 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내용이 무척 길어서 쉽사리 접근할 엄두를 내지 못했지만 마치고 나니 뿌듯하네요. 정말 중요한 개념이라고 생각하며 내용을 살펴보았고, 많은 분들에게 도움이 될 수 있으리라 생각하여 망설이지 않고 번역 작업을 진행했습니다. 삽화가 다양하게 들어있어서, 이해를 돕고자 삽화의 내용도 직접 다시 그려봤는데 글씨를 더 잘 쓰지 못해 조금 아쉬웠네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 디자인 패턴에 대해 조금이라도 들어 보신 분들은 옵저버 패턴(Observer Pattern)에 대해 들어보셨을 겁니다. 그 패턴과 Pub-Sub 가 어떻게 다른가? 라고 궁금해하실 수도 있으리라 생각해서 아래의 첨부자료를 남깁니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://jistol.github.io/software%20engineering/2018/04/11/observer-pubsub-pattern/&quot;&gt;Observer 패턴과 Publisher/Subscriber(Pub-Sub) 패턴의 차이점 &amp;middot; Jistol Github Page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a style=&quot;color: #0687f0;&quot; href=&quot;https://hackernoon.com/observer-vs-pub-sub-pattern-50d3b27f838c&quot;&gt;Observer vs Pub-Sub pattern &amp;ndash; Hacker Noon&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://www.rinae.dev/posts/why-every-beginner-front-end-developer-should-know-publish-subscribe-pattern-kr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.rinae.dev/posts/why-every-beginner-front-end-developer-should-know-publish-subscribe-pattern-kr&lt;/a&gt;&lt;/p&gt;</description>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/165</guid>
      <comments>https://calssess.tistory.com/165#entry165comment</comments>
      <pubDate>Mon, 13 Mar 2023 12:29:32 +0900</pubDate>
    </item>
    <item>
      <title>npm, yarn, pnpm 비교해보기</title>
      <link>https://calssess.tistory.com/164</link>
      <description>&lt;h2 id=&quot;table-of-contents&quot; data-ke-size=&quot;size26&quot;&gt;Table of Contents&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#introduction&quot;&gt;Introduction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B4%80%EB%A6%AC%EC%9E%90%EC%9D%98-%EC%97%AD%EC%82%AC&quot;&gt;자바스크립트 패키지 관리자의 역사&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%EC%84%A0%EA%B5%AC%EC%9E%90-npm&quot;&gt;선구자 npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%EB%A7%8E%EC%9D%80-%ED%98%81%EB%AA%85%EC%9D%84-%EA%B0%80%EC%A0%B8%EC%98%A8-yarn-classic&quot;&gt;많은 혁명을 가져온 yarn classic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#pnpm-%EB%B9%A0%EB%A5%B4%EA%B3%A0-%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B8-%EB%94%94%EC%8A%A4%ED%81%AC-%EA%B4%80%EB%A6%AC&quot;&gt;pnpm 빠르고 효율적인 디스크 관리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#yarn-berry-plug-n-play&quot;&gt;yarn berry, plug n play&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%ED%8C%A8%ED%82%A4%EC%A7%80-%EB%A7%A4%EB%8B%88%EC%A0%80-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot;&gt;패키지 매니저 설치하기&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#npm&quot;&gt;npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#yarn-classic&quot;&gt;yarn classic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#yarn-berry&quot;&gt;yarn berry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#pnpm&quot;&gt;pnpm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0&quot;&gt;프로젝트 구조&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#npm-1&quot;&gt;npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#yarn-classic-1&quot;&gt;yarn classic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#yarn-berry%EC%99%80-node_modules&quot;&gt;yarn berry와&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#yarn-berry-with-pnp&quot;&gt;yarn berry with pnp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#pnpm-1&quot;&gt;pnpm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#lock-%ED%8C%8C%EC%9D%BC%EA%B3%BC-dependency-%EC%A0%80%EC%9E%A5&quot;&gt;Lock 파일과 dependency 저장&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#cli-commands&quot;&gt;CLI commands&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B4%80%EB%A6%AC&quot;&gt;의존성 관리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B4%80%EB%A0%A8&quot;&gt;패키지 관련&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%EC%9E%90%EC%A3%BC-%EC%93%B0%EC%9D%B4%EB%8A%94-%EC%BB%A4%EB%A7%A8%EB%93%9C&quot;&gt;자주 쓰이는 커맨드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%EC%84%B1%EB%8A%A5%EA%B3%BC-%EB%94%94%EC%8A%A4%ED%81%AC-%EA%B4%80%EB%A6%AC%EC%9D%98-%ED%9A%A8%EC%9C%A8%EC%84%B1&quot;&gt;성능과 디스크 관리의 효율성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%EB%B3%B4%EC%95%88&quot;&gt;보안&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#npm-2&quot;&gt;npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#yarn&quot;&gt;yarn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#pnpm-2&quot;&gt;pnpm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%EA%B2%B0%EB%A1%A0&quot;&gt;결론&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm#%EC%B0%B8%EA%B3%A0&quot;&gt;참고&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;introduction&quot; data-ke-size=&quot;size26&quot;&gt;Introduction&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 에서 시작한 node package management의 역사는, 이제 3가지 옵션이 주어져 있다. yarn 1.0 (이제 yarn classic 이라고 부르겠다) 과 yarn 2.0 (yarn berry) 두 가지 버전도 사뭇 다른 점이 많다는 것을 감안한다면, 이제 크게 4가지 선택지가 존재 한다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 위 3가지 패키지 관리자들은 아래와 같은 기본적인 기능 (node 모듈을 설치하고, 관리하는 등)을 제공하고 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;metadata 작성 및 관리&lt;/li&gt;
&lt;li&gt;모든 dependencies 일괄 설치 또는 업데이트&lt;/li&gt;
&lt;li&gt;dependencies 추가, 업데이트, 삭제&lt;/li&gt;
&lt;li&gt;스크립트 실행&lt;/li&gt;
&lt;li&gt;패키지 퍼블리쉬&lt;/li&gt;
&lt;li&gt;보안 검사&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 설치 속도나 디스크 사용량, 또는 기존 워크 플로우 등과 어떻게 매칭 시킬지와 같은 기능 외적인 요구 사항에 따라 패키지 관리자를 선택하는 시대가 도래했다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;겉으로는 기능적으로 비슷해보이고 무엇을 선택하든 별 차이는 없어보이지만, 패키지 관리자들의 내부 동작은 매우 다르다. npm 과 yarn의 경우 flat 한 node_modules 폴더에 dependencies 를 설치했다. 그러나 이러한 전략은 비판에서 자유롭지 못하다. (어떤 문제인지는 뒤에서 설명하도록 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 등장한 pnpm은 이러한 dependencies를 중첩된 node_modules 폴더에 효율적으로 저장하기 시작했고, yarn berry는 plug and play (pnp) 모드를 도입하여 이러한 문제를 해결하기 시작했다.&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;a href=&quot;https://www.npmjs.com/&quot;&gt;npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://classic.yarnpkg.com/lang/en/&quot;&gt;yarn classic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/yarnpkg/berry&quot;&gt;yarn berry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pnpm.io/ko/&quot;&gt;pnpm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm 홈페이지에 있는 yarn과 npm을 쓰레기통에 쳐박은 이미지가 매우 인상적이다.   vue가 react를 쓰레기통에 쳐박는 이미지를 달아놨다면...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;자바스크립트-패키지-관리자의-역사&quot; data-ke-size=&quot;size26&quot;&gt;자바스크립트 패키지 관리자의 역사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두가 잘 알고 있는 것 처럼 최초의 패키지 매니저는 2010년 1월에 나온 npm 이다. npm 은 패키지 매니저가 어떤 동작을 해야하는 지에 대한 핵심적인 개념을 잡았다고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10여년이 넘는 시간 동안 npm 이 존재했는데, yarn, pnpm 등이 등장하게 된 것일까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;node_modules&lt;span&gt;&amp;nbsp;&lt;/span&gt;효율화를 위한 다른 구조 (nested vs flat, node_modules, vs pnp mode)&lt;/li&gt;
&lt;li&gt;보안에 영향을 미치는 호이스팅 지원&lt;/li&gt;
&lt;li&gt;성능에 영향을 미칠 수 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;lock파일 형식&lt;/li&gt;
&lt;li&gt;디스크 효율성에 영향을 미치는 패키지를 디스크에 저장하는 방식&lt;/li&gt;
&lt;li&gt;대규모 모노레포의 유지 보수성 과 속도에 영향을 미치는 workspace라 알려진 멀티 패키지 관리 및 지원&lt;/li&gt;
&lt;li&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;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다양한 기능 구현 가능성과 유연함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 이 최초로 등장하 이래로 이러한 니즈가 어떻게 나타났는지, yarn classic은 그 이후 등장해서 어떻게 해결했는지, pnpm이 이러한 개념을 어떻게 확장했는지, yarn berry가 전통적인 개념과 프로테스에 의해 설정된 틀을 깨기 위해 어떠한 노력을 했는지 간략한 역사를 파악해보자.&lt;/p&gt;
&lt;h3 id=&quot;선구자-npm&quot; data-ke-size=&quot;size23&quot;&gt;선구자 npm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격적으로 시작하기에 앞서 재밌는 사실을 이야기 해보자면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;npm은&lt;span&gt;&amp;nbsp;&lt;/span&gt;node package manager의 약자가 아니다. npm의 전신은 사실&lt;span&gt;&amp;nbsp;&lt;/span&gt;pm이라 불리는 bash 유틸리티인데, 이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;pkgmakeinst의 약자다. 그리고 이의 node 버전이&lt;span&gt;&amp;nbsp;&lt;/span&gt;npm인 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/npm/cli#is-npm-an-acronym-for-node-package-manager&quot;&gt;https://github.com/npm/cli#is-npm-an-acronym-for-node-package-manager&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm 이전에는 프로젝트의 dependencies를 수동으로 다운로드하고 관리하였기 때문에 엄청난 혁명을 가져왔다고 볼 수 있다. 이와 더불어 메타데이터를 가지고 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;package.json와 같은 개념, dependencies를&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules라 불리는 폴더에 설치한다는 개념, 커스텀 스크립트, public &amp;amp; private 패키지 레지스트리와 같은 개념들 모두 npm에 의해 도입되었다.&lt;/p&gt;
&lt;h3 id=&quot;많은-혁명을-가져온-yarn-classic&quot; data-ke-size=&quot;size23&quot;&gt;많은 혁명을 가져온 yarn classic&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineering.fb.com/2016/10/11/web/yarn-a-new-package-manager-for-javascript/&quot;&gt;2016년의 블로그 글&lt;/a&gt;에서, 페이스북은 구글과 몇몇 다른 개발자들과 함께 npm이 가지고 있던 일관성, 보안, 성능 문제 등을 해결하기 위한 새로운 패키지 매니저를 만들기 위한 시도를 진행 중이라고 발표 했다. 그리고 이듬해&lt;span&gt;&amp;nbsp;&lt;/span&gt;Yet Another Resource Negotiator의 약자인 yarn을 발표했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn은 대부분의 개념과 프로세스에 npm을 기반으로 설계했지만, 이외에 패키지 관리자 환경에 큰 영향을 미쳤다. npm과 대조적으로, yarn은 초기버전의 npm의 주요 문제점 중 하나였던 설치 프로세스의 속도를 높이기 위해 작업을 병렬화 하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn은 dx(개발자 경험), 보안 및 성능에 대한 기준을 높였으며, 다음과 같은 개념을 패키지 매니저에 도입하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;native 모노레포 지원&lt;/li&gt;
&lt;li&gt;cache-aware 설치&lt;/li&gt;
&lt;li&gt;오프라인 캐싱&lt;/li&gt;
&lt;li&gt;lock files&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn classic은 2020년 부터 유지보수 모드로 전환되었다. 그리고 1.x 버전은 모두 레거시로 간주하고 yarn classic으로 이름이 바뀌었다. 현재는 yarn berry에서 개발과 개선이 이루어지고 있다.&lt;/p&gt;
&lt;h3 id=&quot;pnpm-빠르고-효율적인-디스크-관리&quot; data-ke-size=&quot;size23&quot;&gt;pnpm 빠르고 효율적인 디스크 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm은 2017년에 만들어졌으며, npm 의 drop-in replacement(설정을 바꿀 필요 없이 바로 사용가능하며, 속도와 안정성 등 다양한 기능 향상이 이루어지는 대체품) 으로, npm만 있다면 바로 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm 제작자들이 생각한 npm 과 yarn의 가장 큰 문제는 프로젝트 간에 사용되는 dependencies의 중복 저장이다. yarn classic이 물론 npm 보다 빠르지만, 두 매니저 모두 node_modules 내부에 flat하게 패키지를 설치하여 (=동일한 디렉토리에 flat하게 저장) 관리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm은 이러한 호이스트 방식 대신, 다른 dependencies를 해결하는 전략인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://pnpm.io/next/symlinked-node-modules-structure&quot;&gt;content-addressable storage&lt;/a&gt;를 사용했다. 이 방법을 사용하면, home 폴더의 글로벌 저장소 (~/.pnpm-store)에 패키지를 저장하는 중첩된 node_modules 폴더가 생성된다. 따라서 모든 버전의 dependencies은 해당 폴더에 물리적으로 한번만 저장되므로, single source of truth를 구성하고, 상당한 디스크 공간을 절약할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 node_modules의 레이아웃을 통해 이루어지고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;symlinks를 사용하여 dependencies의 중첩된 구조를 생성한다. 여기서 폴더 내부의 모든 패키지 파일은 저장소에 대한 하드 링크로 구성되어 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2920&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2P4z6/btr24MFtWJg/wKvUUgYgT8iiPXknqZLAQK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2P4z6/btr24MFtWJg/wKvUUgYgT8iiPXknqZLAQK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2P4z6/btr24MFtWJg/wKvUUgYgT8iiPXknqZLAQK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2P4z6%2Fbtr24MFtWJg%2FwKvUUgYgT8iiPXknqZLAQK%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;2920&quot; height=&quot;1392&quot; data-origin-width=&quot;2920&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pnpm.io/blog/2021/12/29/yearly-update&quot;&gt;https://pnpm.io/blog/2021/12/29/yearly-update&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;yarn-berry-plug-n-play&quot; data-ke-size=&quot;size23&quot;&gt;yarn berry, plug n play&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn berry 는 2020년 1월에 출시되었으며 yarn classic의 업그레이드 버전이다. yarn 팀은 본질적으로 새로운 코드 베이스와 새로운 원칙을 가진 완전히 새로운 패키지 매니저라는 것을 분명하게 하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn berry라고 부르기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn berry에서 눈여겨 봐야 할 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://yarnpkg.com/features/pnp/&quot;&gt;plug n play&lt;/a&gt;로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://yarnpkg.com/features/pnp#fixing-node_modules&quot;&gt;node_modules를 fix 위한 전략&lt;/a&gt;이다. node_modules를 생성하는 대신,&lt;span&gt;&amp;nbsp;&lt;/span&gt;.pnp.cjs라 불리는 의존성 lookup 파일이 생성되는데, 이는 중첩된 폴더 구조 대신 단일 파일 이기 때문에 더 효율적으로 처리할 수 있다. 또한 모든 패키지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;.yarn/cache&lt;span&gt;&amp;nbsp;&lt;/span&gt;폴더 내부에 zip 파일로 저장되므로, node_modules 폴더보다 더 디스크 공간을 적게 차지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 변화는, 릴리즈 이후에 많은 논란을 일으켰다. pnp의 breaking change는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://blog.hao.dev/state-of-yarn-2-berry-in-2021&quot;&gt;메인테이너들로 하여금 기존에 존재하는 패키지를 업데이트 하게 끔 만들었다.&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;새로운 pnp 방식은 default로 설정되었고, node_modules로 돌아가는 것 또한 간단하지 않았다. 이 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=bPae4Z8BFt8&quot;&gt;많은 유명한 개발자들이 yarn berry를 opt-in으로 만들지 않은 것에 대해 비판하기 시작했다.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn berry 팀은 이후 릴리즈에서 많은 문제를 해결하고자 노력했다. PnP의 비호환성을 해결하기 위해 default 작동 모드를 쉽게 바꾸기 위한 몇가지 방법을 제안했다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/yarnpkg/berry/tree/master/packages/plugin-nm&quot;&gt;node_modules plugin&lt;/a&gt;의 도움으로, 기본적인 node_modules로 돌아가는 데 한 줄의 코드만으로 가능해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yarnpkg.com/features/pnp#compatibility-table&quot;&gt;호환성 표&lt;/a&gt;에서 볼 수 있듯이, 많은 대형 프로젝트 들이 점차 yarn berry를 지원하는 방향으로 가기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 3가지 패키지 매니저 중에서 가장 최근에 나왔지만, 패키니 매니저 환경에 많은 영향을 미쳤다. 2020년말, pnpm도 plug n play 방식을 지원하기 시작했다.&lt;/p&gt;
&lt;h2 id=&quot;패키지-매니저-설치하기&quot; data-ke-size=&quot;size26&quot;&gt;패키지 매니저 설치하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지 매니저를 사용하기 위해서는, 개발자의 로컬 혹은 CI/CD 시스템에 설치해야 한다.&lt;/p&gt;
&lt;h3 id=&quot;npm&quot; data-ke-size=&quot;size23&quot;&gt;npm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nodejs 내부에 npm이 내장되어 있으므로, 추가적으로 작업을 할 필요가 없다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/nvm-sh/nvm&quot;&gt;nvm&lt;/a&gt;이나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://volta.sh/&quot;&gt;volta&lt;/a&gt;를 사용하면, node와 npm 버전을 관리하는데 매우 유용하게 쓸 수 있다.&lt;/p&gt;
&lt;h3 id=&quot;yarn-classic&quot; data-ke-size=&quot;size23&quot;&gt;yarn classic&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm i -g yarn으로 설치하면 된다.&lt;/p&gt;
&lt;h3 id=&quot;yarn-berry&quot; data-ke-size=&quot;size23&quot;&gt;yarn berry&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yarnpkg.com/getting-started/migration&quot;&gt;yarn classic에서 yarn berry로 넘어가는 방법&lt;/a&gt;으로 추천할만한 것은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;yarn 1.x 등 최신버전으로 업데이트&lt;/li&gt;
&lt;li&gt;yarn set version berry&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yarnpkg.com/getting-started/install#install-corepack&quot;&gt;사실 추천하는 방법&lt;/a&gt;은 Corepack을 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://nodejs.org/api/corepack.html&quot;&gt;Corepack&lt;/a&gt;은 yarn berry 개발자에 의해 만들어진 도구로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/nodejs/TSC/issues/904&quot;&gt;package manager manager&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;(;;;) 라는 이름으로 처음 제안되었고, node lts v16에 머지되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Corepack의 도움으로 node는 yarn classic, yarn berry, pnpm의 바이너리를 shim으로 가지고 있기 때문에 npm의 대체 패키지 매니저를 별도로 설치할 필요는 없다. 이 shim을 활용하면, yarn과 pnpm 명령어를 명시적으로 설피할 필요 없이, 실행할 수 잇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Corepack은 nodejs@16.9.0 부터 사전 설치되며, 이전 버전에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;npm install -g corepack으로 설치할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Corepack을 사용하기 위해서는, 먼저 활성화를 해야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;$ corepack enable
$ corepack prepare yarn@3.1.1 --activate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;pnpm&quot; data-ke-size=&quot;size23&quot;&gt;pnpm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm 도 마찬가지 두 가지 방법으로 설치 할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;$ npm i -g pnpm&lt;/li&gt;
&lt;li&gt;$ corepack prepare pnpm@6.24.2 --activate&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;프로젝트-구조&quot; data-ke-size=&quot;size26&quot;&gt;프로젝트 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 구조를 살펴보면, 각 패키지 매니저의 주요 특성을 한눈에 살펴볼 수 있다. 특정 패키지 매니저를 구성하는데 사용하는 파일과, 설치단계에서 생성되는 파일을 쉽게 알아볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로, 모든 패키지 매니저는 모든 중요한 메타 정보를&lt;span&gt;&amp;nbsp;&lt;/span&gt;package.json에 저장한다. 또한 루트 레벨에 설정파일을 사용하여 프라이빗 레지스트리나 dependency resolution 방법을 설정할 수 있다. 그리고 이 단계에서 dependencies를 파일 구조 (node_modules)에 저장하고 lock 파일이 생성된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 workspaces에 대해서는 다루지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;npm-1&quot; data-ke-size=&quot;size23&quot;&gt;npm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$npm install&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;$npm i&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령어를 실행하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;package-lock.json이 생성되고&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules&lt;span&gt;&amp;nbsp;&lt;/span&gt;폴더도 생성된다. 이 외에도&lt;span&gt;&amp;nbsp;&lt;/span&gt;.npmrc&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정 파일도 생성될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;.
├── node_modules/
├── .npmrc
├── package-lock.json
└── package.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;yarn-classic-1&quot; data-ke-size=&quot;size23&quot;&gt;yarn classic&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$yarn을 실행하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn.lock과&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules&lt;span&gt;&amp;nbsp;&lt;/span&gt;폴더가 생성된다. 마찬가지로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://classic.yarnpkg.com/en/docs/yarnrc&quot;&gt;.yarnrc&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일&lt;/a&gt;도 옵셔널로 생성할 수 있다. 이에 더해&lt;span&gt;&amp;nbsp;&lt;/span&gt;.npmrc&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일이 있으면 이를 이용할 수도 있다. 그리고 캐시 폴더인&lt;span&gt;&amp;nbsp;&lt;/span&gt;.yarn/cache/와 현재 yarn classic의 버전을 저장하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;.yarn/releases/도 생성될 수 있다. 이처럼 설정에 따라서 다양하게 변경될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;.
├── .yarn/
│   ├── cache/
│   └── releases/
│       └── yarn-1.22.17.cjs
├── node_modules/
├── .yarnrc
├── package.json
└── yarn.lock
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;yarn-berry와-node_modules&quot; data-ke-size=&quot;size23&quot;&gt;yarn berry와&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;install mode에 관계 없이, yarn berry 프로젝트에서는 다른 패키지 관리자보다 더 많은 파일 보다 폴더를 처리해야 한다. 일부는 선택사항이고, 그리고 일부는 필수 사항이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn berry는 더이상&lt;span&gt;&amp;nbsp;&lt;/span&gt;.npmrc&lt;span&gt;&amp;nbsp;&lt;/span&gt;다&lt;span&gt;&amp;nbsp;&lt;/span&gt;.yarnrc를 사용하지 않는다. 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://yarnpkg.com/configuration/yarnrc&quot;&gt;yarnrc.yml&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정 파일&lt;/a&gt;을 필요로 한다. 전통적인&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules를 생성하는 워크플로우가 존재하는 경우,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://yarnpkg.com/configuration/yarnrc#nodeLinker&quot;&gt;nodeLinker config&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일을 아래와 같은 형태로 제공해야 한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# .yarnrc.yml
nodeLinker: node-modules # or pnpm
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ yarn을 실행하면, 모든 의존성을&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules에 설치한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn.lock&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일이 생성되는데, 이 파일은 기존&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn classic과 호환되지는 않는다. 또한 오프라인 모드에서 설치를 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;.yarn/cache&lt;span&gt;&amp;nbsp;&lt;/span&gt;폴더도 생성된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;releases&lt;span&gt;&amp;nbsp;&lt;/span&gt;폴더는 프로젝트에서 사용하는 yarn berry의 버전을 저장하기 위해 옵셔널로 생성된다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;.
├── .yarn/
│   ├── cache/
│   └── releases/
│       └── yarn-3.1.1.cjs
├── node_modules/
├── .yarnrc.yml
├── package.json
└── yarn.lock
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;yarn-berry-with-pnp&quot; data-ke-size=&quot;size23&quot;&gt;yarn berry with pnp&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PnP 모드에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://yarnpkg.com/features/pnp&quot;&gt;strict&lt;/a&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://yarnpkg.com/features/pnp#pnp-loose-mode&quot;&gt;loose&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;모드가 있는데, 일단은 모드에 상관없이&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn을 실행하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;.yarn/cache와&lt;span&gt;&amp;nbsp;&lt;/span&gt;.yarn/unplugged,&lt;span&gt;&amp;nbsp;&lt;/span&gt;.pnp.cjs&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn.lock&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일이 생성된다. strict 모드는 기본 값이고, loose는 아래 처럼 옵셔널로 설정해두어야 한다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;# .yarnrc.yml
nodeLinker: pnp
pnpMode: loose
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PnP 프로젝트에서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;.yarn/&lt;span&gt;&amp;nbsp;&lt;/span&gt;폴더 내부에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;release/외에도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://yarnpkg.com/getting-started/editor-sdks&quot;&gt;ide 지원&lt;/a&gt;을 위한&lt;span&gt;&amp;nbsp;&lt;/span&gt;sdk/&lt;span&gt;&amp;nbsp;&lt;/span&gt;폴더를 포함할 가능성이 높다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored&quot;&gt;이외에도 사용례에 따라서, 다양한 폴더들이 생성될 수 있다.&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;.
├── .yarn/
│   ├── cache/
│   ├── releases/
│   │   └── yarn-3.1.1.cjs
│   ├── sdk/
│   └── unplugged/
├── .pnp.cjs
├── .pnp.loader.mjs
├── .yarnrc.yml
├── package.json
└── yarn.lock
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;pnpm-1&quot; data-ke-size=&quot;size23&quot;&gt;pnpm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm도 다른 패키지 매니저와 마찬가지로&lt;span&gt;&amp;nbsp;&lt;/span&gt;package.json&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 필요하다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;$ pnpm i를 실행하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 생성되는 것 까지는 다른 패키지 관리자와 동일하지만, 앞서 언급한&lt;span&gt;&amp;nbsp;&lt;/span&gt;content-addressable storage approach라는 특성 때문에 이후의 구조가 완전히 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm은 자체 lock 파일인&lt;span&gt;&amp;nbsp;&lt;/span&gt;pnp-lock.yml을 생성한다. 그리고 마찬가지로&lt;span&gt;&amp;nbsp;&lt;/span&gt;.npmrc로 설정을 추가할 수도 있다.&lt;/p&gt;
&lt;h2 id=&quot;lock-파일과-dependency-저장&quot; data-ke-size=&quot;size26&quot;&gt;Lock 파일과 dependency 저장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급한 것 처럼, 모든 패키지 매니저는 각자 다른 형태의 lock 파일이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 lock 파일의 정의를 먼저 살펴보면, lock 파일이란 매 설치시 결정적이고 (= 항상 같은 버전을 설치하고) 예측가능한 특성을 보장하기 위하여, 각 버전의 정확한 의존성 버전을 저장하고 있는 파일을 의미한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;package.json은 정확한 버전이 기재되어 있는 것이 아니고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;gt;= 1.2.5와 같은 형식의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.npmjs.com/about-semantic-versioning&quot;&gt;버전 범위 aka 시멘틱 버저닝&lt;/a&gt;이 존재하기 때문에, lock파일이 없다면 매 설치마다 설치하는 버전이 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock 파일은 또한 체크섬이 존재하는데, 이에 대해서는 보안 관련 섹션에서 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 lock 파일은 npm@5 부터 (package-lock.json), pnpm은&lt;span&gt;&amp;nbsp;&lt;/span&gt;pnpm-lock.yaml, yarn은&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn.lock&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 섹션에서 언급했던 것처럼, 전통적인 접근 방식으로는 모든 의존성을&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules을 설치하는 방법을 npm, yarn classic, pnpm(의 경우엔 구조가 조금 다르다) 사용하는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn berry의 PnP 모드에서는 조금 다른 모습을 볼 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules대신, 모든 의존성을&lt;span&gt;&amp;nbsp;&lt;/span&gt;zip&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일로 압축하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;.yarn/cache와&lt;span&gt;&amp;nbsp;&lt;/span&gt;.pnp.cjs&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모두 잘알고 있는 것처럼, 모든 팀원이 (모든 머신에서) 같은 버전을 설치하는 것을 보장하기 위해 lock 파일은 버전 컨트롤 내부에 포함시키는 것이 좋다.&lt;/p&gt;
&lt;h2 id=&quot;cli-commands&quot; data-ke-size=&quot;size26&quot;&gt;CLI commands&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cli 커맨드는 워낙 많고 다양하여, 여기에서 모든 것을 다루지는 않으려고 한다. 아래 내용은 개발과정에서 자주 쓰일 수 있는 커맨드를 모아 둔 것이다.&lt;/p&gt;
&lt;h3 id=&quot;의존성-관리&quot; data-ke-size=&quot;size23&quot;&gt;의존성 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npmyarn classicyarn berrypnpm&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;install deps&lt;/td&gt;
&lt;td&gt;npm install&lt;/td&gt;
&lt;td&gt;yarn install&lt;span&gt;&amp;nbsp;&lt;/span&gt;or&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn&lt;/td&gt;
&lt;td&gt;like classic&lt;/td&gt;
&lt;td&gt;pnpm install&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;update deps&lt;/td&gt;
&lt;td&gt;npm update&lt;/td&gt;
&lt;td&gt;yarn upgrade&lt;/td&gt;
&lt;td&gt;yarn semver up&lt;/td&gt;
&lt;td&gt;pnpm update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;update deps to latest&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;yarn upgrade --latest&lt;/td&gt;
&lt;td&gt;yarn up&lt;/td&gt;
&lt;td&gt;pnpm update --latest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;update deps interactively&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;yarn upgrade-interactive&lt;/td&gt;
&lt;td&gt;like classic&lt;/td&gt;
&lt;td&gt;pnpm up -- interactive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;add specific dep&lt;/td&gt;
&lt;td&gt;npm i react&lt;/td&gt;
&lt;td&gt;yarn add react&lt;/td&gt;
&lt;td&gt;like classic&lt;/td&gt;
&lt;td&gt;pnpm add react&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;add specific dep in dev&lt;/td&gt;
&lt;td&gt;npm i -D babel&lt;/td&gt;
&lt;td&gt;yarn add -D babel&lt;/td&gt;
&lt;td&gt;like Classic&lt;/td&gt;
&lt;td&gt;pnpm add -D babel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uninstall deps&lt;/td&gt;
&lt;td&gt;npm uninstall react&lt;/td&gt;
&lt;td&gt;yarn remove react&lt;/td&gt;
&lt;td&gt;like Classic&lt;/td&gt;
&lt;td&gt;pnpm remove react&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uninstall deps without update&lt;span&gt;&amp;nbsp;&lt;/span&gt;package.json&lt;/td&gt;
&lt;td&gt;npm uninstall --no-save&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;패키지-관련&quot; data-ke-size=&quot;size23&quot;&gt;패키지 관련&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/ruyadorno/ntl&quot;&gt;ntl&lt;/a&gt;과 같은 바이너리 파일 처럼, development 환경에서 유틸리티 도구를 구성하는 패키지를 관리하는 명령어를 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn berry에서는 보안상의 이유로 패키지에서 지정한 바이너리 또는&lt;span&gt;&amp;nbsp;&lt;/span&gt;package.json에 명시된 실행할 수 있다는 것을 염두해 두어야 한다. 이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;pnpm에서도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npmyarn classicyarn berrypnpm&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;install, update, remove package globally&lt;/td&gt;
&lt;td&gt;npm i -g ntl&lt;/td&gt;
&lt;td&gt;yarn global ad ntl&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;pnpm add --global ntl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;run binaries from terminal&lt;/td&gt;
&lt;td&gt;npm exec ntl&lt;/td&gt;
&lt;td&gt;yarn ntl&lt;/td&gt;
&lt;td&gt;yarn ntl&lt;/td&gt;
&lt;td&gt;pnpm ntl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;run binaries from script&lt;/td&gt;
&lt;td&gt;ntl&lt;/td&gt;
&lt;td&gt;ntl&lt;/td&gt;
&lt;td&gt;ntl&lt;/td&gt;
&lt;td&gt;ntl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dynamic package execution&lt;/td&gt;
&lt;td&gt;npx ntl&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;yarn dlx ntl&lt;/td&gt;
&lt;td&gt;pnpm dlx ntl&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;자주-쓰이는-커맨드&quot; data-ke-size=&quot;size23&quot;&gt;자주 쓰이는 커맨드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npmyarn classicyarn berrypnpm&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;publish&lt;/td&gt;
&lt;td&gt;npm publish&lt;/td&gt;
&lt;td&gt;yarn publish&lt;/td&gt;
&lt;td&gt;yarn npm publish&lt;/td&gt;
&lt;td&gt;pnpm publish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list installed deps&lt;/td&gt;
&lt;td&gt;npm ls&lt;/td&gt;
&lt;td&gt;yarn list&lt;/td&gt;
&lt;td&gt;like Classic&lt;/td&gt;
&lt;td&gt;pnpm list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list outdated deps&lt;/td&gt;
&lt;td&gt;npm outdated&lt;/td&gt;
&lt;td&gt;yarn outdated&lt;/td&gt;
&lt;td&gt;yarn upgrade-interactive&lt;/td&gt;
&lt;td&gt;pnpm outdated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;print info about deps&lt;/td&gt;
&lt;td&gt;npm explain ntl&lt;/td&gt;
&lt;td&gt;yarn why ntl&lt;/td&gt;
&lt;td&gt;like Classic&lt;/td&gt;
&lt;td&gt;pnpm why ntl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;init project&lt;/td&gt;
&lt;td&gt;npm init&lt;/td&gt;
&lt;td&gt;yarn init&lt;/td&gt;
&lt;td&gt;yarn init&lt;/td&gt;
&lt;td&gt;pnpm init&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;성능과-디스크-관리의-효율성&quot; data-ke-size=&quot;size26&quot;&gt;성능과 디스크 관리의 효율성&lt;/h2&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;a href=&quot;https://p.datadoghq.eu/sb/d2wdprp9uki7gfks-c562c42f4dfd0ade4885690fa719c818?tpl_var_npm=%2A&amp;amp;tpl_var_pnpm=%2A&amp;amp;tpl_var_yarn-classic=%2A&amp;amp;tpl_var_yarn-modern=%2A&amp;amp;tpl_var_yarn-nm=%2A&amp;amp;tpl_var_yarn-pnpm=no&amp;amp;from_ts=1645791374255&amp;amp;to_ts=1653567374255&amp;amp;live=true&quot;&gt;https://p.datadoghq.eu/sb/d2wdprp9uki7gfks-c562c42f4dfd0ade4885690fa719c818?tpl_var_npm=%2A&amp;amp;tpl_var_pnpm=%2A&amp;amp;tpl_var_yarn-classic=%2A&amp;amp;tpl_var_yarn-modern=%2A&amp;amp;tpl_var_yarn-nm=%2A&amp;amp;tpl_var_yarn-pnpm=no&amp;amp;from_ts=1645791374255&amp;amp;to_ts=1653567374255&amp;amp;live=true&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pnpm.io/benchmarks&quot;&gt;https://pnpm.io/benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능으로 미뤄 보건데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn berry&lt;span&gt;&amp;nbsp;&lt;/span&gt;+&lt;span&gt;&amp;nbsp;&lt;/span&gt;Plug n Play strict가 가장 설치도 빠르고 디스크 효율적인 모습을 보여주었고, 그다음으로는 pnpm이 뒤를 이었다.&lt;/p&gt;
&lt;h2 id=&quot;보안&quot; data-ke-size=&quot;size26&quot;&gt;보안&lt;/h2&gt;
&lt;h3 id=&quot;npm-2&quot; data-ke-size=&quot;size23&quot;&gt;npm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm은 그 역사가 오래된 만큼 사건 사고도 많았다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/npm/npm/issues/19883&quot;&gt;과거 npm v5.7.0에서 파일시스템 권한을 바꿀 수 있는 버그&lt;/a&gt;가 발견된 적도 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;sudo npm&lt;span&gt;&amp;nbsp;&lt;/span&gt;명령어를 사용하면, 시스템 파일의 소유권을 변경하게 되어 os를 사용할 수 없게된 적이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2018년에는 비트코인과 관련된 사건 사고도 있었다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.npmjs.com/package/event-stream&quot;&gt;EventStream&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident.html&quot;&gt;패키지 v3.3.6에서 악의적인 의존성이 추가되어, 개발자의 컴퓨터에서 비트코인을 훔치고자 하는 악의적인 코드가 존재한 바 있다.&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하기 위해, 요즘 최신버전의 npm 에서는 package-lock.json에서 SHA-512 알고리즘을 확인하여 설치하고자 하는 패키지의 무결성을 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전반적으로 npm은 사건사고가 많았던 것 만큼, 보안 문제에 각별히 신경을 많이 쓰고 있는 추세다.&lt;/p&gt;
&lt;h3 id=&quot;yarn&quot; data-ke-size=&quot;size23&quot;&gt;yarn&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn classic, yarn berry 둘다 처음부터&lt;span&gt;&amp;nbsp;&lt;/span&gt;yarn.lock에 지정된 체크섬을 활용하여 각 패키지의 무결성을 확인한다. 또한&lt;span&gt;&amp;nbsp;&lt;/span&gt;package.json&lt;span&gt;&amp;nbsp;&lt;/span&gt;내부에 선언되지 않은 의심스러운 패키지가 존재하면 설치가 중단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yarn berry는 이에 더해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/yarnpkg/berry/issues/2784#issuecomment-831825366&quot;&gt;package.json에서 명시한 의존성의 바이너리 파일만 실행할 수 있다.&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이는 pnpm과 유사하다.&lt;/p&gt;
&lt;h3 id=&quot;pnpm-2&quot; data-ke-size=&quot;size23&quot;&gt;pnpm&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm 또한 체크섬을 활용하여 패키지의 무결성을 확인한다. pnpm은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.mo4tech.com/deep-thoughts-on-modern-package-managers-why-do-i-now-recommend-pnpm-over-npm-yarn-2.html&quot;&gt;npm과 yarn classic에서 이슈가 되는 패키지 호이스팅&lt;/a&gt;을 하지 않기 때문에 이러한 문제를 피한다. 이 대신, 위험한 dependency 액세스의 위험성을 제거하는 내부에 중첩된&lt;span&gt;&amp;nbsp;&lt;/span&gt;node_modules폴더를 생성한다. 즉, dependency가 package.json 에서 명시적으로 선언된 경우에만 다른 dependency에 액세스 할 수 있다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 대부분의 패키지 매니저들은 모두 사용하기에 무리가 없는 수준까지 기능이 구성되어 있다. 대부분의 패키지 매니저가 기능성 사이에서 동등함을 보이고 있다. 물론, 그 아래에서 동작하는 방식은 매우 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm은 npm과 비슷해보이지만, 종속성 관리 측면에서 매우 다른 모습을 보인다. pnpm을 사용하면 성능이 향상되고, 디스크 효율성을 극대화 할 수 있다. yarn classic도 훌륭한 선택지이지만, 레거시로 간주되고 가까운 미래에 지원이 중단될 수도 있는 가능성이 존재해서 선택하는 것을 추천하지는 않는다. yarn berry의 plug n play 는 완전히 새로운 혁신으로 다가왔지만, 아직 그 모든 잠재력을 달성한 것 같지는 않다. 그럼에도 요즘 사람들이 많이 쓰는 패키지 매니저는 yarn berry의 pnp 인 것으로 보인다. 성능과 디스크 효율성, 속도 모두에서 뛰어난 모습을 보이고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것 저것 생각하기 쉽지 않고, 또 빠르고 쉽게 접근하고 싶다면 npm을 쓰는 것도 나쁘지 않다. 물론 다른 패키지 매니저에 비해서 성능이나 속도면에서 뒤쳐지는 감이 있지만, 긴 역사를 기반으로 한 많은 문서와 시행착오를 확인할 수 있는 다양한 글들은, 초보자들이 접근하기에는 가장 용이한 선택지가 될 것이다.&lt;/p&gt;
&lt;h2 id=&quot;참고&quot; data-ke-size=&quot;size26&quot;&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.logrocket.com/javascript-package-managers-compared/&quot;&gt;https://blog.logrocket.com/javascript-package-managers-compared/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/wantedjobs/yarn-berry-%EC%A0%81%EC%9A%A9%EA%B8%B0-1-e4347be5987&quot;&gt;https://medium.com/wantedjobs/yarn-berry-%EC%A0%81%EC%9A%A9%EA%B8%B0-1-e4347be5987&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://toss.tech/article/node-modules-and-yarn-berry&quot;&gt;https://toss.tech/article/node-modules-and-yarn-berry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/0923884&quot;&gt;https://d2.naver.com/helloworld/0923884&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/7553804&quot;&gt;https://d2.naver.com/helloworld/7553804&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://yceffort.kr/2022/05/npm-vs-yarn-vs-pnpm&lt;/a&gt;&lt;/p&gt;</description>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/164</guid>
      <comments>https://calssess.tistory.com/164#entry164comment</comments>
      <pubDate>Fri, 10 Mar 2023 15:01:43 +0900</pubDate>
    </item>
    <item>
      <title>CRA SSR without eject</title>
      <link>https://calssess.tistory.com/163</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1072&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nb0DC/btrZzK4Pl9y/1QnwXcElpJWSkfyJZNm45K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nb0DC/btrZzK4Pl9y/1QnwXcElpJWSkfyJZNm45K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nb0DC/btrZzK4Pl9y/1QnwXcElpJWSkfyJZNm45K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnb0DC%2FbtrZzK4Pl9y%2F1QnwXcElpJWSkfyJZNm45K%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;1280&quot; height=&quot;1072&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1072&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 페이지는 사실 서버사이드 렌더링을 사용하진 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 SSR 의 전제조건은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. CRA 를 Eject 하지 않는다.&lt;/b&gt;&lt;br /&gt;한번 eject 하면 되돌릴 수 없고, react-scripts 가 해주는 많은 편의기능을 포기하는 동시에 업데이트도 골치아파 진다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/timarney/react-app-rewired&quot;&gt;timarney/react-app-rewired&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;와 같은 패키지도 존재하지만 이 역시 메이저 업데이트를 바로 지원하지 않을 가능성이 높고, create-react-app 의 dependency 도 충분히 피곤하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 전체 페이지를 렌더링한다&lt;/b&gt;&lt;br /&gt;SEO 를 위해서 전체 페이지 렌더링이 필수적인 것은 아니나 분명 전체 페이지를 그리는 것이 좋은 것은 자명하고, First Meaningful Paint 라는 측면에서도 그렇고, 유저에게 매번 로딩화면을 보여주고 싶지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 로그인 처리도 한다.&lt;/b&gt;&lt;br /&gt;많은 SPA 가 로그인처리를 browser 에 jwt 등을 저장하는 방식을 사용하여, 로딩 시에 다시한번 서버쪽 API 를 접근하게 하는데, 이 부분을 역시 해소하고 싶었다. 가급적이면 서버에서 처리를 해서 한번 화면이 그려지면 repaint 되는 부분을 최소화 하고 싶었다.&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_1_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. TTFB 를 고려한다.&lt;/b&gt;&lt;br /&gt;사실 초반에는 큰 문제가 없었고, 어플리케이션에 담기는 데이터 양이 늘어나면서 생겨난 문제로 어쩔 수 없는 부분이 있더라도 속도를 개선하고 싶었다.&lt;/p&gt;
&lt;h3 id=&quot;1-cra-eject-&quot; data-ke-size=&quot;size23&quot;&gt;1. CRA 를 eject 하지 않는다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QWER.GG 는 CRA 의 CSS 를 SASS 로만 변경하여 사용하고 있다. 따라서 일반적인 component 의 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import &amp;lsquo;./Button.scss&amp;rsquo;;

import React from 'react';
import classNames from 'classnames';

type Props = {
  className?: string;
} &amp;amp; React.ButtonHTMLAttributes&amp;lt;HTMLButtonElement&amp;gt;;

const Button: React.FC&amp;lt;Props&amp;gt; = ({ className, ...props }) =&amp;gt; {
  return (
    &amp;lt;button 
      className={classNames('Button', className)}
      {...props}
    /&amp;gt;
  );
}

export default Button;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 두가지 제약사항(?)이 생겨난다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Typescript 를 컴파일 해야 한다.&lt;/li&gt;
&lt;li&gt;Sass 파일이 ts 코드 상에서 import 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 react 코드에 접근하려면 build 되어야 하고, CRA 의 build process 는 eject 하지 않으면 접근할 수 없기 때문에 다른 방법을 사용해야 했다. 다행히 typescript 의 기본 compiler 인 tsc 가 jsx 또한 문제없이 처리하기 때문에, tsc 를 사용하기로 했다. 그런데&lt;span&gt;&amp;nbsp;&lt;/span&gt;import &amp;lsquo;./Button.scss&amp;rsquo;;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 부분이 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 시도 - 빌드시에 scss 파일을 스킵하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.ts&lt;span&gt;&amp;nbsp;&lt;/span&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;.js&lt;span&gt;&amp;nbsp;&lt;/span&gt;같은 일반적인 스크립트 extension 이 아니면 tsc 는 처리할 수 없고, 빌드만 스킵하더라고 해도 해당 코드가 script 상에 포함되어 있어, node.js 런타임 에서 해당 component 를 불러와 서버 사이드 렌더링을 시도할 때 syntax error 를 throw 하게 된다. 때문에 다른 방법이 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 시도 - 전처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CRA 에서 webpack 을 &amp;nbsp;이미 포함하기 때문에 다시 설치해서 빌드할 수 없었고, webpack 빌드는 기본적으로 browser 를 위한 것이라 node.js 서버 쪽 빌드와 잘 어울리지 않았다. 애초에 node.js server 에서는 code 를 하나의&lt;span&gt;&amp;nbsp;&lt;/span&gt;.js&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일로 만들 이유가 없기 때문에 불필요한 요소라 생각했다. 물론 그렇게 해도 되었겠지만, 경험상 어울리지 않는 조합을 엮는 행위는 미래의 생산성에 분명히 영향을 끼쳤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;webpack 을 쓰지 않고 전처리를 하려면 무엇을 사용할까 고민했다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;태초에 gulp 가 있었다.&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;webpack 이전에 많이 사용되던 툴 중 grunt 는 설정파일이 귀찮으니 패스., gulp 를 사용하기로 했다. 코드는 상당히 단순하다&lt;span&gt;&amp;nbsp;&lt;/span&gt;.scss&lt;span&gt;&amp;nbsp;&lt;/span&gt;구문이 있는 줄을 찾아내서&lt;span&gt;&amp;nbsp;&lt;/span&gt;// Removed&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 교체한다. 코드는 다음과 같다.&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_2_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const gulp = require(&amp;lsquo;gulp&amp;rsquo;);
const replace = require(&amp;lsquo;gulp-replace&amp;rsquo;);

gulp.task(&amp;lsquo;build&amp;rsquo;, () =&amp;gt; {
  return gulp.src(&amp;lsquo;src/**/*.{ts,tsx}&amp;rsquo;, { base: &amp;lsquo;src&amp;rsquo; })
    .pipe(replace(/.+\.s?css.+/g, &amp;lsquo;// Removed&amp;rsquo;))
    .pipe(replace(/.+\.yml.+/g, &amp;lsquo;// Removed&amp;rsquo;))
    .pipe(gulp.dest(&amp;lsquo;server&amp;rsquo;));
});
&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;일단 여기까지 했으니 큰 난관은 헤처왔고, 나머지는 빌드 과정에 녹여내면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;{
  &quot;scripts:&quot; {
    &amp;ldquo;build:server&amp;rdquo;: &amp;ldquo;rimraf server &amp;amp;&amp;amp; gulp build &amp;amp;&amp;amp; npm run tsc&amp;rdquo;,
    &amp;ldquo;tsc&amp;rdquo;: &amp;ldquo;node node_modules/typescript/bin/tsc -p tsconfig.server.json&amp;rdquo;,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;tsconfig.server.json&lt;span&gt;&amp;nbsp;&lt;/span&gt;이란 파일을 새로 만든 이유는 약간의 설정이 달라야 하기 때문이다. CRA 의&lt;span&gt;&amp;nbsp;&lt;/span&gt;webpack&lt;span&gt;&amp;nbsp;&lt;/span&gt;은 기본적으로 tsc 는 typecheck 만 하고 실제 jsx 나 code compile 은 babel 을 사용하기 때문에 두 타겟의 설정은 달라져야 한다. 자세한 설명보다는 현재 사용하고 있는 설정 파일을 첨부한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 파일이 일반적으로 react 에 필요한&lt;span&gt;&amp;nbsp;&lt;/span&gt;tsconfg.json&lt;span&gt;&amp;nbsp;&lt;/span&gt;이고&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;es5&quot;,
    &quot;lib&quot;: [
      &quot;es6&quot;,
      &quot;dom&quot;,
      &quot;dom.iterable&quot;,
      &quot;esnext&quot;
    ],
    &quot;allowJs&quot;: true,
    &quot;skipLibCheck&quot;: true,
    &quot;esModuleInterop&quot;: true,
    &quot;allowSyntheticDefaultImports&quot;: true,
    &quot;strict&quot;: true,
    &quot;forceConsistentCasingInFileNames&quot;: true,
    &quot;module&quot;: &quot;esnext&quot;,
    &quot;moduleResolution&quot;: &quot;node&quot;,
    &quot;resolveJsonModule&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;noEmit&quot;: true,
    &quot;typeRoots&quot;: [
      &quot;@types&quot;
    ],
    &quot;jsx&quot;: &quot;preserve&quot;
  },
  &quot;include&quot;: [
    &quot;src&quot;
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요없는 부분을 삭제하고 일부 수정한&lt;span&gt;&amp;nbsp;&lt;/span&gt;tsconfig.server.json&lt;span&gt;&amp;nbsp;&lt;/span&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;outDir&quot;: &quot;build/&quot;,
    &quot;module&quot;: &quot;commonjs&quot;,
    &quot;target&quot;: &quot;es6&quot;,
    &quot;noImplicitAny&quot;: false,
    &quot;sourceMap&quot;: true,
    &quot;esModuleInterop&quot;: true,
    &quot;resolveJsonModule&quot;: true,
    &quot;allowSyntheticDefaultImports&quot;: true,
    &quot;jsx&quot;: &quot;react&quot;,
    &quot;moduleResolution&quot;: &quot;node&quot;
  },
  &quot;include&quot;: [
    &quot;server&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래전에 작성한 코드라 불필요한 설정이 들어가있을 수 있으나 그냥 첨부한다. 이 정도라도 누군가에겐 큰 도움이 될 수 있으니.&lt;/p&gt;
&lt;h3 id=&quot;2-&quot; data-ke-size=&quot;size23&quot;&gt;2. 전체 페이지를 렌더링한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기부터 코드가 많이 변경되어 몇가지 핵심적인 요소만 언급한다. QWER.GG 는 Restful API 를 redux store 에 저장하는 방식으로 개발을 시작해서 현재 Apollo 기반 GraphQL 을 사용하고 있다. 아직도 redux state 를 완전히 걷어내지는 못했고 따라서 redux state 주입 코드와 apollo 데이터 주입 코드가 공존하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 어떻게 state 를 주입할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 redux 는 서버 렌더링이 간단하다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;react-redux&lt;span&gt;&amp;nbsp;&lt;/span&gt;의&lt;span&gt;&amp;nbsp;&lt;/span&gt;Provider&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 store 를 만들어서 주입해주면 된다. 그런데 문제는 CRA 를 그대로 사용하려면 템플릿을 사용하지 않고&lt;span&gt;&amp;nbsp;&lt;/span&gt;index.html&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 그대로 살려야 한다는 점이다. eject 하면 간단하겠지만, 상술했듯 최대한 원형을 보존하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;react-scripts&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 업데이트 이점을 살리는 것이 기본 전제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 파일을 불러온다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const indexTemplate = fs.readFileSync(path.resolve(__dirname, &amp;lsquo;../../build/index.html&amp;rsquo;), &amp;lsquo;utf8&amp;rsquo;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;빌드된 index.html 파일&lt;/b&gt;을 불러오는 것이다. 이 부분도 참 귀찮은 부분 중 하나였는데 빌드 되기 전 index.html 을 사용하게 되면 빌드된&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;script src=&quot;...&quot; /&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;구문이 포함되어 있지 않기 때문에 반드시 cra 의 build 된 이후의 코드를 사용해야 한다.&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_3_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 필요한 코드를 다음과 같이&lt;span&gt;&amp;nbsp;&lt;/span&gt;String.prototype.replace&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;를 사용해서 교체해준다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/cheeriojs/cheerio&quot;&gt;cheeriojs/cheerio&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 쓰면 훨씬 쉬웠겠지만 그땐 거기까지 생각이 닿지 않았고, 다음과 같이 다소 원시적인 방법으로 수정해주었다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;export function renderDefault(req: express.Request) {
  const language = getLanguageFromRequest(req);
  const theme = getThemeFromRequest(req);
  const helmet = Helmet.renderStatic();

  return indexTemplate
    .replace('lang=&quot;en&quot;', `lang=&quot;${language}&quot;`)
    .replace('data-theme=&quot;dark&quot;', `data-theme=&quot;${theme}&quot;`)
    .replace('window.__language__', `window.__language__='${language}'`)
    .replace(/&amp;lt;title&amp;gt;.*?&amp;lt;\/title&amp;gt;/g, helmet.title.toString())
    .replace('&amp;lt;meta name=&quot;placeholder&quot;&amp;gt;', helmet.meta.toString())
    .replace('&amp;lt;link rel=&quot;placeholder&quot;&amp;gt;', helmet.link.toString());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_4_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;킬링 포인트는 index.html 에 넣어준 placeholder 코드들. 다시 말하지만 cheerio 를 사용하면 훨씬 덜 원시적으로 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;html lang=&quot;en&quot; data-theme=&quot;dark&quot;&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;meta name=&quot;placeholder&quot;&amp;gt;
    &amp;lt;link rel=&quot;placeholder&quot;&amp;gt;
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 실제로 데이터 주입하기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 데이터를 주입할때는 그냥 절차적으로 진행했다. default state 를 가져와서 부어주는 방식. 역시 원시적인&lt;span&gt;&amp;nbsp;&lt;/span&gt;String.prototype.replace&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 교체해주었다. 다른점이라면 index.html 이 아니라 renderToString 된 코드를 수정했다는 정도.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export async function renderFromServer(req: express.Request) {
  const preloaded = await getDefaultState(req);
  const apolloClient = getApolloClient(req);
  const hydratedApp = await getHydratedApp(req, preloaded, apolloClient);

  // TODO: 여기가 가장 느림. 퍼포먼스 개선 포인트
  await getDataFromTree(hydratedApp);

  const rendered = ReactDOMServer.renderToString(hydratedApp);

  return renderDefault(req)
    .replace(&amp;lsquo;window.__PRELOADED_STATE__={}&amp;rsquo;, `window.__PRELOADED_STATE__=${JSON.stringify(preloaded).replace(/&amp;lt;/g, '\\u003c')}`)
    .replace(&amp;lsquo;window.__APOLLO_STATE__={}&amp;rsquo;, `window.__APOLLO_STATE__=${JSON.stringify(apolloClient.extract()).replace(/&amp;lt;/g, '\\u003c')}`)
    .replace(&amp;lsquo;&amp;lt;div id=&amp;ldquo;root&amp;rdquo;&amp;gt;&amp;lt;/div&amp;gt;&amp;rsquo;, `&amp;lt;div id=&quot;root&quot;&amp;gt;${rendered}&amp;lt;/div&amp;gt;`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_5_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 말하는데 왠만하면 cheerio 를 쓰자. 결과물은 devtools 로 보면 이렇게 된다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;window.__APOLLO_STATE__&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 일부분인데 이런걸 browser 에 뿌려줘야 한다는 사실이 진짜 너무 괴롭다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pbISF/btrZxd0RG87/XVQUjv8DgSLB7ZISVq0LiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pbISF/btrZxd0RG87/XVQUjv8DgSLB7ZISVq0LiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pbISF/btrZxd0RG87/XVQUjv8DgSLB7ZISVq0LiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpbISF%2FbtrZxd0RG87%2FXVQUjv8DgSLB7ZISVq0LiK%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;1280&quot; height=&quot;558&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저에서 반복 쿼리를 하지 않기위한 preloaded  state&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function getHydratedApp(
  req: express.Request,
  preloadedState: any,
  apolloClient: ApolloClient&amp;lt;NormalizedCacheObject&amp;gt;,
) {
  const url = req.originalUrl;
  const { store } = configureStore(preloadedState);
  const language = getLanguageFromRequest(req);

  return (
    &amp;lt;ApolloProvider client={apolloClient}&amp;gt;
      &amp;lt;Provider store={store}&amp;gt;
        &amp;lt;StaticRouter location={url} context={{ url }}&amp;gt;
          &amp;lt;GlobalStateProvider cookies={req.cookies}&amp;gt;
            &amp;lt;App language={language} /&amp;gt;
          &amp;lt;/GlobalStateProvider&amp;gt;
        &amp;lt;/StaticRouter&amp;gt;
      &amp;lt;/Provider&amp;gt;
    &amp;lt;/ApolloProvider&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Provider 가 무척 많다고 생각되는 건 기분 탓이다. hook, redux, apollo, react-router 가 섞여 있다보니 자연스럽게 이렇게 된다. 왜 이렇게 됬냐고 하기 전에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://seokjun.kim/spa-is-aweful/&quot;&gt;싱페어 (SPA) 의 피로감&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 글을 읽고 오자. 다만 이렇게 하다보니 위에 주석에도 남겼지만 Performance 문제가 발생한다. 후술하겠다.&lt;/p&gt;
&lt;h3 id=&quot;3-&quot; data-ke-size=&quot;size23&quot;&gt;3. 로그인 처리도 한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 처리를 위해서는 Cookie 에서 로그인 처리를 해주어야 한다. browser 가 가지고 있는 long time storage 중 서버와 동시에 사용할 수 있는 유일한 부분이기 때문이다. 요즘엔 fetch 도 표준이 바뀌어서 same-origin 기준 cookie 를 기본적으로 보내기 때문에 다소 편리해지긴 했다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Fetch%EC%9D%98_%EC%82%AC%EC%9A%A9%EB%B2%95&quot;&gt;Using Fetch - Web API | MDN&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;링크를 참고하자.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;* 보통 fetch는 **쿠키를 보내거나 받지 않습니다.**  사이트에서 사용자 세션을 유지 관리해야하는 경우 인증되지 않는 요청이 발생합니다. 쿠키를 전송하기 위해서는 자격증명(credentials) 옵션을 반드시 설정해야 합니다.
 [2017년 8월 25일](https://github.com/whatwg/fetch/pull/585)  이후. 기본 자격증명(credentials) 정책이 same-origin 으로 변경되었습니다. 파이어폭스는 61.0b13 이후 변경되었습니다.
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_6_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;static site 즉 빌드 된 index.html 을 CDN 을 통해서 서빙하지 않으면 이런 장점이 생긴다. nginx 와 같은 웹서버에서 load balancing 을 할때 reverse proxy 를 이용해서 같은 도메인으로 request 를 보낼 수 있게 하면, 로그인 처리를 jwt 와 같은 걸 사용하지 않고도 node.js 의 session 처럼 관리할 수 있다. 다만 이런 경우 ip address 등의 항목이 유실될 수 있지 않도록 설정에 주의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getHydratedApp 함수의 아래 코드가 바로 이런 부분을 처리해주기 위함이다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;GlobalStateProvider cookies={req.cookies}&amp;gt;
  &amp;lt;App language={language} /&amp;gt;
&amp;lt;/GlobalStateProvider&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GlobalState 를 cookie 에 넣어주게 되면, 어떤 state 를 서버에서 렌더링 할때도 유용하게 사용할 수 있다. QWER.GG 에서는 default state 를 주입하기 위해 주로 사용한다. geocode lookup 이라거나 i18n 처리 등의 작업도 해당된다.&lt;/p&gt;
&lt;h3 id=&quot;4-ttfb-&quot; data-ke-size=&quot;size23&quot;&gt;4. TTFB 를 고려한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상당히 많은 과정과 시행착오가 생략되었지만 기본적으로 SSR 은 다음과 같은 순서로 이루어진다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본적으로 필요한 state 를 주입한다.&lt;br /&gt;theme 설정, geocode lookup, application 구동에 필요한 기본 설정들&lt;/li&gt;
&lt;li&gt;route 가 어딘지 확인하고 필요한 데이터를 가져온다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;redux 라면 필요한 state 를 주입할테고, (http 나 직접 db 에 접근하거나)&lt;/li&gt;
&lt;li&gt;graphql 이라면 query 를 execute 해줄것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터와 함께 page 를 렌더링하고&lt;/li&gt;
&lt;li&gt;title 이나 meta 등 react root 외에 부분들을 rendering 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 부분을 connection 핸들링 까지 해야 하다보니 react 의 SSR 은 기본적으로 빠를 수가 없다. 모든 route 의 component 를 쪼개서 부분부분 미리 rendering 을 하는 방식이라면 나름대로 가능한 부분도 존재하겠지만, React 의 핵심 코드에 접근해서 마개조를 해야하기 때문에 작은 팀에겐 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 CPU 도 많이 쓰고 Memory 도 많이 사용한다. 전체 react 코드를 recursive 하게 훑어야 하다보니 당연히 cpu 의존도가 커지고, 그 과정에서 해당 페이지의 모든 API 에 접근해야 하니 당연히 memory 도 많이 쓰게 된다. 그러다보니 고작(?) SSR 서버 주제에 서버 리소스를 과하게 차지하는 기현상이 생겨난다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;이쯤되면 React 를 쓰는게 너무 괴롭다&amp;hellip;&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;작은 팀에서 감당하기엔 어려운지라 프론트 서버가 너무 맣ㄴ은 리소스를 먹게되면 SSR 을 포기하는 코드를 만들어 두었다.&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_7_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const renderServerSide = (function() {
  const monitor = monitorSystemUsage(1 * A_SECOND).init();

  return async (req: express.Request, res: express.Response) =&amp;gt; {
    if (req.path === '/') {
      return renderDefault(req);
    }

    if (monitor.usage.memory &amp;gt; 70 || monitor.usage.cpu &amp;gt; 70) {
      logger.warn(`System usage exceeds 70%. Skip SSR. Current mem: ${monitor.usage.memory}%, cpu: ${monitor.usage.cpu}%`);

      return renderDefault(req);
    }

    return await renderFromServer(req);
  };
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;덕분에 지금은 TTFB 가 최대 10초(!!!!!!) 였던 것이 최대 2초로 줄어들었고, 인덱스 페이지 (/) 는 어차피 SEO 에서 의미가 없기 때문에 SSR 을 포기했다. 컨텐츠가 있는 페이즈가 아니고서는 검색엔진에서 가져갈 데이터가 거의 없기 때문이기도 하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kCP2H/btrZxeyHbi7/92DQZdDd6oIDZat4vbWus0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kCP2H/btrZxeyHbi7/92DQZdDd6oIDZat4vbWus0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kCP2H/btrZxeyHbi7/92DQZdDd6oIDZat4vbWus0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkCP2H%2FbtrZxeyHbi7%2F92DQZdDd6oIDZat4vbWus0%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;1280&quot; height=&quot;673&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;673&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;UA 를 보고 검색엔진인 경우 SSR 해주는 것도 좋은 방법이겠지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;귀찮아서&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;당분간은 현재 구조를 유지하기로 했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 id=&quot;--2&quot; data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React CRA 를 eject 하지 않고도 충분히 SSR 을 달성할 수 있다. 다만 그대로 사용할 순 없고, 전처리를 해주거나 전처리가 필요하지 않은 styled-component 같은 패키지&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;(안써봄)&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용하면 된다. React 가 개발 생산성 측면에서 매우 뛰어난 것은 사실이나, SSR 이라는 어찌보면 별것 아닌 기능에 많은 리소스가 투여되어야 한다는 점이 무척 아쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글의 서두에 적어놓았듯이 작은 스타트업에게 절대적으로 큰 부분을 차지하는 것이 검색엔진을 통한 유입인데, 개발 편의성을 위해서 비지니스 이점을 포기한다는 것은 장기적인 관점에서 마케팅 비용을 더 크게 지불해야 한다는 것이기 때문에 React 를 위시한 SPA 의 미래가 다소 어둡게 보인다.&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_8_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html&quot;&gt;Phoenix LiveView&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;나 거기에서 영감을 받은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://laravel-livewire.com/&quot;&gt;Laravel Livewire&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;등도 개발이 되고 있으니 앞으로의 frontend 나아가 웹 개발의 패러다임이 크게 변할지도 모르겠다는 생각이 든다. LiveView 스타일 (? 뭐라고 해야할지도 잘 모르겠 다.) 의 웹 개발 방식이 PHP 의 부활을 가져올지도 모르겠다 Elixir 보다는 아무래도 PHP 가 대중적이니 말이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;기승전라이브뷰&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://www.seokjun.kim/react-ssr-the-record-of-pain/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.seokjun.kim/react-ssr-the-record-of-pain/&lt;/a&gt;&lt;/p&gt;</description>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/163</guid>
      <comments>https://calssess.tistory.com/163#entry163comment</comments>
      <pubDate>Thu, 16 Feb 2023 15:55:59 +0900</pubDate>
    </item>
    <item>
      <title>Elixir + Phoenix LiveView + Svelte, Why?</title>
      <link>https://calssess.tistory.com/162</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=lAaD-6OQSHE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.youtube.com/watch?v=lAaD-6OQSHE&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=lAaD-6OQSHE&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/ifm14/hyRDY3UYwO/nsR8X3G7k3O8usrHk8pKC0/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/lAaD-6OQSHE&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&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;h3 id=&quot;elixir&quot; data-ke-size=&quot;size23&quot;&gt;Elixir&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BJlzz/btrZuDyElgF/tisTm4odlMkXE8LkQ6EuoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BJlzz/btrZuDyElgF/tisTm4odlMkXE8LkQ6EuoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BJlzz/btrZuDyElgF/tisTm4odlMkXE8LkQ6EuoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBJlzz%2FbtrZuDyElgF%2FtisTm4odlMkXE8LkQ6EuoK%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;1280&quot; height=&quot;310&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무나 아름다운 파이프라인 오퍼레이터...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Elixir 를 좋아하게 된 건 순전히 pipeline 오퍼레이터 때문이었다. 한참 FP 를 해보겠다고 lodash/fp 를 많이 사용할 때였는데, 함수 하나에 여러가지 행동을 하는 코드를 작성하는 곳에 주로 사용하였다. 말하니 복잡한데 다음 코드라고 생각하면 쉽다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;const doSomething = flow( 
  split(&amp;lsquo;,&amp;rsquo;),
  map((x) =&amp;gt; x.toUpperCase()),
  filter((x) =&amp;gt; x.startsWith(&amp;lsquo;A&amp;rsquo;)),
);
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_1_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한두개 쓸때는 괜찮았는데, 쓰다보니까&lt;span&gt;&amp;nbsp;&lt;/span&gt;flow&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 매번 쓰는게 귀찮았고, 이것저건 관련 자료를 읽다보니&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/tc39/proposal-pipeline-operator&quot;&gt;여기&lt;/a&gt;까지 도달했다. ES Next 제안 중 하나로, 위 코드와 같은 function chain 에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;|&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용하자는 것이다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const doSomething = (str) =&amp;gt; str
  |&amp;gt; split(&amp;lsquo;,&amp;rsquo;)
  |&amp;gt; map((x) =&amp;gt; x.toUpperCase())
  |&amp;gt; filter((x) =&amp;gt; x.startsWith(&amp;lsquo;A&amp;rsquo;))
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_2_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flow&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 기본적으로 higher-order function 이라 즉시 사용하려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;const value = flow(a(), b((x) =&amp;gt; x), c())(x)&lt;span&gt;&amp;nbsp;&lt;/span&gt;와 같은 구조를 택해야 해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;못생겼다&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;종종 실수를 했는데 pipeline operator &amp;nbsp;|&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;는 꽤 괜찮은 표현식이라고 생각했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;This proposal introduces a new operator |&amp;gt; similar to &amp;nbsp;&lt;a href=&quot;https://en.wikibooks.org/wiki/F_Sharp_Programming/Higher_Order_Functions#The_.7C.3E_Operator&quot;&gt;F#&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;, &amp;nbsp;&lt;a href=&quot;http://caml.inria.fr/pub/docs/manual-ocaml/libref/Pervasives.html#VAL%28%7C%3E%29&quot;&gt;OCaml&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;, &amp;nbsp;&lt;a href=&quot;https://hexdocs.pm/elixir/Kernel.html#%7C%3E/2&quot;&gt;Elixir&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;, &amp;nbsp;&lt;a href=&quot;https://package.elm-lang.org/packages/elm/core/latest/Basics#%7C%3E&quot;&gt;Elm&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;, &amp;nbsp;&lt;a href=&quot;https://docs.julialang.org/en/v1/base/base/#Base.:%7C%3E&quot;&gt;Julia&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;, &amp;nbsp;&lt;a href=&quot;https://docs.hhvm.com/hack/expressions-and-operators/pipe&quot;&gt;Hack&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;, and &amp;nbsp;&lt;a href=&quot;http://livescript.net/#piping&quot;&gt;LiveScript&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;, as well as UNIX pipes and &amp;nbsp;&lt;a href=&quot;https://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Function.html#v:-38-&quot;&gt;Haskell&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rsquo;s &amp;amp;. It&amp;rsquo;s a backwards-compatible way of streamlining chained function calls in a readable, functional manner, and provides a practical alternative to extending built-in prototypes.&lt;/blockquote&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_3_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 제안 글에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;|&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용하는 언어가 몇가지 소개되었는데 Node.js 말고 새로운 백엔드 언어를 공부해봐야지 하는 와중에 체인파트너스&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://daybit.com/&quot;&gt;암호화폐 거래소&lt;/a&gt;에 Elixir 를 사용했다는걸 알게되었고, &amp;ldquo;오?&amp;rdquo; 하는 마음에 둘러봤더니&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;패턴 매칭에서 무릎꿇었다&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;재밌어 보여서 관심을 갖게 되었다. 지금은 언어적인 면에서 패턴 매칭, 가드, Atom 등과 같은 Feature 도 매력적이라고 생각하지만, 시작은 어쨋든&lt;span&gt;&amp;nbsp;&lt;/span&gt;|&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이었다.&lt;/p&gt;
&lt;h3 id=&quot;phoenix&quot; data-ke-size=&quot;size23&quot;&gt;Phoenix&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어 자체를 공부하는건 나한테는 항상 관심 외의 일이었고, 뭐라도 만들어 보는 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;Done by doing 이라고 쓰고 맨땅에서 헤딩한다고 읽음&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;빠른 길이라 생각해 웹 프레임웍을 찾아보게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HlfYJ/btrZrYwNZfV/GCGXeawMJmT5nUSOm3eQ41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HlfYJ/btrZrYwNZfV/GCGXeawMJmT5nUSOm3eQ41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HlfYJ/btrZrYwNZfV/GCGXeawMJmT5nUSOm3eQ41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHlfYJ%2FbtrZrYwNZfV%2FGCGXeawMJmT5nUSOm3eQ41%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;1280&quot; height=&quot;602&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Phoenix Framework 는 Erlang Cowboy 를 기반으로 한 웹 프레임워크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;피닉스는 Elixir 의 Ruby on Rails 라고 생각하면 된다. Opinionated framework 고, Elixir 의 개발자 Jos&amp;eacute; Valim 은 Ruby 개발자 였고, Phoenix framework 를 만든 Crhis McCord 또한 Ruby on Rails 개발자였기 때문에 여러모로 유사한 아키텍쳐를 가지고 있다. ORM 도 Ruby on Rails 의 그것과 상당히 유사하다. 실제로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=25105581&quot;&gt;Ruby on Rails 에서 LiveView 같은걸 만들다가 concurrency 이슈로 Elixir 로 전향했다 하니&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;nbsp;어느정도 그럴만도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시에 쓰려다가 포기한 점은 Phoenix 의 ORM 은 Ecto 라는 모듈을 사용하는데 Postgres 가 기본이고 MySQL 도 사용 가능하다. Phoenix 는 새로운 프로젝트를 생성할때&lt;span&gt;&amp;nbsp;&lt;/span&gt;$ mix new [APP]&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용하는데,&lt;span&gt;&amp;nbsp;&lt;/span&gt;--no-ecto&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 옵션을 주지 않으면 Postgres 설정 없이 기본 상태에서 서버를 실행할 수도 없다.&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_4_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어릴때부터 나는 &amp;ldquo;귀찮다&amp;rdquo; 는 말을 많이 해서 어머니께도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;쿠사리&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;꾸중을 들은 적이 종종 있을만큼 귀찮은 걸 싫어하는데, 그러다보니 새로운 프로젝트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;alter&lt;span&gt;&amp;nbsp;&lt;/span&gt;걱정 없는 MongoDB 를 많이 쓴다. Ecto 는 DB 연결을 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://hexdocs.pm/ecto/Ecto.Adapter.html&quot;&gt;Adapter&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 필요한데 MongoDB adapter 가 없고 (정확하게는 있긴 한데 active 하게 개발되진 않는 것 같다), ecto 없이 쓰자니 공부에도 큰 도움이 안될 것 같아 접어두었다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Egzyj/btrZxI7pKRl/C3l7wo562hRuWoRm3RhfW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Egzyj/btrZxI7pKRl/C3l7wo562hRuWoRm3RhfW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Egzyj/btrZxI7pKRl/C3l7wo562hRuWoRm3RhfW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEgzyj%2FbtrZxI7pKRl%2FC3l7wo562hRuWoRm3RhfW0%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;1280&quot; height=&quot;548&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자랑은 아니지만 피닉스 컨트리뷰터고, 예 그렇습니다. 주석 한줄 고쳤구요, 자랑이 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직까지는 Elixir 라는 언어와 ErlangVM 의 장점을 제외하고 Phoenix 는 Ruby on Rails 에서 많은 영향을 받았다는 것 외에 특별한 개성은 없다고 느껴졌다. 하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.phoenixframework.org/blog/build-a-real-time-twitter-clone-in-15-minutes-with-live-view-and-phoenix-1-5&quot;&gt;Phoenix LiveView&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 매력적인 툴이라고 생각한다. LiveView 는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;브라우저에서 JS 를 몰아내자&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;서버에서 렌더링 된 HTML 을 웹소켓을 통해 조작하는 방식이다. 즉 websocket 을 통해 서버에서 state 를 관리하고 render tree 에서 변경된 부분을 browser 에 message 형태로 전달하여 DOM 를 업데이트하는 개념이라고 보면 될 것 이다. 딱 봐도 서버에 부담을 크게 전가될 것으로 보이는데 그게 맞다. 위에서도 언급했듯이 RoR 에서 concurrency 문제로 구현하기 어려웠기 때문에, 동시처리가 강력한 Erlang VM 기반의 Elixir 에서 만들게 된 것이다. 대표적인 SPA 라이브러리인 React 나 Vue.js 를 사용하기 어려웠던 PHP 나 Haskell 등도 영향받아 Laravel LiveWire 라던가 Haskell IHP 와 같은 기술도 개발되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 모든 처리를 서버에서 한다는 의미는 browser 의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;널럴한&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메모리를 거의 사용하지 않는다는 의미와도 연결되는데 file 처리, 이미지 lazyload 나 animation 등 브라우저에서 직접 처리하는 편이 편리한 부분은 LiveView 로 구현이 까다롭거나 거의 불가능한 부분도 분명히 존재한다.&lt;/p&gt;
&lt;h3 id=&quot;svelte&quot; data-ke-size=&quot;size23&quot;&gt;Svelte&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LiveView 단독으로 복잡합 인터렉션을 구현하기는 어렵고, 그래서 Frontend View Library 가 필요했다. 이 동네 깡패인 React 를 사용하는 것도 좋은 선택이겠지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.seokjun.kim/time-to-stop-react/&quot;&gt;개인적으로는 React 가 점점 개발하기 어려워 지고 있다고 생각하기 때문에&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Svelte 를 선택했다. 가독성을 크게 해치지 않는 선에서 코드가 줄어들면 무조건 이득이라고 생각한다. React 는 hook 이 도입되면서 가독성에 다소 희생이 있었다고 생각하고, Svelte 가 이러한 면에서 더 편한 라이브러리 라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Phoenix 에 Svelte 를 도압힌다는 부분에서는 좀 더 나아가면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://micro-frontends.org/&quot;&gt;Micro Frontends&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라고 하는 괴상한 컨셉과도 연결되는 면이 있다. 물론 단순히 React, Vue, Svelte, Elm 등 원하는 걸 쓰자! 이런 건 아니지만, frontend 의 기술 스택에 대한 자유도를 높일 수 있다는 점에서 접점이 있다고 본다. dev.to 의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://dev.to/ender_minyard/why-you-should-stop-using-react-g7c&quot;&gt;Stop Using React&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 글의 댓글에 보면 이런 말이 나온다.&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_5_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;I think this is not the problem with React (well, apart from point 5), but with modern webdev in general. React is a good tool for single-page applications (so is Vue or Angular) but&lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;in many cases you simply don&amp;rsquo;t need and should not have a SPA in the first place&lt;/b&gt;.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이게 맞다. 대부분의 경우 SPA (혹은 모던 웹 언어라고 해야 하나?) 는 필요 없다. 한편으로는 정말 복잡한 인터렉션이 필요한 웹 어플리케이션에서는 어차피 느려서 사용할 수 없다. 대부분의 Frontend 는 Data 를 Presentation 하는 역할이 주가 되고, 복잡한 인터렉션이 필요한 부분은 많지 않다. 반대로 복잡한 인터렉션이 필요한 부분에는 SPA 가 최적이라는 뜻이기도 하고.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jjalbot.com/&quot;&gt;짤봇&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서는 업로드에서 메모리에 이미지를 들고 있고 여러 처리를 한 후 서버로 리퀘스트를 보내는데, 이러한 일에서는 LiveView 와 같은 방식으로 구현하면 서버쪽 메모리에 부담이 크고 따라서 브라우저에서 처리하는 것이 바람직하다. Svelte 나 React 는 그러한 처리를 하기에 좋은 컴포넌트를 만들 수 있는 Library 지만, React&lt;span&gt;&amp;nbsp;&lt;/span&gt;Function&lt;span&gt;&amp;nbsp;&lt;/span&gt;Component 의 비동기 처리 방식에서 너무 고통을 받은 터라 Svelte 를 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 생각해보면 Elixir 와 Elm 이 언어적으로 유사한 부분이 있어서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;s&gt;둘다 마이너여서&lt;/s&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;종종 함께 언급되는데, elm 쪽으로 가보는 것도 좋은 선택이었을 것 같다. 근데 얘는 진짜 너무 마이너라서 엄두가 안났다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://seokjun.kim/elixir-phoenix-liveview-svelte/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://seokjun.kim/elixir-phoenix-liveview-svelte/&lt;/a&gt;&lt;/p&gt;</description>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/162</guid>
      <comments>https://calssess.tistory.com/162#entry162comment</comments>
      <pubDate>Thu, 16 Feb 2023 15:14:09 +0900</pubDate>
    </item>
    <item>
      <title>JS .js vs .mjs</title>
      <link>https://calssess.tistory.com/161</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;.mjs&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Vs&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;.js&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 Node.js의 모듈 시스템은 commonjs를 사용했습니다(require과 module.exports를 사용하는..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 ECMAscript 모듈 시스템이 표준이 되었고, Node.js는 이를 지원하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 는 .cjs 파일로 commonjs 모듈 시스템을 지원했고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.mjs파일로 ECMAsript 모듈 시스템을 지원했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러고 js파일은 둘 다 모두를 지원하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(default는 commonjs)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(package.json에 &quot;type&quot; :&quot;module&quot;쓰면 ecma 모듈 지원)&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;size18&quot;&gt;&lt;b&gt;.JS&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1674801864837&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const b = require(&quot;./main2.js&quot;);

console.log(b.a);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;받아오는 js&lt;/p&gt;
&lt;pre id=&quot;code_1674801864837&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function hello() {
  return &quot;hello&quot;;
}
const a = &quot;a&quot;;
module.exports = {
  hello: hello,
  a: a,
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보내주는 js&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;.&lt;b&gt;mjs&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;OR&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;package.json에 타입 추가&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1674801864838&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { hello, a } from &quot;./main2.mjs&quot;;

console.log(`${hello()} ${a}`);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;받아오는 js&lt;/p&gt;
&lt;pre id=&quot;code_1674801864838&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;function hello() {
  return &quot;hellozz&quot;;
}
const a = &quot;a&quot;;

export { hello, a };&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보내주는 js&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처ㅣ https://kjs-dev.tistory.com/entry/JS-js-%EC%99%80-mjs?category=1025503?category=1025503&lt;/p&gt;</description>
      <category>Web/Js</category>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/161</guid>
      <comments>https://calssess.tistory.com/161#entry161comment</comments>
      <pubDate>Fri, 27 Jan 2023 15:44:42 +0900</pubDate>
    </item>
    <item>
      <title>[react] Zustand</title>
      <link>https://calssess.tistory.com/160</link>
      <description>&lt;h1 id=&quot;zustand--독일어로-상태&quot;&gt;zustand : 독일어로 상태&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;react state-management &quot;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/pmndrs/zustand&quot;&gt;zustand&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;상태관리가-왜-필요한가&quot; data-ke-size=&quot;size26&quot;&gt;상태관리가 왜 필요한가&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자와의 활발한 인터랙션을 모두 처리할 수 있으며, 실시간 통신을 통해 데이터를 받아오고 이를 기반으로 렌더링 하는 등 다양한 기능을 포함하고 있다. 그렇지만 기반이 되는 핵심은 애플리케이션 수준에서는 자체적으로 상태를 관리해야하는 역할이 필수적으로 요구된다는 점이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 상태란 오늘날 웹 프론트엔드 개발에 있어서 상태라는 것은 크게 보면 웹 애플리케이션을 렌더(render)하는데 있어 영향을 미칠 수 있는 값이라고 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Plain Javascript Object hold information influences the output of render ___ 공식문서&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;독립적인&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;컴포넌트 단위로 구성되어있다. 하나의 단위에서만 생활하던 때는 상태라는 것이 쉽게 공유되었다. 부족단위, 마을단위, 학교단위 등 단위 내 상태는 마이크하나로 전파 될 수 있었다. React 에서는 hook&lt;span&gt;&amp;nbsp;&lt;/span&gt;useState를 사용해 하나의 컴포넌트에서 충분히 상태를 관리했고 인근 옆 단위까지도 훌륭히 상태 전파의 기능을 수행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목해야할 점은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;독립&lt;/b&gt;이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 독립적인 컴포넌트 단위에서의 공유는 쉬웠는데, 이젠 그렇지 않은 시대가 되었다. 글로벌시대에 글로벌하게 상태를 관리 할 수는 없을까라는 의문이 생기기 시작했다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서는 자체적으로 상태를 관리할 수 있도록 여러 기능을 제공하고 있다. 하지만 사용자의 입장에서는 효율적으로 전역 상태를 관리해야하는 필요가 생겨났다. 그래서 등장한 것이 상태관리 라이브러리다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redux&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;Context API&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mobx&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;Recoil&lt;span&gt;&amp;nbsp;&lt;/span&gt;/&lt;span&gt;&amp;nbsp;&lt;/span&gt;zustand&lt;span&gt;&amp;nbsp;&lt;/span&gt;/ etc&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 상태관리 라이브러리가 있지만, 이번 프로젝트에는 zustand를 사용해보고자 한다.&lt;/p&gt;
&lt;h2 id=&quot;다시-zustand &quot; data-ke-size=&quot;size26&quot;&gt;다시 zustand &lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy api based on hooks, isn't boilerplatey or opinionated.&lt;br /&gt;간단한 Flux 원칙을 사용하는 작고 빠르고 확장 가능한 상태 관리 솔루션입니다. Hook 기반으로 하는 편리한 API가 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zustand를 사용하고자 마음먹은 건 한 블로그의 제목을 보고서였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blacklobster.tistory.com/3&quot;&gt;개쉽다! Zustand 사용법 - React 상태관리 라이브러리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개쉽다는데 안 써볼 이유가 없었다. 내가 사용해본 상태관리 라이브러리는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Recoil뿐인데 사용한 이유도 리액트 Hook 과 비슷해 쉽다고 들어서였다. 짧은 프로젝트 기간에서 효율을 내기위해 낮은 러닝커브의 라이브러리를 선택했다. 이번엔 그냥 쉬워서라기보다 장단점을 고려해 zustand를 사용해보고자 한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand는 Context API 사용을 배제하고 클로저를 활용하여 스토어 내부 상태를 관리한다. 따라서 createStoreHook 을 호출하여 리턴된 useStore 를 어느 컴포넌트에서나 import하여 원하는대로 사용하더라도 같은 스토어를 바라보게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;flux-기반&quot; data-ke-size=&quot;size23&quot;&gt;flux 기반&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 MVC 패턴&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XTGLu/btrWH2mjlao/lRQbx4pXtzZHKPEZJvVIQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XTGLu/btrWH2mjlao/lRQbx4pXtzZHKPEZJvVIQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XTGLu/btrWH2mjlao/lRQbx4pXtzZHKPEZJvVIQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXTGLu%2FbtrWH2mjlao%2FlRQbx4pXtzZHKPEZJvVIQK%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;1280&quot; height=&quot;691&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;691&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Model : 저장소&lt;br /&gt;Controller : Model의 데이터 관리 (CRUD)&lt;br /&gt;View : 받아온 Model의 데이터를 사용자에게 보여줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이스북은 &amp;ldquo;MVC는 정말 눈 깜짝할 사이에 복잡해진다&amp;rdquo;고 말하며 이 문제의 해결 방안으로 단방향 데이터 흐름을 가지는 Flux 패턴을 고안했다고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점강화 Flux 패턴&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;779&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nIuG8/btrWD4L4GpY/aK4K3s1qSCCWshkJNpSyHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nIuG8/btrWD4L4GpY/aK4K3s1qSCCWshkJNpSyHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nIuG8/btrWD4L4GpY/aK4K3s1qSCCWshkJNpSyHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnIuG8%2FbtrWD4L4GpY%2FaK4K3s1qSCCWshkJNpSyHK%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;779&quot; height=&quot;255&quot; data-origin-width=&quot;779&quot; data-origin-height=&quot;255&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flux는 사용자 입력을 기반으로 Action을 만들고 Action을 Dispatcher에 전달하여 Store(Model)의 데이터를 변경한 뒤 View에 반영하는 단방향의 흐름으로 애플리케이션을 만드는 아키텍처&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://code-cartoons.com/articles/a-cartoon-guide-to-flux/&quot;&gt;예시&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요소들은 단방향 흐름에 따라 순서대로 역할을 수행하고, View로부터 새로운 데이터 변경이 생기면 처음부터 다시 이 순서대로 실행합니다. 이렇게 함으로써 예외 없이 데이터를 처리할 수 있게 된다고 한다. 아무튼 그렇다고 한다.&lt;/p&gt;
&lt;h3 id=&quot;간단한-장점&quot; data-ke-size=&quot;size23&quot;&gt;간단한 장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;굉장히 쉽다. 동작을 이해하기 위해 알아야 하는 코드 양이 아주 적다. 핵심 로직의 코드 줄 수가 약 42줄밖에 되지 않는다. (VanillaJS 기준)&lt;/li&gt;
&lt;li&gt;보일러플레이트가 거의 없다. (Context API랑 비교)&lt;/li&gt;
&lt;li&gt;redux Devtools를 사용할 수 있어 debugging에 용이하다.&lt;/li&gt;
&lt;li&gt;상태 변경 시 불필요한 리랜더링을 일으키지 않도록 제어하기 쉽다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id=&quot;zustand-사용법&quot;&gt;zustand 사용법 &amp;zwj; &lt;/h1&gt;
&lt;h2 id=&quot;설치&quot; data-ke-size=&quot;size26&quot;&gt;설치&lt;/h2&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;npm install zustand
yarn add zustand&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;상태를-관리하는-store-만들기&quot; data-ke-size=&quot;size26&quot;&gt;상태를 관리하는 store 만들기&lt;/h2&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;// store.js

import create from &quot;zustand&quot;;
import { devtools } from &quot;zustand/middleware&quot;;

// set 함수를 통해서만 상태를 변경할 수 있다
const useStore = create(
    devtools((set) =&amp;gt; ({
        isLogin: false,
        toggleIsLogin: () =&amp;gt; set((state) =&amp;gt; ({ isLogin: !state.isLogin })),

        count: 1, //state

      
      	// set 함수 사용 #1 현재 상태를 기반으로 새로운 상태를 리턴하는 함수
        increaseCount: () =&amp;gt; {
            // count 1만큼 증가
            // set method로 상태 변경 가능
            set((state) =&amp;gt; ({ count: state.count + 1 }));
        },

        // set 함수 사용 #2 아예 변경하려는 상태 값
        setCnt: (input) =&amp;gt; {
            // 입력받은 input만큼 count 설정
            set({ count: input });
        },

      

        clearCnt: () =&amp;gt; {
            // count 초기화
            set((state) =&amp;gt; ({ count: 0 }));
        },
    }))
);

// redux devtools 사용하기
// const useStore = create(devtools(myStore));

export default useStore;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;store.js javascript 파일 만들기&lt;br /&gt;나는 src / status / store.js 파일을 만들었다.&lt;/li&gt;
&lt;li&gt;제일 상단에 zustand의 create를 불러와준다. 그래야 사용할 수 있다.&lt;br /&gt;import create from &quot;zustand&quot;;&lt;/li&gt;
&lt;li&gt;store를 생성한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스토어는 Hook 이다.&lt;/li&gt;
&lt;li&gt;상태를 통합적으로 관리한다. 신병훈련소 느낌&lt;/li&gt;
&lt;li&gt;어떤 type이든 넣을 수 있다. (원시, 객체, 함수)&lt;/li&gt;
&lt;li&gt;set함수는 상태를 병합한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;// store hook 생성
const useStore = create(
    (set) =&amp;gt; ({
        
      isLogin: false, //state
      toggleIsLogin: () =&amp;gt; set((state) =&amp;gt; ({ isLogin: !state.isLogin })),
      
    })
);

export default useStore;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 상태가 진짜 관리 되는지 확인 차 boolean type의 객체로 버튼 클릭 시 토글되는지 실험을 해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isLogin&lt;span&gt;&amp;nbsp;&lt;/span&gt;state에 false를 담아놨다.&lt;br /&gt;toggleIsLogin&lt;span&gt;&amp;nbsp;&lt;/span&gt;state에는 함수를 넣고&lt;span&gt;&amp;nbsp;&lt;/span&gt;set함수를 넣어&lt;span&gt;&amp;nbsp;&lt;/span&gt;isLoginstate 상태를 호출될 때마다 반전시키도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex)&lt;br /&gt;toggleIsLogin&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; 호출 되면 -&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;isLogin&lt;span&gt;&amp;nbsp;&lt;/span&gt;: true&lt;br /&gt;toggleIsLogin&lt;span&gt;&amp;nbsp;&lt;/span&gt;-&amp;gt; 호출 되면 -&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;isLogin&lt;span&gt;&amp;nbsp;&lt;/span&gt;: false&lt;/p&gt;
&lt;h2 id=&quot;컴포넌트에-바인딩한다-살포시-적용&quot; data-ke-size=&quot;size26&quot;&gt;컴포넌트에 바인딩한다. 살포시 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;store를 컴포넌트에 불러오는 방법은 2가지가 있다.&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;	// #1 select 함수를 사용해 import 하여 사용하기
     const isLogin = useStore((state) =&amp;gt; state.isLogin);

	// #2 구조분해 할당을 통해 가져오기 
     const { isLogin } = useStore();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관적인 방법은 #2 구조분해 할당을 통해 가져오는 방법인 것 같다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const App = () =&amp;gt; {

    const { isLogin, toggleIsLogin } = useStore();

    return (
        &amp;lt;div&amp;gt;
            &amp;lt;p&amp;gt;{&quot;&quot; + isLogin}&amp;lt;/p&amp;gt;
      		&amp;lt;button onClick={toggleIsLogin}&amp;gt;
                &amp;lt;b&amp;gt;버튼 클릭 시 백만원&amp;lt;/b&amp;gt;
            &amp;lt;/button&amp;gt;

        &amp;lt;/div&amp;gt;
    );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;887&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6ptPz/btrWCpxbOsB/grUZpRExNvIKkewlVtR7S1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6ptPz/btrWCpxbOsB/grUZpRExNvIKkewlVtR7S1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6ptPz/btrWCpxbOsB/grUZpRExNvIKkewlVtR7S1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/6ptPz/btrWCpxbOsB/grUZpRExNvIKkewlVtR7S1/img.gif&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;887&quot; height=&quot;426&quot; data-origin-width=&quot;887&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 쉽게 위 블로거가 말한대로 개쉽게 전역상태관리를 적용하였다.&lt;/p&gt;
&lt;h3 id=&quot;조금-더-적용해보기&quot; data-ke-size=&quot;size23&quot;&gt;조금 더 적용해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ji-musclecode.tistory.com/32?category=552408&quot;&gt;김관장 블로거님의 zustand 글 참고&lt;/a&gt;해서 추가로 구현해보고자 했다.&lt;br /&gt;근데 그대로 복붙해서도 안되는 것이었다. 그래서 공식문서를 참고했다.&lt;br /&gt;set함수를 사용할 때 괄호를 잘 써야 오류가 안난다. 중괄호 대괄호가 많기 때문에 주의하기!&lt;/p&gt;
&lt;pre class=&quot;pf&quot;&gt;&lt;code&gt;//store.js

const useStore = create(
    devtools((set) =&amp;gt; ({
        isLogin: false,
        toggleIsLogin: () =&amp;gt; set((state) =&amp;gt; ({ isLogin: !state.isLogin })),

        count: 1, //state

      	// 함수를 전달하여 상태를 갱신하는 경우
        increase: () =&amp;gt; {
            // count 1만큼 증가
            // set method로 상태 변경 가능
            set((state) =&amp;gt; ({ count: state.count + 1 }));
        },

      
      	// 객체를 직접 전달하여 상태를 갱신하는 경우
        setCnt: (input) =&amp;gt; { //매개변수를 입력받아,
            // 입력받은 input만큼 count 설정
            set({ count: input });
        },

        clearCnt: () =&amp;gt; {
            // count 초기화
            set((state) =&amp;gt; ({ count: 0 }));
        },
    }))
);&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;//app.js


const App = (props) =&amp;gt; {
    const { isLogin, count, increase, toggleIsLogin, setCnt, clearCnt } =
        useStore();

    return (
        &amp;lt;div&amp;gt;
            &amp;lt;div&amp;gt;
                &amp;lt;p&amp;gt;{&quot;&quot; + isLogin}&amp;lt;/p&amp;gt;
                &amp;lt;button onClick={toggleIsLogin}&amp;gt;
                    &amp;lt;b&amp;gt;버튼 클릭 시 백만원&amp;lt;/b&amp;gt;
                &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;div&amp;gt;
                &amp;lt;div&amp;gt;현재 Cnt == {count}&amp;lt;/div&amp;gt;
                &amp;lt;button onClick={increase}&amp;gt;[+1]&amp;lt;/button&amp;gt;
                &amp;lt;button onClick={() =&amp;gt; setCnt(10)}&amp;gt;[set_10]&amp;lt;/button&amp;gt;
                &amp;lt;button onClick={clearCnt}&amp;gt;[clear]&amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
    );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYurVl/btrWCFNhSc7/h4ci2bF13B98T5dGvQbUS1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYurVl/btrWCFNhSc7/h4ci2bF13B98T5dGvQbUS1/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYurVl/btrWCFNhSc7/h4ci2bF13B98T5dGvQbUS1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bYurVl/btrWCFNhSc7/h4ci2bF13B98T5dGvQbUS1/img.gif&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;1069&quot; height=&quot;487&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아름답게 잘동하는것을 볼 수 있다. 이정도 기능은 사실&lt;span&gt;&amp;nbsp;&lt;/span&gt;useState로도 충분하고 차고 넘치지만 어디에서나 사용할 수 있는 상태라는 점에서 맘에 들고 무려&lt;span&gt;&amp;nbsp;&lt;/span&gt;Recoil보다 사용방법이 쉬웠다.&lt;/p&gt;
&lt;h3 id=&quot;작동원리&quot; data-ke-size=&quot;size23&quot;&gt;작동원리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rHRhj/btrWDbZh2ft/3tcu3v5KJuXiO8fwNQGekK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rHRhj/btrWDbZh2ft/3tcu3v5KJuXiO8fwNQGekK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rHRhj/btrWDbZh2ft/3tcu3v5KJuXiO8fwNQGekK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrHRhj%2FbtrWDbZh2ft%2F3tcu3v5KJuXiO8fwNQGekK%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;1280&quot; height=&quot;768&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ui.toast.com/weekly-pick/ko_20210812&quot;&gt;출처&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;redux-devtool을-사용해보자&quot; data-ke-size=&quot;size26&quot;&gt;redux-devtool을 사용해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zustand는 Middleware로 Devtools를 지원한다.&lt;br /&gt;&lt;a href=&quot;https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko&amp;amp;refresh=1&quot;&gt;Redux DevTools&lt;/a&gt;를 Chrome 웹 스토어에서 설치하고 store를 devtools 로 묶어 주면된다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// store.js
// redux devtools 사용하기

import create from &quot;zustand&quot;;
import { devtools } from &quot;zustand/middleware&quot;; // #1 import 하기

const useStore = create(
    devtools((set) =&amp;gt; ({ // set을 devtools로 묶어주기
       ...
    }))
);


// const useStore = create(devtools(myStore)); // 이렇게 해도 된다는데 난 안되더라...

export default useStore;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;682&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2XT1y/btrWD5c7OR6/FYXlQNW5uGCpaD62FAa7kK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2XT1y/btrWD5c7OR6/FYXlQNW5uGCpaD62FAa7kK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2XT1y/btrWD5c7OR6/FYXlQNW5uGCpaD62FAa7kK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/2XT1y/btrWD5c7OR6/FYXlQNW5uGCpaD62FAa7kK/img.gif&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;1280&quot; height=&quot;682&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;682&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h1 id=&quot;참고링크&quot;&gt;참고링크&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pmndrs/zustand&quot;&gt;https://github.com/pmndrs/zustand&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rldnrl.dev/blog/zustand&quot;&gt;https://www.rldnrl.dev/blog/zustand&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ji-musclecode.tistory.com/32?category=552408&quot;&gt;https://ji-musclecode.tistory.com/32?category=552408&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blacklobster.tistory.com/3&quot;&gt;https://blacklobster.tistory.com/3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ui.toast.com/weekly-pick/ko_20210812&quot;&gt;https://ui.toast.com/weekly-pick/ko_20210812&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@longroadhome/FE-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-1%EB%B6%80&quot;&gt;react 상태관리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@andy0011/Flux-%ED%8C%A8%ED%84%B4%EC%9D%B4%EB%9E%80&quot;&gt;flux 패턴&lt;/a&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;출처: &lt;a href=&quot;https://velog.io/@ho2yahh/react-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-Zustand-%EB%A5%BC-%EC%95%84%EC%8B%AD%EB%8B%88%EA%B9%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@ho2yahh/react-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-Zustand-%EB%A5%BC-%EC%95%84%EC%8B%AD%EB%8B%88%EA%B9%8C&lt;/a&gt;&lt;/p&gt;</description>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/160</guid>
      <comments>https://calssess.tistory.com/160#entry160comment</comments>
      <pubDate>Thu, 19 Jan 2023 12:33:36 +0900</pubDate>
    </item>
    <item>
      <title>ES6 Map(), Set()</title>
      <link>https://calssess.tistory.com/159</link>
      <description>&lt;div&gt;
&lt;p id=&quot;b0b1&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;얼마 전부터 회사 업무를 진행할 때 본격적으로, 그리고 의식적으로 ES6 에 도입된 문법을 적용하고 있는데, 그중에서 가장 자주 활용하는 자료구조, Map 과 Set 에 대해 이야기해보려고 합니다. 이 글의 모티브는 상당부분&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;http://hacks.mozilla.or.kr/2015/12/es6-in-depth-collections/&quot;&gt;Mozilla 웹기술블로그&lt;/a&gt;에 기반합니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;사내세미나에서 발표한 내용&lt;/b&gt;을 글로 정리했습니다.&lt;/p&gt;
&lt;h1 id=&quot;cd23&quot; data-selectable-paragraph=&quot;&quot;&gt;Map&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;d1d4&quot; data-selectable-paragraph=&quot;&quot;&gt;Map() 은 자바스크립트의 key-value 페어(pair) 로 이루어진 컬렉션&lt;/li&gt;
&lt;li id=&quot;2ee0&quot; data-selectable-paragraph=&quot;&quot;&gt;key 를 사용해서 value 를 get, set 할 수 있음&lt;/li&gt;
&lt;li id=&quot;a0bd&quot; data-selectable-paragraph=&quot;&quot;&gt;key 들은 중복될 수 없음: 하나의 key 에는 하나의 value 만&lt;/li&gt;
&lt;li id=&quot;a9bb&quot; data-selectable-paragraph=&quot;&quot;&gt;key 로 사용할 수 있는 데이터형: string, symbol(ES6), object, function &amp;gt;&amp;gt; number 는 사용할 수 없음에 주의!&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;// 새로운 map 을 만들고 map 에 key, value 엔트리를 추가
let me = new Map();
me.set('name', 'kevin');
me.set('age', 28);
console.log(me.get('age'); // 28// 대괄호를 사용해서 map 을 선언하는 방법
const roomTypeMap = new Map(
  [
    [&quot;01&quot;, &quot;원룸(오픈형)&quot;],
    [&quot;02&quot;, &quot;원룸(분리형)&quot;],
    [&quot;03&quot;, &quot;원룸(복층형)&quot;],
    [&quot;04&quot;, &quot;투룸&quot;],
    [&quot;05&quot;, &quot;쓰리룸&quot;]
  ]
);// 새로운 map 을 만들고 그 데이터를 기존의 [key, value] 페어컬렉션으로 채움
let you = new Map().set('name', 'paul').set('age', 34);
console.log(you.get('name')); // 'paul'// has(): 주어진 key 가 존재하는지 확인
console.log(me.has('name')); // true// size: map 에 담겨진 엔트리의 개수를 조회
console.log(you.size); // 2// delete(): 엔트리를 삭제
me.delete('age');
console.log(me.has('age')); // false// clear(): 모든 엔트리를 삭제
you.clear();
console.log(you.size); // 0&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;a9e7&quot; data-selectable-paragraph=&quot;&quot;&gt;&amp;lt;참고 1&amp;gt; Map 과 Object 비교&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;93e3&quot; data-selectable-paragraph=&quot;&quot;&gt;Object 의 key 는 string 과 symbol(ES6) 만 가능하지만, map 은 어떤 값도 가능&lt;/li&gt;
&lt;li id=&quot;b701&quot; data-selectable-paragraph=&quot;&quot;&gt;Object 에서는 크기를 추적해서 알 수 있지만, map 은 손쉽게 얻을 수 있음(size)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;05ac&quot; data-selectable-paragraph=&quot;&quot;&gt;Set&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;1418&quot; data-selectable-paragraph=&quot;&quot;&gt;Set() 은 value 들로 이루어진 컬렉션(&amp;ldquo;집합&amp;rdquo;이라는 표현이 적절)&lt;/li&gt;
&lt;li id=&quot;9dcc&quot; data-selectable-paragraph=&quot;&quot;&gt;Array 와는 다르게 Set 은 같은 value 를 2번 포함할 수 없음&lt;/li&gt;
&lt;li id=&quot;7e75&quot; data-selectable-paragraph=&quot;&quot;&gt;따라서 Set 에 이미 존재하는 값을 추가하려고 하면 아무 일도 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;// 비어있는 새로운 set 을 만듬
let setA = new Set();// 새로운 set 을 만들고 인자로 전달된 iterable 로 인자를 채움
let setB = new Set().add('a').add('b');setB.add('c');
console.log(setB.size); // 3// has(): 주어진 값이 set 안에 존재할 경우, true 를 반환
// indexOf() 보다 빠름. 단, index 가 없음
console.log(setB.has('b')); // true// set 에서 주어진 값을 제거
setB.delete('b');
console.log(setB.has('b')); // false// set 안의 모든 데이터를 제거
setB.clear();
console.log(setB.size); // 0&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;a5db&quot; data-selectable-paragraph=&quot;&quot;&gt;&amp;lt;TODO&amp;gt; has() 는 indexOf() 보다 빠르다. 다만, index 이 존재하지 않기때문에 index 로 value 로 접근할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;h1 id=&quot;b5db&quot; data-selectable-paragraph=&quot;&quot;&gt;&amp;lt;참고 2&amp;gt; Spread 연산자&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;bb2d&quot; data-selectable-paragraph=&quot;&quot;&gt;&amp;hellip;&lt;/li&gt;
&lt;li id=&quot;6c7f&quot; data-selectable-paragraph=&quot;&quot;&gt;이터러블 오브젝트(iterable object)의 엘리먼트를 하나씩 분리하여 전개&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// string == iterable object
console.log([...'music']); // ['m', 'u', 's', 'i', 'c']&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;2079&quot; data-selectable-paragraph=&quot;&quot;&gt;&amp;lt;참고 3&amp;gt; for 문들&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;d6a3&quot; data-selectable-paragraph=&quot;&quot;&gt;for 문&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;let sampleArr = [1, 2, 3, 4, 5];
for (let i = 0, length = sampleArr.length; i &amp;lt; length; i++) {
  console.log(sampleArr[i]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;428e&quot; data-selectable-paragraph=&quot;&quot;&gt;forEach: ES5 자바스크립트 배열 메서드&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let sampleArr = [1, 2, 3, 4, 5];
sampleArr.forEach(v =&amp;gt; console.log(v));&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;d4a5&quot; data-selectable-paragraph=&quot;&quot;&gt;for-in: Object 를 순회하기 위한&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;let sampleObj = {
  a: 1,
  b: 'hello',
  c: [1, 2]
}
for (let key in sampleObj) {
  console.log(key);
  console.log(sampleObj[key]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;7686&quot; data-selectable-paragraph=&quot;&quot;&gt;for-of: 배열의 요소들, 즉 data 를 순회하기 위한(string 도 가능)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;let sampleArr = [1, 2, 3, 4, 5];
let (for value of sampleArr) {
  console.log(value);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;bc0c&quot; data-selectable-paragraph=&quot;&quot;&gt;일반 객체(Object)는 iterable 하지 않다!&lt;/li&gt;
&lt;li id=&quot;9cdd&quot; data-selectable-paragraph=&quot;&quot;&gt;for-of 나 &amp;hellip;(spread 연산자)를 사용할 수 없다!&lt;/li&gt;
&lt;li id=&quot;81d2&quot; data-selectable-paragraph=&quot;&quot;&gt;for-in 으로나 순회할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;h1 id=&quot;c511&quot; data-selectable-paragraph=&quot;&quot;&gt;Map 의 iterable object&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;2580&quot; data-selectable-paragraph=&quot;&quot;&gt;map.keys(), map.values()&lt;/li&gt;
&lt;li id=&quot;c359&quot; data-selectable-paragraph=&quot;&quot;&gt;map 안의 key 혹은 value 들을 순회할 수 있는 iterable object 를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;let me = new Map().set('a', 1).set('b', 2);
console.log([...me.keys()]); // ['a', 'b']
console.log([...me.values()]); // [1, 2]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;8ff3&quot; data-selectable-paragraph=&quot;&quot;&gt;map.entries(), map.next()&lt;/li&gt;
&lt;li id=&quot;48f1&quot; data-selectable-paragraph=&quot;&quot;&gt;map 안의 모든 엔트리들을 순회할 수 있는 iterable object 를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;let you = new Map().set('Seoul', 28).set('Tokyo', 26);
let iterObj = you.entries();
console.log(iterObj.next()); // {value: ['Seoul', 28], done: false}
console.log(iterObj.next()); // {value: ['Tokyo', 26], done: false}
console.log(iterObj.next()); // {value: undefined, done: true}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;bb6b&quot; data-selectable-paragraph=&quot;&quot;&gt;for-of, map.forEach();&lt;/li&gt;
&lt;li id=&quot;fa5c&quot; data-selectable-paragraph=&quot;&quot;&gt;forEach 의 경우, 인자 순서가 이상한데(key, value 순서가 반대) Array.prototype.forEach() 구문과 통일성을 유지하기 위함(value, index, array 순서인 것)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;let we = new Map().set('car', 30).set('bus', 45);// for-of 로 map 순회하기
for (let [key, value] of we) {
  console.log(key + '^' + value);
}
// 차례대로 'car^30', 'bus^45' 출력// forEach 로 map 순회하기
we.forEach((value, key, map) =&amp;gt; {
  console.log(key + '$' + value);
});
// 차례대로 'car$30', 'bus$45' 출력&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;7801&quot; data-selectable-paragraph=&quot;&quot;&gt;자바스크립트 배열 메서드에 존재하는 map, filter 메서드는 Map 에 존재하지 않는다. 하지만 아래와 같은 방식으로 우회해서 사용이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;let me = new Map().set('a', 1).set('b', 2);// value 가 1 이상인 엔트리만 filtering 하기
let map1 = new Map(
    [...me]
    .filter(([k, v]) =&amp;gt; v &amp;gt; 1)
);
console.log([...map1.entries()]) // [['b', 2]]// key 뒤에 'super' 문자열을 붙이고, value 에 1을 더하기
let map2 = new Map(
    [...me]
    .map(([k, v]) =&amp;gt; [k + &quot;super&quot;, v + 1])
);
console.log([...map2.entries()]) // [['asuper, 2], [bsuper, 3]]&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;bbd1&quot; data-selectable-paragraph=&quot;&quot;&gt;Set 의 iterable object&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;567d&quot; data-selectable-paragraph=&quot;&quot;&gt;set.values();&lt;/li&gt;
&lt;li id=&quot;ed71&quot; data-selectable-paragraph=&quot;&quot;&gt;기본적으로 Set 의 prototype 메서드로 keys() 는 존재하지 않고, values() 만 존재하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Set/values&quot;&gt;MDN&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;의 설명에 따르면, map 오브젝트와 동일하게 동작하기 때문에 Set.prototype.keys() 는 Set.prototype.values() 와 같은 결과&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;let setA = new Set();
setA.add('a');
setA.add('b');
setA.add('a');console.log([...setA.keys()]); // ['a', 'b']
console.log([...setA.values()]); // ['a', 'b']&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;9a32&quot; data-selectable-paragraph=&quot;&quot;&gt;set.entries();&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;let setB = new Set();
setB.add('Korea');
setB.add('Japan');
setB.add('China');let entries = setB.entries();console.log(entries.next()); 
// {value: ['Korea', 'Korea'], done: false}
console.log(entries.next()); 
// {value: ['Japan', 'Japan'], done: false}
console.log(entries.next()); 
// {value: ['China', 'China'], done: false}
console.log(entries.next()); 
// {value: undefined, done: true}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;7abd&quot; data-selectable-paragraph=&quot;&quot;&gt;for-of, set.forEach();&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;let setC = new Set();
setC.add('Korea');
setC.add('Japan');
setC.add('China');for (let key of setC) {
  console.log(key);
}
// 차례대로 'Korea', 'Japan', 'China' 출력setC.forEach((v, k) =&amp;gt; {
  console.log(v);
})
// 차례대로 'Korea', 'Japan', 'China' 출력&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&quot;a136&quot; data-selectable-paragraph=&quot;&quot;&gt;Set: 집합연산&lt;/h1&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dXLqcP/btrWCGyB8NP/YEn4McTqMxY4zN7ay92T10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dXLqcP/btrWCGyB8NP/YEn4McTqMxY4zN7ay92T10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dXLqcP/btrWCGyB8NP/YEn4McTqMxY4zN7ay92T10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdXLqcP%2FbtrWCGyB8NP%2FYEn4McTqMxY4zN7ay92T10%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;700&quot; height=&quot;689&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
스위프트 집합연산 &amp;mdash;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/CollectionTypes.html&quot;&gt;링크&lt;/a&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;b19c&quot; data-selectable-paragraph=&quot;&quot;&gt;union(합집합), intersection(교집합), difference(차집합)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;let setA = new Set([1, 2, 3, 4, 5]);
let setB = new Set([4, 5, 6, 7, 8]);// 합집합
let unionSet = new Set([...setA, ...setB])
for (let value of unionSet) {
  console.log(value);
}
// 차례대로 1, 2, 3, 4, 5, 6, 7, 8 출력// 교집합
let intersectionSet = new Set(
  [...setA].filter(v =&amp;gt; setB.has(v))
);
for (let value of intersectionSet) {
  console.log(value);
}
// 차례대로 4, 5 출력// 차집합
let differenceSet = new Set(
  [...setA].filter(v =&amp;gt; !setB.has(v))
);
for (let value of differenceSet) {
  console.log(value);
}
// 차례대로 1, 2, 3 출력&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li id=&quot;d522&quot; data-selectable-paragraph=&quot;&quot;&gt;symmetricDifference&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Symmetric Difference
var set1 = new Set([1, 2, 3, 4, 5]);
var set2 = new Set([3, 4, 5, 6, 7]);var symmetricDifferenceSet = new Set(
[...[...set1].filter(x =&amp;gt; !set2.has(x)), ...[...set2].filter(x =&amp;gt; !set1.has(x))]
)
for (let value of symmetricDifferenceSet) {
  console.log(value);
}
// 차례대로 1, 2, 6, 7 출력&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p id=&quot;3545&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 Map 과 Set 에 대해 자세히 알아보았습니다.&lt;/p&gt;
&lt;p id=&quot;9c8b&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;단순히 key 와 value 를 set 하거나 value 를 set 하는 것뿐만 아니라, iterable object 의 특성을 살려서 map 과 set 을 순회하는 것을 알아보았습니다. 더 나아가서는 map 과 set 에서는 지원하지 않는 배열 메서드(Array.prototype) 인 map, filter 를 적용해보고, 집합연산까지도 진행해보았습니다.&lt;/p&gt;
&lt;p id=&quot;8961&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;억지로 배열의 형태로, 기본 객체형태로 코딩하기 보다는 적재적소의 자료구조의 특성에 맞게 코딩하는 습관을 기르면 좋겠습니다.&lt;/p&gt;
&lt;p id=&quot;5859&quot; data-selectable-paragraph=&quot;&quot; data-ke-size=&quot;size16&quot;&gt;다음에는 윗글에서도 잠깐 나왔지만, 왜 배열의 indexOf 메서드보다 Set 의 has 가 더 &amp;ldquo;빠른지&quot; 알아보겠습니다^^ 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처:&lt;a href=&quot;https://medium.com/@hongkevin/js-5-es6-map-set-2a9ebf40f96b&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/@hongkevin/js-5-es6-map-set-2a9ebf40f96b&lt;/a&gt;&lt;/p&gt;</description>
      <category>Web/Js</category>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/159</guid>
      <comments>https://calssess.tistory.com/159#entry159comment</comments>
      <pubDate>Thu, 19 Jan 2023 12:21:52 +0900</pubDate>
    </item>
    <item>
      <title>React 18의 useSyncExternalStore, Tearing 현상은 무엇인가?</title>
      <link>https://calssess.tistory.com/158</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;React18의 Tearing현상과 이를 해결하기 위한 useSyncExternalStore에 대해 알아봅시다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;원문 :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://blog.saeloun.com/2021/12/30/react-18-usesyncexternalstore-api&quot;&gt;https://blog.saeloun.com/2021/12/30/react-18-usesyncexternalstore-api&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1674027044134&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Meet the new hook useSyncExternalStore, introduced in React 18 for external stores&quot; data-og-description=&quot;Ruby on Rails and ReactJS consulting company. We also build mobile applications using React Native&quot; data-og-host=&quot;blog.saeloun.com&quot; data-og-source-url=&quot;https://blog.saeloun.com/2021/12/30/react-18-usesyncexternalstore-api&quot; data-og-url=&quot;https://blog.saeloun.com/2021/12/30/react-18-useSyncExternalStore-api.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/la6n7/hyPf40duxH/fBfwWWwvrC7GnT7Ftlp170/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600,https://scrap.kakaocdn.net/dn/cMyCKx/hyPf9mT0mg/gdIjkYJB6BKOrnrX65OTNK/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600,https://scrap.kakaocdn.net/dn/gP1nP/hyPhzdb9er/Jqi3ZEu6GPLi0SzEm7fOVK/img.png?width=4968&amp;amp;height=1968&amp;amp;face=0_0_4968_1968&quot;&gt;&lt;a href=&quot;https://blog.saeloun.com/2021/12/30/react-18-usesyncexternalstore-api&quot; data-source-url=&quot;https://blog.saeloun.com/2021/12/30/react-18-usesyncexternalstore-api&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/la6n7/hyPf40duxH/fBfwWWwvrC7GnT7Ftlp170/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600,https://scrap.kakaocdn.net/dn/cMyCKx/hyPf9mT0mg/gdIjkYJB6BKOrnrX65OTNK/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600,https://scrap.kakaocdn.net/dn/gP1nP/hyPhzdb9er/Jqi3ZEu6GPLi0SzEm7fOVK/img.png?width=4968&amp;amp;height=1968&amp;amp;face=0_0_4968_1968');&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;Meet the new hook useSyncExternalStore, introduced in React 18 for external stores&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Ruby on Rails and ReactJS consulting company. We also build mobile applications using React Native&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.saeloun.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;div&gt;&lt;span&gt;&lt;b&gt;useSyncExternalStore&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;API에 대해 알아보기 전에&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;이 새로운 훅을 이해하기에 유용한 용어에 익숙해지도록 하겠습니다.&lt;/span&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Concurrent rendering&lt;span&gt;&amp;nbsp;&lt;/span&gt;and startTransition API&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;&lt;span&gt;동시성은 작업의 우선 순위를 지정하여 여러 작업을&lt;span&gt;&amp;nbsp;&lt;/span&gt;동시에 실행하는 메커니즘입니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;이 개념은 Dan Abramov가 전화 통화에 비유하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;쉽게 설명합니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/46#discussioncomment-846786&quot;&gt;with an analogy of phone calls&lt;/a&gt;&lt;/span&gt;
&lt;figure id=&quot;og_1674027044135&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Glossary + Explain Like I'm Five &amp;middot; Discussion #46 &amp;middot; reactwg/react-18&quot; data-og-description=&quot;tl;dr This thread contains plain English and explain-like-I'm-five definitions of the terms and concepts used throughout other discussions. Scroll to comments to find (or add) a term and explan...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/reactwg/react-18/discussions/46#discussioncomment-846786&quot; data-og-url=&quot;https://github.com/reactwg/react-18/discussions/46&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/OTWa3/hyPf1vEZRN/kSaO1UKEE072lj1KQwgqA1/img.png?width=1200&amp;amp;height=600&amp;amp;face=995_118_1045_174&quot;&gt;&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/46#discussioncomment-846786&quot; data-source-url=&quot;https://github.com/reactwg/react-18/discussions/46#discussioncomment-846786&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/OTWa3/hyPf1vEZRN/kSaO1UKEE072lj1KQwgqA1/img.png?width=1200&amp;amp;height=600&amp;amp;face=995_118_1045_174');&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;Glossary + Explain Like I'm Five &amp;middot; Discussion #46 &amp;middot; reactwg/react-18&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;tl;dr This thread contains plain English and explain-like-I'm-five definitions of the terms and concepts used throughout other discussions. Scroll to comments to find (or add) a term and explan...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;581&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZ7s2m/btrWBijNN3B/s0MUvz7QQwtYpLy51ADQM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZ7s2m/btrWBijNN3B/s0MUvz7QQwtYpLy51ADQM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZ7s2m/btrWBijNN3B/s0MUvz7QQwtYpLy51ADQM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZ7s2m%2FbtrWBijNN3B%2Fs0MUvz7QQwtYpLy51ADQM0%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;1280&quot; height=&quot;581&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;581&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_4_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mqnEt/btrIGzyfdzx/6k7I1JSoqo7Yk2JY5CjOQ0/img.png&quot; data-lightbox=&quot;lightbox&quot; data-alt=&quot;set~함수를 통한 상태 업데이트를 리액트가 지연 처리할 수 있으며, 때로 렌더링 결과를 버릴 수 있음(프레임 드랍)&quot;&gt;&lt;/span&gt;set~함수를 통한 상태 업데이트를 리액트가 지연 처리할 수 있으며, 때로 렌더링 결과를 버릴 수 있음(프레임 드랍)&lt;/p&gt;
&lt;div&gt;&lt;span&gt;&lt;b&gt;startTransition&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;API를 사용하여 렌더링하는 동안 앱의 응답성을 유지하도록 선택할 수 있습니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;즉, React는 렌더링을 일시 중지 할 수 있습니다. 이를 통해 브라우저는 그 사이의 이벤트를 처리할 수 있습니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;이전 게시물의 startTransition API에 대한 자세한 내용을 확인하세요.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;a href=&quot;https://blog.saeloun.com/2021/09/09/react-18-introduces-starttransition-api&quot;&gt;the startTransition API, which we have written in our previous post.&lt;/a&gt;&lt;/span&gt;
&lt;figure id=&quot;og_1674027044136&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Keep the React app responsive even during large screen updates with startTransition API introduced in React 18&quot; data-og-description=&quot;Ruby on Rails and ReactJS consulting company. We also build mobile applications using React Native&quot; data-og-host=&quot;blog.saeloun.com&quot; data-og-source-url=&quot;https://blog.saeloun.com/2021/09/09/react-18-introduces-starttransition-api&quot; data-og-url=&quot;https://blog.saeloun.com/2021/09/09/react-18-introduces-startTransition-api.html&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iAlwY/hyPf7WWJ6e/wz4f0KtnEhqareUcICmQe1/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600,https://scrap.kakaocdn.net/dn/btC2iW/hyPhsd50gg/TxDeisLnmR3Ye7ZJS5HOVK/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600&quot;&gt;&lt;a href=&quot;https://blog.saeloun.com/2021/09/09/react-18-introduces-starttransition-api&quot; data-source-url=&quot;https://blog.saeloun.com/2021/09/09/react-18-introduces-starttransition-api&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iAlwY/hyPf7WWJ6e/wz4f0KtnEhqareUcICmQe1/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600,https://scrap.kakaocdn.net/dn/btC2iW/hyPhsd50gg/TxDeisLnmR3Ye7ZJS5HOVK/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Keep the React app responsive even during large screen updates with startTransition API introduced in React 18&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Ruby on Rails and ReactJS consulting company. We also build mobile applications using React Native&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.saeloun.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;External store&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_5_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;b&gt;외부 저장소는 우리가 구독할 수 있는 것입니다.&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;외부 저장소의 예로는 Redux 저장소, Zustand&lt;span&gt;&amp;nbsp;&lt;/span&gt;저장소, 전역 변수, 모듈 범위 변수, DOM 상태 등이 있습니다.&lt;/span&gt;&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Internal stores&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;&lt;span&gt;내부 저장소에는 props, context, useState, useReducer가 포함됩니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Tearing&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;&lt;b&gt;&lt;span&gt;Tearing은 시각적(UI) 불일치를 나타냅니다. UI가 동일한 상태에&lt;span&gt;&amp;nbsp;&lt;/span&gt;대해 여러 형태를 나타냄을 의미합니다.&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;
&lt;div id=&quot;translation&quot;&gt;
&lt;div&gt;&lt;span&gt;React 18 이전에는 이 문제가 발생하지 않았습니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;그러나 React 18에서는 렌더링 중에 React가 일시 중지(suspend)됩니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;즉 concurrent 렌더링이 이 문제를 유발할 수 있습니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;이러한 일시 중지 사이에 업데이트는 렌더링에 사용되는 데이터와 관련된 변경 사항을 가져올 수 있습니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;UI가 동일한 데이터에 대해 두 개의 다른 값을 표시하도록 합니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/69&quot;&gt;WG discussion of tearing&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;lt;- 옆의 디스커션에서 논의된 내용을 살펴봅시다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;컴포넌트는 색상을 가져오기 위해 일부 외부 저장소에 액세스해야 합니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;b&gt;&lt;span&gt;동기 렌더링을 사용하면 UI에서 렌더링되는 색상이 일관됩니다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDjPHO/btrWBjbWyZa/GpEOOmpul5VC59BX07Ht9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDjPHO/btrWBjbWyZa/GpEOOmpul5VC59BX07Ht9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDjPHO/btrWBjbWyZa/GpEOOmpul5VC59BX07Ht9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDjPHO%2FbtrWBjbWyZa%2FGpEOOmpul5VC59BX07Ht9k%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;1280&quot; height=&quot;507&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
동기 렌더링은 티어링을 유발하지 않음&lt;/div&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_6_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_3_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;concurrent(동시) 렌더링에서 스토어의 데이터 'blue'를 이용해 처음에 렌더링한 색상은 파란색입니다.&lt;br /&gt;렌더링 도중 React가 스토어를 업데이트하면 데이터는 'red'으로 업데이트됩니다.&lt;br /&gt;React는 업데이트된 값 red를 활용하여 렌더링을 계속합니다.&lt;br /&gt;&lt;b&gt;이는 '티어링'으로 알려진 UI의 불일치를 유발합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dK3Lt1/btrWx8I1sWT/4FbU2guXXAvaYPM6sEw6Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dK3Lt1/btrWx8I1sWT/4FbU2guXXAvaYPM6sEw6Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dK3Lt1/btrWx8I1sWT/4FbU2guXXAvaYPM6sEw6Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdK3Lt1%2FbtrWx8I1sWT%2F4FbU2guXXAvaYPM6sEw6Kk%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;1280&quot; height=&quot;500&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yNNfp/btrIFI2R9pA/LIVOvQAo8e11IKrHITNXsk/img.png&quot; data-lightbox=&quot;lightbox&quot; data-alt=&quot;동시성 렌더링에 의한 티어링 발생&quot;&gt;&lt;/span&gt;동시성 렌더링에 의한 티어링 발생&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;div id=&quot;translation&quot;&gt;
&lt;div&gt;&lt;span&gt;이 문제를 해결하기 위해 React 팀은&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;변경 가능한 외부 소스에서 데이터를 안전하고 효율적으로 읽을 수 있도록&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;useMutableSource 훅을 추가했습니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/reactjs/rfcs/blob/main/text/0147-use-mutable-source.md&quot;&gt;the React team added useMutableSource hook&lt;/a&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;그러나 리액트 워킹 그룹(react Working Group)의 구성들은&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;기존 오픈소스 라이브러리의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;구현에서&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;useMutableSource를 채택하기 어렵게 만드는 기존 API 컨트랙트의 결함(&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/84&quot;&gt;flaws with the existing API contract&lt;/a&gt;)을 보고했습니다.&lt;/span&gt;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 라이브러리들은 렌더링 시 ref를 이전 렌더링에서 사용한 데이터로 참조하는 패턴을 사용하고 있었지만,&lt;br /&gt;리액트 코어 팀은 ref를 공유 데이터, 전역변수와 같이 불안전한 데이터로 생각하고 있었습니다.&lt;br /&gt;그래서 렌더링 중간에 ref를 읽고 쓰는 것은 당연히 버그가 생기는게 맞다고 생각했었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;렌더링 중에 ref 값을 읽거나 쓰는 것은 지연 초기화 패턴(&lt;a href=&quot;https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily&quot; data-turbo-frame=&quot;&quot;&gt;the lazy initialization pattern&lt;/a&gt;을 구현하는 경우에만 안전합니다.&lt;/li&gt;
&lt;li&gt;ref는 다른 것과 마찬가지로 변경 가능한 소스(&lt;a href=&quot;https://github.com/reactjs/rfcs/blob/master/text/0147-use-mutable-source.md&quot; data-turbo-frame=&quot;&quot;&gt;mutable source&lt;/a&gt;)이므로 다른 타입의 읽기는 안전하지 않습니다(&lt;a href=&quot;https://github.com/facebook/react/pull/18545&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/facebook/react/pull/18545/hovercard&quot; data-turbo-frame=&quot;&quot;&gt;unsafe&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;지연 초기화 패턴이 아닌 writing는 사실상 부작용(&lt;a href=&quot;https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects&quot; data-turbo-frame=&quot;&quot;&gt;side effects&lt;/a&gt;)이 있기 때문에 안전하지 않습니다(&lt;a href=&quot;https://github.com/facebook/react/pull/18545&quot; data-hovercard-type=&quot;pull_request&quot; data-hovercard-url=&quot;/facebook/react/pull/18545/hovercard&quot; data-turbo-frame=&quot;&quot;&gt;unsafe&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 수 많은 기존 오픈소스 라이브러리 메인테이너들에과 사용자에게 어려움을 줄 것이었으므로,&lt;br /&gt;많은 논의 끝에&amp;nbsp;useMutableSource&amp;nbsp;훅이 재설계되고 이름이&amp;nbsp;useSyncExternalStore로 변경되었습니다.&lt;br /&gt;useSyncExternalStore는 이름처럼 동기적으로 렌더링하게 되었고,&lt;br /&gt;이는 기존 오픈소스 라이브러리들에 큰 영향을 끼치지 않게 되었습니다.&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div id=&quot;aswift_7_host&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;Understanding useSyncExternalStore hook&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div&gt;&lt;span&gt;React 18에서 사용할 수 있는 새로운 useSyncExternalStore 훅을 사용하면 저장소의 값을 적절하게 구독할 수 있습니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/86&quot;&gt;The new useSyncExternalStore hook&lt;/a&gt;&lt;/span&gt;
&lt;figure id=&quot;og_1674027044138&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;useMutableSource &amp;rarr; useSyncExternalStore &amp;middot; Discussion #86 &amp;middot; reactwg/react-18&quot; data-og-description=&quot;Since experimental useMutableSource API was added, we&amp;rsquo;ve made changes to our overall concurrent rendering model that have led us to reconsider its design. Members of this Working Group have also re...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/reactwg/react-18/discussions/86&quot; data-og-url=&quot;https://github.com/reactwg/react-18/discussions/86&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bk6DnM/hyPhvhAod4/GQxxbDajoSGMhhcqij15fk/img.png?width=1200&amp;amp;height=600&amp;amp;face=1010_118_1051_163&quot;&gt;&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/86&quot; data-source-url=&quot;https://github.com/reactwg/react-18/discussions/86&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bk6DnM/hyPhvhAod4/GQxxbDajoSGMhhcqij15fk/img.png?width=1200&amp;amp;height=600&amp;amp;face=1010_118_1051_163');&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;useMutableSource &amp;rarr; useSyncExternalStore &amp;middot; Discussion #86 &amp;middot; reactwg/react-18&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Since experimental useMutableSource API was added, we&amp;rsquo;ve made changes to our overall concurrent rendering model that have led us to reconsider its design. Members of this Working Group have also re...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;마이그레이션을 단순화하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;React는 새로운 패키지 use-sync-external-store를 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;a href=&quot;https://www.npmjs.com/package/use-sync-external-store&quot;&gt;To help simplify the migration, React provides a new package use-sync-external-store&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1674027044139&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;use-sync-external-store&quot; data-og-description=&quot;Backwards compatible shim for React's useSyncExternalStore. Works with any React that supports hooks.. Latest version: 1.2.0, last published: 2 months ago. Start using use-sync-external-store in your project by running &amp;#96;npm i use-sync-external-store&amp;#96;. Ther&quot; data-og-host=&quot;www.npmjs.com&quot; data-og-source-url=&quot;https://www.npmjs.com/package/use-sync-external-store&quot; data-og-url=&quot;https://www.npmjs.com/package/use-sync-external-store&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dnKzTe/hyPhzYAEkV/Pd10U6AyD3GesqVCm7WRTk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/use-sync-external-store&quot; data-source-url=&quot;https://www.npmjs.com/package/use-sync-external-store&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dnKzTe/hyPhzYAEkV/Pd10U6AyD3GesqVCm7WRTk/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;use-sync-external-store&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Backwards compatible shim for React's useSyncExternalStore. Works with any React that supports hooks.. Latest version: 1.2.0, last published: 2 months ago. Start using use-sync-external-store in your project by running `npm i use-sync-external-store`. Ther&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.npmjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 패키지의 shim(/shim)는 어떤 리액트의 버전과도 잘 동작합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1674027044139&quot; class=&quot;typescript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useSyncExternalStore} from 'react';

// or

// Backwards compatible shim
import {useSyncExternalStore} from 'use-sync-external-store/shim';

//Basic usage. getSnapshot must return a cached/memoized result
useSyncExternalStore(
  subscribe: (callback) =&amp;gt; Unsubscribe
  getSnapshot: () =&amp;gt; State
) =&amp;gt; State

// Selecting a specific field using an inline getSnapshot
const selectedField = useSyncExternalStore(store.subscribe, () =&amp;gt; store.getSnapshot().selectedField);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;useSyncExternalStore 훅은 두 개의 함수를 필요로 합니다.&lt;/span&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;span&gt;콜백 함수를 등록하는 'subscribe' 함수&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;'getSnapshot'은 구독 값이 마지막 시간 이후 변경되었는지, 렌더링되었는지 확인하는 데 사용됩니다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;문자열이나 숫자와 같은 변경할 수 없는 값이거나 캐시/메모된 객체여야 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;그런 다음 훅에서 변경할 수 없는 값을 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;getSnapshot 결과를 자동으로 메모하는 API 버전입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1674027044140&quot; class=&quot;capnproto&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector';

const selection = useSyncExternalStoreWithSelector(
  store.subscribe,
  store.getSnapshot,
  getServerSnapshot,
  selector,
  isEqual
);&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;span&gt;Daishi Kato의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://youtu.be/oPfSC5bQPR8?t=694&quot;&gt;React 18 for External Store Libraries talk&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;강연에서 논의된 예를 확인해 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1674027044140&quot; class=&quot;javascript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import React, { useState, useEffect, useCallback, startTransition } from &quot;react&quot;;

// library code

const createStore = (initialState) =&amp;gt; {
  let state = initialState;
  const getState = () =&amp;gt; state;
  const listeners = new Set();
  const setState = (fn) =&amp;gt; {
    state = fn(state);
    listeners.forEach((l) =&amp;gt; l());
  }
  const subscribe = (listener) =&amp;gt; {
    listeners.add(listener);
    return () =&amp;gt; listeners.delete(listener);
  }
  return {getState, setState, subscribe}
}

const useStore = (store, selector) =&amp;gt; {
  const [state, setState] = useState(() =&amp;gt; selector(store.getState()));
  useEffect(() =&amp;gt; {
    const callback = () =&amp;gt; setState(selector(store.getState()));
    const unsubscribe = store.subscribe(callback);
    callback();
    return unsubscribe;
  }, [store, selector]);
  return state;
}

//Application code

const store = createStore({count: 0, text: 'hello'});

const Counter = () =&amp;gt; {
  const count = useStore(store, useCallback((state) =&amp;gt; state.count, []));
  const inc = () =&amp;gt; {
    store.setState((prev) =&amp;gt; ({...prev, count: prev.count + 1}))
  }
  return (
    &amp;lt;div&amp;gt;
      {count} &amp;lt;button onClick={inc}&amp;gt;+1&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

const TextBox = () =&amp;gt; {
  const text = useStore(store, useCallback((state) =&amp;gt; state.text, []));
  const setText = (event) =&amp;gt; {
    store.setState((prev) =&amp;gt; ({...prev, text: event.target.value}))
  }
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;input value={text} onChange={setText} className='full-width'/&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}

const App = () =&amp;gt; {
  return(
    &amp;lt;div className='container'&amp;gt;
      &amp;lt;Counter /&amp;gt;
      &amp;lt;Counter /&amp;gt;
      &amp;lt;TextBox /&amp;gt;
      &amp;lt;TextBox /&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dj8vf4/btrWCIhIqAW/tO163BH6I07J5nkYPYsFFK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dj8vf4/btrWCIhIqAW/tO163BH6I07J5nkYPYsFFK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dj8vf4/btrWCIhIqAW/tO163BH6I07J5nkYPYsFFK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dj8vf4/btrWCIhIqAW/tO163BH6I07J5nkYPYsFFK/img.gif&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;326&quot; height=&quot;228&quot; data-origin-width=&quot;326&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;div&gt;&lt;span&gt;코드의 어딘가에서 startTransition을 사용하면 티어링이 발생합니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;Tearing 문제를 해결하기 위해 이제 useSyncExternalStore API를 사용할 수 있습니다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span&gt;useEffect 및 useState 훅 대신 useSyncExternalStore를 사용하도록 라이브러리의 useStore 훅을 수정하겠습니다.&lt;/span&gt;&lt;/div&gt;
&lt;pre id=&quot;code_1674027044142&quot; class=&quot;coffeescript&quot; data-ke-language=&quot;typescript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { useSyncExternalStore } from 'react';

const useStore = (store, selector) =&amp;gt; {
  return useSyncExternalStore(
    store.subscribe,
    useCallback(() =&amp;gt; selector(store.getState(), [store, selector]))
  )
}&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;외부 저장소에서 useSyncExternalStore 훅으로 마이그레이션하는 것은 쉽고 잠재적인 문제를 피하기 위해 권장됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span&gt;모든 인터페이스가 동일하기 때문에, 다른 라이브러리로 마이그레이션 하는것도 쉬울듯...&amp;nbsp;&lt;/span&gt;&lt;/blockquote&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span&gt;&lt;b&gt;어떤 라이브러리들이 concurrent rendering의 영향을 받을까요?&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;렌더링하는 동안 변경 가능한 외부 데이터에 액세스하지 않고 React 프롭, 상태 또는 컨텍스트를 사용하여 정보만 전달하는 컴포넌트 및 사용자 정의 훅이 있는 라이브러리는 영향을 받지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;데이터 가져오기, 상태 관리 또는 스타일링(Redux, MobX, Relay)을 처리하는 라이브러리가 영향을 받습니다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;React 외부에 상태를 저장하기 때문입니다.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;동시 렌더링을 사용하면 React가 알지 못하는 사이에 이러한 외부 데이터 저장소를 렌더링 중에 업데이트할 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&lt;span&gt;useSyncExternalStore 훅에 대해 좀 더 자세히 알아보려면 아래 링크를 읽어보세요&lt;/span&gt;&lt;/div&gt;
&lt;div id=&quot;translation&quot;&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/reactjs/rfcs/pull/147&quot;&gt;RFC: useMutableSource&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/84&quot;&gt;Discussion regarding useMutableSource and selector stability&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;a href=&quot;https://github.com/reactwg/react-18/discussions/86&quot;&gt;Journey of useMutableSource to useSyncExternalStore&lt;/a&gt;&lt;/span&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;&lt;span&gt;참조: &lt;a href=&quot;https://itchallenger.tistory.com/650&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://itchallenger.tistory.com/650&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Web/Js</category>
      <author>Calssess</author>
      <guid isPermaLink="true">https://calssess.tistory.com/158</guid>
      <comments>https://calssess.tistory.com/158#entry158comment</comments>
      <pubDate>Wed, 18 Jan 2023 16:30:57 +0900</pubDate>
    </item>
  </channel>
</rss>