<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>삼바의 성장 블로그</title>
    <link>https://sambalim.tistory.com/</link>
    <description>개발이 즐거운 프론트엔드 개발자의 블로그입니다.

블로그를 통해 성장해나가는 모습을 보실 수 있을겁니다.  &amp;zwj;♂️</description>
    <language>ko</language>
    <pubDate>Wed, 6 May 2026 19:36:19 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>SambaLim</managingEditor>
    <image>
      <title>삼바의 성장 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/3026089/attach/4b014b5f8f0a48309aebacf086bd4cb1</url>
      <link>https://sambalim.tistory.com</link>
    </image>
    <item>
      <title>useQuery 의 변경점 이해하기</title>
      <link>https://sambalim.tistory.com/172</link>
      <description>&lt;p&gt;Dominik의 트위터에 &lt;code&gt;useQuery&lt;/code&gt;의 변경점에 대한 내용이 올라왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c39kP3/btsvYTDfZrc/guWtHNKJqDtqIFhMPmRlY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c39kP3/btsvYTDfZrc/guWtHNKJqDtqIFhMPmRlY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c39kP3/btsvYTDfZrc/guWtHNKJqDtqIFhMPmRlY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc39kP3%2FbtsvYTDfZrc%2FguWtHNKJqDtqIFhMPmRlY0%2Fimg.png&quot; width=&quot;100%&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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;이미 우리는 콜백(&lt;code&gt;onSuccess&lt;/code&gt;, &lt;code&gt;onError&lt;/code&gt;, &lt;code&gt;onSettled&lt;/code&gt;)을 많이 사용하고 있습니다. 따라서 콜백을 제거하는 것에 대해 부정적으로 생각할 수 있지만, 기존의 콜백은 예상대로 동작하지 않을 수 있기에 변경이 필요했습니다.&lt;/p&gt;
&lt;p&gt;API에서 중요한 요소중 하나는 일관성입니다. 사람들은 추상화되어있는 API에서 일관된 응답을 받기를 원합니다. 하지만 &lt;code&gt;useQuery&lt;/code&gt;의 콜백들은 그렇지 않습니다.&lt;/p&gt;
&lt;h1&gt;Callback 들여다보기&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;useQuery&lt;/code&gt;에는 성공했을 때, 실패했을 때, 그리고 둘과 관계없이 실행되는 &lt;code&gt;onSuccess&lt;/code&gt;, &lt;code&gt;onError&lt;/code&gt;, &lt;code&gt;onSettled&lt;/code&gt; 콜백이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;onError&lt;/code&gt;를 사용하는 예시를 보면, &lt;code&gt;onError&lt;/code&gt; 콜백을 사용하여 오류가났을 경우 에러 알림을 보여주는 부수효과(side effect)를 실행합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function useTodos() {
  return useQuery({
    queryKey: [&amp;#39;todos&amp;#39;, &amp;#39;list&amp;#39;],
    queryFn: fetchTodos,
    onError: (error) =&amp;gt; {
      toast.error(error.message)
    },
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사용자들은 이런 &lt;strong&gt;직관적인&lt;/strong&gt; API를 좋아합니다. 이러한 콜백을 사용하지 않는다면, 부수효과를 처리하기위해 무시무시한 &lt;code&gt;useEffect&lt;/code&gt; 훅을 사용해야하는 것에 두려움을 느낍니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function useTodos() {
  const query = useQuery({
    queryKey: [&amp;#39;todos&amp;#39;, &amp;#39;list&amp;#39;],
    queryFn: fetchTodos,
  })

  React.useEffect(() =&amp;gt; {
    if (query.error) {
      toast.error(query.error.message)
    }
  }, [query.error])

  return query
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예시에 &lt;code&gt;useTodos()&lt;/code&gt; 사용하는 함수 컴포넌트의 리렌더링으로 인해 hook을 두 번 호출한다면, 우리는 두 개의 에러 메시지(toast)를 보게됩니다. 사실 이는 &lt;code&gt;onError&lt;/code&gt; 콜백을 사용했을 경우도 마찬가지입니다.&lt;/p&gt;
&lt;p&gt;중복으로 호출했을 경우, 콜백은 각각의 컴포넌트에서의 값(클로져)을 가지고 실행됩니다.&lt;/p&gt;
&lt;p&gt;이 문제를 해결하는 가장 좋은 방법은 &lt;code&gt;QueryClient&lt;/code&gt;를 설정할 때, 전역 cache-level을 사용하는 것 입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error) =&amp;gt;
      toast.error(`Something went wrong: ${error.message}`),
  }),
})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 설정한 콜백은 Query당 한 번만 호출합니다. 또한 &lt;code&gt;useQuery&lt;/code&gt;를 호출한 컴포넌트 밖에서 호출되기 때문에 클로져문제가 발생하지 않습니다.&lt;/p&gt;
&lt;h1&gt;On-demand message 정의하기&lt;/h1&gt;
&lt;p&gt;Query마다 콜백의 &lt;code&gt;error&lt;/code&gt;내부에 있는 값이 아닌 다른 메시지를 보여주고 싶을 경우, Query의 &lt;code&gt;meta&lt;/code&gt; 필드를 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;meta&lt;/code&gt; 객체는 어떠한 정보든 넣을 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) =&amp;gt; {
      if (query.meta.errorMessage) {
        toast.error(query.meta.errorMessage)
      }
    },
  }),
})

export function useTodos() {
  return useQuery({
    queryKey: [&amp;#39;todos&amp;#39;, &amp;#39;list&amp;#39;],
    queryFn: fetchTodos,
    meta: {
      errorMessage: &amp;#39;Failed to fetch todos&amp;#39;,
    },
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;상태 동기화 (State syncing)&lt;/h1&gt;
&lt;p&gt;콜백 API를 삭제하는 또다른 이유는 많은 사람들이 콜백을 상태 동기화를 하는데 사용하기 때문입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function useTodos() {
  const [todoCount, setTodoCount] = React.useState(0)
  const { data: todos } = useQuery({
    queryKey: [&amp;#39;todos&amp;#39;, &amp;#39;list&amp;#39;],
    queryFn: fetchTodos,
    //  please don&amp;#39;t
    onSuccess: (data) =&amp;gt; {
      setTodoCount(data.length)
    },
  })

  return { todos, todoCount }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;콜백 API가 사용자들에게 위의 예제와 같은 상태 동기화를 하도록 유도한다는 단점이 있습니다. 이러한 상태 동기화도 마찬가지로 앱이 예상하는대로 실행되지 않게합니다.&lt;/p&gt;
&lt;h2&gt;추가적인 렌더 사이클&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;setTodoCount&lt;/code&gt;는 또다른 렌더 사이클을 추가합니다. 이는 앱이 필요없는 렌더링을 하게할 뿐만 아니라(문제가 되거나 안될 수도 있음) 잘못된 값을 기자고 렌더링을 할 수 도 있게 합니다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;fetchTodos&lt;/code&gt;는 길이가 5인 목록을 반환한다고 가정해 보겠습니다. 위 코드에서 렌더링 사이클은 세 번입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;todos&lt;/code&gt;는 &lt;code&gt;undefined&lt;/code&gt;이고 길이는 0입니다. Query가 fetch되는 동안의 초기 상태이며 올바른 상태입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;todos&lt;/code&gt;는 길이가 5인 배열이 되고 &lt;code&gt;todoCount&lt;/code&gt;는 0이 됩니다. 이건 &lt;code&gt;useQuery&lt;/code&gt;와 &lt;code&gt;onSuccess&lt;/code&gt;는 이미 실행을 마쳤고 &lt;code&gt;setTodoCount&lt;/code&gt;는 예약된 중간의 렌더링 사이클입니다. 값들이 동기화되지 않았기 때문에 잘못된 상태입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;todos&lt;/code&gt;는 길이가 5인 배열이 되고 &lt;code&gt;todoCount&lt;/code&gt;는 5가 됩니다. 이게 최종 상태이며 다시 올바른 상태가 되었습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위의 예제는 크게 걱정될 정도는 아니지만, 2번에서 동기화되지 않은 상태로 렌더링 된다는 사실은 끔찍합니다. 이로인해 끔찍한 버그가 생성될 수 있습니다.&lt;/p&gt;
&lt;p&gt;이러한 문제를 가장 쉽게 해결하는 방법은 상태를 파생시키는 것 입니다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function useTodos() {
  const { data: todos } = useQuery({
    queryKey: [&amp;#39;todos&amp;#39;, &amp;#39;list&amp;#39;],
    queryFn: fetchTodos,
  })

  const todoCount = todos?.length ?? 0

  return { todos, todoCount }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 동기화가 깨질 일이 없습니다.&lt;/p&gt;
&lt;h2&gt;실행되지 않을 수 있는 콜백&lt;/h2&gt;
&lt;p&gt;React Query를 점진적으로 적용해갈때 Redux와 같은 상태관리 라이브러리와의 동기화가 필요합니다. 아직 React Query를 모두 사용할 수 없는 부분에 대해 아래의 예시와 같이 사용하곤 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function useTodos() {
  const { dispatch } = useDispatch()

  return useQuery({
    queryKey: [&amp;#39;todos&amp;#39;, &amp;#39;list&amp;#39;],
    queryFn: fetchTodos,
    onSuccess: (data) =&amp;gt; {
      dispatch(setTodos(data))
    },
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React Query를 비동기 상태 관리자로 사용하는 경우, &lt;code&gt;onSuccess&lt;/code&gt; 콜백에서 &lt;code&gt;staleTime&lt;/code&gt;을 정의한 캐싱된 데이터를 읽어오는 것은 심각한 문제를 일으킬 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;filters&lt;/code&gt;와 &lt;code&gt;staleTime&lt;/code&gt;을 추가한 예시를 봐보겠습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function useTodos(filters) {
  const { dispatch } = useDispatch()

  return useQuery({
    queryKey: [&amp;#39;todos&amp;#39;, &amp;#39;list&amp;#39;, { filters }],
    queryFn: () =&amp;gt; fetchTodos(filters),
    staleTime: 2 * 60 * 1000,
    onSuccess: (data) =&amp;gt; {
      dispatch(setTodos(data))
    },
  })
}&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;todos를 &lt;code&gt;done: true&lt;/code&gt;로 필터링하고, React Query가 해당 데이터를 캐시에 저장하고, &lt;code&gt;onSuccess&lt;/code&gt;는 redux에 넣습니다.&lt;/li&gt;
&lt;li&gt;todos를 &lt;code&gt;done: false&lt;/code&gt;로 필터링하고, 동일한 과정이 진행됩니다.&lt;/li&gt;
&lt;li&gt;todos를 다시 &lt;code&gt;done: true&lt;/code&gt;로 필터링하면 앱이 고장납니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이와같은 과정을 통해 &lt;code&gt;onSuccess&lt;/code&gt;가 다시 호출되지 않아 &lt;code&gt;dispatch&lt;/code&gt; 가 실행되지 않습니다. 따라서 &lt;code&gt;useTodos&lt;/code&gt;의 데이터는 잘 필터된 데이터이지만, Redux에서 불러오는 값은 필터링되지 않았을 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;staleTime&lt;/code&gt; 에 정의된 시간동안 최신의 데이터를 가져오기 위해 &lt;code&gt;queryFn&lt;/code&gt; 을 호출하지 않습니다. 캐시된 데이터를 사용하는 것은 re-fetch를 피할 수 있다는 장점이 있지만, &lt;code&gt;onSuccess&lt;/code&gt;가 호출되지 않아 싱크가 맞지않을 수 있습니다.&lt;/p&gt;
&lt;h1&gt;onDataChanged&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;data&lt;/code&gt;가 변경될때마다 호출되는 &lt;code&gt;onDataChanged&lt;/code&gt; 를 구현하고자 해도 &lt;code&gt;useEffect&lt;/code&gt; 없이 만들 수 없습니다. 하지만 이를 구현하기 위해 또 &lt;code&gt;useEffect&lt;/code&gt;를 사용할 수 밖에 없습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function useTodos(filters) {
  const { dispatch } = useDispatch()

  const query = useQuery({
    queryKey: [&amp;#39;todos&amp;#39;, &amp;#39;list&amp;#39;, { filters }],
    queryFn: () =&amp;gt; fetchTodos(filters),
    staleTime: 2 * 60 * 1000,
  })

  React.useEffect(() =&amp;gt; {
    if (query.data) {
      dispatch(setTodos(query.data))
    }
  }, [query.data])

  return query
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이를 통해 구현은 가능하지만, 권장할만한 코드는 아닙니다.&lt;/p&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;p&gt;기존 &lt;code&gt;useQuery&lt;/code&gt;의 콜백(&lt;code&gt;onSuccess&lt;/code&gt;, &lt;code&gt;onError&lt;/code&gt;, &lt;code&gt;onSettled&lt;/code&gt;)은 데이터의 일관성을 보장할 수 없는 문제가 있습니다. 이는 API가 일관성을 가져야한다는 것에 부합하지 않습니다.&lt;/p&gt;
&lt;p&gt;또한 이를 &lt;code&gt;useState&lt;/code&gt;를 사용하는 상태와 동기화하여 사용하는 경우가 있는데 절대 그렇게 사용해서는 안됩니다. 이는 개발자가 의도하지 않은 렌더링을 추가하여 잘못된 동작을 초래할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이러한 이유로 &lt;code&gt;useQuery&lt;/code&gt;의 API가 변경될 예정입니다.&lt;/p&gt;
&lt;h1&gt;참고&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose&quot;&gt;https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;번역글: &lt;a href=&quot;https://velog.io/@cnsrn1874/breaking-react-querys-api-on-purpose&quot;&gt;https://velog.io/@cnsrn1874/breaking-react-querys-api-on-purpose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>FrontEnd/React.js</category>
      <category>react-query</category>
      <category>react-query api</category>
      <category>react-query v5 변경점</category>
      <category>useQuery api 변경점</category>
      <category>useQuery 변경점</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/172</guid>
      <comments>https://sambalim.tistory.com/172#entry172comment</comments>
      <pubDate>Wed, 27 Sep 2023 16:57:37 +0900</pubDate>
    </item>
    <item>
      <title>의식의 흐름에 따라 보는 &amp;lt;img&amp;gt; preload</title>
      <link>https://sambalim.tistory.com/171</link>
      <description>&lt;h1&gt;의식의 흐름에 따라 보는 &lt;img&gt; preload&lt;/h1&gt;
&lt;p&gt;전면팝업 Braze HTML의 이미지가 늦게 그려지는 현상이 발생하였습니다.&lt;/p&gt;
&lt;p&gt;Nextjs에서 봤던 preload를 듣고 적용하여 팝업에 필요한 리소스 불러오는 시간을 네트워크 slow 3G 기준 8초 줄일 수 있었습니다.&lt;/p&gt;
&lt;h1&gt;Next.js&lt;/h1&gt;
&lt;p&gt;Next.js docs의 &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; 컴포넌트 API로 가보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;priority&lt;/code&gt; 속성 &lt;a href=&quot;http://latentflip.com/loupe/&quot;&gt;설명&lt;/a&gt;에서 &lt;code&gt;preload&lt;/code&gt;관련한 설명을 발견할 수 있었습니다. &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;priority={false} // {false} | {true}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;When true, the image will be considered high priority and &lt;a href=&quot;https://web.dev/preload-responsive-images/&quot;&gt;preload&lt;/a&gt;.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;You should use the &lt;code&gt;priority&lt;/code&gt; property on any image detected as the &lt;a href=&quot;https://nextjs.org/learn/seo/web-performance/lcp&quot;&gt;Largest Contentful Paint (LCP)&lt;/a&gt; element.&lt;/p&gt;
&lt;h2&gt;LCP(Largest Contentful Paint)&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://web.dev/i18n/ko/lcp/&quot;&gt;https://web.dev/i18n/ko/lcp/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;페이지가 처음으로 로드를 시작한 시점을 기준으로 뷰포트 내에 있는 가장 큰 이미지 또는 텍스트 블록의 렌더링 시간을 보고합니다.&lt;/p&gt;
&lt;h1&gt;코드에서 찾아보기&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;packages/next/src/client/image-component.tsx&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;export const Image = forwardRef&amp;lt;HTMLImageElement | null, ImageProps&amp;gt;(...)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ref로 &lt;code&gt;HTMLImageElement&lt;/code&gt;, Props로 &lt;code&gt;ImageProps&lt;/code&gt; 를 받아 컴포넌트를 만드는 것을 예측할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;return (
  &amp;lt;&amp;gt;
    {
      &amp;lt;ImageElement
        {...imgAttributes}
        unoptimized={imgMeta.unoptimized}
        placeholder={imgMeta.placeholder}
        fill={imgMeta.fill}
        onLoadRef={onLoadRef}
        onLoadingCompleteRef={onLoadingCompleteRef}
        setBlurComplete={setBlurComplete}
        setShowAltText={setShowAltText}
        ref={forwardedRef}
      /&amp;gt;
    }
    {imgMeta.priority ? (
      &amp;lt;ImagePreload
        isAppRouter={isAppRouter}
        imgAttributes={imgAttributes}
      /&amp;gt;
    ) : null}
  &amp;lt;/&amp;gt;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;반환하는 부분을 보니 &lt;code&gt;priority&lt;/code&gt; 에 따라 &lt;code&gt;&amp;lt;ImagePreload&amp;gt;&lt;/code&gt; 를 반환하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;function ImagePreload({
  isAppRouter,
  imgAttributes,
}: {
  isAppRouter: boolean
  imgAttributes: ImgProps
}) {
  // ...

  return (
    &amp;lt;Head&amp;gt;
      &amp;lt;link
        key={
          &amp;#39;__nimg-&amp;#39; +
          imgAttributes.src +
          imgAttributes.srcSet +
          imgAttributes.sizes
        }
        rel=&amp;quot;preload&amp;quot;
        // Note how we omit the `href` attribute, as it would only be relevant
        // for browsers that do not support `imagesrcset`, and in those cases
        // it would cause the incorrect image to be preloaded.
        //
        // https://html.spec.whatwg.org/multipage/semantics.html#attr-link-imagesrcset
        href={imgAttributes.srcSet ? undefined : imgAttributes.src}
        {...opts}
      /&amp;gt;
    &amp;lt;/Head&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;Head&amp;gt;&lt;/code&gt; 컴포넌트 내에 &lt;code&gt;&amp;lt;link rel=&amp;quot;preload&amp;quot; href=&amp;quot;...&amp;quot; /&amp;gt;&lt;/code&gt; 형태로 사용하고 있는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;h2&gt;적용&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;
  &amp;lt;meta
    name=&amp;quot;viewport&amp;quot;
    content=&amp;quot;width=device-width, initial-scale=1.0, viewport-fit=cover&amp;quot;
  /&amp;gt;
  &amp;lt;script&amp;gt;
  // NOTE: 이 부분을 수정하여 딥링크와 이미지를 변경할 수 있습니다.
  const sliderItemList = [
    {
    click_eventlog_name: &amp;#39;banner1&amp;#39;,
    href: &amp;#39;link://main-shopping?tab=2325&amp;#39;,
    img_url:
      &amp;quot;images/64ab5c66660a240e6a30b637/original.jpeg?1688951910&amp;quot;,
    },
    {
    click_eventlog_name: &amp;#39;banner2&amp;#39;,
    href: &amp;#39;link://main-shopping?tab=2161&amp;#39;,
    img_url:
      &amp;quot;images/64b48e93ccaa4520ccec9f28/original.png?1689554579&amp;quot;,
    },
    {
    click_eventlog_name: &amp;#39;banner3&amp;#39;,
    href: &amp;#39;link://event-content?id=9704&amp;#39;,
    img_url:
      &amp;quot;images/64ab5c65171a3d24b4a078be/original.png?1688951909&amp;quot;,
    },
  ];

  document.head.innerHTML += sliderItemList.reduce(function(acc, item) {
    return acc + `&amp;lt;link rel=&amp;quot;preload&amp;quot; as=&amp;quot;image&amp;quot; href=&amp;quot;${item.img_url}&amp;quot; /&amp;gt;`;
  }, &amp;#39;&amp;#39;);
  &amp;lt;/script&amp;gt;

  // ...
&amp;lt;/head&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>FrontEnd</category>
      <category>image preload</category>
      <category>nextjs preload</category>
      <category>nextjs priority</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/171</guid>
      <comments>https://sambalim.tistory.com/171#entry171comment</comments>
      <pubDate>Thu, 14 Sep 2023 19:51:58 +0900</pubDate>
    </item>
    <item>
      <title>setTimeout 들여다보기</title>
      <link>https://sambalim.tistory.com/170</link>
      <description>&lt;h1&gt;들어가기&lt;/h1&gt;
&lt;p&gt;오픈소스 스터디에서 &lt;code&gt;setTimeout&lt;/code&gt;을 들여다보며 알게된 것을 공유합니다.&lt;/p&gt;
&lt;h2&gt;Event Loop&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://latentflip.com/loupe/&quot;&gt;http://latentflip.com/loupe/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이벤트루프를 설명하기 위한 서비스입니다. Call Stack, Web Apis, Task Queue(Callback Queue) 세 영역으로 나누어 좌측의 코드가 실행되는 동안 어떠한 영역에서 작업이 이루어지고 이동하는지 설명하고 있습니다. &lt;code&gt;setTimeout&lt;/code&gt; 과 같이 비동기적으로 실행되는 함수는 콜백이 실행되기 전까지 Web Apis에서 대기하고 실행되기 전에 Task Queue에 담겼다가 Call Stack이 비면 실행됩니다.&lt;/p&gt;
&lt;h2&gt;V8, Blink&lt;/h2&gt;
&lt;p&gt;Chromium은 우리가 작성한 Javascript 코드를 실행하기 위해 V8이라는 엔진을 사용합니다. 하지만 브라우저에 렌더링하는 코드는 V8에 포함되어있지 않습니다. 따라서 V8은 렌더링엔진으로 Blink를 사용합니다. 이때 Blink 인터페이스를 V8에 바인딩하여 사용하기위해 Web IDL을 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;V8: EcmaScript spec&lt;/li&gt;
&lt;li&gt;Blink: &lt;a href=&quot;https://html.spec.whatwg.org/multipage/&quot;&gt;whatwg&lt;/a&gt; spec&lt;/li&gt;
&lt;li&gt;Web IDL: &lt;a href=&quot;https://www.chromium.org/blink/webidl/&quot;&gt;https://www.chromium.org/blink/webidl/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;코드 들여다보기&lt;/h1&gt;
&lt;h2&gt;setTimeout&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;third_party/blink/renderer/modules/scheduler/dom_timer.cc&lt;/code&gt; (&lt;a href=&quot;https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/scheduler/dom_timer.cc;l=285;bpv=0;bpt=0?q=dom_timer.cc&quot;&gt;링크&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;코드의 내용은 정확히 모르겠지만, &lt;code&gt;nesting_level&lt;/code&gt; 이라는 것과 &lt;code&gt;timeout.is_zero()&lt;/code&gt;를 가지고 &lt;code&gt;TaskType&lt;/code&gt;을 정하는 로직이 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Select TaskType based on nesting level.
TaskType task_type;
if (nesting_level_ &amp;gt;= kMaxTimerNestingLevel) {
  task_type = TaskType::kJavascriptTimerDelayedHighNesting;
} else if (timeout.is_zero()) {
  task_type = TaskType::kJavascriptTimerImmediate;
  DCHECK_LT(nesting_level_, max_nesting_level);
} else {
  task_type = TaskType::kJavascriptTimerDelayedLowNesting;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 &lt;code&gt;TaskType&lt;/code&gt;을 따라가면 무려 84개의 타입을 만나볼 수 있습니다. &lt;/p&gt;
&lt;p&gt;&lt;code&gt;third_party/blink/public/platform/task_type.h&lt;/code&gt; (&lt;a href=&quot;https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/blink/public/platform/task_type.h;drc=9d4eb7ed25296abba8fd525a6bdd0fdbf4bcdd9f;bpv=1;bpt=0;l=24&quot;&gt;링크&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// https://html.spec.whatwg.org/multipage/webappapis.html#timers
// For tasks queued by setTimeout() or setInterval().
//
// Task nesting level is &amp;lt; 5 and timeout is zero.
kJavascriptTimerImmediate = 72,
// Task nesting level is &amp;lt; 5 and timeout is &amp;gt; 0.
kJavascriptTimerDelayedLowNesting = 73,
// Task nesting level is &amp;gt;= 5.
kJavascriptTimerDelayedHighNesting = 10,
// Note: The timeout is increased to be at least 4ms when the task nesting
// level is &amp;gt;= 5. Therefore, the timeout is necessarily &amp;gt; 0 for
// kJavascriptTimerDelayedHighNesting.&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;TaskPriority&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;third_party/blink/renderer/platform/scheduler/common/task_priority.h&lt;/code&gt; (&lt;a href=&quot;https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/scheduler/common/task_priority.h;l=14;bpv=1;bpt=1&quot;&gt;링크&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;정확한 내용은 모르겠지만, Task Queue에서 최적화를 위한 작업들이 있다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;enum class TaskPriority : base::sequence_manager::TaskQueue::QueuePriority {
  // Priorities are in descending order.
  kControlPriority = 0,
  kHighestPriority = 1,
  kExtremelyHighPriority = 2,
  kVeryHighPriority = 3,
  kHighPriorityContinuation = 4,
  kHighPriority = 5,
  kNormalPriorityContinuation = 6,
  kNormalPriority = 7,
  kDefaultPriority = kNormalPriority,
  kLowPriorityContinuation = 8,
  kLowPriority = 9,
  kBestEffortPriority = 10,

  // Must be the last entry.
  kPriorityCount = 11,
};&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;nesting level&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;If timeout is less than 0, then set timeout to 0.&lt;/li&gt;
&lt;li&gt;If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Step 11 of the algorithm at
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html requires
// that a timeout less than 4ms is increased to 4ms when the nesting level is
// greater than 5.
constexpr int kMaxTimerNestingLevel = 5;
constexpr base::TimeDelta kMinimumInterval = base::Milliseconds(4);
constexpr base::TimeDelta kMaxHighResolutionInterval = base::Milliseconds(32);

// A timer with a long timeout probably doesn&amp;#39;t need to run at a precise time,
// so allow some leeway on it. On the other hand, a timer with a short timeout
// may need to run on time to deliver the best user experience.
bool precise = (timeout &amp;lt; kMaxHighResolutionInterval);&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;예시&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;let start = Date.now();
let times = [];

setTimeout(function run() {
  times.push(Date.now() - start); // remember delay from the previous call

  if (start + 100 &amp;lt; Date.now()) alert(times); // show the delays after 100ms
  else setTimeout(run); // else re-schedule
}, 0);

// ex. 0,0,0,0,5,10,15,20,25,30,35,40,45,50,55,60,66,71,75,80,85,90,94,99,105&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;time&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;base/time/time_mac.mm&lt;/code&gt; (&lt;a href=&quot;https://source.chromium.org/chromium/chromium/src/+/main:base/time/time_mac.mm;l=251;bpv=1;bpt=0&quot;&gt;링크&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Chromium은 운영체제가 제공하는 타이머 기능을 활용하여 시간이 경과를 확인합니다. 따라서 시간과 관련한 파일이 OS별로 존재합니다.&lt;/p&gt;
&lt;h1&gt;참고링크&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://latentflip.com/loupe/&quot;&gt;http://latentflip.com/loupe/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke#syntax&quot;&gt;https://dev.to/lydiahallie/javascript-visualized-promises-async-await-5gke#syntax&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://javascript.info/settimeout-setinterval#zero-delay-settimeout&quot;&gt;https://javascript.info/settimeout-setinterval#zero-delay-settimeout&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Language/JavaScript</category>
      <category>setTimeout</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/170</guid>
      <comments>https://sambalim.tistory.com/170#entry170comment</comments>
      <pubDate>Thu, 14 Sep 2023 19:47:01 +0900</pubDate>
    </item>
    <item>
      <title>React hydration 알아보기</title>
      <link>https://sambalim.tistory.com/169</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;기존 React 어플리케이션은 번들된 js를 가져와 DOM을 렌더링하는 CSR(Client Side Rendering) 방식으로 렌더링을 하였습니다. 이러한 방식은 번들된 js 코드를 사용하므로 서버구성을 간단하게 가져갈 수 있었고 초기에 번들된 js를 가져오므로 사용자에게 마치 네이티브 어플리케이션처럼 동작하는 경험을 제공해줄 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 CSR은 번들된 js로 어플리케이션의 콘텐츠를 화면에 렌더링(FCP)하고 상호작용 까지 걸리는 시간(TTI)이 증가하는 단점을 가지고 있습니다. 따라서 이와 같은 단점을 해결하고자 상황에 따라 SSR을 선택합니다. SSR은 서버에서 정적 페이지를 렌더링하고 JS파일들도 번들링한 후에 Client Side로 전달해주는데, 이 경우 서버에서는 DOM을 조작할 수 없기에 DOM에는 동적인 이벤트가 없는 상태일 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이와 같은 정적 페이지를 동적으로 사용할 수 있는 기술이 등장하였고 이를 Hydration이라고 부릅니다.&lt;/p&gt;
&lt;h1&gt;hydrate(), hydrateRoot()&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React element를 렌더링하고, Client Side에서 서버에서 렌더링된 마크업을 재사용하기 위해 기존에는 ReactDOM.hydrate() React v18부터 hydrateRoot()를 사용합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;hydrate()&lt;/h3&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;hydrate(reactNode, domNode, callback?)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactDOM.hydrate()함수는 기존 DOM element와 React 컴포넌트를 결합하고, React 컴포넌트가 이전에 서버에서 렌더링된 결과를 가져와 이를 사용하여 초기 상태를 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 상태를 설정한다는 공통점이 있어 ReactDOM.render() 함수와 비교되기도 합니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;render(element, container, callback?)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactDOM.render()함수는 특정 컴포넌트를 container 요소에 하위로 주입하여 렌더링을 처리해주는데 ReactDOM.hydrate() 함수는 특정 컴포넌트를 domNode 요소에 하위로 hydrate처리만 합니다. 이해를 돕기 위해 정리하자면 ReactDOM.hydrate()는 렌더링을 통해 새로운 DOM을 생성하는 것이 아니라 기존 DOM Tree에서 해당하는 DOM element를 찾아 정해진 자바스크립트 속성(ex. 이벤트 리스너)들만 추가합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;hydrateRoot()&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const root = hydrateRoot(domNode, reactNode, options?)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React v18부터 hydrate()함수가 hydrateRoot()함수로 대체되었습니다. 이 함수는 컴포넌트를 렌더링할 DOM element를 생성하고, 해당 요소에 root 컴포넌트를 렌더링합니다. 이를 통해 기존 DOM element를 재사용하지 않고 새로운 DOM element를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 hydrate() 함수와 유사하지만, 비동기적인 상태 업데이트를 하는데 강점이 있습니다. startTransition()함수와 함께 사용하여 컴포넌트의 상태 업데이트를 비동기적으로 처리할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느린 FCP, TTI로 인해 사용자가 불편을 느끼는 경우가 있었고, 이를 개선하기 위해 SSR을 사용하였습니다. 이때 서버에서 전해준 정적파일에 자바스크립트 속성을 부여하여 동적으로 사용할 수 있도록 Hydration을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js, Gatsby와 같은 프레임워크에서 다양한 렌더링을 지원해주면서 렌더링을 사용할때 어떤 원리로 동작하는지 이해하는 것이 중요해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다양한 렌더링을 이해하여 상황에 맞추어 원하는 렌더링을 사용하고 문제들을 해결해 나갈 수 있는 것이 중요합니다. 이 글이 Hydration을 이해하는데 조금이라도 도움이 되기를 바랍니다.&lt;/p&gt;
&lt;h1&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://react.dev/reference/react-dom/hydrate&quot;&gt;https://react.dev/reference/react-dom/hydrate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/reference/react-dom/client/hydrateRoot&quot;&gt;https://react.dev/reference/react-dom/client/hydrateRoot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gatsbyjs.com/docs/conceptual/react-hydration/&quot;&gt;https://www.gatsbyjs.com/docs/conceptual/react-hydration/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@huurray/React-Hydration-에-대하여&quot;&gt;https://velog.io/@huurray/React-Hydration-에-대하여&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>FrontEnd/React.js</category>
      <category>hydrate hydrateRoot 차이</category>
      <category>react hydration</category>
      <category>SSR hydration</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/169</guid>
      <comments>https://sambalim.tistory.com/169#entry169comment</comments>
      <pubDate>Tue, 2 May 2023 21:11:41 +0900</pubDate>
    </item>
    <item>
      <title>bfcache 알아보기</title>
      <link>https://sambalim.tistory.com/168</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;BFCache(Back-Foward Cache)는 브라우저의 뒤로, 앞으로 버튼을 사용할 때 페이지를 즉시 로드할 수 있도록 도와주는 역할을 합니다. 이는 느린 네트워크, 장치를 사용하는 사용자의 경험을 향상시켜줍니다. 따라서 사용자의 경험을 위해 bfcache 최적화에 대해 이해하는 것이 중요합니다.&lt;/p&gt;
&lt;h1&gt;bfcache란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfcache는 사용자가 다른 곳으로 이동할 때 JS의 힙 메모리 영역을 포함한 전체 스냅샷을 저장하는 캐시입니다. 이를 통해 사용자가 이전페이지로 돌아가고자 했을 때 빠르게 전체 페이지를 보여줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 스냅샷이기 때문에, 리소스를 다시 다운로드 할 필요가 없습니다. 네트워크 요청이 일어나지 않고 스크롤 위치 또한 복원해줍니다. Task Queue에 대기 중이었던 작업 (setTimeout, Promise)등도 보존되었다가 다시 실행되기도 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;bfcache가 활성화되지 않은 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 페이지를 로드하기 위해 새 요청이 시작되고 해당 페이지가 반복 방문에 대해 얼마나 최적화되었는지에 따라 브라우저는 리소스의 일부 또는 전체를 다시 다운로드하고, 다시 구문 분석하고, 다시 실행해야 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;bfcache가 활성화된 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 페이지를 즉각적으로 다시 로드합니다. 네트워크에 연결하지 않고도 전체 페이지를 메모리에서 복원할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;bfcache 작동 방식&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfcache의 cache는 HTTP cache와 다르게 동작합니다. bfcache는 메모리에 있는 전체 페이지의 스냅샷을 캐싱하는 반면, HTTP cache는 이전에 작성된 요청에 대한 응답만 포함합니다. 따라서 페이지 로드하는 데 필요한 모든 요청이 HTTP cache에서 이행될 수 있는 경우는 매우 드물기 때문에 bfcache 복원을 사용하는 경우 더 빠르게 페이지를 로드할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위험성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfcache는 메모리에 전체 페이지의 스냅샷을 캐싱하기 때문에 setTimeout과 같이 Task Queue에 대기중이었던 현재 실행중인 코드 또한 보존합니다. 브라우저가 보류중인 timer을 일시 중지하고 bfcache로 페이지를 복원했을 때 다시 실행하는데, 이는 매우 혼란스럽거나 예기치 않은 동작을 일으킬 수 있습니다.&lt;/p&gt;
&lt;h1&gt;bfcache를 관찰하는 api&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfcache는 브라우저가 자동으로 하는 최적화이지만, 개발자가 페이지를 최적화 하고 성능을 측정하고 조정할 수 있도록 bfcache의 동작을 이해하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfcache를 관찰하는데 주로 페이지 전환 이벤트(pageshow, pagehide)입니다. 이는 bfcache가 사용되는 경우 거의 모든 브라우저에서 지원해주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-01 19.22.53.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blN6Ue/btsdhxZyPtk/C9hKURer7qM8siv9Jpuxc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blN6Ue/btsdhxZyPtk/C9hKURer7qM8siv9Jpuxc0/img.png&quot; data-alt=&quot;PageTransitionEvent&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blN6Ue/btsdhxZyPtk/C9hKURer7qM8siv9Jpuxc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblN6Ue%2FbtsdhxZyPtk%2FC9hKURer7qM8siv9Jpuxc0%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;496&quot; height=&quot;307&quot; data-filename=&quot;스크린샷 2023-05-01 19.22.53.png&quot; data-origin-width=&quot;1444&quot; data-origin-height=&quot;894&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;PageTransitionEvent&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 추가된 페이지 생명 주기 이벤트(freeze, resume)은 bfcache를 확인하는데 도움을 주지만, 최신의 Chromium기반 브라우저에서만 지원됩니다. 예를들어 freeze, resume을 사용하면 CPU 사용량을 최소화 하기 위하여 백그라운드 탭을 프리징할 때에 이 이벤트를 쓸 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위험성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 종류 및 버전에 따라 bfcache의 사용여부를 알 수 없으며 캐시가 되는 상황이나 조건이 달라 예측이 어렵습니다. 또한 캐시가 메모리에 저장되어 있는 시간도 브라우저마다 달라 bfcache가 보장되지 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PageTransitionEvent&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Session history의 페이지가 현재 페이지가 아닌 경우 실행됩니다. pageshow, pagehide 이벤트를 포함합니다. 이를 통해 bfcache를 관찰할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pageshow&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;window.addEventListener('pageshow', (event) =&amp;gt; {
  if (event.persisted) {
    console.log('This page was restored from the bfcache.');
  } else {
    console.log('This page was loaded normally.');
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pageshow 이벤트는 페이지가 처음 로드될 때와 페이지가 bfcache에서 복원될 때 load 이벤트 직후에 발생합니다. pageshow 이벤트에는 persisted 속성이 있으며, 페이지가 bfcache에서 복원된 경우 true 이고 그렇지 않은 경우 false 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pageshow 이벤트 직전에 resume 이벤트가 발생하기도 하지만, 백그라운드에서 정지되었던 탭에서 다시 돌아왔거나 하는 경우도 발생하므로 bfcache를 관찰하기에는 적합하지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pagehide&lt;/h3&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;window.addEventListener('pagehide', (event) =&amp;gt; {
  if (event.persisted === true) {
    console.log('This page *might* be entering the bfcache.');
  } else {
    console.log('This page will unload normally and be discarded.');
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pagehide 도 persisted 속성을 가지고 있고 만약 persisted가 false라면 bfcache에 들어가지 않을 것이라고 확신할 수 있습니다. 하지만 true라면 페이지가 캐시된다는 보장이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유사하게 freeze이벤트는 pagehide이벤트 직후에 실행되지만(persisted 속성이 true인 경우) 이는 브라우저가 페이지를 캐시하려고 한다는 의미일 뿐, 캐시된다는 것을 보장하지는 못합니다.&lt;/p&gt;
&lt;h1&gt;bfcache로 페이지 최적화 하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 페이지가 bfcache로 캐싱되는 것은 아니며 캐싱되더라도 저장되어있는 시간이 보장되지 않기 때문에 캐싱이 더 의미있을 수 있도록 무엇이 bfcache를 부적격하게 하는지 이해하고 개발하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 페이지를 캐시할 수 있도록 하는 모범사례는 다음과 같습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;unload 이벤트 사용 금지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unload 이벤트 리스너가 페이지에 존재하기만 해도 브라우저는 페이지가 bfcache에 부적격하다고 판단합니다. unload 이벤트는 bfcache이전에 발생하고, unload를 사용한 것은 페이지가 더 이상 존재하지 않는다는 가정을 하게하기 때문에 문제를 야기할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;beforeunload는 조건이 있을 때만 사용하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;beforeunload는 크롬과 사파리에는 영향을 받지 않지만, 파이어폭스의 경우 bfcache를 무력화할 수 있으므로 사용해서는 안됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래의 예시와 같이 필요한 경우에 한해 조건부로 이벤트를 추가하고 제거하는 방식으로 사용할 것을 권장합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function beforeUnloadListener(event) {
  event.preventDefault();
  return event.returnValue = 'Are you sure you want to exit?';
};

// A function that invokes a callback when the page has unsaved changes.
onPageHasUnsavedChanges(() =&amp;gt; {
  window.addEventListener('beforeunload', beforeUnloadListener);
});

// A function that invokes a callback when the page's unsaved changes are resolved.
onAllChangesSaved(() =&amp;gt; {
  window.removeEventListener('beforeunload', beforeUnloadListener);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;window.opener 참조 제거&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;window.open() 또는 target=&quot;_blank&quot; 속성으로 새 창/탭에서 열린 페이지는 자신의 부모 페이지에 대한 참조(window.opener)를 가지는데, 이것이 null이 아닐 경우 bfcache에 부적격한 것으로 판단합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 rel=&quot;noopener&quot;를 사용해 window.opener를 생성하지 않아야, 하는데 이는 Tabnabbing보안 취약점 공격과도 연관된 부분이기도 하므로 추가하는 것이 좋습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;열려있는 연결 닫기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지를 bfcache에 넣으면 모든 예약된 JS 태스크가 일시중지되고 cache에서 나올 때 다시 시작됩니다. 따라서 PageTransitionEvent 를 통해 작업을 미리 중단/재개하거나 제거/추가하는 편이 좋습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지에 끝나지 않은 indexedDB transaction이 있는 경우&lt;/li&gt;
&lt;li&gt;fetch나 XMLHttpRequest가 진행 중인 경우&lt;/li&gt;
&lt;li&gt;WebSocket, WebRTC 연결이 살아 있는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 페이지가 위의 경우에 해당한다면, pagehide 혹은 freeze에서 이러한 연결을 모두 끊어 버리는 것이 좋습니다. 그리고 pageshow 혹은 resume 이벤트를 통해 페이지가 다시 살아난다면 API를 다시 연결해두면 됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;페이지가 캐싱 가능한지 테스트&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-01 21.53.02.png&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/908C2/btsdegKSU2n/NOvmKpnvig1fHaKkC5FKzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/908C2/btsdegKSU2n/NOvmKpnvig1fHaKkC5FKzK/img.png&quot; data-alt=&quot;chrome://flags&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/908C2/btsdegKSU2n/NOvmKpnvig1fHaKkC5FKzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F908C2%2FbtsdegKSU2n%2FNOvmKpnvig1fHaKkC5FKzK%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;588&quot; height=&quot;231&quot; data-filename=&quot;스크린샷 2023-05-01 21.53.02.png&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;592&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;chrome://flags&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크롬에서 chrome://flags/ 로 접속하여 &lt;b&gt;Back-forward cache&lt;/b&gt; 를 활성화하여 테스트해볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Chrome Devtools에서는 페이지를 테스트하여 bfcache에 최적화 되어있는지도 확인해볼 수 있습니다. 이는 Application &amp;gt; Cache &amp;gt; Back-foward Cache에서 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-05-01 21.54.31.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;1204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0G0cq/btsdo9p6KXR/razFBBSsVm95HoRK8BY821/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0G0cq/btsdo9p6KXR/razFBBSsVm95HoRK8BY821/img.png&quot; data-alt=&quot;Chrome Devtools&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0G0cq/btsdo9p6KXR/razFBBSsVm95HoRK8BY821/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0G0cq%2Fbtsdo9p6KXR%2FrazFBBSsVm95HoRK8BY821%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;419&quot; height=&quot;459&quot; data-filename=&quot;스크린샷 2023-05-01 21.54.31.png&quot; data-origin-width=&quot;1098&quot; data-origin-height=&quot;1204&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Chrome Devtools&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;bfcache 비활성화하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bfcache를 비활성화 할 수 있는방법이 있습니다. 최상위 페이지의 Response header에 Cache-Control에 no-store를 추가하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Cache-Control: no-store
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 옵션은 HTTP cache를 위한 옵션이여서 의아할 수는 있지만, bfcache 비활성화가 가능합니다. 하지만 이것은 bfcache뿐만 아니라 다른 캐시도 하지 않도록 유도하여 의도하지 않은 변경이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 개발자가 직접 bfcache만을 명시적으로 비활성화를 할 수는 없습니다.&lt;/p&gt;
&lt;h1&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://web.dev/bfcache/&quot;&gt;https://web.dev/bfcache/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yceffort.kr/2020/11/back-forward-cache&quot;&gt;https://yceffort.kr/2020/11/back-forward-cache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@sejinkim/BackForward-Cache-A.K.A.-bfcache&quot;&gt;https://velog.io/@sejinkim/BackForward-Cache-A.K.A.-bfcache&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Language/JavaScript</category>
      <category>bfcache</category>
      <category>뒤로가기 스크롤위치</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/168</guid>
      <comments>https://sambalim.tistory.com/168#entry168comment</comments>
      <pubDate>Mon, 1 May 2023 22:03:55 +0900</pubDate>
    </item>
    <item>
      <title>자바스크립트 모듈 시스템</title>
      <link>https://sambalim.tistory.com/167</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;처음 HTML에서 Javascript를 사용하여 프로그래밍을 할때는 대부분의 스크립트가 독립적인 작업을 수행하여 일반적으로 큰 스크립트가 필요하지 않았습니다.&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;CJS(CommonJS)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JS 생태계를 브라우저뿐만 아니라 범용 언어로 사용할 수 있도록 만든 모듈시스템입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AMD(Asynchronous Module Definition)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 오래된 모듈 시스템 중 하나로 require.js라는 라이브러리를 통해 처음 개발되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;UMD(Universal Module Definition)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AMD와 CJS와 같은 다양한 모듈 시스템을 함께 사용하기 위해 만들어졌습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ESM(ECMAScript Modules)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ES6부터 추가된 자바스크립트의 모듈 시스템입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;CJS(CommonJS)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 Node.js 환경에서 많이 사용되며 require()로 다른 모듈에서 내보낸 변수와 함수를 가져오고 module.exports 구문을 사용하여 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 코드&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// require()을 사용하여 다른 모듈의 변수와 함수를 가져옵니다.
var lib = require('package/lib');

// 가져온 모듈을 사용할 수 있습니다.
function foo () {
  lib.log('hello world!');
}

// 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.
exports.foobar = foo;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;require() 함수를 이용하여 모듈을 로딩할 때, 동기적인 방식으로 로딩됩니다. 해당 모듈이 로딩되기 전에 다른 코드가 실행되지 않습니다.&lt;/li&gt;
&lt;li&gt;CJS 모듈은 파일 기반으로 정의되며, 파일 이름이 모듈 이름으로 사용됩니다.&lt;/li&gt;
&lt;li&gt;CJS는 npm(Node Package Manager)에서 지원하는 다양한 라이브러리와 패키지를 사용할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;AMD(Asynchronous Module Definition)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CJS는 모든 파일이 로컬 디스크에 있어 필요할 때 바로 불러올 수 있는 상황을 전제로 합니다. 즉 동기적인 동작이 가능한 서버사이드 자바스크립트 환경을 전재로합니다. 이는 브라우저에서 모듈이 다운로드 될때까지 아무것도 할 수 없는 상태가 된다는 치명적인 단점을 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMD는 이러한 문제를 해결하기 위해 비동기적으로 모듈을 로딩하고 사용할 수 있도록 합니다. 따라서 AMD는 주로 브라우저 환경에서 사용됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 코드&lt;/h2&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;// define()을 사용하여 다른 모듈을 가져옵니다. 이는 콜백함수의 매개변수에 담깁니다.
define(['package/lib'], function (lib) {
  // 가져온 모듈을 사용할 수 있습니다.
  function foo () {
    lib.log('hello world!');
  }

  // 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.
  return {
    foobar: foo,
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AMD는 비동기적으로 모듈을 로딩하고 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;define() 함수를 사용하여 모듈을 정의합니다. 모듈 로딩과 모듈 실행을 분리하여 정의할 수 있습니다.&lt;/li&gt;
&lt;li&gt;AMD는 모듈 로딩 시점에 의존성을 관리합니다. 모듈이 로딩되기 전에 필요한 의존성 모듈이 먼저 로딩됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;UMD(Universal Module Definition)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UMD는 CJS, AMD가 서로 호환되지 않는 문제가 생겨 이를 해결하기 위해 생겨났습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CJS, AMD의 호환을 위해 UMD는 다음과 같이 구성됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈 로더를 확인하는 즉시 실행 함수(IIFE)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 함수는 매개변수로 root(전역 범위)와 factory(모듈을 선언하는 함수)를 가집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모듈을 생성하는 익명 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;scheme&quot;&gt;&lt;code&gt;(function (root, factory) {
  if (typeof define === 'function' &amp;amp;&amp;amp; define.amd) {
    // AMD
    define(['exports', 'b'], factory);
  } else if (typeof exports === 'object' &amp;amp;&amp;amp; typeof exports.nodeName !== 'string') {
    // CJS
    factory(exports, require('b'));
  } else {
    factory((root.commonJsStrict = {}), root.b);
  }
}(this, function (exports, b) {
  // 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.
  exports.action = function () {};
}));
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;ESM(ECMAScript Modules)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6부터 지원하는 자바스크립트 공식 모듈시스템 입니다. export와 import를 사용하여 다른 모듈의 변수, 함수를 불러오거나 외부로 내보내는 것이 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 브라우저들에서 import, export 지원하기 시작했지만 크로스 브라우징을 위해서 Babel을 사용하는 것이 필요합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;예제 코드&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 다른 모듈의 변수와 함수를 가져옵니다.
import lib from 'package/lib';

// 가져온 모듈을 사용할 수 있습니다.
function foo () {
  lib.log('hello world!');
}

// 다른 모듈에서 변수와 함수를 사용할 수 있도록 내보냅니다.
export { foo as foobar };
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모듈은 항상 use strict로 실행됩니다. 선언되지 않은 변수에 값을 할당하는 등의 코드는 에러를 발생시킵니다.&lt;/li&gt;
&lt;li&gt;동일한 모듈이 여러 곳에서 사용되더라도 모듈은 최초 호출 시 한 번만 실행됩니다.&lt;/li&gt;
&lt;li&gt;ESM은 정적 로딩(Static Loading) 방식을 사용합니다. 런타임 이전에 모듈을 로딩하여 불필요한 로딩시간을 줄이고 의존성 관리와 코드 최적화를 용이하게 합니다.&lt;/li&gt;
&lt;li&gt;브라우저 환경에서 type=&quot;module&quot; 속성이 추가된 스크립트는 마치 defer 속성을 붙인 것 처럼 실행됩니다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;async 속성 추가도 가능합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트의 모듈시스템이 어떻게 생겨났는지 이야기하고 CJS부터 ESM까지 자바스크립트의 모듈 시스템의 예제 코드와 특징을 보았습니다. 각각 모듈 시스템은 서로 다른 사용 목적과 환경에 따라 적합한 모듈 시스템을 선택하여 사용해야합니다.&lt;/p&gt;
&lt;h1&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://beomy.github.io/tech/javascript/cjs-amd-umd-esm/&quot;&gt;https://beomy.github.io/tech/javascript/cjs-amd-umd-esm/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ko.javascript.info/modules-intro&quot;&gt;https://ko.javascript.info/modules-intro&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Language/JavaScript</category>
      <category>AMD</category>
      <category>cjs</category>
      <category>CJS vs ESM</category>
      <category>CommonJS</category>
      <category>ESM</category>
      <category>umd</category>
      <category>자바스크립트 모듈</category>
      <category>자바스크립트 모듈 시스템</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/167</guid>
      <comments>https://sambalim.tistory.com/167#entry167comment</comments>
      <pubDate>Sun, 16 Apr 2023 16:33:11 +0900</pubDate>
    </item>
    <item>
      <title>Pagination 알아보기</title>
      <link>https://sambalim.tistory.com/166</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 가게에서 사용하는 메뉴(ex. 떡볶이, 돈까스 등)가 페이지네이션이 안되어있어 6000개가 넘는 가게들의 경우 메뉴와 연관있는 서비스들이 느리게 동작한다는 것을 확인하였습니다. Image CDN을 통한 처리, 코드 스플리팅을 통해서도 한계가 있어 서비스내에서 메뉴들을 페이지네이션하려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지네이션의 대표적인 두 방법으로 Offset Pagination, Cursor Pagination이 있습니다. 이 둘을 비교해보고 어떤 것을 도입해볼지 생각해봅니다.&lt;/p&gt;
&lt;h1&gt;Offset Pagination&lt;/h1&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /items?offset=10&amp;amp;limit=20
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset과 limit 라는 예약어를 사용해서 데이터를 일정한 크기로 분할하고, 페이지당 항목 수와 페이지 수를 결정하는데 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;offset은 앞에서부터 건너 뛸 값의 갯수를 뜻하고 limit 는 몇 개의 값을 출력할 것인지를 정합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 SQL에서 Offset pagination을 하고자하면 어떻게 조회할지 SQL을 예시로 보도록 하겠습니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM POST
LIMIT {출력할 값의 갯수}
OFFSET (({현재 페이지} - 1) * {출력할 값의 갯수});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같이 쉬운 방법으로 페이지 단위의 데이터를 조회할 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Offset Pagination의 단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset Pagination은 위의 SQL과 같이 간단하게 조회할 수 있다는 장점이 있지만, 데이터 규모가 크거나 데이터 수정이 빈번한 경우 단점을 가지고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 변경에 대한 민감성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset Pagination은 수정, 생성, 삭제가 반복되는 서비스에서 효율적으로 동작하지 않습니다. offset과 limit를 사용하여 시작위치와 페이지당 항목 수를 지정하기 때문에 시작위치 이전에 해당하는 데이터가 변경되면 이전 페이지와 다른 내용이 화면에 표시될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 화면에서는 데이터의 변화가 있을 경우 해당 데이터의 id 값으로 지금까지 불려온 데이터 전체를 조회하여 수정해주거나 서버에 지금까지 조회했던 데이터를 다시 요청해야합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대규모 데이터 세트에서의 성능 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset Pagination에는 데이터의 위치를 특정할 수 있는 값이 없이 일정한 범위의 데이터만 가져오기 때문에, 데이터 베이스에서 많은 수의 행을 생략하고 조회를 하여 성능상 이슈가 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 offset 값이 1억을 넘어가는 경우, 데이터베이스는 offset + limit의 행을 처리하고 앞의 offset인 1억개를 버리도록합니다. 따라서 응답의 속도가 늦어지게 됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 분할 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset Pagination은 일정한 크기(limit)로 데이터를 분할하기 때문에 데이터의 전체 크기에 비해 분할한 크기가 너무 작거나 큰 경우 조회에 대한 결과가 일관성 없이 제공됩니다. 따라서 일관성을 유지하기 위해 데이터의 분할 방법을 조정해야할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;Cursor Pagination&lt;/h1&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /items?after=abc123&amp;amp;limit=20
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset Pagination이 offset과 limit를 가지고 우리가 원하는 데이터가 몇 번째에 있는지 집중했다면, Cursor Pagination은 우리가 원하는 데이터가 어떤(after)데이터 다음에 있다는 것에 집중합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL에서 Cursor Pagination을 어떻게 구현하는지 알아보도록 합니다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT *
FROM POST
WHERE some_column &amp;gt; {이전 페이지의 마지막 값}
ORDER BY some_column ASC
LIMIT {출력할 값의 갯수}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 페이지의 마지막 값으로 some_column 을 정하고 정렬한 후, 페이지의 크기를 제한하여 제공합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cursor Pagination을 사용하므로 해결되는 Offset Pagination의 단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset Pagination의 단점인 데이터의 일관성, 대규모 데이터에 대한 성능문제등을 Cursor Pagination으로 해결할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 변경에 대한 민감성에 대한 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor Pagination은 커서(after)를 사용하여 다음 페이지를 가져오기 때문에 데이터의 변경에 상대적으로 덜 민감합니다. 데이터가 변경되면 커서가 변경된 데이터의 위치를 가리키도록 업데이트되기 때문입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대규모 데이터 세트에서의 성능 문제 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Offset Pagination에서 offset만큼의 데이터를 생략하는 것을 Cursor Pagination에서는 이전 페이지의 마지막 항목에 대한 정보를 사용하여 다음 페이지를 가져오기 때문에 대규모 데이터 세트에서의 성능 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;프론트엔드에서 대응하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 집합의 크기, 예상 페이지 수등을 고려하여 적절한 Pagination을 선택해야 합니다. 만약 페이지 수가 적고 값의 수 또한 적다면 Offset Pagination을 선택할 수 있을겁니다. 하지만 데이터가 늘어남에 따라 각 페이지에 대해 페이지에 대한 offset값을 계산하는 로직이 추가되므로 성능에 영향을 미칠 수 있습니다. 그리고 응답에 대한 속도가 늦어져 이에 대한 처리도 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor Pagination을 선택한다면 이전 페이지 마지막 값의 id 를 전달하기 때문에 이를 기록하는 것이 필요합니다. 하지만 페이지에 대한 계산이 줄어들기 때문에 복잡성 감소, 성능 향상을 기대할 수 있습니다. 사용자가 스크롤을 통해 자연스럽게 데이터가 로드되는 것을 구현할 경우 Cursor Pagination이 더 적합합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;useSWRInfinite&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor Pagination을 구현해야하는 서비스에서는 SWR을 사용하고 있기 때문에 SWR을 사용하여 이를 구현할 수 있게하는 useSWRInfinite를 이해하려합니다.&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;import useSWRInfinite from 'swr/infinite'
 
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useSWR과 유사하게, 이 새로운 Hook은 요청 키, fetcher 함수, 옵션을 반환하는 함수를 받습니다.&amp;nbsp;useSWR 이 반환하는 모든 값을 반환하며, 추가로 두 개의 값을 포함합니다. (size , setSize)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 이를 Component내에서 사용한다면 아래의 예시코드와 같이 사용할 수 있습니다. (Cursor Pagination을 사용한 코드입니다.)&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useState } from &quot;react&quot;;
import useSWRInfinite from &quot;swr/infinite&quot;;

function ExampleComponent() {
  const fetcher = (url) =&amp;gt; fetch(url).then((res) =&amp;gt; res.json());
  
  const getKey = (pageIndex, previousPageData) =&amp;gt; {
    // 처음 요청할 때
    if (pageIndex === 0) {
      return `/api/data?cursor=null`;
    }

    // 더 이상 가져올 데이터가 없을 때
    if (!previousPageData) {
      return null;
    }

    // 다음 페이지 데이터 가져오기
    return `/api/data?cursor=${previousPageData.nextCursor}`;
  };

  const { data, error, size, setSize } = useSWRInfinite(getKey, fetcher);

  if (error) return &amp;lt;div&amp;gt;에러 발생!&amp;lt;/div&amp;gt;;
  if (!data) return &amp;lt;div&amp;gt;데이터를 불러오는 중...&amp;lt;/div&amp;gt;;

  const dataList = data.flatMap((pageData) =&amp;gt; pageData.items);
  
  const handleLoadMore = () =&amp;gt; {
    if (data[size - 1].nextCursor !== null) {
      setSize(size + 1);
    }
  };

  return (
    &amp;lt;div&amp;gt;
      {dataList.map((item) =&amp;gt; (
        &amp;lt;div key={item.id}&amp;gt;{item.name}&amp;lt;/div&amp;gt;
      ))}
      {data[size - 1].nextCursor !== null &amp;amp;&amp;amp; (
        &amp;lt;button onClick={handleLoadMore}&amp;gt;더 불러오기&amp;lt;/button&amp;gt;
      )}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메뉴의 갯수가 엄청나게 많은 가게들이 등장하면서 서비스의 성능 향상을 위해 페이지네이션을 도입하기로 했습니다. 따라서 페이지네이션을 도입하기 앞서 대표적인 두 방법 Offset, Cursor Pagination을 알아보고 프론트엔드 개발자 입장에서는 어떤 점을 고려해야하는지 알아보았습니다. 또한 현재 사용하고 있는 SWR에서 Cursor Pagination을 어떻게 구현할 수 있을지 코드를 통해 알아보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 기획자, BE 개발자와 각 Pagination의 특징을 공유하고 이를 바탕으로 각 서비스에 맞는 Pagination을 선택할 수 있도록 해야겠습니다.&lt;/p&gt;</description>
      <category>FrontEnd</category>
      <category>useSWRInfinite</category>
      <category>프론트엔드 Cursor Pagination</category>
      <category>프론트엔드 Pagination</category>
      <category>프론트엔드 페이지네이션</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/166</guid>
      <comments>https://sambalim.tistory.com/166#entry166comment</comments>
      <pubDate>Sun, 2 Apr 2023 20:37:54 +0900</pubDate>
    </item>
    <item>
      <title>React Query와 SWR 비교하기</title>
      <link>https://sambalim.tistory.com/165</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;React 어플리케이션을 만들 때 서버로부터 데이터를 가져오고 사용하는 것을 구현하는 것은 상당히 어려운 일입니다. 따라서 이를 돕기 위해 리액트에서 데이터를 가져오고, 캐싱하며 이를 관리할 수 있는 효과적인 방법을 제공하는 라이브러리들이 생겨났습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 라이브러리들을 데이터 페칭(data-fetching) 라이브러리라고 부르는데 근래 가장 잘 알려진 라이브러리로는 React Query와 SWR이 있습니다. 이 두 라이브러리가 어떤 특징을 가지고 있는지 그리고 어떤 차이점이 있는지 이 글을 통해 소개해보려합니다.&lt;/p&gt;
&lt;h1&gt;React Query&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;react-query.png&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nbqIa/btrZLTUFEuk/kUrPm6vHeSOYkkExQh3kH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nbqIa/btrZLTUFEuk/kUrPm6vHeSOYkkExQh3kH0/img.png&quot; data-alt=&quot;React Query&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nbqIa/btrZLTUFEuk/kUrPm6vHeSOYkkExQh3kH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnbqIa%2FbtrZLTUFEuk%2FkUrPm6vHeSOYkkExQh3kH0%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;565&quot; height=&quot;293&quot; data-filename=&quot;react-query.png&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;React Query&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query는 React앱이 숨쉴 수 있도록(Breeze) 데이터를 가져오고, 캐싱하고 서버의 상태와 동기화(Synchronizing)을 쉽게 합니다. React Query의 주요 특징들을 먼저 알아봅시다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Built-in caching&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query는 특정 쿼리를 기반으로 데이터를 저장할 수 있는 빌트인 캐시가 가능합니다. 프로그래밍을 하며 캐싱을 다루는 것은 가장 어려운 일들 중에 하나인데 React Query는 이를 도와줍니다. 캐시를 네트워크 요청 상태에 따라 자동으로 관리하고 업데이트하여 페칭을 쉽게할 수 있게 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Stale-while-revalidate&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query로 데이터 페칭을 하면 데이터가 캐시에 저장되는데 staleTime 이 경과되기 전에 동일한 데이터에 대한 페칭을 하면 React Query는 새로운 요청을 하는 대신 캐시된 데이터를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 사용자가 앱를 이용하며 데이터 페칭이 일어날 때도 앱을 계속 사용할 수 있도록 합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Advanced cache invalidation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query에서는 캐시 무효화를 제공하기 위한 기능들을 제공합니다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;import { useQuery, useQueryClient } from 'react-query'
 
// Get QueryClient from the context
const queryClient = useQueryClient()
 
queryClient.invalidateQueries('todos')
 
// Both queries below will be invalidated
const { isLoading, error, data, refetch } = useQuery('todos', fetchTodoList)
// const { isLoading, error, data, refetch } = useQuery(['todos', { page: 1 }], fetchTodoList)

// Cache Invalidation
refetch();
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;SWR(Stale-While-Revalidate)&lt;/h1&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;swr.png&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp4OCV/btrZIGvR1o9/Jj53X12ObPWQt2vzH3XcY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp4OCV/btrZIGvR1o9/Jj53X12ObPWQt2vzH3XcY0/img.png&quot; data-alt=&quot;SWR&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp4OCV/btrZIGvR1o9/Jj53X12ObPWQt2vzH3XcY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp4OCV%2FbtrZIGvR1o9%2FJj53X12ObPWQt2vzH3XcY0%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;456&quot; height=&quot;287&quot; data-filename=&quot;swr.png&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;523&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;SWR&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWR은 React의 데이터 페칭을 쉽게할 수 있는 API들을 가진 가볍고 빠른 라이브러리입니다. SWR은 stale-while-revalidate 정책을 통해 네트워크 요청을 줄여 새로운 데이터를 가져오는데 최적화되어있습니다. SWR의 주요 특징들도 알아봅시다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Global Cache&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWR은 url을 key로 사용하여 이를 기반으로 데이터를 저장할 수 있는 글로벌 캐시를 제공합니다. 캐시는 네트워크 요청에 따라 자동으로 관리 및 업데이트되므로 데이터를 효율적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Lightweight and fast&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWR은 가볍고 빠르다는 것을 아주아주 강조합니다. 기본적인 데이터 페칭에 맞추어 최적화되어있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSR / ISR / SSG support&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 Vercel에서 Next.js를 만드는 팀에서 만들고 있어 여러 렌더링 패턴을 사용하는 것에 이점이 있습니다. 뿐만아니라 같이 사용할때 호환성 문제가 있다면 빠른 도움을 기대할 수 있습니다. (서로에 대해 고민하고 문서가 작성되어있어 공식문서에서 도움을 받기 좋습니다.)&lt;/p&gt;
&lt;h1&gt;React Query와 SWR의 차이점&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Getting Started&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query와 SWR의 가장 기본적인 초기 세팅을 보면 React Query가 조금 더 복잡합니다. React Query에서 useQueryClient 를 사용하기 위해 QueryClientProvider 를 설정해줘야합니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import {
   useQuery,
   useMutation,
   useQueryClient,
   QueryClient,
   QueryClientProvider,
 } from 'react-query'
 import { getTodos, postTodo } from '../my-api'
 
 // Create a client
 const queryClient = new QueryClient()
 
 function App() {
   return (
     // Provide the client to your App
     &amp;lt;QueryClientProvider client={queryClient}&amp;gt;
       &amp;lt;Todos /&amp;gt;
     &amp;lt;/QueryClientProvider&amp;gt;
   )
 }
 
 function Todos() {
   // Access the client
   const queryClient = useQueryClient()
 
   // Queries
   const query = useQuery('todos', getTodos)
 
   // Mutations
   const mutation = useMutation(postTodo, {
     onSuccess: () =&amp;gt; {
       // Invalidate and refetch
       queryClient.invalidateQueries('todos')
     },
   })
 
   return (
     &amp;lt;div&amp;gt;
       &amp;lt;ul&amp;gt;
         {query.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;
 
       &amp;lt;button
         onClick={() =&amp;gt; {
           mutation.mutate({
             id: Date.now(),
             title: 'Do Laundry',
           })
         }}
       &amp;gt;
         Add Todo
       &amp;lt;/button&amp;gt;
     &amp;lt;/div&amp;gt;
   )
 }
 
 render(&amp;lt;App /&amp;gt;, document.getElementById('root'))
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 SWR은 이러한 작업들이 필요하지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fetcher = (...args) =&amp;gt; fetch(...args).then(res =&amp;gt; res.json())

import useSWR from 'swr'

function Profile () {
  const { data, error, isLoading } = useSWR('/api/user/123', fetcher)

  if (error) return &amp;lt;div&amp;gt;failed to load&amp;lt;/div&amp;gt;
  if (isLoading) return &amp;lt;div&amp;gt;loading...&amp;lt;/div&amp;gt;

  // render data
  return &amp;lt;div&amp;gt;hello {data.name}!&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(API 문서만봐도 React Query보다 잘 정리되어있다는 인상을 받을 수 있었습니다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Built-in caching vs Global cache&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query와 SWR은 둘다 데이터 페칭을 하고 캐싱을 활용하지만 캐싱 정책의 이름이 Built-in caching, Global cache로 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query의 Built-in caching은 각 쿼리마다 캐싱을 합니다. 이 캐싱된 데이터를 통해 앱내에서 동일한 쿼리에 대한 요청이 있을 때 캐싱된 데이터를 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query에서는 캐싱된 데이터가 오래될 경우 새로고침을 할 수 있는 메소드(staleTime)을 제공합니다. (SWR에서는 staleTimeout 이 이와같은 기능을 합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 SWR의 Global cache는 모든 쿼리를 하나의 전역 캐시에 저장하여 관리합니다. 따라서 각각의 쿼리에 대해 캐싱관리를 해줄 필요가 없습니다. 그리고 전역 캐시를 사용하므로인해 서로다른 컴포넌트들 사이에서도 쉽게 데이터를 공유할 수 있게 해줍니다. 하지만 SWR의 Global cache는 관리하기 용이하지만 그 규모가 커지는 경우 메모리 누수와 같은 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Query는 각 쿼리에 대해 서로다른 key 를 설정할 수 있습니다. 이 key 에 따라 네트워크 응답 데이터가 저장되고 검색하는 것에 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 SWR은 요청 URL을 key로 사용합니다. 따라서 서로다른 컴포넌트에서 동일한 URL을 요청하는 경우 모두 동일한 캐싱 데이터를 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하자면 React Query는 개발자가 정한 key 를 기반으로 캐싱을 하고 SWR은 요청 URL을 key 로 사용합니다. 따라서 React Query에서는 각 쿼리에 대해 캐싱설정을 할 수 있으며 원하는 경우에 캐싱 데이터를 반환하는 설정을 할 수 있습니다. 반면 SWR은 요청 URL을 key 로 사용하여 앱 전체에서 동일한 요청인 경우 큰 설정 없이 이전 요청의 캐싱 데이터를 사용할 수 있습니다. 하지만 그 규모가 커지면 메모리 누수와 같은 문제가 발생할 수 있어 관리가 필요합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DevTools&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SWR이 v2에서 부터 사용을 위해 별도의 셋업이 필요없고 기능이 좀 더 보완되긴 했지만 React Query에 비교하면 DevTools 기능이 아쉽습니다. React Query의 Devtools에서는 쿼리를 조회할 수 있는 것 뿐만아니라 실시간으로 캐시를 Refetch, Invalidate, Reset, Remove를 해볼 수도 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-18 오후 4.46.22.png&quot; data-origin-width=&quot;376&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AWKxv/btrZItb5sPe/LFHlPkQME8WK9wU4wGlazk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AWKxv/btrZItb5sPe/LFHlPkQME8WK9wU4wGlazk/img.png&quot; data-alt=&quot;React Query의 Actions&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AWKxv/btrZItb5sPe/LFHlPkQME8WK9wU4wGlazk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAWKxv%2FbtrZItb5sPe%2FLFHlPkQME8WK9wU4wGlazk%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;289&quot; height=&quot;86&quot; data-filename=&quot;스크린샷 2023-02-18 오후 4.46.22.png&quot; data-origin-width=&quot;376&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;React Query의 Actions&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 데이터 페칭을 도와주는 React Query, SWR을 사용하는 측면에서 비교해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 쿼리에 대한 캐싱 설정이 필요하고 요구사항이 복잡한 데이터를 가져오는 경우에는 React Query가 가볍고 빠른 라이브러리를 선호하고 기본적인 데이터를 가져오는 경우 SWR이 더 좋은 선택지가 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-18 오후 4.42.09.png&quot; data-origin-width=&quot;2646&quot; data-origin-height=&quot;1644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8auN4/btrZQce7aPQ/AhNt5tMLXf9J9YZGhaQ881/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8auN4/btrZQce7aPQ/AhNt5tMLXf9J9YZGhaQ881/img.png&quot; data-alt=&quot;2022년 2월에 조회한 npm trends&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8auN4/btrZQce7aPQ/AhNt5tMLXf9J9YZGhaQ881/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8auN4%2FbtrZQce7aPQ%2FAhNt5tMLXf9J9YZGhaQ881%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;561&quot; height=&quot;349&quot; data-filename=&quot;스크린샷 2023-02-18 오후 4.42.09.png&quot; data-origin-width=&quot;2646&quot; data-origin-height=&quot;1644&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2022년 2월에 조회한 npm trends&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 React Query, SWR 모두 잘 관리되고 있고 커뮤니티가 커서 두 라이브러리 모두 좋은 선택지가 될 수 있습니다. 따라서 두 라이브러리를 비교해보고 진행할 프로젝트에 더 적합한 라이브러리를 선택할 수 있도록 해야합니다.&lt;/p&gt;</description>
      <category>FrontEnd/React.js</category>
      <category>React Query</category>
      <category>react query swr 비교</category>
      <category>React query vs SWR</category>
      <category>SWR</category>
      <category>swr react query 비교</category>
      <category>SWR vs React Query</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/165</guid>
      <comments>https://sambalim.tistory.com/165#entry165comment</comments>
      <pubDate>Sat, 18 Feb 2023 16:59:06 +0900</pubDate>
    </item>
    <item>
      <title>JSX.Element와 ReactElement 차이 이해하기</title>
      <link>https://sambalim.tistory.com/164</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-02-07 오전 12.38.27.png&quot; data-origin-width=&quot;533&quot; data-origin-height=&quot;276&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1nrVq/btrYhbqdImn/fwCajIgSnOkUy8AjVXzW1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1nrVq/btrYhbqdImn/fwCajIgSnOkUy8AjVXzW1K/img.png&quot; data-alt=&quot;대표이미지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1nrVq/btrYhbqdImn/fwCajIgSnOkUy8AjVXzW1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1nrVq%2FbtrYhbqdImn%2FfwCajIgSnOkUy8AjVXzW1K%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;533&quot; height=&quot;276&quot; data-filename=&quot;스크린샷 2023-02-07 오전 12.38.27.png&quot; data-origin-width=&quot;533&quot; data-origin-height=&quot;276&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대표이미지&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트를 사용하며 ReactElement와 JSX.Element는 같은 의미로 사용되곤 합니다. 하지만 이 두 개념에는 개발자가 이해해야하는 중요한 차이점이 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ReactElement&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactElement는 리액트 라이브러리에서 저수준의 UI 컴포넌트를 가리키는데 사용합니다. ReactElement는 컴포넌트의 props , 그리고 자식들(children)을 의미하는 객체입니다. ReactElement는 React.creatElement 메소드를 통해 만들 수 있으며, React.creatElement는 컴포넌트의 타입, props , 그리고 그의 자식들을 인수로 받습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;JSX.Element&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면에 JSX.Element 는 JSX 문법으로 생성된 element를 의미하는 타입입니다. JSX 문법은 개발자가 리액트에서 HTML처럼 보이는 방식으로 컴포넌트를 작성할 수 있도록 하는 문법입니다. JSX 로 작성된 컴포넌트가 컴파일되면 React.createElement호출의 연속으로 변환됩니다. 여기서 JSX 로 생성된 요소의 타입은 JSX.Element 입니다.&lt;/p&gt;
&lt;h1&gt;예시&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactElement와 JSX.Element를 설명하기 위한 짧은 예시입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 ReactElement 를 보는 예시입니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;const user = {
  name: 'Sung Ho',
  age: 30
};

const UserComponent = (props) =&amp;gt; {
  return React.createElement(
    'div',
    {},
    React.createElement(
      'h1',
      {},
      props.name
    ),
    React.createElement(
      'p',
      {},
      `Age: ${props.age}`
    )
  );
};

const userElement = React.createElement(UserComponent, user);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시에서 userElement 는 UserComponent 나타내는 ReactElement 입니다. 이 ReactElement 는 ReactDOM.render 메소드를 통해 DOM으로 렌더링될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 JSX.Element의 예시를 보겠습니다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const UserComponent = (props) =&amp;gt; {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;{props.name}&amp;lt;/h1&amp;gt;
      &amp;lt;p&amp;gt;Age: {props.age}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};

const userElement = &amp;lt;UserComponent {...user} /&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 예시 또한 userElement 는 UserComponent 를 나타냅니다. 하지만 JSX.Element 타입인 것을 볼 수 있습니다. (이 코드가 컴파일 되었을 때는 첫 번째 예시처럼 ReactElement 로 변환될 것입니다.)&lt;/p&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReactElement와 JSX.Element은 모두 리액트에서 UI 컴포넌트를 나타내는 타입입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ReactElement는 리액트의 기본 타입으로, 컴포넌트 또는 HTML tag(React DOM component)를 나타냅니다.&lt;/li&gt;
&lt;li&gt;JSX.Element 는 리액트의 확장 타입으로, JSX 문법으로 작성된 component를 나타냅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 타입 모두 동일한 역할을 하지만, JSX.Element 타입은 보다 구체적으로 component가 JSX로 작성되었음을 나타냅니다. 따라서 JSX.Element 타입을 사용하면 코드의 가독성이 높아지고, 타입 추론이 더 쉬워질 수 있습니다.&lt;/p&gt;</description>
      <category>FrontEnd/React.js</category>
      <category>JSX.Element</category>
      <category>JSX.Element vs ReactElement</category>
      <category>ReactElement</category>
      <category>ReactElement vs JSX.Element</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/164</guid>
      <comments>https://sambalim.tistory.com/164#entry164comment</comments>
      <pubDate>Tue, 7 Feb 2023 00:41:31 +0900</pubDate>
    </item>
    <item>
      <title>삼바의 2022 회고록</title>
      <link>https://sambalim.tistory.com/163</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;올해 많은 변화들이 있었고 그 변화들 속에서도 꾸준히 공부하고 멘탈을 관리하며 성장하는 한 해였습니다. 회사에서 조금 더 책임을 느끼며 일을 하였고 팀원, 외주분들과 함께 일하기 좋은 방법들을 찾고 개선해나갔습니다. 올해 목표했던 업무들도 해내고 회사에서 인정도 받았지만 나에게 그리고 우리 서비스들에 부족한 점들을 알게되었습니다. 2022년 동안 진행했던 일들에 대해 회고를 진행해보려합니다.&lt;/p&gt;
&lt;h1&gt;업무&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 리액트(React) 생태계 속에서 많은 기능들을 구현하는데 시간을 보낸 한해였습니다. 장고 템플릿(Django Template)에서 구현되어 있는 기능들을 리액트로 리뉴얼하며 디자이너, 기획자와 이야기하며 사용자에게 더 나은 UI/UX를 제공하기 위해 고민하고 직접 구현하여 사용자의 불편을 줄여나갔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 서비스를 위해 레포를 구성하고 CI/CD에 대해 고민하는 시간이 늘어났습니다. 여러명이 일하는 환경속에서 더나은 방법을 고민하고 적용해나갔습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사장님사이트 셀프서비스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 대부분의 시간동안 기존 장고 템플릿를 통해 구현되어있던 사장님사이트의 셀프서비스 서비스들(ex. 품절 관리, 메뉴 관리)을 리액트로 전환하는 작업을 진행했습니다. 기획자분들이 각 서비스들의 기능을 파악하는데 도움을 드리고 디자이너와 더 나은 UI/UX에 대해 이야기하고 공통 컴포넌트로 만들 수 있는 부분들을 분리하고 사용처에서 쉽게 사용할 수 있도록 Custom Hook을 만들기도 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;거의 달마다 서비스 하나씩을 배포했는데 스쿼드에서 함께 일하는 기획자, BE, 디자이너, QE(Quality Engineer) 모두 소통이 잘되고 호흡이 잘맞아 재미있게 일할 수 있었습니다. 자신의 영역이 아니더라도 먼저 의견을 제시하기도 하고 진행하는 일에서 이슈가 있을 경우 슬랙 채널에 메시지를 하거나 허들을 통해 편하게 의견을 나눌 수 있었는데 만들어나가는 서비스에 대한 이해수준을 맞추고 서로의 어려움을 이해할 수 있어서 좋았습니다. 처음에 산정하였던 일정대로 혹은 조금 빠르게 서비스들을 배포할 수 있었고 배포 후에도 큰 이슈없이 잘동작하여 배포에 대한 부담감도 줄어들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에는 레포내에서 리액트를 통해 특정 기능을 구현하는 일에 중점을 두었었다면 올해부터 사장님향 서비스의 레포들을 관리하게 되어 성능 개선에도 관심을 갖고 개선작업들을 진행하였습니다. 예를 들면 Webpack Config를 통해 관리되는 프로젝트에서 lazy 를 사용한 코드 스플리팅과 트리쉐이킹(Tree Shaking)을 통해 TTI(Time To Interactive)를 어떻게 줄일 수 있는지 공부하고 적용해보았습니다. 작업하며 공통된 레이아웃도 코드 스플리팅을 하여 사용자에게 리로드를 통해 하얀 화면을 보는 불편함을 드리기도 했지만 코드 스플리팅 영역을 수정하며 개선할 수 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대상&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_5084.JPG&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uOAQP/btrUYQbZHE6/fs2SY6nwQyN3o4a2q1mrB0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uOAQP/btrUYQbZHE6/fs2SY6nwQyN3o4a2q1mrB0/img.jpg&quot; data-alt=&quot;대상 :yay:&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uOAQP/btrUYQbZHE6/fs2SY6nwQyN3o4a2q1mrB0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuOAQP%2FbtrUYQbZHE6%2Ffs2SY6nwQyN3o4a2q1mrB0%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;661&quot; height=&quot;368&quot; data-filename=&quot;IMG_5084.JPG&quot; data-origin-width=&quot;1622&quot; data-origin-height=&quot;904&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;대상 :yay:&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 진행한 Awards에서 사장님사이트 셀프서비스가 무려 &amp;ldquo;대상&amp;rdquo;에 선정되었습니다. 생각도 못한 일이였고 받으면서도 얼떨떨했습니다. 일년동안 함께 고생한 동료들이 많이 생각났고 상을 받고 있는 동안에도 내일 진행될 배포를 위해 고생하고 있는 팀원들이 생각이났습니다. 사실 상을 받은 후에 행사에 참여하지 못하고 내려가서 내일 배포를 위해 일을 했고 우리가 일하는 것에 변화는 없었습니다. 하지만 그동안 1위 업체에 비교당하고 안좋은 인식을 가지고 있던 사장님사이트의 서비스들을 개선하는 것에 회사에서도 관심을 가지고 응원한다는 느낌을 받아 기분이 좋았습니다. 그리고 리더분중 한 분이 특히나 좋아하셨는데 그 웃음 가득한 얼굴이 잊혀지지가 않습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;여러명이 함께 개발하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FE 조직이 커지면서 한 레포내에서 동시에 일하는 개발자가 5명까지 늘어나게 되었습니다. 여러명이 효율적으로 일할 수 있도록 CI/CD에 대해 관심을 가지게 되었고 작게는 husky를 적용하는 것 부터 브랜치 전략 수정, 멀티 스테이징 지원, 배포 자동화등을 공부하고 적용하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자연스럽게(?) 이끄는 입장이 되었는데 처음으로 이런 역할을 하다보니 저도 부담감이 있었습니다. 그래서 팀원분들께 솔직하게 저도 이렇게 해보는 것이 처음이라 시행착오가 있을 수 있는데 착오가 없도록 잘 찾아보고 POC(Proof Of Concept)해서 팀원분들께 공유한 후에 하나하나 해보겠다고 했습니다. 그리고 제가 하는 일들에 대해 편하게 의견을 나누고 함께 만들어나가자고 말씀드렸습니다. 이를 위해서 작게는 슬랙채널을 만들어 질문을 편하게 할 수 있도록 하였고 크게는 우리 팀원들끼리 진행되는 내용에 대해 공유할 수 있도록 위클리를 만들기도 했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;브랜치 전략&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-01 오후 4.05.43.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKzfBx/btrVaTxEQnn/qCGbEme8ZNMuzQeK9vjuN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKzfBx/btrVaTxEQnn/qCGbEme8ZNMuzQeK9vjuN0/img.png&quot; data-alt=&quot;DTL(Domain Tech Leader) 인호님의 따뜻한 댓글 :)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKzfBx/btrVaTxEQnn/qCGbEme8ZNMuzQeK9vjuN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKzfBx%2FbtrVaTxEQnn%2FqCGbEme8ZNMuzQeK9vjuN0%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;634&quot; height=&quot;207&quot; data-filename=&quot;스크린샷 2023-01-01 오후 4.05.43.png&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;DTL(Domain Tech Leader) 인호님의 따뜻한 댓글 :)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러명이 한 레포에서 일하게 되면서 브랜치 전략을 어떻게 가져갈지 고민이 많이 되었습니다. 처음에는 각 기능들이 독립된 환경에서 개발될 수 있고 개발 순서와 배포 순서를 따로 생각할 수 있도록 Gitflow를 도입했고 원했던 목적과는 맞았었지만 사용하면서 공통 작업으로 메인 작업의 흐름이 방해되는 점, master 브랜치와 작업 브랜치의 거리가 멀어지면서 Conflict를 풀어내기 어려워지는 점 등의 단점이 있어 TBD(Trunked Based Development)로 전환하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브랜치 전략을 도입하며 팀원들과 이해수준을 맞추기 위해 각 전략을 도입하게된 이유, 도입하는 방법, 사용하는 방법, 우려하는 점들을 담은 글을 작성하고 위클리 시간을 통해 공유하였습니다. 이를 준비하며 팀원들에게 시행착오를 하지 않게 하기위해 POC를 진행하고 논리적으로 설득할 수 있게 보다 완성된 글을 작성하려 노력했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 브랜치 전략들을 알아보고 도입, 수정해나가면서 완벽한 브랜치 전략은 없다는 생각을 했습니다. 내년에도 우리 서비스의 특성에 맞추어 팀원들과 함께 도입하고 점점 개선해나갈 것 입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Deployment&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-01 오후 4.15.59.png&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;1010&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBBUnZ/btrU0umag1x/KUyCQqoBJjFTHjVVer5i0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBBUnZ/btrU0umag1x/KUyCQqoBJjFTHjVVer5i0k/img.png&quot; data-alt=&quot;Github Actions&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBBUnZ/btrU0umag1x/KUyCQqoBJjFTHjVVer5i0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBBUnZ%2FbtrU0umag1x%2FKUyCQqoBJjFTHjVVer5i0k%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;644&quot; height=&quot;370&quot; data-filename=&quot;스크린샷 2023-01-01 오후 4.15.59.png&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;1010&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Github Actions&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주로 AWS의 S3, CF(CloudFront) 서비스를 사용하여 서버리스한 환경속에서 배포를 하고 있는데 우리가 일하는 환경에 맞추어 멀티스테이징을 구성하고 멀티스테이징에 원하는 브랜치를 배포할 수 있도록 작업을 하였습니다. 그동안 만들어둔 Workflow대로 Github Action을 사용하기만 하다가 직접 작성을 해보는 경험을 할 수 있었습니다. 또한 S3, CF, Route53과 같은 서비스들을 DevOps 팀원분들과 생성하고 사용하는 경험을 할 수 있어 좋았습니다. 보안, 오류 페이지 처리, 캐싱등에 대해 고민하고 더 나은 방법들을 적용해나갔습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;베트남 외주&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;IMG_2777.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9lT6R/btrU0t8C3DX/HktvJNGfGYvPTIKE1zZheK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9lT6R/btrU0t8C3DX/HktvJNGfGYvPTIKE1zZheK/img.jpg&quot; data-alt=&quot;하노이 호텔에서 본 전경&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9lT6R/btrU0t8C3DX/HktvJNGfGYvPTIKE1zZheK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9lT6R%2FbtrU0t8C3DX%2FHktvJNGfGYvPTIKE1zZheK%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;543&quot; height=&quot;407&quot; data-filename=&quot;IMG_2777.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하노이 호텔에서 본 전경&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사의 주주가 바뀌고 진행되야하는 프로젝트가 늘어남에따라 회사에서 외주를 시도해보게 되었습니다. 따라서 외주로 진행되는 FE 프로젝트를 위해 서비스 구성, 문서 작성등을 했습니다. 우리 조직이 아닌 외부 조직에게 우리가 일하는 방식에 대해 설명해야했고 제가 직접 일정체크까지 하진 않았지만 작업이 잘진행되고 있는지 확인이 필요했습니다. 외주분들에게 다양한 의견을 듣고 우리가 선택한 것들이 맞는지 다시 생각해볼 수 있는 계기가 되었습니다. 우리가 선택한 라이브러리들에 대해 설명해야되었고 모호한 부분에 대해서는 날카롭게 질문이 들어왔습니다. 이러한 부분들에 이야기하고 더 나은 방법들을 선택하기 위해 공부하는 시간을 갖을 수 있어 좋았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 외주라는 특성으로 인해 외주 개발자들은 자신이 하던 방식 그대로 빠르게 개발하는 것에 초점을 가지고 있었는데 이는 우리가 서비스를 생각하는 관점과 달라 부딪히는 부분들이 있었습니다. 또한 영어로도 한국어로도 완전하게 대화하기가 어려워 확인하는 작업이 지속적으로 필요했습니다. 이에 대해 컨벤션, 가이드 등은 문서를 통해 소통하고자 했고 코드리뷰를 통해 지속적으로 서로의 싱크를 맞추는 작업을 했습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;채용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해는 기술면접에 들어가기 시작하기도 했고 작년과 마찬가지로 회사 블로그에 글도 작성하고 채용설명회도 참여하였습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술면접&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에 대한 생각을 정리하기 위해 호수공원을 50바퀴는 돌았을겁니다. 우리와 함께 일하는 개발자를 채용하기 위해 어떤 자세로 면접에 임해야하는지 면접을 보시는 분께 어떤 것들을 여쭤봐야할지 고민이 많았습니다. 이러한 고민들이 이어지다보니 면접 스크립트를 여러차례 수정하기도 하고 어떤 질문들이 핵심을 꼬집을 수 있는지 고민하게 되었습니다. 면접을 통해 하게된 생각들에 대해서는 추후에 글을 따로 작성하도록 하겠습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블로그&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS Conf에 앞서서 회사의 FE 조직을 소개하기 위해 회사 블로그에 &lt;a href=&quot;https://techblog.yogiyo.co.kr/fe-chapter-introduce-a738875d8be6&quot;&gt;&amp;ldquo;요기요의 FE Chapter를 소개합니다.&amp;rdquo;&lt;/a&gt;을 작성하였습니다. FE 조직이 어떤 일들을 어떻게 진행하고 있고 어떠한 문화를 가지고 있는지 소개하는 내용을 담고 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;채용설명회&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1_oYj73lUVgFjrGN-jC-jiUA.webp&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;782&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IWDjY/btrU7H5zlBI/e9YTBsE6Ra7ZbiivjsB3BK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IWDjY/btrU7H5zlBI/e9YTBsE6Ra7ZbiivjsB3BK/img.webp&quot; data-alt=&quot;상상을 현실로 FE벤져스&amp;amp;amp;rdquo; 캡쳐&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IWDjY/btrU7H5zlBI/e9YTBsE6Ra7ZbiivjsB3BK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIWDjY%2FbtrU7H5zlBI%2Fe9YTBsE6Ra7ZbiivjsB3BK%2Fimg.webp&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;654&quot; height=&quot;365&quot; data-filename=&quot;1_oYj73lUVgFjrGN-jC-jiUA.webp&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;782&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;상상을 현실로 FE벤져스&amp;amp;rdquo; 캡쳐&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내에서 진행한 채용설명회의 &amp;ldquo;상상을 현실로 FE벤져스&amp;rdquo;라는 세션에 참여햐였습니다. 카메라 앞에서 엄청 떨렸는데 나름(?) 경력자라고 최대한 떨지 않고 같이간 팀원들에게 힘을주려고 노력했던 것이 기억에 남습니다. FE 개발자로 지원하게될 분들에게 도움이 되기를 바라며 참여했습니다.&lt;/p&gt;
&lt;h1&gt;외부활동&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 교육에 관심을 가지고 멘토링, 리뷰어 등에 참여해보았습니다. 개발자로 성장해나가시는 분들의 다양한 관점을 볼 수 있었고 함께 고민하고 나아가며 저 또한 성장할 수 있었습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부스트캠프 FE 리뷰어&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-01 오후 5.40.20.png&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zb5A7/btrU3aUMwnQ/9stW9KV1OWVhzQY4qUhG2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zb5A7/btrU3aUMwnQ/9stW9KV1OWVhzQY4qUhG2k/img.png&quot; data-alt=&quot;추가된 Organizations!&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zb5A7/btrU3aUMwnQ/9stW9KV1OWVhzQY4qUhG2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fzb5A7%2FbtrU3aUMwnQ%2F9stW9KV1OWVhzQY4qUhG2k%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;465&quot; height=&quot;253&quot; data-filename=&quot;스크린샷 2023-01-01 오후 5.40.20.png&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;추가된 Organizations!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 커넥트에서 진행하는 부스트캠프 웹, 모바일 7기에 리뷰어로 참여했습니다. 약 2달의 시간동안 3개의 프로젝트에 대해 4명의 캠퍼(수강생)분들과 리뷰를 하는 시간을 가졌습니다. FE 마스터가 이전 세미나에서 뵈었던 조은님이라 신기했고 규모가 엄청 커서 놀라웠습니다. 각 프로젝트 전후로 킥오프 미팅과 회고미팅을 가지며 캠퍼분들과 많은 이야기를 할 수 있었고 저의 경험들을 전해드릴 수 있어서 좋았습니다. 바닐라 JS로 진행되는 프로젝트부터 리액트를 사용한 프로젝트들을 보며 저 또한 기본기에 대해 배울 수 있었고 구현하는데 다양한 방법들을 사용하는 것을 보고 배울 수 있었습니다. 캠퍼분들이 진행하는 것에 대해 제가 리팩토링 하는 방법, 사용하는 라이브러리와 선정 이유, 리액트에 대한 생각등을 전달해드릴 수 있어서 좋았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;고용과 미래 멘토링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인천에서 개발자를 꿈꾸는 분들과 멘토링을 진행하였습니다. FE 개발자가 되기 위해 어떤 것들을 학습해야하는지 어떤 준비들을 해야하는지 이야기하는 시간을 가졌습니다. 코로나 확진자가 줄어드는 시기여서 대면으로 진행할 수 있어 좋았습니다.&lt;/p&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 다이나믹한 한해였습니다. 개인적으로는 결혼을 하기도 했고 업무에서는 작년보다 더 책임감을 가지고 진행해야하는 일들이 많았습니다. 그리고 동시에 진행되는 일들이 많기도 했습니다.(생각해보니 베트남도 다녀왔어요!) 부족함을 알기에 일들을 꾸준히 해내기 위해 낮에는 일하고 밤에는 인강보고 공부하고 정리했습니다. 덕분에 상도 받아보고 주변사람들에게 칭찬받는 한 해가 된거같아 뿌듯합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 디자인 시스템에서 컴포넌트들을 만든 이야기, FE 스터디, 함수형 프로그래밍 스터디한 이야기(코틀린 해봤어요!) 등도 담고 싶었지만 내용이 너무 커질거같아 다 담지는 못했습니다. 코로나로 인한 거리두기가 해제되며 오프라인 컨퍼런스도 참여하고 배우게 된 것도 많고 동기부여된 점에 대해서도 적고싶은데 아쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어쩌면 주니어의 마지막일지 모르는 이제 시니어가 되어야하는 경계선에 서있는지 모르겠습니다. 하는 말과 행동에 책임이 더 커지고 더 나은 서비스와 팀을 위해 알아야할 것이 많아졌습니다. 부족할 수 있겠지만 이러한 일들을 해낼 수 있도록 내년에도 꾸준히 공부하고 일하며 나아가도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 제 기나긴 회고록을 읽어주셔서 감사합니다.  &lt;/p&gt;</description>
      <category>Dev</category>
      <category>개발자 회고록</category>
      <category>프론트엔드 개발자 회고록</category>
      <author>SambaLim</author>
      <guid isPermaLink="true">https://sambalim.tistory.com/163</guid>
      <comments>https://sambalim.tistory.com/163#entry163comment</comments>
      <pubDate>Sun, 1 Jan 2023 20:10:56 +0900</pubDate>
    </item>
  </channel>
</rss>