<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>corgi</title>
        <link>https://velog.io/</link>
        <description>베르나르 성운성운</description>
        <lastBuildDate>Sat, 26 Nov 2022 08:07:07 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>corgi</title>
            <url>https://velog.velcdn.com/images/corgi-world/profile/a5788515-dc08-451c-934e-65acd2cbc2ed/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. corgi. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/corgi-world" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Next.js] React Query, Prisma와 함께하는 무한 스크롤 (Infinite Scroll)]]></title>
            <link>https://velog.io/@corgi-world/Next.js-React-Query-Prisma%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4</link>
            <guid>https://velog.io/@corgi-world/Next.js-React-Query-Prisma%EC%99%80-%ED%95%A8%EA%BB%98%ED%95%98%EB%8A%94-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4</guid>
            <pubDate>Sat, 26 Nov 2022 08:07:07 GMT</pubDate>
            <description><![CDATA[<h1 id="react-query-useinfinitequery와-prisma-cursor-based-pagination로-구현해보는-무한-스크롤">React Query (useInfiniteQuery)와 Prisma (Cursor-based pagination)로 구현해보는 무한 스크롤</h1>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/0c4ce2b2-4a19-42e9-b252-253213bdcc23/image.gif" alt=""></p>
<p><code>React Query</code>의 <code>useInfiniteQuery</code>와 <code>Prisma</code>의 <code>Cursor-based pagination</code>을 사용하여 무한 스크롤에 필요한 모든 것을 구현해 보겠습니다! 이번 구현에 사용한 데이터는 아래 첨부한 이미지와 같으며, 전체 코드는 <a href='https://github.com/corgi-world/infinite-scroll'>이 곳</a>에서 확인하실 수 있습니다!</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/7507ed62-d8ff-432e-aafb-3d768741d0b0/image.png" alt=""></p>
<p>효율적인 정보 전달을 위해 <code>React Query</code>와 <code>Prisma</code>의 기본적인 사용법은 모두 숙지 되었다는 가정하에, 무한 스크롤 구현에 필요한 기능들만 간단하게 다루겠습니다.</p>
<p>출발~ 😙😙😙</p>
<h2 id="pagenation">Pagenation</h2>
<p>당연히~ 가장 먼저 서버를 구현해야 합니다. 전체 데이터 중 원하는 개수씩 순차적으로 끊어서 보내줄 수 있도록 <code>prisma query</code>를 작성해 보겠습니다!</p>
<blockquote>
<p>Pagenation을 어떻게 구현해야 할까?</p>
</blockquote>
<p>처음에는 단순하게 개수를 기준으로 전체 데이터를 끊으려고 했습니다. 예를 들어, 다음과 같이 (유저1, 유저2, 유저3, 유저<del>, 유저10) 총 10개의 데이터가 있다고 가정해 보겠습니다. 한 번에 5개씩 데이터를 보여주고 싶다면, 처음에는 1</del>5번 데이터를 불러오고 그 다음에는 6~10번 데이터를 불러오면 될 것입니다. 이 방법은 아래와 같은 코드로 구현할 수 있습니다.</p>
<pre><code class="language-ts">const userList = await prisma.user.findMany({
  skip: 5 * page,
  take: 5,
})</code></pre>
<p>15개의 데이터를 건너뛰고 16번째 데이터부터 20번째 데이터까지 불러오고 싶으면 skip에 15를, take에 5를 넣어주면 됩니다. 간단하죠? <a href='https://www.prisma.io/docs/concepts/components/prisma-client/pagination#offset-pagination'><code>Prisma</code>의 공식문서</a>에서는 해당 기능을 아래와 같은 이미지로 설명하고 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/70f853da-98ab-41a5-a801-ab16b2d51c18/image.png" alt=""></p>
<p>간단하게 구현할 수 있는 방법이지만, 약간의 문제가 있습니다. 만약, 최근에 가입된 유저 순으로 정렬하여 데이터를 불러오다가 새로운 유저가 회원가입하면 어떻게 될까요? 아래 메모장의 설명처럼 유저5가 두 번 불러와 질 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/a074e4a5-14d0-43be-9283-c2c53120c7a4/image.png" alt=""></p>
<p>따라서 개수를 기준으로 정하는 것이 아닌, 현재 페이지의 마지막 데이터를 기준으로 다음 페이지의 데이터를 불러와야 합니다. 위의 예시에서는 유저5를 기준으로 다음 페이지의 데이터를 불러와야겠죠? <code>Prisma</code>의 <code>Cursor-based-pagination</code>으로 이러한 방법을 간단하게 구현할 수 있습니다.</p>
<pre><code class="language-ts">const userList = await prisma.user.findMany({
  skip: 1,
  take: 5,
  cursor: {
    id: targetId
  }
})</code></pre>
<p>위 코드의 예시처럼 <code>cursor</code> 객체에 조건을 설정하여 해당 조건에 맞는 데이터를 기준으로 정할 수 있습니다. 도입부에 첨부된 User Table 이미지 기억나시나요? User Table은 id, nickname, password의 필드로 구성되어 있습니다. 이 중 고유한 값인 id 필드가 지정한 값(targetId)인 데이터가 기준이 됩니다. nickname 필드 또한 고유한 값이라면, id 대신 nickname을 사용할 수도 있습니다.</p>
<p>근데 skip은 왜 또 필요할까요? 마찬가지로 <a href='https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination'><code>Prisma</code>의 공식문서</a>를 확인해 보겠습니다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/76832861-7ea1-43bf-89c3-500a34ea91df/image.png" alt=""></p>
<p>바로 <code>cursor</code>를 포함하여 take개를 가져오기 때문입니다. 저희는 현재 페이지의 마지막 데이터를 <code>cursor</code>로 정하기로 했으니, <code>skip: 1</code>이 필요한 것입니다.</p>
<blockquote>
<p>이제 정말 구현해볼까요?!</p>
</blockquote>
<pre><code class="language-ts">/* /pages/api/userList.ts */

const TAKE_COUNT = 5;

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse&lt;IUserResponse&gt;
) {
  const { id } = req.query;
  const isFirstPage = !id;

  const pageCondition = {
    skip: 1,
    cursor: {
      id: id as string,
    },
  };

  const userList = await client.user.findMany({
    /*
    where: { },
    orderBy: { }
    */
    take: TAKE_COUNT,
    ...(!isFirstPage &amp;&amp; pageCondition),
  });

  const length = userList.length;
  res.status(200).json({ userList: 0 &lt; length ? userList : undefined });
}</code></pre>
<p>현재 페이지 마지막 데이터의 <code>id</code>를 <code>query parameter</code>로 받아옵니다. 만약, 첫 페이지의 데이터를 불러오는 경우라면 당연히 <code>id</code>가 존재하지 않겠죠? 따라서 <code>isFirstPage</code>가 <code>true</code>일 경우 <code>skip</code>과 <code>cursor</code>를 설정하지 않습니다.</p>
<p>페이지를 쭉쭉 넘겨 <code>cursor</code>가 전체 데이터의 마지막 데이터가 됐을 경우, 더 이상 불러올 데이터가 없기 때문에 <code>findMany</code> 함수는 빈 배열을 <code>return</code>합니다. 그렇기 때문에 <code>userList</code>의 <code>length</code>를 확인하여 <code>0 &lt; length</code>일 때만 <code>userList</code>를 <code>return</code>하고 그렇지 않을 경우 <code>undefined</code>를 <code>return</code>합니다.</p>
<blockquote>
<p>살짝 싱숭생숭 하신가요?</p>
</blockquote>
<p>현재 페이지 마지막 데이터의 <code>id</code>를 어떻게 넘겨주는지? 왜 굳이 <code>undefined</code>를 <code>return</code>하는지? 이런 부분들은 다음 차례인 <code>React Query</code>의 <code>useInfiniteQuery</code> 구현 방법을 확인해 보시면 명확해지실 겁니다!</p>
<h2 id="useinfinitequery">useInfiniteQuery</h2>
<pre><code class="language-ts">const {
  data,
  hasNextPage,
  fetchNextPage,
} = useInfiniteQuery({
  queryKey,
  queryFn: ({ pageParam = &quot;&quot; }) =&gt; fetchPage(pageParam),
  getNextPageParam: (lastPage, allPages) =&gt; lastPage.nextCursor,
})</code></pre>
<p>위의 예시 코드가 구현에 필요한 전부입니다! <code>useInfiniteQuery</code>는 굉장히 많은 기능들을 제공하지만, 무한 스크롤 구현을 위한 최소한의 기능들만 하나씩 살펴보겠습니다.</p>
<ol>
<li><p><code>getNextPageParam</code> : <code>pageParam</code>과 <code>hasNextPage</code>의 값을 결정하는 함수이며, 매개변수로 <code>lastPage</code>와 <code>allPages</code>를 넘겨 받을 수 있습니다. <code>lastPage</code>는 <a href='https://tanstack.com/query/v4/docs/reference/useInfiniteQuery'>공식문서</a>에서 &#39;마지막 페이지&#39;라는 뜻으로 사용되는데요, 저희는 지금까지 &#39;현재 페이지&#39;라는 단어를 사용했기 때문에 편의상 &#39;현재 페이지&#39;라고 부르겠습니다! <code>getNextPageParam</code>은 <code>단일 변수</code> 혹은 <code>undefined</code>를 <code>return</code> 해야 합니다. 이렇게 <code>return</code>된 값은 <code>queryFn</code>의 <code>pageParam</code>과 <code>hasNextPage</code>의 값으로 사용됩니다.</p>
</li>
<li><p><code>data</code> : 서버로부터 응답받은 결과이며, 우리가 원하는 <code>TData[]</code>는 <code>data.pages</code>에 담겨 있습니다.</p>
</li>
<li><p><code>hasNextPage</code> : 다음에 더 불러올 페이지가 있는지 확인할 수 있는 값입니다. 이 값은 <code>getNextPageParam</code>에 의해 결정됩니다. <code>getNextPageParam</code>가 <code>단일 변수</code>를 <code>return</code>할 경우 <code>true</code>, <code>undefined</code>를 <code>return</code>할 경우 <code>false</code>가 됩니다.</p>
</li>
<li><p><code>queryFn: ({ pageParam = &quot;&quot; })</code> : <code>getNextPageParam</code>이 <code>return</code>한 값이 <code>pageParam</code>의 값으로 사용됩니다. 최초 요청 시 정의한 기본 값이 사용됩니다. (예시에서는 <code>&quot;&quot;</code>)</p>
</li>
<li><p><code>fetchNextPage</code> : 다음 페이지를 요청할 때 호출하는 함수입니다.</p>
</li>
<li><p>전체적인 흐름은 다음과 같습니다.</p>
<ol>
<li><code>hasNextPage = !!getNextPageParam()</code></li>
<li><code>hasNextPage</code>가 <code>true</code>이면 <code>fetchNextPage()</code></li>
<li><code>pageParam = getNextPageParam()</code></li>
<li><code>fetchPage(pageParam)</code></li>
<li><code>render(data.pages)</code></li>
</ol>
</li>
</ol>
<blockquote>
<p>구현된 코드를 살펴보겠습니다!</p>
</blockquote>
<pre><code class="language-ts">/* /queries/user.ts */

export function useFetchUserList() {
  const queryResult = useInfiniteQuery&lt;IUserResponse&gt;(
    [&quot;user&quot;],
    ({ pageParam = &quot;&quot; }) =&gt; get(`?id=${pageParam}`),
    {
      getNextPageParam: ({ userList }) =&gt;
        userList ? userList[userList.length - 1].id : undefined,
    }
  );

  return queryResult;
}

const service = axios.create({
  baseURL: &quot;/api/userList&quot;,
});
function get(queryString: string) {
  return service.get(queryString).then((response) =&gt; response.data);
}</code></pre>
<p>기능 단위로 쪼개고 서버 코드와 함께 살펴보겠습니다.</p>
<pre><code class="language-ts">/* /queries/user.ts */

({ pageParam = &quot;&quot; }) =&gt; get(`?id=${pageParam}`)

/* /pages/api/userList.ts */

const { id } = req.query;
const isFirstPage = !id;</code></pre>
<p><code>pageParam</code>을 <code>query parameter</code>를 통해 서버로 전송합니다. 최초 요청 시 기본 값이 <code>&quot;&quot;</code>이기 때문에 <code>isFirstPage</code>는 <code>true</code>가 됩니다.</p>
<pre><code class="language-ts">/* /queries/user.ts */

getNextPageParam: ({ userList }) =&gt;
  userList ? userList[userList.length - 1].id : undefined;

/* /pages/api/userList.ts */

const pageCondition = {
  skip: 1,
  cursor: {
    id: id as string,
  },
};</code></pre>
<p><code>getNextPageParam</code>에서 현재 페이지의 <code>userList</code>가 존재한다면? <code>userList</code>의 마지막 데이터의 <code>id</code>를 <code>return</code>하여 <code>queryFn</code>의 <code>pageParam</code>으로 사용합니다. 이렇게 서버로 전달된 <code>id</code>가 <code>cursor</code>로 사용되는 것입니다.</p>
<p>현재 페이지의 <code>userList</code>가 존재하지 않는 경우는 어떤 경우일까요?</p>
<pre><code class="language-ts">/* /pages/api/userList.ts */

const userList = await client.user.findMany({
  /*
    where: { },
    orderBy: { }
    */
  take: TAKE_COUNT,
  ...(!isFirstPage &amp;&amp; pageCondition),
});

const length = userList.length;
res.status(200).json({ userList: 0 &lt; length ? userList : undefined });</code></pre>
<p>서버 코드를 설명할 때 더 이상 불러올 데이터가 없을 경우 <code>undefined</code>를 응답한다고 했습니다. 현재 페이지의 <code>userList</code>가 존재하지 않는 경우는 더 이상 불러올 데이터가 없을 때이므로 <code>getNextPageParam</code>는 <code>undefined</code>를 <code>return</code>하여 <code>hasNextPage</code>가 <code>false</code>가 되는 것입니다.</p>
<blockquote>
<p>이제 데이터를 불러오기만 하면 됩니다!</p>
</blockquote>
<pre><code class="language-ts">/* /pages/index.tsx */

export default function Home() {
  const { data, hasNextPage, fetchNextPage } = useFetchUserList();

  const handleClick = () =&gt; {
    if (hasNextPage) {
      fetchNextPage();
    }
  };

  const renderUserList = () =&gt; {
    if (data &amp;&amp; data.pages) {
      const userList = data.pages.reduce&lt;IUser[]&gt;((prev, { userList }) =&gt; {
        if (userList) prev.push(...userList);
        return prev;
      }, []);

      return userList.map(({ id, nickname }) =&gt; (
        &lt;User key={id} id={id} nickname={nickname} /&gt;
      ));
    }
  };

  return (
    &lt;Wrapper&gt;
      {renderUserList()}
      {hasNextPage &amp;&amp; &lt;Button onClick={handleClick}&gt;Next Page&lt;/Button&gt;}
    &lt;/Wrapper&gt;
  );
}</code></pre>
<p><code>data.pages</code>는 <code>[[], [], [], []]</code>와 같은 형식으로 되어있습니다. 이를 1차원 배열로 풀기 위해 <code>renderUserList</code> 함수에서 <code>reduce</code>를 사용했습니다. 우선 데이터를 page 단위로 잘 불러오나 확인하기 위해 버튼을 두었고 이를 누를 때마다 다음 페이지 데이터를 불러오도록 하였습니다.</p>
<blockquote>
<p>잘 되나 볼까요?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/68c8631b-1a00-40b6-83dc-ebffd2bf5cbf/image.gif" alt=""></p>
<h2 id="intersection-observer-api">Intersection Observer API</h2>
<p>이제 마지막입니다! 버튼을 눌렀을 때가 아닌 스크롤을 바닥까지 내렸을 때 다음 페이지를 불러와야 합니다. 이러한 기능을 구현하기 위해 바닥에 <code>div</code> 태그를 두고 이 태그가 뷰포트 내에 감지됐을 때 <code>fetchNextPage</code> 함수를 호출할 것입니다.</p>
<p><code>Intersection Observer API</code>를 사용하면 특정 요소와 상위 요소의 뷰포트가 교차하는 것을 감지할 수 있습니다. 바로 구현된 코드를 확인해 보겠습니다.</p>
<pre><code class="language-ts">/* /components/Observer.tsx */

export default function Observer({ handleIntersection }: IProps) {
  const target = useRef(null);

  useEffect(() =&gt; {
    const observer = new IntersectionObserver(
      ([entry]: IntersectionObserverEntry[]) =&gt; {
        if (entry.isIntersecting) {
          handleIntersection();
        }
      },
      { threshold: 1 }
    );

    if (target.current) {
      observer.observe(target.current);
    }

    return () =&gt; observer.disconnect();
  }, []);

  return &lt;Wrapper ref={target}&gt;이게 보이면? 다음 데이터를!&lt;/Wrapper&gt;;
}</code></pre>
<p><code>IntersectionObserver</code> 객체를 생성할 때 매개변수로, 감지됐을 때 호출될 콜백 함수를 넣어줄 수 있습니다. 이 콜백 함수는 <code>observer.observe(target.current)</code>로 처음 감지를 시작했을 때도 호출 됩니다. 따라서 원하는 요소가 감지됐을 때만 <code>fetchNextPage</code>를 호출하기 위해 <code>entry.isIntersecting</code>로 감지 여부를 확인하는 과정이 필요합니다.</p>
<p>마지막으로, 이전에 추가해놓은 버튼을 제거하고 <code>Observer</code> 컴포넌트를 적용해 보겠습니다.</p>
<pre><code class="language-ts">/* /pages/index.tsx */

export default function Home() {
  const { data, hasNextPage, fetchNextPage } = useFetchUserList();

  const handleIntersection = () =&gt; {
    if (hasNextPage) {
      fetchNextPage();
    }
  };

  const renderUserList = () =&gt; {
    if (data &amp;&amp; data.pages) {
      const userList = data.pages.reduce&lt;IUser[]&gt;((prev, { userList }) =&gt; {
        if (userList) prev.push(...userList);
        return prev;
      }, []);

      return userList.map(({ id, nickname }) =&gt; (
        &lt;User key={id} id={id} nickname={nickname} /&gt;
      ));
    }
  };

  return (
    &lt;Wrapper&gt;
      {renderUserList()}
      {hasNextPage &amp;&amp; &lt;Observer handleIntersection={handleIntersection} /&gt;}
    &lt;/Wrapper&gt;
  );
}</code></pre>
<blockquote>
<p>완성 ~!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/0c4ce2b2-4a19-42e9-b252-253213bdcc23/image.gif" alt=""></p>
<p><code>Observer</code> 컴포넌트의 높이를 작게 하고 배경색과 동일하게 하면 아주 자연스러운 동작이 가능합니다!</p>
<h2 id="마무리">마무리</h2>
<p>최대한 간략하게 정리해보고 싶었는데 이상하게 글이 길어졌네요. 잘못된 내용은 지적해주시면 정말 감사하겠습니다.</p>
<p>안녕~ 😙</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Next.js] Large Page Data Warning 해결하기!]]></title>
            <link>https://velog.io/@corgi-world/Next.js-Large-Page-Data-Warning-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@corgi-world/Next.js-Large-Page-Data-Warning-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 26 Oct 2022 10:23:33 GMT</pubDate>
            <description><![CDATA[<h1 id="warning-data-for-page-add-is-117-mb-which-exceeds-the-threshold-of-128-kb-this-amount-of-data-can-reduce-performance">Warning: data for page &quot;/add&quot; is 1.17 MB which exceeds the threshold of 128 kB, this amount of data can reduce performance.</h1>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/1edfbe3f-8fcb-45a3-91a8-cc46b2b24662/image.png" alt=""></p>
<h2 id="개요-😎">개요 😎</h2>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/149dce0c-695a-41ff-abfd-0aa5c81bb5ce/image.gif" alt=""></p>
<p>개인 프로젝트로 대학 시간표 관련 서비스를 만들고 있다. 에브리타임(<a href="https://everytime.kr/)%EC%9D%84">https://everytime.kr/)을</a> 참고하여 시간표 추가 화면을 구현하던 중에 Next.js의 Large Page Data Warning이 발생하여 해결했는데, 해결 과정에서 배운 것이 많아 정리해 보았다.</p>
<h2 id="문제-😇">문제 😇</h2>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/b779f2b3-e9be-4e0e-8f6c-f4b80d836236/image.png" alt=""></p>
<p>학교 홈페이지에 공개된 강의 시간표(.excel)를 필요한 정보만 추출하고 .json 파일로 만들어서 시간표 추가 화면에서 사용하였다.</p>
<p>강의 시간표는 학기가 시작되면 거의 바뀌지 않기 때문에, <code>getStaticProps</code>에서 .json 파일을 읽어와 <code>pageProps</code>를 통해 넘겨주는 방식으로 구현하였다. 이번 프로젝트를 통해 Next.js를 처음 사용해보았는데, <code>getStaticProps</code>를 사용하여 외부 데이터를 불러오니 컴포넌트에서 <code>isLoading</code>과 같은 비동기 서버 상태를 관리할 수고를 덜 수 있고 컴포넌트 내부 코드도 간결해져 굉장히 만족스러웠다.</p>
<blockquote>
<p>술술 풀리나 했는데...</p>
</blockquote>
<p>빌드 중에 Warning: data for page &quot;/add&quot; is 1.17 MB which exceeds the threshold of 128 kB, this amount of data can reduce performance. 경고 문구가 콘솔에 출력되는 것을 보았다. <code>getStaticProps</code>에서 2개 학기의 강의 시간표를 모두 불러왔더니 add.html 파일의 사이즈가 1 MB가 넘어버렸다. 128 KB를 초과하는 양의 데이터는 성능을 저하시킬 수 있다고 하여 LightHouse로 측정해보았다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/b9c0248c-ed3d-46b8-9a25-22961ebf3324/image.png" alt=""></p>
<blockquote>
<p>성능에는 큰 문제 없어 보이지만...</p>
</blockquote>
<p>당장은 문제가 안되더라도 추후 서비스를 지속적으로 운영한다고 가정 했을 때 제공하는 모든 학기의 데이터를 하나의 HTML 파일에 모두 담는 것은 좋은 구현 방법이 아니라고 생각했다. (1년치 데이터가 1.17 MB, 10년치 데이터는 11 MB...?!)</p>
<p>우선 문제 해결에 앞서 정확히 어떤 문제인지 파악해야 하기 때문에 공식 문서를 찾아봤다. <a href="https://nextjs.org/docs/messages/large-page-data">https://nextjs.org/docs/messages/large-page-data</a></p>
<p>SSR(Server-Side Rendering)의 구동 방식은 브라우저가 server-side에서 만들어진 정적 HTML을 먼저 받아 rendering 한 뒤, page 구동에 필요한 JS를 받는다. 이후 정적 HTML에 JS를 연결하는 과정을 거치는데, 이러한 과정을 수화(Hydration)라고 한다. 공식 문서를 확인해보면 client는 수화하기 전에 page data를 구문 분석한다고 명시되어 있다. page data가 커지면 커질수록 구문 분석에 많은 시간이 소요되고 그만큼 수화 과정이 지연될 수 있으므로 page data의 임계를 128 KB로 정한 것 같다.</p>
<p>강의 시간표 불러오는 방법을 개선하여 이 문제를 해결한다면 HTML이 가벼워져, FCP와 LCP 등의 성능 지표가 개선될 것이고 구문 분석에 소요되는 시간이 줄어 TTI 또한 개선되지 않을까? 하는 설레는 마음으로 개선 방법을 고민하였다.</p>
<h2 id="해결-🤔">해결 🤔</h2>
<p>page data의 양이 임계를 초과하여 발생한 문제이므로, <code>getStaticProps</code>에서 불러올 데이터의 양을 줄여야 했다. 기존의 .json 파일에서 학기에 따른 카테고리(전공 or 교양) 목록을 분리하고 해당 파일만 불러와 HTML을 미리 만들어두고 카테고리에 해당하는 강의 목록은 프론트 단에서 <code>react query</code>를 사용하여 page가 rendering될 때 불러오는 방식으로 수정하였다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/f10d8032-ef77-49bf-80a7-9b2506c0ed99/image.png" alt=""></p>
<pre><code class="language-typescript">export async function getStaticProps() {
  const folderPath = path.join(process.cwd(), &quot;/public/static/timetables/json&quot;);

  const categoryFilePath = `${folderPath}/categoryMap.json`;
  const categoryFileData = fs.readFileSync(categoryFilePath, &quot;utf-8&quot;);
  const categoryMap = JSON.parse(categoryFileData) as ICategoryMap;
  const semesters = Object.keys(categoryMap);

  return {
    props: {
      semesters,
      categoryMap,
    },
  };
}</code></pre>
<pre><code class="language-typescript">// semesters와 categoryMap은 pageProps로 받아온다.
function useFilterTimetables(semesters: string[], categoryMap: ICategoryMap) {
  const [semester, setSemester] = useState(semesters[0]);
  const categories = categoryMap[semester];
  const [category, setCategory] = useState(categories[0]);
  const [keyword, setKeyword] = useState(&quot;&quot;);

  const { data: filteredTimetables } = useTimetables(semester, category, keyword);

  return {
    semesters,
    semester,
    setSemester,
    categories,
    category,
    setCategory,
    keyword,
    setKeyword,
    filteredTimetables: filteredTimetables,
  };
}</code></pre>
<blockquote>
<p>성능을 측정 해볼까?!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/c732c8e6-8ba1-42de-aaf6-9efb44977c4e/image.png" alt=""></p>
<p><code>getStaticProps</code>에 모든 데이터를 몰아 넣어 Large Page Data Warning이 발생했을 때보다 성능이 낮게 측정되었다. 가장 크게 문제가 되는 LCP는, HTML rendering --&gt; JS Download --&gt; Hydration 의 모든 과정을 거치고 나서야 <code>react query</code>를 통해 강의 목록 data를 불러오는 것이기 때문에, 강의 목록이 기존보다 늦게 rendering 되어서 발생한 것이다.</p>
<p>풀 다운 메뉴로 선택하는 학기와 카테고리(전공 or 교양)는 고정된 값이기 때문에 첫 번째 학기(2022-02)와 첫 번째 카테고리(국어국문학과)의 강의 목록 또한 고정된 값이다. 따라서 가장 먼저 표시되는 2022-02 학기의 국어국문학과 강의 목록 또한 <code>getStaticProps</code>에서 불러온 뒤 pageProps로 넘겨주는 것이 성능 개선에 도움 될 것이라고 생각되어 수정하였다.</p>
<pre><code class="language-typescript">export async function getStaticProps() {
  const folderPath = path.join(process.cwd(), &quot;/public/static/timetables/json&quot;);

  const categoryFilePath = `${folderPath}/categoryMap.json`;
  const categoryFileData = fs.readFileSync(categoryFilePath, &quot;utf-8&quot;);
  const categoryMap = JSON.parse(categoryFileData) as ICategoryMap;
  const semesters = Object.keys(categoryMap);

  const semester = semesters[0];
  const categories = categoryMap[semester];
  const category = categories[0];

  const timetablesFilePath = `${folderPath}/${semester}.json`;
  const timetablesFileData = fs.readFileSync(timetablesFilePath, &quot;utf-8&quot;);
  const timetables = JSON.parse(timetablesFileData) as ITimetable[];
  const firstIndexTimetables = timetables.filter(
    (timetable) =&gt; timetable.category === category
  );

  return {
    props: {
      semesters,
      categoryMap,
      firstIndexTimetables,
    },
  };
}</code></pre>
<p>현재 <code>react query</code>는 아래와 같이 사용하고 있는데, query key [&#39;2022-02&#39;, &#39;국어국문학과&#39;, &#39;&#39;]의 데이터는 pageProps를 통해 받아오는 것으로 수정했기 때문에 초기 query key에서는 fetch를 할 필요가 없다.</p>
<pre><code class="language-typescript">  const queryString = `?semester=${semester}&amp;category=${category}&amp;keyword=${keyword}`;

  const filteredTimetables = useQuery&lt;ITimetable[]&gt;(
    [semester, category, keyword],
    () =&gt; get(timetablesService, queryString),
    { staleTime: 60 * 1000 * 60 }
  );</code></pre>
<p>불필요한 네트워크 요청을 막기 위해 아래와 같은 방법으로 initialData를 추가하였다.</p>
<pre><code class="language-typescript">export function useTimetables(
  semester: string,
  category: string,
  keyword: string,
  useInitialData: boolean,
  initialData: ITimetable[]
) {
  const queryString = `?semester=${semester}&amp;category=${category}&amp;keyword=${keyword}`;

  const filteredTimetables = useQuery&lt;ITimetable[]&gt;(
    [semester, category, keyword],
    () =&gt; get(timetablesService, queryString),
    { initialData: useInitialData ? initialData : undefined, staleTime: 60 * 1000 * 60 }
  );

  return filteredTimetables;
}</code></pre>
<blockquote>
<p>결과는?!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/b6435492-5955-430f-8350-96c1001d051c/image.png" alt=""></p>
<p>의도했던 대로 LCP가 개선되었다!</p>
<p>이렇게 강의 목록을 불러오는 방식을 변경하여 Large Page Data Warning을 해결하고 1 MB가 넘던 HTML 파일의 크기를 140 KB로 줄일 수 있었다. 😀</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[원티드 프리온보딩 프론트엔드 코스 3번 과제 회고]]></title>
            <link>https://velog.io/@corgi-world/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%BD%94%EC%8A%A4-3%EB%B2%88-%EA%B3%BC%EC%A0%9C-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@corgi-world/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%BD%94%EC%8A%A4-3%EB%B2%88-%EA%B3%BC%EC%A0%9C-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 11 Aug 2022 12:29:04 GMT</pubDate>
            <description><![CDATA[<h1 id="3-광고-플랫폼-대시보드">#3 광고 플랫폼 대시보드</h1>
<p>이번 과제는~
광고 플랫폼 대시보드를 만드는 것이다!</p>
<blockquote>
<p>🥰 열심히 노력한 8팀의 결과물 🥰
<del><em>(혹시 모를 보안상의 이유로 생략!)</em></del></p>
</blockquote>
<h2 id="😎-난-뭘-했나">😎 난 뭘 했나?!</h2>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/a84a4362-1405-41b5-86fe-05a0f73e0071/image.gif" alt=""></p>
<ul>
<li><code>react query</code>를 사용한 데이터 통신 구현<ul>
<li>통합 광고 현황 불러오기</li>
<li>매체 현황 불러오기</li>
<li>광고 목록 불러오기</li>
<li>광고 추가, 수정 및 삭제</li>
</ul>
</li>
</ul>
<h2 id="😵-고민-그리고-또-고민">😵 고민... 그리고 또 고민...</h2>
<blockquote>
<p>과제를 진행하면서 했던 고민들과 상세한 작업 내용을 기록해보자!</p>
</blockquote>
<h3 id="caching을-사용하자">caching을 사용하자!</h3>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/b3315a34-90ad-41d5-8b9d-df66dc26acac/image.gif" alt=""></p>
<p>이번 과제에서는 대시보드에서 데이터를 주차별로 불러오거나, 광고의 진행 상태를 구분하여 불러오는 등 <strong>데이터를 선택하여 불러오는 경우</strong>가 많았다. 풀다운 메뉴를 통해 유저가 선택한 주의 데이터만 불러오는데, 유저가 5주차까지 데이터를 확인한 후 다시 1주차 데이터를 보고 싶어 1주차를 선택했을 경우, 1주차 <strong>데이터를 새로 불러올 필요</strong>가 있을까? 에 대한 고민을 하게 되었다.</p>
<p>불필요한 request를 막아, <strong>속 시원한 사용자 경험</strong>을 제공하기 위해 caching을 사용하고 싶었고 caching을 <strong>가장 간단한 방법으로 구현</strong>할 수 있는 <strong><code>react query</code></strong>를 사용하기로 하였다.</p>
<blockquote>
<p>react query 말고 다른 대안은 없었나?</p>
</blockquote>
<p>수많은 서버 상태 관리 라이브러리 중에서 <code>react query</code>를 선택한 이유는, <strong>가장 많이 사용되는 라이브러리</strong>였기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/d47a8994-9ca4-4aec-ac55-4d04346d82f5/image.png" alt=""></p>
<p>가장 많이 사용되기 때문에, <strong>참고할 수 있는 자료가 많을 것</strong>이고 참고할 수 있는 자료가 많기 때문에, 다른 라이브러리에 비해 상대적으로 <strong>배우기 쉬울 것</strong>이라고 판단했다.</p>
<p>1주일 동안 과제를 진행하면서, 본인 작업 외의 다른 팀원들의 코드를 보는 것은 쉬운 일이 아니다. 더욱이 서버 상태 관리 라이브러리를 사용해 본 적이 없는 팀원들이 더 많았기 때문에, 라이브러리 선정은 <strong>러닝커브를 최우선으로 고려</strong>하였다.</p>
<blockquote>
<p>네~ 드셔도 됩니다~</p>
</blockquote>
<p><code>react query</code>는 <strong>key를 기준으로 cache된 데이터를 관리</strong>하기 때문에, 아래 코드와 같이 데이터를 구분하는 string과 시작 날짜(startDate)를 key로 지정하여 관리하였다.</p>
<pre><code class="language-javascript">export function useGetDashboard(startDate: string, endDate: string) {
  const query = `?date_gte=${startDate}&amp;date_lte=${endDate}`;

  const overall = useQuery([OVERALL, startDate], () =&gt; get(overallService, query));
  const platform = useQuery([PLATFORM, startDate], () =&gt; get(platformService, query));

  return [overall, platform];
}</code></pre>
<p>key를 기준으로 데이터가 cache되어 있다고 해서 <code>react query</code>가 데이터를 새로 요청하지 않는 것이 아니다. <code>react query</code>는 <strong>해당 데이터가 신선하지 않다고 판단</strong>되면, 서버에 새로운 데이터를 요청한다. 따라서 staleTime을 설정하여, <strong>내가 불러온 데이터는 이 동안은 신선</strong>하다! 그러므로 새로운 데이터를 요청할 필요가 없다! 라는 것을 <code>react-query</code>에게 알려주어야 한다.</p>
<pre><code class="language-javascript">const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60,
    },
  },
});</code></pre>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/5647af63-a2d2-42b2-b216-5d026049db4e/image.gif" alt=""></p>
<p>완성!</p>
<p>첫 선택은 데이터를 요청하지만, 이후부터는 데이터를 <strong>새로 요청하지 않는 것을 확인</strong>할 수 있다!</p>
<blockquote>
<p>변경된 데이터를 불러옵시다!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/ebb3a3ee-08c1-4707-b4a6-097e37d3e0c0/image.gif" alt=""></p>
<p>광고 데이터를 추가, 수정, 삭제하면 <strong>변경된 데이터</strong>를 새로 불러와야 한다. 위의 첨부한 영상은 데이터 삭제를 위한 mutate 이후 아무런 동작도 하지 않아, 광고 목록이 변경되지 않는 것이다. 특별한 작업 없이, 기존에 관리되던 <strong>쿼리(key)를 무효화</strong>하는 것으로 해결할 수 있다.</p>
<pre><code class="language-javascript">export function useDeleteCampaign(id: number) {
  const query = `/${id}`;
  const { mutateAsync } = useMutation(() =&gt; _delete(campaignService, query));

  const queryClient = useQueryClient();

  return async function () {
    await mutateAsync();
    await invalidateQueriesByName(queryClient, CAMPAIGN_CONSTANTS.CAMPAIGN);
  };
}

type typeDataName = typeof OVERALL | typeof PLATFORM | typeof CAMPAIGN;
export function invalidateQueriesByName(queryClient: QueryClient, name: typeDataName) {
  return queryClient.invalidateQueries({
    predicate: (query) =&gt; {
      return query.queryKey[0] === name;
    },
  });
}</code></pre>
<p>공식 문서의 <code>invalidateQueries</code> 부분을 읽어보면, staleTime이 만료되지 않은 상태이더라도 <strong>무효화한 쿼리를 stale 상태로 변경</strong>하고 <code>useQuery</code> hook을 통해 데이터를 새로 불러온다는 것을 알 수 있다. <code>invalidateQueries</code>를 사용함으로써 <strong>복잡한 로직 추가 없이</strong> 변경된 데이터를 새로 불러올 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/ba7367d0-5e28-4e80-a8d3-0c029ad29bd6/image.png" alt=""></p>
<h3 id="우아한-비동기처리-흉내내기">우아한 비동기처리 흉내내기!</h3>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/29ca8874-e798-47a9-80c2-30034ebcadf8/image.png" alt=""></p>
<p>이번 과제의 대시보드 탭에서는 통합 광고 현황(overall)과 매체 현황(platform) 2개의 데이터를 불러와야 했다. 통합 광고 현황의 경우 지난 주차의 데이터와 비교하여 증감을 표시해야 했기 때문에 총 <strong>3개의 비동기 데이터의 상태</strong>를 관리해야 했다. 만약 비동기 데이터의 상태를 <strong>컴포넌트 내부에서 관리</strong>했다면, 코드는 아래와 같았을 것이다.</p>
<pre><code class="language-javascript">function DashboardContainer() {
  const [prevOverall] = useGetDashboard(prevStartDate, prevEndDate);
  const [currOverall, currPlatform] = useGetDashboard(currStartDate, currEndDate);

  if (prevOverall.isLoading) {
    return &lt;Loader /&gt;;
  }
  if (currOverall.isLoading) {
    return &lt;Loader /&gt;;
  }
  if (currPlatform.isLoading) {
    return &lt;Loader /&gt;;
  }

  return &lt;&gt;&lt;/&gt;;
}</code></pre>
<p>이러한 문제를 해결할 방법을 찾던 도중에 아래의 영상을 보게 되었다.</p>
<p><a href="https://www.youtube.com/watch?v=FvRtoViujGg">https://www.youtube.com/watch?v=FvRtoViujGg</a>
(토스ㅣSLASH 21 - 프론트엔드 웹 서비스에서 우아하게 비동기 처리하기)</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/288d0b1e-fa5e-4bae-8766-5d7da2bdf26a/image.png" alt=""></p>
<p>함수 본래의 <strong>역할 외의 코드들이 많아지면</strong>, 함수의 크기가 커지고 복잡도가 높아지기 때문에 <strong>함수의 역할이 흐려지고</strong> 어떤 함수인지 파악하기 힘들어진다. 따라서 <strong>성공하는 경우에만 집중</strong>하여 함수의 복잡도를 낮춰 <strong>함수가 하는 일을 명확하게</strong> 드러낼 수 있도록 해야 한다.</p>
<p>컴포넌트에서 성공한 상태에만 집중할 수 있도록 <strong>로딩 상태를 외부에 위임</strong>함으로써 동기적인 코드와 큰 차이 없는 코드를 만들어 <strong>컴포넌트의 역할을 명확하게 하는 것을 목표</strong>로 한다. 이러한 목표 달성을 위해 해당 영상에서는 <strong><code>suspense</code></strong>를 이용하는 방법을 가이드 해 주고 있다.</p>
<blockquote>
<p>도전!</p>
</blockquote>
<pre><code class="language-javascript">// pages/dashboard/index.tsx

&lt;Suspense fallback={&lt;Loader /&gt;}&gt;
  &lt;DashboardContainer
    currStartDate={currStartDate}
    currEndDate={currEndDate}
    prevStartDate={prevStartDate}
    prevEndDate={prevEndDate}
  /&gt;
&lt;/Suspense&gt;</code></pre>
<pre><code class="language-javascript">// pages/dashboard/DashboardContainer.tsx

function DashboardContainer() {
  const [{ data: prevOverall }] = getDashboard(prevStartDate, prevEndDate);
  const [{ data: currOverall }, { data: currPlatform }] = getDashboard(currStartDate, currEndDate);

  return &lt;&gt;&lt;/&gt;;
}</code></pre>
<p>컴포넌트 내부 <strong>비동기 데이터의 로딩 상태</strong>를 컴포넌트를 쓰는 곳, <strong><code>suspense</code>에 위임</strong>하여 우아한 비동기 처리를 흉내 내보았다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[원티드 프리온보딩 프론트엔드 코스 4번 과제 회고]]></title>
            <link>https://velog.io/@corgi-world/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%BD%94%EC%8A%A4-4%EB%B2%88-%EA%B3%BC%EC%A0%9C-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@corgi-world/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%BD%94%EC%8A%A4-4%EB%B2%88-%EA%B3%BC%EC%A0%9C-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Thu, 11 Aug 2022 01:21:59 GMT</pubDate>
            <description><![CDATA[<h1 id="4-지원-화면과-관리자-페이지">#4 지원 화면과 관리자 페이지</h1>
<p>이번 과제는~
<a href="https://snplab.io">https://snplab.io</a>
에스앤피랩의 기업 과제로 지원 화면과 관리자 페이지를 만드는 것이다!</p>
<blockquote>
<p>🥰 열심히 노력한 8팀의 결과물 🥰
<del><em>(혹시 모를 보안상의 이유로 생략!)</em></del></p>
</blockquote>
<h2 id="😎-난-뭘-했나">😎 난 뭘 했나?!</h2>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/ebf22550-efcc-4a55-abd2-68cf9be20c2d/image.gif" alt=""></p>
<ul>
<li>지원 화면 폼 구현<ul>
<li>지원자의 정보를 입력할 수 있는 6개의 input 구현</li>
<li>input에 validation 적용</li>
<li>scroll 가능한 picker 구현</li>
</ul>
</li>
</ul>
<h2 id="😵-고민-그리고-또-고민">😵 고민... 그리고 또 고민...</h2>
<blockquote>
<p>과제를 진행하면서 했던 고민들과 상세한 작업 내용을 기록해보자!</p>
</blockquote>
<h3 id="반복되는-코드를-어떻게-깔끔하게-줄일까">반복되는 코드를 어떻게 깔끔하게 줄일까?</h3>
<p>지원자의 정보를 입력받는 text input은 placeholder와 validation 등 <strong>약간의 설정 차이</strong>만 있을 뿐 모두 <strong>동일한 기능</strong>을 가지고 있다. 따라서 5개의 text input을 <strong>일괄적</strong>으로 관리하고 그려낼 방법을 고민하였다.</p>
<blockquote>
<p>관심사를 분리하자!</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/c370a652-595c-488d-b0bb-fa7a30744825/image.png" alt=""></p>
<p>input의 설정들과 validation에 필요한 정규식을 <strong>별도의 파일</strong>로 관리하고 설정들을 모아 배열로 만들어, 5개의 input을 <strong>반복문을 통해 일괄적</strong>으로 그려주었다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/e5405a34-7d86-4867-9bb3-3e3876a522e2/image.png" alt=""></p>
<blockquote>
<p>radio button은 2번째에 위치해야 하는데요?</p>
</blockquote>
<p>에스앤피랩의 과제 명세에는, <strong>이름 다음에는 성별</strong>을 선택할 수 있는 radio button이 위치해야 한다고 명시되어 있다. 하지만, 위의 코드와 같이 반복문을 통해 input을 일괄적으로 그릴 경우 radio button은 6번째에 위치하게 된다. 이러한 문제를 해결하기 위해 <strong>CSS의 <code>order</code> 속성</strong>을 이용하였다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/1a858c46-1771-4ce7-8a26-60a7a382e8ab/image.png" alt=""></p>
<blockquote>
<p>적절한 도구를 사용해볼까?</p>
</blockquote>
<p>input을 제어하고 validation 하려면 value와 error, 최소 <strong>2개의 state</strong>가 필요하고 추가로 onChange event까지 handling 해주어야 한다. 5개의 input에 이를 모두 구현하는 것은 <strong>비생산적인 업무</strong>라고 판단했다. 따라서 이러한 문제를 <strong>효율적으로 해결</strong>할 수 있는 <code>react-hook-form</code>을 사용하기로 하였다.</p>
<pre><code class="language-javascript">option: {
  required: true,
  pattern: {
    value: EMAIL_VALIDATION,
    message: &#39;&quot;@&quot;, &quot;.com&quot;을 필수로 입력해주세요.&#39;,
  },
}</code></pre>
<p><code>react-hook-form</code>은 위와 같은 방식으로 input에 validation을 설정할 수 있다. form이 submit 되면, 설정한 정규식과 input의 value를 비교하여, 일치하지 않을 시 설정한 message를 errors 객체에 담아준다. 이렇게 <strong>validation의 실행과 error state의 관리를 위임</strong>할 수 있다.</p>
<h3 id="picker를-만들자">picker를 만들자!</h3>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/85f67f46-210b-4948-bb2b-2830da705fa1/image.gif" alt=""></p>
<p><a href="https://www.youtube.com/watch?v=qHzSQrLjxlQ">https://www.youtube.com/watch?v=qHzSQrLjxlQ</a></p>
<p>어떻게 만들어야 할까...? 를 고민하던 중에 코딩애플님의 이미지 슬라이더를 만드는 유튜브를 보게 되었다. 영상 내용을 간단하게 요약해 보자면, <strong>선택 가능한 모든 item 그려놓고</strong> 부모 컴포넌트의 <code>overflow</code> 속성을 <code>hidden</code>으로 설정하여 <strong>당장 보일 필요가 없는 item들은 숨긴다.</strong> 그리고 유저의 입력에 따라 <strong>item들의 위치를 변경</strong>하면, 완성!</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/526000a8-4c97-41e9-bea2-dc5b51784148/image.png" alt=""></p>
<p>스크롤을 동작하거나 item을 click하면 index를 변경한다. 예를 들어, scroll을 아래로 내리거나, 현재 선택된 item보다 밑에 있는 item을 선택하면 index를 증가시킨다. 이렇게 <strong>변화하는 index에 따라 item 목록의 Y 값을 조절</strong>하면 완성!</p>
<blockquote>
<p>로직을 한 눈에 파악하기 쉽게 <code>overflow: hidden</code>을  제거해보면?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/97889ee8-7c5d-4b25-a1fe-f8d1de8d0d38/image.gif" alt=""></p>
<p><code>overflow: hidden</code>이 얼마나 중요한 역할을 하는지 확인할 수 있다!</p>
<h3 id="데이터를-또-불러와야-할까요">데이터를 또 불러와야 할까요?</h3>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/a33b9e5a-3fb4-4802-b1d0-f94291b39505/image.png" alt=""></p>
<p>관리자는 우리 팀이 만든, 위와 같이 생긴 페이지에서 다양한 기준으로 지원자들을 정렬하여 확인할 수 있다. 이번 과제의 작업에 들어가기 전, 팀원 모두가 모여 과제를 분석하고 설계할 때 서버로부터 데이터를 불러오는 것은 <strong>admin page가 mount 됐을 때, 딱 한 번</strong>만 하는 것으로 결정하였다. 관리자가 페이지를 이동하거나 정렬 기준을 변경할 때마다 새로운 데이터를 불러온다면, 지나친 로딩 시간으로 <strong>좋은 사용자 경험</strong>을 제공하지 못할 것이라고 판단했기 때문이다.</p>
<blockquote>
<p>그럼, 데이터의 신뢰성은 어떻게 보장하죠?</p>
</blockquote>
<p>중간 발표 때 위의 내용을 발표하였더니, 멘토님께서 <strong>데이터의 신뢰성</strong>을 어떻게 보장할 것인지 여쭤보셨다. 데이터를 한 번만 불러오기 때문에, 관리자가 admin page에 <strong>진입한 이후에 추가된 지원자</strong>는 목록으로 보여줄 수 없는 문제가 있었기 때문이다.</p>
<p>나는 이번 과제는 데이터의 신뢰성을 <strong>보장할 필요가 없다</strong>고 생각했다. 보통 지원을 받을 때는 <strong>마감 기한</strong>이라는 것이 존재하고, 관리자가 admin page를 <strong>본격적으로 사용하는 때는 마감 이후</strong>일 것이며, 마감을 못 맞춘 지원자는 <strong>포함할 필요가 없을 것</strong>이라고 생각했기 때문이다.</p>
<p>하지만, 또 곰곰이 생각해 보니, 이렇게 극단적이고 <strong>엄격(😢)한 사용자 경험</strong> 또한 좋지 못할 것이라고 판단하여 <strong>안전장치</strong>를 추가로 마련하기로 하였다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/6ff91c7a-9e67-401d-a99a-236878775d7e/image.png" alt=""></p>
<p>admin page 우측 상단에 <strong>새로운 데이터를 갱신</strong>할 수 있는 버튼을 구현하였다. 해당 버튼을 click하면, 서버로부터 새로운 데이터를 갱신해오고, <strong>가장 최근 갱신된 시각을 표시</strong>하였다. 이렇게 함으로써 속도와 신뢰성 두 마리(🐰)를 다 잡을 수 있었다!</p>
<blockquote>
<p>다른 서비스들은 어떻게 하고 있을까?</p>
</blockquote>
<p>모든 구현을 마치고 완성된 과제를 돌아보던 중, <strong>실제 운영 중인 서비스</strong>들은 어떻게 하고 있는지 궁금하였다. 그러던 중 팀원이 보내준 링크를 확인하였는데...</p>
<p><a href="https://www.ontactlearning.com/c91af4b4-1ad0-4ffa-bfd5-bad51deef563">https://www.ontactlearning.com/c91af4b4-1ad0-4ffa-bfd5-bad51deef563</a></p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/f61700b8-7310-4890-91a5-09d05a2fb38d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/f8fa51a5-359a-4d98-a774-f851910d3588/image.png" alt=""></p>
<p>우리 팀의 의도와 동일한 방식으로 서비스 중인 업체가 있다는 것을 확인하고, 해당 서비스를 레퍼런스 삼아 <strong>자신있게 발표</strong>할 수 있었다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[원티드 프리온보딩 프론트엔드 코스 6번 과제 회고]]></title>
            <link>https://velog.io/@corgi-world/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%BD%94%EC%8A%A4-6%EB%B2%88-%EA%B3%BC%EC%A0%9C-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@corgi-world/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%BD%94%EC%8A%A4-6%EB%B2%88-%EA%B3%BC%EC%A0%9C-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 06 Aug 2022 13:57:10 GMT</pubDate>
            <description><![CDATA[<h1 id="6-호텔-예약-서비스">#6 호텔 예약 서비스</h1>
<p>이번 과제는~
<a href="https://www.tripbtoz.com">https://www.tripbtoz.com</a>
트립비토즈의 기업 과제로 간단한 호텔 예약 서비스를 만드는 것이다!</p>
<blockquote>
<p>🥰 열심히 노력한 8팀의 결과물 🥰
<del><em>(혹시 모를 보안상의 이유로 생략!)</em></del></p>
</blockquote>
<h2 id="😎-난-뭘-했나">😎 난 뭘 했나?!</h2>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/f285e776-63ce-4448-a4d3-95adf6d9b63a/image.gif" alt=""></p>
<ul>
<li>상단 서치 바 구현<ul>
<li>데스크탑, 태블릿, 모바일 사이즈 적응형 + 반응형으로 대응</li>
<li>설정된 검색 조건 전역 상태로 관리</li>
<li>검색 입력창에 debounce 적용</li>
<li>스크롤 방향에 따라 표시 여부 결정</li>
<li>팝업<ul>
<li>데스크탑 - 포커스를 잃으면 자동으로 닫힘</li>
<li>모바일 - 전체 화면을 모두 덮고 내부 컴포넌트 스크롤 가능</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="😵-고민-그리고-또-고민">😵 고민... 그리고 또 고민...</h2>
<blockquote>
<p>과제를 진행하면서 했던 고민들과 상세한 작업 내용을 기록해보자!</p>
</blockquote>
<h3 id="기기별-사이즈-대응-어떻게-할까">기기별 사이즈 대응, 어떻게 할까?</h3>
<p>나는 여태껏 제대로 된 반응형 디자인을 구현해 본 경험이 없었다. 기껏해야 flex 혹은 grid를 사용하여 전체적인 너비를 조정하는 것 정도? 하지만, 이번 과제의 서치 바는 꽤 많은 것들이 변해야 했다.</p>
<p>가장 큰 변화는, 검색 조건 변화 이후의 동작이다. <strong>데스크탑 사이즈</strong>에서는 <strong>검색 버튼을 눌렀을 때</strong> 설정된 검색 조건을 반영하여 새로운 데이터를 불러오지만, <strong>모바일 사이즈</strong>에서는 검색 조건이 <strong>변하는 즉시</strong> 반영하여 새로운 데이터를 불러와야 했다. 이 외에도 팝업 변경과 검색 input에 debounce 적용 등, 디자인뿐만 아니라 다르게 처리해야 할 JS 로직이 많았다.</p>
<blockquote>
<p>반응형으로, 한 컴포넌트 내에서 모두 처리하면 너무 복잡해질 것 같은데?</p>
</blockquote>
<p>컴포넌트를 분리하지 않을 경우, 사이즈에 따른 로직 처리로 코드가 <strong>필요 이상으로 복잡</strong>해질 것 이고 이에 따라 구현과 버그 수정 모두 힘들어 질 것이라고 판단했다. 따라서 데스크탑 사이즈와 모바일 사이즈를 각각 다루는 <strong>컴포넌트를 따로 구현</strong>하고 태블릿 사이즈는 모바일 컴포넌트에서 반응형으로 대응하는 것으로 결정했다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/e8dfcbca-cd50-4568-a26f-32a292ba0e30/image.png" alt=""></p>
<p>search 폴더 내에 <strong>desktop과 mobile 폴더</strong>를 만들어 각 사이즈를 대응할 컴포넌트들을 만들었고 <code>search/index.tsx</code>에서 불러와 <strong>현재 사이즈에 맞는 컴포넌트</strong>를 렌더링 해주었다. 최종적으로 서치 바를 사용하는 main page에서 <code>search/index.tsx</code>를 불러와 사용하였다.</p>
<blockquote>
<p>반복되는 코드를 줄이자!</p>
</blockquote>
<p>기본적으로 같은 기능을 하기 때문에 반복되는 코드가 발생할 수밖에 없다. 따라서 <strong>반복되는 로직</strong>은 모두 <strong>커스텀 훅</strong>으로 관리하였고 팝업과 팝업 내부에서 사용하는 컴포넌트는 모두 외부에서 불러오게끔 구현하였다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/fa092a3d-ae52-4eda-a10e-4b957d9bfc9a/image.png" alt=""></p>
<h3 id="설정된-검색-조건을-어떻게-전달할까">설정된 검색 조건을 어떻게 전달할까?</h3>
<p>검색 조건을 사용하여 데이터를 불러오는 곳은 <code>main/index.tsx</code>이다. 서치 바는 <code>main/search/</code>에 존재하는데, 사이즈에 따라 컴포넌트가 별도로 존재하므로 한 깊이 (<code>/desktop or mobile</code>) 더 들어가야 한다. 또한 서치 바는 3개의 하위 컴포넌트로 구성되어 있으며, 데이터를 설정하는 곳은 하위 컴포넌트에서 띄운 팝업 내부의 컴포넌트이다.</p>
<p>정리를 하자면, 사용자가 설정한 데이터를 총 <strong>4단계 위</strong>로 퍼 올려야 하는 상황이 발생한 것이다.</p>
<blockquote>
<p>props drilling의 담당 일진은...</p>
</blockquote>
<p>검색 조건을 <strong>전역 상태로 관리</strong>하면 간단히 해결할 수 있는 문제지만, 당시 우리 팀은 전역 상태를 도입할 필요가 없는 상황이었다. 때문에, 이 문제 하나만, 이 문제 하나를 해결하기 위해 전역 상태를 도입하는게 맞을까? 라는 생각에 쉽게 결정을 내리지 못했다.</p>
<p>결국(😂) 팀원들에게 현재 상황을 공유하고 의견을 물었는데... 자신있게(?) 하라는 조언을 얻고 <strong><code>recoil</code></strong>을 사용하여 해당 문제를 해결하게 되었다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/476bf553-4929-4a3c-9dad-dd0e1a4f2f2b/image.png" alt=""></p>
<p>유저가 설정할 수 있는 3개의 검색 조건을 커스텀 훅으로 구현하여 desktop/mobile <strong>컴포넌트에서 재사용</strong> 하였으며, 실제 데이터 검색에 사용하는 검색 조건 두 개의 <strong>변경을 감지</strong>하는 커스텀 훅을 추가로 구현하였다.</p>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/6ffdc101-59c9-4901-9a58-7e7986d273f7/image.png" alt=""></p>
<p>데스크탑 사이즈에서는 검색 버튼을 눌렀을 때 검색 조건을 반영하여 새로운 데이터를 불러오고 모바일 사이즈 일 때에는 변경된 검색 조건을 즉시 반영하여 새로운 데이터를 불러온다.</p>
<p>검색 조건을 사용하여 데이터를 불러오는 main page에서 검색 조건(payload)를 state로 관리하고 있으며, 해당 state를 <code>react query</code>의 key로 사용하고 있다. 즉, main page로부터 내려받은 setPayload를 호출하여 payload를 변경하기만 하면, <strong>별도의 추가 로직 없이</strong> 새로운 데이터를 불러올 수 있다.</p>
<h3 id="제어-컴포넌트에는-어떻게-debounce를-걸죠">제어 컴포넌트에는 어떻게 debounce를 걸죠...?</h3>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/007a315c-7377-4310-83c5-fcd12c0f830d/image.gif" alt=""></p>
<p>모바일 사이즈일 경우 변경된 검색 조건을 즉시 반영하여 새로운 데이터를 불러온다. 따라서 text input에 별도의 처리를 하지 않을 경우, &#39;문&#39;을 검색하려고 할 때 &#39;ㅁ&#39;, &#39;무&#39;, &#39;문&#39; 이와 같이 총 3번의 request가 발생하게 된다. 이러한 <strong>불필요한 request</strong>를 막기 위해 <strong>debounce</strong>를 사용하려고 했는데...</p>
<blockquote>
<p>값이... 안 변해요... 😇</p>
</blockquote>
<pre><code class="language-javascript">const onChange = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
  debouncedSet(e.target.value);
};

return { keyword, onChange };</code></pre>
<pre><code class="language-javascript">const { keyword, onChange } = useKeywordState();

return &lt;Input value={keyword} onChange={onChange} /&gt;;</code></pre>
<p>처음에는 단순하게 접근하여, onChange 내부에서 debounce를 사용하였더니 입력한 대로 값이 변하지 않는 문제가 발생하였다. <em>(<strong>onChange에서 set한 value로 input을 제어</strong>했기 때문 😢)</em></p>
<blockquote>
<p>제어 방법을 바꿔볼까?</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/a242aa77-751c-40af-990d-e025c05d559e/image.png" alt=""></p>
<p>onChange가 아닌 <strong>onKeyUp</strong>에서 debounce를 사용하고 input의 상위 컴포넌트에서 <strong>useEffect</strong>로 input value의 변화를 감지하여 input element에 <strong>직접 값을 써주었다.</strong></p>
<p>onKeyUp을 사용한 이유는, <code>inputRef.current.value = x;</code>와 같은 방식으로 input element에 직접 값을 쓸 때 <strong>onChange 이벤트가 호출 될 것</strong>이라고 생각했기 때문이다. 블로그를 작성하는 지금, 혹시나? 해서 테스트 해봤는데, 직접 값을 쓸 때에는 onChange 이벤트가 <strong>호출되지 않는 것</strong>을 확인하였다... 😭</p>
<blockquote>
<p>이벤트가 어찌 됐든, 이론상으론 문제가 없는 제어 방법인 것 같은데...</p>
</blockquote>
<p>input에 값을 빠르게 썼다, 지웠다를 반복하면 지우는 과정에서 이전에 입력됐던 값이 다시 input에 써지는 원인 모를 <strong>문제가 간헐적으로 발생</strong>하였다. (onChange로 해도 똑같음 😂) 정확한 원인은 파악하지 못했지만, 아마도 <code>useEffect</code>에서 <strong>실시간으로 값을 제어</strong>하는 것이 아니기 때문에 <strong>키 입력과 업데이트 사이</strong>의 알 수 없는 <strong>간극(?)</strong>이 생긴 것이라고 추측된다.</p>
<p>이러한 문제를 꼭 해결하고 싶었지만... 과제의 제출기한은 정해져있고, input을 꼭 제어해야 하는 상황이 아니었기 때문에, input을 <strong>제어하지 않는</strong> 방법으로 debounce를 사용했다.</p>
<p><em>강해져서 돌아와... 꼭 복수하겠습니다...</em></p>
<h3 id="304-서버에-문제가-있는데">304...? 서버에 문제가 있는데...?</h3>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/41682c05-c02c-420f-a8a8-0f11f7acf884/image.gif" alt=""></p>
<p>우리 팀은 데이터를 불러올 때 더 좋은 사용자 경험을 위해 스켈레톤 UI를 적용하였다. 하지만, 예약 내역 페이지로 이동했다가 다시 돌아올 때는 스켈레톤 UI가 나타나지 않고, <strong>꽤 긴 로딩 시간</strong> 후에 데이터가 그려지는 문제를 확인하였다.</p>
<p>새로고침 했을 때와 페이지를 이동했을 때 모두 <code>react query</code>를 통해 request를 보내는 것을 확인하였다. 그래서 처음에는 <strong>요청을 보내고 응답을 받는 과정</strong>에서 문제가 발생한 것이라고 추측했다.</p>
<p>나름대로 이런저런 확인을 해보던 도중 <strong><code>json-server</code>의 로그</strong>를 확인할 수 있었고, 페이지를 이동한 뒤 돌아올 때에는 <strong>응답(상태) 코드로 304</strong>를 받는다는 것을 알게 되었다.</p>
<p>여태껏 서버의 응답에서 데이터만 꺼내 썼지, 응답 코드는 확인해본 경험이 없었기 때문에, 처음에는 304가 <strong>에러 코드</strong> 중 하나인 줄 🤭 알았다.</p>
<blockquote>
<p>그래서 304가 뭔데?</p>
</blockquote>
<p>이전에 클라이언트가 받아 갔던 <strong>리소스가 수정되지 않아서</strong>, 새로운 데이터를 보내줄 필요 없이 클라이언트가 본인의 <strong>로컬 캐시를 재사용</strong>하면 될 때 서버는 304 응답 코드를 보내준다. 이때 응답 메시지의 <code>body</code>에는 아무런 내용이 없기 때문에 <strong>불필요한 네트워크 부하</strong>를 줄일 수 있다.</p>
<p>즉, 예약 내역 페이지에서 메인 페이지로 되돌아 올 때는, 이전에 불러왔던 데이터와 <strong>동일한 데이터를 요청</strong>한 것이기 때문에, 서버로부터 데이터를 새로 받는 것이 아니라 로컬 캐시로 리다이렉트하여 <strong>캐시된 데이터</strong>를 받아와 그렸던 것이다.</p>
<pre><code class="language-javascript">return (
  &lt;&gt;
    {hotels?.map((hotel) =&gt; (
      &lt;Card key={hotel.id} {...hotel} /&gt;
    ))}
    {isLoading &amp;&amp; &lt;Skeleton /&gt;}
  &lt;/&gt;
);</code></pre>
<p>이 문제를 확인했을 당시에는 무한 스크롤을 구현하기 전, 위의 코드와 같은 상태였다. 페이지를 되돌아올 때는 캐시를 사용하기 때문에, <code>isLoading</code>이 <code>false</code>라서 스켈레톤 UI 없이 바로 호텔 목록이 표시되었던 것이고 <strong>1,000개 이상의 호텔 목록을 한 번에</strong> 그려야 했기 때문에 긴 로딩 시간이 발생했던 것이었다.</p>
<blockquote>
<p>캐시 사용을 중지해 보면 어떻게 될까?</p>
</blockquote>
<p>처음 예상한 시나리오는, 구글 개발자 도구에서 <strong>캐시 사용을 중지</strong>하면 페이지를 되돌아올 때도 사용할 캐시가 없기 때문에 <strong>새로운 데이터</strong>를 서버로부터 받아오는 것이었다.</p>
<p>캐시 사용 중지 이후 같은 동작으로 확인해보니, <code>json-server</code>의 로그에서 <strong>304 응답 코드</strong>가 사라지고 개발자 도구 네트워크 탭에서 확인할 수 있는 전송 크기 또한 <strong>10kb</strong> 이상으로 늘어난 것을 확인할 수 있었다. 하지만, 여전히 <strong>스켈레톤 UI는 나타나지 않았다.</strong></p>
<p>스켈레톤 UI가 나타나지 않았다는 것은 <code>react query</code>의 <code>isLoading</code>이 <code>true</code>가 되지 않았다는 것이다. 즉, <strong>새로운 데이터</strong>를 서버로부터 받아오지 않은 것이다. 또한 스켈레톤 UI 없이 1,000개 이상의 호텔 목록을 한 번에 그리기 때문에, 페이지를 되돌아 올 때 <strong>긴 로딩 시간이 여전히 발생</strong>하였다.</p>
<blockquote>
<p>react query의 cache 넌 누구세요...? 😇</p>
</blockquote>
<p>내가 작업을 맡은 기능도 아니였고 이후에 무한 스크롤을 적용할 경우, 한 페이지에 불러올 데이터가 제한되기 때문에 <strong>자연스레 사라질 문제</strong>였다. 하지만, <strong>도대체 왜 이러나</strong> 싶어 이런저런 시도를 해보던 중 아래와 같은 코드를 추가하여 <code>react query</code>의 <code>cache</code>기능을 제한하는 설정을 추가해 보았는데... </p>
<pre><code class="language-javascript">const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 0,
    },
  },
});</code></pre>
<blockquote>
<p>의도한 대로 동작은 하는데...</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/corgi-world/post/b10c817b-4b03-4272-99e6-49f102344a5e/image.gif" alt=""></p>
<p><code>react query</code>의 <code>cache</code>기능을 제한하고 테스트해보니 처음에 의도했던 대로 동작하였다. 내가 고쳐보고 싶었던 것은, 페이지가 <strong>이동되기 전에 발생했던 로딩 시간</strong>을 페이지 이동에 막힘이 없도록 페이지 <strong>이동이 완료된 뒤로 옮기는 것</strong>이었다.</p>
<p>정리하면서 곰곰히 생각해 보니, 사실 이 문제는 304 응답 코드보다 <strong><code>react query</code>의 <code>cache</code> 기능은 어떻게 동작하는가</strong> 그리고 <strong><code>isLoading</code>은 언제 <code>true</code>가 되는가</strong> 이 두 의문이 더 중요한 문제였던 것 같다.</p>
<p><em>공부를 더 한 다음에 꼭 다시 정리해보자...</em></p>
<h2 id="😇-마무리">😇 마무리</h2>
<p>과제의 품 자체가 큰 편은 아니었지만, 나 같은 어린이 개발자들은 꼭 한 번씩은 다뤄 바야 할 문제들로 구성되어 있어 굉장히 알찬 시간을 보냈던 것 같다.</p>
<p>지금 개인적으로 급한 일들 얼른~얼른 처리하고 <strong>처음부터, 혼자 다시 구현해볼 예정</strong>이다. 특히 <strong>달력 컴포넌트와 무한 스크롤</strong> 쪽은 구현 과정에서 정말 많이 배울 것 같아 굉장히 큰 기대가 된다. 😎</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[원티드 프리온보딩 프론트엔드  코스 숏에세이]]></title>
            <link>https://velog.io/@corgi-world/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%EC%BD%94%EC%8A%A4-%EC%88%8F%EC%97%90%EC%84%B8%EC%9D%B4</link>
            <guid>https://velog.io/@corgi-world/%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%EC%BD%94%EC%8A%A4-%EC%88%8F%EC%97%90%EC%84%B8%EC%9D%B4</guid>
            <pubDate>Sat, 18 Jun 2022 03:56:12 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>프리온보딩 코스에는 참가 기업에 지원해야 하는 제도가 있습니다.
왜 이런 제도가 있을까요?</p>
</blockquote>
<p>가장 큰 이유는 참가자들의 동기부여를 위해서라고 생각합니다. 막연하게 커리큘럼을 따라가는 것보다 지원하려는 기업을 목표로 학습하는 것이 몰입에 더욱 도움 되기 때문입니다. 또한 참가자는 참가 기업에서 공개한 사용 중인 기술 스택을 확인하여, 자체적인 학습 방향을 설계할 수 있습니다.</p>
<p>다음으로는 프리온보딩 코스를 지속적으로 운영하기 위해서입니다. 프리온보딩 코스를 운영하기 위해 주관사와 참가 기업들은 많은 비용을 지불합니다. 따라서 기업들의 참가를 유도하기 위해서는 좋은 개발자를 채용할 수 있는 환경을 제공해야 합니다. 이러한 이유로 참가자들에게 참가 기업에 지원해야 하는 제도를 만들었다고 생각합니다.</p>
<blockquote>
<p>지원하고 싶은 참가 기업은 어디인지 작성해주세요.</p>
</blockquote>
<p>신입 개발자들에게 있어 좋은 기업은, 빠르게 성장할 수 있는 환경이 갖춰진 기업이라고 생각합니다. 따라서 코드 리뷰를 중요하게 생각하는 기업이나 사내 스터디를 장려하는 기업에 입사하고 싶습니다. 이러한 환경이 갖춰지기 위해서는 우선 기업 내에 개발자가 많아야 하기 때문에, 기업의 직원 수와 규모를 가장 우선되는 조건으로 선택했습니다.</p>
<p>또한 기업에서 운영 중인 서비스의 관심도와 원티드 측에서 공개한 정보가 상세한 기업들을 우선되는 조건으로 선택했습니다.</p>
<p>이러한 이유로 제가 지원하고 싶은 기업은 아래와 같습니다.</p>
<ul>
<li>놀이의발견</li>
<li>지엔에이컴퍼니</li>
<li>트립비토즈</li>
<li>누비랩</li>
</ul>
<blockquote>
<p>사전 과제
<a href="https://github.com/corgi-world/wanted-pre-onboarding-fe">https://github.com/corgi-world/wanted-pre-onboarding-fe</a></p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>