<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>good_sang.log</title>
        <link>https://velog.io/</link>
        <description>개발이 너무 좋다. 정말 잘 하고 싶다.</description>
        <lastBuildDate>Sun, 01 Sep 2024 10:58:30 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>good_sang.log</title>
            <url>https://velog.velcdn.com/images/good_sang/profile/f64f5210-0f8a-46bb-a8c7-a350b6fb02e0/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. good_sang.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/good_sang" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Next.js 서버 컴포넌트에서 MSW로 모킹한 데이터 React-query로 가져오기]]></title>
            <link>https://velog.io/@good_sang/Next.js-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C-MSW%EB%A1%9C-%EB%AA%A8%ED%82%B9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-React-query%EB%A1%9C-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</link>
            <guid>https://velog.io/@good_sang/Next.js-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C-MSW%EB%A1%9C-%EB%AA%A8%ED%82%B9%ED%95%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-React-query%EB%A1%9C-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0</guid>
            <pubDate>Sun, 01 Sep 2024 10:58:30 GMT</pubDate>
            <description><![CDATA[<h3 id="개요"><strong>개요</strong></h3>
<p>Aigoo 프로젝트에서  모의 데이터를 사용하기 위해 MSW를 도입했습니다. React 환경에서는 MSW를 쉽게 사용할 수 있었지만, Next.js의 서버 컴포넌트에서 MSW를 사용할 때 예상치 못한 문제가 발생했습니다. <strong>서버 컴포넌트에서 MSW가 <code>undefined</code> 값을 반환하는 문제</strong>를 해결하기 위해 Next.js의 서버 렌더링 과정을 파악하고 해결 방안을 찾아 보았습니다.</p>
<h3 id="기술-스택">기술 스택</h3>
<p><strong>Next.js</strong></p>
<ul>
<li>app route 버전 사용</li>
<li>서버 컴포넌트를 통한 서버 사이드 렌더링 적용</li>
</ul>
<p><strong>MSW(Mock Service Worker)</strong></p>
<ul>
<li>네트워크 요청을 가로채서 모의(mock) 데이터를 반환</li>
<li>클라이언트 및 node.js 모두에서 동작 가능</li>
<li>특정 API 엔드포인트에 대한 모의 응답 설정 가능</li>
</ul>
<p><strong>React Query(tanstack query)</strong></p>
<ul>
<li>5 버전 사용</li>
<li>prefetch, caching, refetch 등의 기능 제공</li>
</ul>
<h3 id="서버-컴포넌트에서-undefined-반환">서버 컴포넌트에서 <code>undefined</code> 반환</h3>
<p>서버 컴포넌트 환경에서 MSW를 사용해 모의 데이터를 가져오려고 했을 때 문제가 발생했습니다. MSW는 정상적으로 API 요청을 intercept했으나, 서버 컴포넌트에서 데이터 페칭 결과가 <code>undefined</code>로 반환되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/3c85f7bc-6d97-454f-adca-01d36f6df0c5/image.png" alt=""></p>
<h3 id="원인-분석">원인 분석</h3>
<p>아래 원인들을 분석해본 결과, 한마디로 서버 컴포넌트 랜더링 과정에서 MSW가 요청을 intercept하는 방식이 맞지 않는다는 것이었다. 서버에서 네트워크 요청을 가로채는 서버 사이드 모킹 솔루션을 적용해야 했습니다. </p>
<ol>
<li><p>Next.js 서버 컴포넌트는 서버에서 실행되고, 클라이언트 컴포넌트는 브라우저에서 실행됩니다. </p>
</li>
<li><p>MSW는 브라우저 환경에서 네트워크 요청을 가로채도록 설계되었습니다.</p>
</li>
<li><p>서버 컴포넌트에서 useQuery를 사용할 때 데이터 페칭은 페이지가 클라이언트로 전달되기 전에 서버에서 발생합니다. 이 시점에서는 MSW가 브라우저 환경에 존재하지 않기 때문에 활성화되지 않습니다.</p>
</li>
<li><p>MSW는 자바스크립트가 브라우저에서 실행될 때 네트워크 요청 가로채기 메커니즘을 설정합니다. 이는 서버가 이미 컴포넌트를 렌더링하고 데이터를 페칭한 이후에 발생합니다.</p>
</li>
<li><p>React Query의 useQuery 훅은 클라이언트 사이드 데이터 페칭을 위해 설계되었습니다. 이를 서버 컴포넌트에서 사용하면 기대한 대로 작동하지 않을 수 있습니다.</p>
</li>
</ol>
<h3 id="서버-컴포넌트-랜더링-과정">서버 컴포넌트 랜더링 과정</h3>
<p>이 문제를 해결하기 위해 서버 컴포넌트의 렌더링 과정에 대한 이해가 필요했습니다. 서버 사이드 렌더링, 디하이드레이션, 하이드레이션 과정을 이해하며 어떻게 문제를 해결해야 할지 인사이트를 얻었습니다.</p>
<ol>
<li><p><strong>서버 사이드 렌더링</strong></p>
<ul>
<li>서버가 페이지 요청을 받습니다.</li>
<li>서버에서 React 컴포넌트를 렌더링하여 HTML을 생성합니다.</li>
<li>이 HTML과 필요한 데이터가 클라이언트로 전송되어 빠른 초기 페이지 로드를 가능하게 합니다.</li>
</ul>
</li>
<li><p><strong>Dehydration</strong> (서버에서 렌더링 후 발생)</p>
<ul>
<li>서버가 컴포넌트를 렌더링하고 필요한 데이터를 가져온 후, <strong>Dehydration</strong>을 수행합니다.</li>
<li>디하이드레이션은 데이터를 직렬화하는 과정입니다.</li>
<li>직렬화된 데이터는 HTML 응답에 포함되며, 일반적으로 페이지 내의 JSON 객체로 삽입됩니다.</li>
</ul>
</li>
<li><p><strong>초기 클라이언트 로드</strong> (브라우저에서 발생)</p>
<ul>
<li>브라우저는 서버에서 미리 렌더링된 HTML을 표시합니다.</li>
</ul>
</li>
<li><p><strong>Hydration</strong> (JavaScript 번들이 로드된 후 클라이언트에서 발생)</p>
<ul>
<li>JavaScript 번들이 로드된 후, <strong>하이드레이션</strong> 과정이 시작됩니다.</li>
<li>하이드레이션이 완료되면 애플리케이션은 사용자와 상호작용이 가능합니다.</li>
</ul>
<p><strong>하이드레이션 과정</strong>
  1) 서버에서 렌더링된 정적 DOM에 JavaScript 이벤트 리스너를 연결한다.
  2) 서버에서 제공된 HTML과 클라이언트 동기화합니다.
  3) 서버에서 직렬화된 데이터를 클라이언트로 가져와 컴포넌트 상태에 반영한다.</p>
</li>
</ol>
<h3 id="해결-방법"><strong>해결 방법</strong></h3>
<p>React Query의<code>useQuery</code> 대신 <code>prefetchQuery</code>를 사용하여 미리 서버에서 데이터를 가져온 후, <strong>dehydrate</strong>와 <strong>hydrate</strong>를 통해 데이터를 직렬화/역직렬화해 처리하는 방식을 사용했습니다.</p>
<h4 id="과정">과정</h4>
<ol>
<li>Next.js의 로더 함수에서 <code>QueryClient</code>를 생성합니다.</li>
<li><code>prefetchQuery</code> 메서드를 사용하여 데이터를 서버에서 미리 가져옵니다.</li>
<li>프리페치된 데이터를 <code>dehydrate</code>하여 클라이언트 측에 전달합니다.</li>
<li>Next.js의 서버 컴포넌트는 미리 가져온 데이터를 클라이언트에서 <code>hydrate</code>하여 화면에 보여줍니다.</li>
</ol>
<pre><code class="language-js">// hook/queries/useLoaderData.ts

import { getStudyMateirals } from &quot;@/lib/api/api&quot;;
import { dehydrate, DehydratedState, QueryClient } from &quot;@tanstack/react-query&quot;;

interface LoaderData {
  dehydratedState: DehydratedState;
}

const useLoaderData = async (): Promise&lt;LoaderData&gt; =&gt; {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery({
    queryKey: [&quot;material&quot;],
    queryFn: async () =&gt; {
      const data = await getStudyMateirals();
      return data.data.studyDatas;
    },
  });

  return {
    dehydratedState: dehydrate(queryClient),
  };
};

export default useLoaderData;</code></pre>
<p><code>HydrationBoundary</code>를 Root에 감싸주고, state에 queryClient를 dehydrate(직렬화)시켜 전달합니다.
이렇게 되면 HydrationBoundary의 children 컴포넌트들은 모두 prefetch된 post 데이터를 사용할 수 있습니다.</p>
<pre><code class="language-js">// app/notes.page.tsx

import Materials from &quot;@/components/material/Materials&quot;;
import useLoaderData from &quot;@/hooks/queries/useLoaderData&quot;;
import { HydrationBoundary } from &quot;@tanstack/react-query&quot;;

const Material = async () =&gt; {
  const { dehydratedState } = await useLoaderData();

  return (
    &lt;HydrationBoundary state={dehydratedState}&gt;
      &lt;Materials /&gt;
    &lt;/HydrationBoundary&gt;
  );
};

export default Material;</code></pre>
<h3 id="결과">결과</h3>
<p>MSW Service worker로 부터 데이터를 응답 받을 것을 확인할 수 있었습니다. </p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/52e68bac-0423-4416-af72-7adef443eb33/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/ac06cc0d-1713-44c4-a8cd-6139a1f7d8e3/image.png" alt=""></p>
<h3 id="시도한-방법들"><strong>시도한 방법들</strong></h3>
<p>이 문제를 해결하기 위해 <strong>서버 컴포넌트에서 API 요청을 모의하는 방법을 변경</strong>해야 합니다. </p>
<p>여러 글을 참고하며 적용해보았지만, 잘 해결되지 않았다. 추후에 다시 한번 시도해보기 위해서 간략하게 내용을 남겨 놓았습니다.</p>
<p>** 1. next의 <a href="https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation">instrumentation</a>을 이용**  </p>
<p>src/instumentation.ts</p>
<pre><code class="language-js">export async function register() {
  if (process.env.NEXT_RUNTIME === &quot;nodejs&quot;) {
    const { server } = await import(&quot;./mocks/server&quot;);
    server.listen();
  }
}
</code></pre>
<p>next.config.mjs</p>
<pre><code class="language-js">experimental: { instrumentationHook: true },
  webpack(config, { isServer }) {
    if (isServer) {
      if (Array.isArray(config.resolve.alias)) {
        config.resolve.alias.push({ name: &quot;msw/browser&quot;, alias: false });
      } else {
        config.resolve.alias[&quot;msw/browser&quot;] = false;
      }
    } else {
      if (Array.isArray(config.resolve.alias)) {
        config.resolve.alias.push({ name: &quot;msw/node&quot;, alias: false });
      } else {
        config.resolve.alias[&quot;msw/node&quot;] = false;
      }
    }

    return config;
  },</code></pre>
<p><strong>2. express 서버 구축해서 서버사이드에서 일어나는 데이터 패칭 로직을 가로채기</strong> </p>
<p>src/mocks/http.ts</p>
<pre><code class="language-js">import { createMiddleware } from &#39;@mswjs/http-middleware&#39;;
import express from &#39;express&#39;;
import cors from &#39;cors&#39;;
import { handlers } from &#39;./handlers&#39;;

const app = express();
const port = 9090;

app.use(cors({ origin: &#39;http://localhost:3000&#39;, optionsSuccessStatus: 200, credentials: true }));
app.use(express.json());
app.use(createMiddleware(...handlers));
app.listen(port, () =&gt; console.log(`Mock server is running on port: ${port}`));</code></pre>
<p>package.json</p>
<pre><code>{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;next dev&quot;,
    &quot;mock&quot;: &quot;npx tsx ./mocks/http.ts&quot;,
  }
}</code></pre><p>axios settings:</p>
<pre><code>axios.defaults.baseURL = &#39;http://localhost:9090&#39;;
start mock server yarn mock, start dev server yarn dev
https://github.com/mswjs/msw/issues/1644#issuecomment-1750722052</code></pre><h3 id="마무리">마무리</h3>
<p>이번 문제를 해결하면서 Next.js 서버 컴포넌트와 MSW의 동작 방식을 이해하게 되었습니다. React query의 prefetchQuery를 사용해서 서버사이드에서 데이터를 Prefetch하고 클라이언트에서 Hydration을 통해 서버 사이드에서 데이터 처리하는 방식도 배울 수 있었습니다. </p>
<p>공식 문서들을 참고하고, 여러 관련 글을 읽어보면서 정확한 내용을 정리하려고 노력했지만, 미흡한 부분이 있을 수 있습니다. 혹시 글을 읽으시면서 잘못된 부분이 있거나, 보완이 필요한 부분이 있다면 댓글 남겨주시면 도움이 될 것 같습니다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://tanstack.com/query/v5/docs/framework/react/guides/ssr#initial-setup">server Rendering &amp; hydration</a></li>
<li><a href="https://yozm.wishket.com/magazine/detail/2271/">Next.js 서버 컴포넌트 이해하기</a></li>
<li><a href="https://github.com/mswjs/msw">msw</a></li>
<li><a href="https://velog.io/@ssoon-m/Next.js-app-directory%EC%9D%98-%EC%84%9C%EB%B2%84-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C-msw-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">Next.js-app-directory의-서버-컴포넌트에서-msw-사용하기</a></li>
<li><a href="https://velog.io/@ubin_ing/react-query-in-server-components">Server Component에서 React Query 사용하기</a></li>
<li><a href="https://blog.rhostem.com/posts/2021-03-20-mock-service-worker">Mock Service Worker로 만드는 모의 서버</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS S3, CloudFront, Route 53를 이용한 웹 호스팅 구축하기]]></title>
            <link>https://velog.io/@good_sang/AWS-S3-CloudFront-Route-53-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@good_sang/AWS-S3-CloudFront-Route-53-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Wed, 07 Aug 2024 08:33:57 GMT</pubDate>
            <description><![CDATA[<h2 id="aws-s3-cloudfront-route-53를-활용한-웹사이트-성능-최적화">AWS S3, CloudFront, Route 53를 활용한 웹사이트 성능 최적화</h2>
<h3 id="aws-아키텍처">AWS 아키텍처</h3>
<p><img src="https://velog.velcdn.com/images/good_sang/post/2963f334-7d9b-4408-a37a-ab8a5bf28cfd/image.png" alt=""></p>
<p><strong>Amazon S3 버킷</strong>
Amazon S3 버킷을 오리진 서버를 지정하고, 리소스 파일(객체의 최종 원본 버전)을 저장합니다. </p>
<p><strong>CloudFront</strong>
Amazon S3 버킷(오리진 서버로)부터 파일을 가져온 다음 CloudFront 엣지 로케이션에 캐싱하여 뷰어에게 콘텐츠를 빠르게 전달합니다. </p>
<p>Amazon S3을 사용하면 오리진에서 CloudFront로 데이터를 전송할 때 항상 무료이기 때문에 함께 사용하는 것을 권장합니다.</p>
<p><strong>Route 53</strong>
누군가 브라우저를 사용하여 <a href="http://www.example.com%EA%B3%BC">www.example.com과</a> 같은 웹사이트에 접속하면 네임 서버들은 웹 서버 또는 Amazon S3 버킷 같은 리소스를 어디에서 찾아야 하는지 브라우저에게 알려줍니다. 트래픽 분산 등 고급 기능도 활용할 수 있습니다.</p>
<p><strong>ACM(Amazon Certificate Manager)</strong>
AWS 기반 웹 사이트 및 애플리케이션의 SSL/TLS 인증서를 제공하고 관리할 수 있습니다. SSL/TLS 인증서를 통해 HTTPS 프로토콜로 안전하게 데이터 통신할 수 있습니다.</p>
<h2 id="aws-인프라-구축-과정"><strong>AWS 인프라 구축 과정</strong></h2>
<h3 id="route-53에-사용자-지정-도메인-등록-및-구성">Route 53에 사용자 지정 도메인 등록 및 구성</h3>
<p>cloudfront에서 HTTPS를 통해 콘텐츠를 안전하게 전송하기 위해서 <strong>SSL 인증서를 등록</strong>해야 합니다. 이때 필요한 도메인을 Route 53에 미리 등록합니다. </p>
<p><strong>사용할 도메인 이름으로 호스팅 영역 생성</strong>
<img src="https://velog.velcdn.com/images/good_sang/post/2c8b53a0-a2dd-4624-9059-dde2c2bfdb01/image.png" alt=""></p>
<p><strong>호스팅 영역 생성 후 Route 53 대시보드</strong> 
<img src="https://velog.velcdn.com/images/good_sang/post/42e27e09-2238-43a0-acad-d093e434e19a/image.png" alt=""></p>
<h3 id="가비아-도메인-구매"><a href="https://www.gabia.com/">가비아 도메인 구매</a></h3>
<p>route 53에서도 도메인을 구매할 수 있지만, 도메인 비용이 저렴한 <strong>가비아</strong>에서 도메인을 구매했습니다. 도메인 구매할 때 route 53에서 <strong>NS의 값/트래픽 라우팅 대상 값</strong>을 확인하여 네임 서버에 입력했습니다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/ba4b86ec-4eb7-437b-a721-13b0ebc819cb/image.png" alt=""></p>
<h3 id="s3-버킷-생성">S3 버킷 생성</h3>
<p>원본 리소스를 저장하기 위해 S3 버킷을 생성합니다.</p>
<ul>
<li>버킷 이름 설정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/eeeafd89-343b-4332-b068-c9d1ff2bbe54/image.png" alt=""></p>
<p>퍼블릿 액세스 차단 해제</p>
<ul>
<li>모든 사람이 웹 사이트 접근할 수 있도록 설정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/d3c5a1b0-cdf7-4fd5-8304-d2b47333ad94/image.png" alt=""></p>
<ul>
<li>버킷 정책 설정<ul>
<li>S3 GetObject 설정</li>
<li>리소스: 해당 버킷의 모든 리소스 접근</li>
<li>주체: 모든 사용자</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/3c0287e3-61fa-4ae1-bebb-60590ca37197/image.png" alt=""></p>
<h3 id="s3에-웹-서비스-파일-업로드">S3에 웹 서비스 파일 업로드</h3>
<p>버킷의 <strong>객체(Objects) 탭</strong>에서 <strong>dist 파일에 있는 리소스를 모두 업로드</strong>했습니다. 아래 이미지는 리소스를 업로드한 상태입니다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/da3bab3f-3b81-4f2e-9160-d7c21222c63b/image.png" alt=""></p>
<h3 id="정적-웹-사이트-호스팅-설정">정적 웹 사이트 호스팅 설정</h3>
<p>버킷 <strong>속성 탭</strong>에서 스크롤을 가장 아래로 내리면 정적 웹 사이트 호스팅 항목이 있습니다. 편집을 눌러서 아래와 같이 설정하여 <strong>웹 사이트를 배포</strong>하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/553d221d-dc97-42dc-b95d-5cdb51ab8c5a/image.png" alt=""></p>
<p>정적 웹 사이트 호스팅 설정을 하면 <strong>버킷 웹 사이트 엔드포인트</strong>로 배포가 된 것을 확인할 수 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/f60a003a-9b66-409f-b9ff-93572fd8ca46/image.png" alt=""></p>
<h3 id="cloudfront-배포">CloudFront 배포</h3>
<p><strong>성능과 보안을 목적</strong>으로 S3 버킷과 연동하여 CloudFront 배포를 생성합니다.</p>
<p><strong>성능</strong> 측면은 <strong>CloudFront  캐싱</strong>을 통해 오리진 서버(S3)에서 직접 응답해야 하는 요청의 수를 줄일 수 있습니다. 해당 요청은 뷰어(사용자)의 위치와 가까운 엣지 로케이션으로 라우팅하여 <strong>빠른 전송 속도로 콘텐츠를 제공</strong>할 수 있습니다. </p>
<p><strong>보안</strong> 측면에서는 CloudFront에서 <strong>HTTPS를 적용</strong>하여 보다 안전하게 콘텐츠를 전달할 수 있습니다. </p>
<h4 id="배포-생성">배포 생성</h4>
<p><img src="https://velog.velcdn.com/images/good_sang/post/b1b2fa22-9818-412b-8c41-0ed8f6de67f8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/b634e556-5efe-43c0-bec9-789eaccffa2c/image.png" alt=""></p>
<h4 id="기본-캐시-동작-behavior">기본 캐시 동작 (Behavior)</h4>
<p><strong>HTTPS 프로토콜</strong>로 CloudFront와 Amazon S3 간의 통신하려면, <strong>뷰어 프로토콜 정책(Viewer Protocol Policy)</strong> 값을 Redirect HTTP to HTTPS 또는 HTTPS Only로 변경해야 합니다. </p>
<p>저는 <strong>Redirect HTTP to HTTPS</strong>로 설정해서 HTTP와 HTTPS두 프로토콜 모두 사용할 수 있지만, HTTP 요청은 자동으로 HTTPS 요청으로 리디렉션되도록 했습니다.</p>
<p>HTTPS 프로토콜을 사용하기 위해서 SSL 인증서가 필요한데, 이부분은 아래에서 설명하겠습니다.</p>
<ul>
<li><strong>뷰어 프로토콜 정책: Redirect HTTP to HTTPS</strong><ul>
<li>HTTP 요청을 HTTPS 요청으로 Redirect 설정 (보안 강화)</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/488087f8-8066-46e3-8735-0910abb26c82/image.png" alt=""></p>
<h2 id="캐시-키-및-원본-요청">캐시 키 및 원본 요청</h2>
<ul>
<li>캐시 정책<blockquote>
<p><a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html#managed-cache-caching-optimized"><strong>CachingOptimized</strong></a>
이 정책은 CloudFront가 <strong>캐시 키에 포함된 값을 최소화</strong>하여 캐시 효율성을 최적화하도록 설계되었습니다. CloudFront는 캐시 키에 쿼리 문자열이나 쿠키를 포함하지 않으며 <strong>정규화된 Accept-Encoding 헤더만 포함</strong>합니다. 이렇게 하면 오리진에서 객체를 반환하거나 CloudFront 엣지 압축이 활성화된 경우 CloudFront에서 Gzip 및 Brotli 압축 형식의 객체를 별도로 캐시할 수 있습니다.</p>
<ul>
<li>최소 TTL: 1초.</li>
<li>최대 TTL: 31,536,000초(365일).</li>
<li>기본 TTL: 86,400초(24시간).</li>
<li>캐시 키에 포함된 헤더: 명시적으로 포함되지 않습니다. 압축된 객체 캐시 설정이 활성화되어 있기 때문에 정규화된 Accept-Encoding 헤더가 포함됩니다. </li>
<li>캐시 키에 포함된 쿠키: 없음.</li>
<li>캐시 키에 포함된 쿼리 문자열: 없음.</li>
<li>압축된 객체 캐시 설정: 활성화됨.</li>
</ul>
</blockquote>
</li>
</ul>
<pre><code>- HTTP Header, 쿠키, querystring 등 정보로 컨텐츠를 캐시할 것인지 설정
- 얼마나 오래 캐시하는지 설정 (TTL)
- 컨텐츠를 압축 저장 관련 설정</code></pre><ul>
<li>원본 요청 정책<ul>
<li>CloudFront가 원본 서버에 요청을 보낼 때 어떤 헤더, 쿠키 및 쿼리 문자열을 포함할지 결정합니다. 여기에 설정된 값은 캐싱에 사용되지 않습니다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/9473012e-6cb5-40a0-9a24-0ea1dc1b68c1/image.png" alt=""></p>
<h3 id="웹-애플리케이션-방화벽waf">웹 애플리케이션 방화벽(WAF)</h3>
<ul>
<li>보안 보호 비활성화로 설정 → 크게 보안이 떨어지지 않는다고 한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/a30b363b-2a3a-4f4d-a38c-44e62b7520a8/image.png" alt=""></p>
<h4 id="설정">설정</h4>
<ul>
<li>아시아권에서만 서비스 운영할 것이기 때문에 가격분류를 북미 유럽 아시아 중동 및 아프리카로 선택했습니다. 가격 분류에 따라 비용이 달라지기 때문에 각 서비스에 따라 선택하시면 됩니다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/c0fac15b-2000-4039-a28f-afd1f857a7f5/image.png" alt=""></p>
<ul>
<li>기본값 루트 객체<ul>
<li>index.html : 진입점 입력
<img src="https://velog.velcdn.com/images/good_sang/post/1313b967-183a-41a9-96df-6e4b63e0038c/image.png" alt=""></li>
</ul>
</li>
</ul>
<h3 id="cloudfront-배포-완료">CloudFront 배포 완료</h3>
<ul>
<li>cloudfront 배포 도메인 생성</li>
<li>해당 도메인으로 웹 사이트 확인 가능</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/2795e723-e937-4b14-851e-ffe65985230f/image.png" alt="">
<img src="https://velog.velcdn.com/images/good_sang/post/a25e1a36-64dc-4f9f-afb5-bf8805cbe2d5/image.png" alt=""></p>
<h3 id="사용자-지정-도메인-이름을-사용하도록-cloudfront-배포-구성">사용자 지정 도메인 이름을 사용하도록 CloudFront 배포 구성</h3>
<h3 id="acm에서-ssl-인증서-발급">ACM에서 SSL 인증서 발급</h3>
<p>최종 사용자와 CloudFront 또는 CloudFront와 오리진 간에 HTTPS를 사용하기 위해서는 SSL 인증서가 필요합니다. <strong>AWS Certificate Manager(ACM)</strong>를 사용하여 <strong>보안 소켓 계층(SSL) 인증서를 요청</strong>합니다. 이때 주의할 점은 반드시 <strong>위치를 버지니아 북부로 설정한 후 인증서 요청</strong>해야 합니다. 위치는 우측 상단에서 설정할 수 있습니다. </p>
<p>가비아에서 구매한 도메인으로 인증서 요청했습니다. 요청 후 Route S3에서 레코드 생성합니다. </p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/99699070-2a87-4ac2-94c4-6d3204e724f8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/aa3fe8c3-66f9-499c-b059-ac1d355140c4/image.png" alt=""></p>
<p>위 이미지는 인증서 발급 완료한 후 캡처해서 검증 상태가 성공인 점 참고해주세요. 원래 검증 대기 중인 인증서는 검증 보류(Pending validation) 상태입니다.</p>
<h3 id="cloudfront-배포에-대체-도메인-이름-추가">CloudFront 배포에 대체 도메인 이름 추가</h3>
<p>*<em>대체 도메인 이름(CNAME)(Alternate domain names(CNAME)) *</em>항목 추가를 선택하여 해당 대체 도메인 이름을 추가합니다. </p>
<p><strong>Custom SSL Certificate(사용자 정의 SSL 인증서) 항목</strong>은 드롭다운 목록에서 인증서를 선택합니다.</p>
<ul>
<li>일반 탭에서 설정 - 편집 클릭</li>
<li>Alternative domain name (CNAMEs) <em>- optional: 구매한 도메인 입력</em></li>
<li>Custom SSL certificate - optional: SSL 인증서 등록</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/367565e0-8cfa-43f1-b116-d87ecf587c84/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/bea3ddc7-a1bb-40c7-b511-54467306ef2c/image.png" alt=""></p>
<h3 id="대체-도메인-이름에서-트래픽을-cloudfront-배포의-도메인-이름으로-라우팅하는-dns-레코드-생성">대체 도메인 이름에서 트래픽을 CloudFront 배포의 도메인 이름으로 라우팅하는 DNS 레코드 생성</h3>
<ul>
<li>트래픽 라우팅 대상: CloudFront 배포에 대한 별칭</li>
<li>아래 해당 CloudFront 도메인 선택</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/69f2acf4-19ce-42ba-ae0a-96c221fe39f5/image.png" alt=""></p>
<ul>
<li>유형 A 레코드 생성된 것을 확인할 수 있다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/317030c4-3a98-4502-bf5a-4541f6899b12/image.png" alt=""></p>
<h2 id="3-성능-측정">3. 성능 측정</h2>
<h3 id="lighthouse-측정">Lighthouse 측정</h3>
<p><img src="https://velog.velcdn.com/images/good_sang/post/5ce0d25b-ae2f-4150-9e09-e531869ac45f/image.png" alt=""></p>
<h3 id="network-탭에서-load-측정">Network 탭에서 Load 측정</h3>
<p>AWS CloudFront 배포가 Netlify 배포에 비해 성능에서 약 4배 이상 빠른 것을 확인할 수 있었습니다. </p>
<blockquote>
<p><strong>DOMContentLoaded</strong>
브라우저가 HTML을 완전히 로드하고 분석했을 때 발생
<strong>Load</strong>
모든 리소스(이미지, 스타일시트, 스크립트 등)가 완전히 로드되었을 때 발생</p>
</blockquote>
<ul>
<li>Netlify 배포
DOMContentLoaded: 737ms, Load: 866ms
<img src="https://velog.velcdn.com/images/good_sang/post/2a01508a-8961-4f9a-a3ae-aa39e7b52c0b/image.png" alt=""></li>
</ul>
<ul>
<li>Cloudfront 배포
DOMContentLoaded: 181ms, Load: 181ms
<img src="https://velog.velcdn.com/images/good_sang/post/b05a11e9-d5fa-4110-9593-108a45f88d07/image.png" alt=""></li>
</ul>
<h3 id="cloudfront-cache-hit-확인">CloudFront Cache Hit 확인</h3>
<p><img src="https://velog.velcdn.com/images/good_sang/post/3d9f1bbc-3917-4a99-9ddc-a07daeae46f7/image.png" alt=""></p>
<h2 id="참고">참고</h2>
<p><a href="https://docs.aws.amazon.com/ko_kr/Route53/latest/DeveloperGuide/getting-started-cloudfront-overview.html">Amazon CloudFront 배포를 사용하여 정적 웹 사이트 제공</a></p>
<p><a href="https://youtu.be/BcJKp4N01Gk?si=pshDTUFK_tRzDtAb">AWS 입문/실전 - 3.4. S3에 업로드하기 / 웹 호스팅 설정하기
</a></p>
<p><a href="https://velog.io/@seesaw/AWS-S3-CloudFront-Route-53%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%A0%95%EC%A0%81-%EC%9B%B9-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%B0%B0%ED%8F%AC">AWS S3 + CloudFront + Route 53을 이용한 정적 웹 사이트 배포</a></p>
<p><a href="https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/Introduction.html">AmazonCloudFront</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[react-router-dom] loader를 알아보자]]></title>
            <link>https://velog.io/@good_sang/React-Route-dom-loader</link>
            <guid>https://velog.io/@good_sang/React-Route-dom-loader</guid>
            <pubDate>Sun, 04 Aug 2024 15:38:27 GMT</pubDate>
            <description><![CDATA[<h2 id="loader를-알아보자">loader를 알아보자</h2>
<p>서버에 리소스를 요청할 때 항상 반복해야 하는 일종의 보일러 플레이트 코드가 있습니다. 커스텀 훅을 생성해서 반복되는 로직을 줄일 수 있지만, 그럼에도 다양한 HTTP 요청 상태를 처리하고 그 데이터를 가져오기 위해서 반드시 작성해야 하는 코드가 있습니다.</p>
<p><strong>loader를 사용하면, 컴포넌트를 렌더링하기 전에 데이터를 요청해서 응답받는 데이터를 가지고 컴포넌트를 렌더링하게 됩니다.</strong> 리액트 라우터 버전 6 이상을 사용하고 있다면 데이터를 가져오고 상태들을 처리하는 코드를 작성할 필요가 없습니다. </p>
<pre><code class="language-js">createBrowserRouter([
  {
    element: &lt;Teams /&gt;,
    path: &quot;teams&quot;,
    children: [
      {
        element: &lt;Team /&gt;,
        path: &quot;:teamId&quot;,
        loader: async ({ params }) =&gt; {
          return fetch(`/api/teams/${params.teamId}.json`);
        },
      },
    ],
  },
]);</code></pre>
<h2 id="loader가-리턴한-데이터에-어떻게-액세스할-수-있을까요">loader()가 리턴한 데이터에 어떻게 액세스할 수 있을까요?</h2>
<p>데이터를 사용하려는 컴포넌트에서 loader 함수가 리턴한 데이터에 사용하기 위해 <strong>react-router-dom에서 useLoaderData를 import</strong>할 수 있습니다. useLoaderData는 가장 가까운 loader 데이터에 액세스하기 위해 실행할 수 있는 훅 입니다. <strong>loader() 함수는 Promise를 리턴합니다.</strong></p>
<pre><code class="language-js">import { useLoaderData } from &#39;react-router-dom&#39;

export function Team() {
  const TeamData = useLoaderData();
  // ...
}</code></pre>
<p>단, <strong>loader 사용한 라우트의 더 높은 수준의 라우트에서는 데이터를 액세스할 수 없습니다.</strong> 만약 상위 수준의 라우트에서 데이터를 엑세스하면 undefined를 반환합니다. </p>
<pre><code class="language-js">import { useLoaderData } from &#39;react-router-dom&#39;

export function Teams() {
  const TeamData = useLoaderData(); // undefined
  // ...
}</code></pre>
<h2 id="loader-코드를-어디에-위치시켜야-할까요">loader() 코드를 어디에 위치시켜야 할까요?</h2>
<p>권장사항은 실제로 그 loader 코드를 사용하는 컴포넌트 파일에 넣는 것입니다. 예를 들어, Example 컴포넌트 파일에 loader 함수를 함께 넣습니다. </p>
<pre><code class="language-jsx">import { useLoaderData } form &quot;react-router-dom&quot;;

export default function Example () {
    const loaderData = useLoaderData();

    return &lt;subExample data={loaderData}&gt;
};

export async function loader () {
    const response = await fetch(&quot;API URL&quot;);

    if (!response.ok) {
        // error handling
    } else {
        const result = await response.json();
        return result
    }
};</code></pre>
<pre><code class="language-jsx">import { RouterProvider, createBrowserRouter } from &quot;react-router-dom&quot;;

import Homepage from &quot;./page/Homepage&quot;;
// as 키워드를 사용해서 loader 별칭을 사용
import Example, { loader as exampleLoader } from &quot;./page/Example&quot;;

const router = createBroswerRouter([
{
    path: &quot;/&quot;,
    element: &lt;RootLayout /&gt;,
    children: [
        { index: true, element: &lt;Homepage /&gt;},
        {
            path: &quot;example&quot;,
            element: &lt;Example /&gt;,
            loader: exampleLoader
        }
    ]
}
])</code></pre>
<h2 id="loader는-언제-실행되는건가요">Loader는 언제 실행되는건가요?</h2>
<p>loader는 우리가 페이지로 이동하기 시작할 때 호출됩니다. <strong>React Router는 데이터를 가져올 때까지, 즉 loader가 작업을 완료할 때까지 대기하고, 가져온 데이터로 페이지를 렌더링합니다.</strong></p>
<p>만약 데이터 응답이 지연된다면, 이벤트가 발생했는데도 아무런 일이 일어나지 않는 것처럼 보일 수 있습니다. 이러한 점을 개선하기 위해 <code>useNavigation</code>을 사용할 수 있습니다. </p>
<h2 id="응답-지연으로-인한-사용자-경험-개선하기">응답 지연으로 인한 사용자 경험 개선하기</h2>
<p>이벤트가 발생한 후 리액트 <code>useNavigation</code> Hook을 사용해서 현재의 라우트 전환 상태를 확인할 수 있습니다. </p>
<p>useNavigation에서 navigation 객체의 속성을 사용해서 상태를 알아낼 수 있습니다. idle (대기),loading (로딩), submitting (제출) 상태를 확인하여 대처할 수 있습니다.</p>
<pre><code class="language-jsx">navigation.state = &#39;idle&#39; | &#39;loading&#39; | &#39;submitting&#39;;

// loading 상태일 때 Loading UI를 표시할 수 있다. </code></pre>
<h2 id="loader에서-사용할-수-있는-코드의-종류">loader에서 사용할 수 있는 코드의 종류</h2>
<p>loader에서 Web API를 사용할 수 있습니다. 쿠키나 로컬 스토리지에 접근하거나, JS 코드를 사용할 수 있습니다. 단, React 훅은 사용할 수 없습니다. </p>
<h2 id="errorelement를-이용한-오류-처리">errorElement를 이용한 오류 처리</h2>
<p>errorElement를 루트 라우트에 추가해서 404 폴백 페이지를 만들거나, 에러 발생시 화면 표시합니다.</p>
<p>children 라우트에도 errorElement를 추가할 수 있습니다. 그럼 해당 라우트에서 에러가 발생할 때만 해당 errorElement를 표시할 수 있습니다. </p>
<pre><code class="language-jsx">import { RouterProvider, createBrowserRouter } from &quot;react-router-dom&quot;;

import Homepage from &quot;./page/Homepage&quot;;
// as 키워드를 사용해서 loader 별칭을 사용
import Example, { loader as exampleLoader } from &quot;./page/Example&quot;;
import ErrorPage from &quot;./page/Error&#39;;

const router = createBroswerRouter([
{
    path: &quot;/&quot;,
    element: &lt;RootLayout /&gt;,
    errorElement: &lt;ErrorPage /&gt;
    children: [
        { index: true, element: &lt;Homepage /&gt;},
        {
            path: &quot;example&quot;,
            element: &lt;Example /&gt;,
            loader: exampleLoader
        }
    ]
}
])</code></pre>
<h2 id="userouteerror-훅-사용해서-thorw-new-response에러객체-처리하기">useRouteError 훅 사용해서 thorw new Response(에러객체) 처리하기</h2>
<pre><code class="language-jsx">// Route에서 발생한 에러를 훅을 통해 겍체로 받을 수 있다. 
import { useRouteError } from &quot;reat-route-dom&quot;;

const Error = () =&gt; {
    const error =    useRouteError();

    let title = &quot;An error occurred!&quot;;
    let message = &quot;Something went wrong!&quot;;

    // loader에서 throw new Response 한 JSON 객체 처리하기
    if (error.status === 500) {
        message = JSON.parse(error.data).message;
    };

    // 경로 못 찾는 경우, 기본적으로 404 에러 발생
    if (error.status == 404) {
        title = &quot;Not Found&quot;;
        message = &quot;Could not find resource or page&quot;;
    };

    return (
        &lt;&gt;
            // 페이지 네비게이션을 추가하면 
            // 에러 페이지에서 다른 페이지로 이동할 수 있어서 사용자 경험 향상
            &lt;Navigation /&gt;
            &lt;PageContent title={title}&gt;
            &lt;p&gt;{message}&lt;/p&gt;
        &lt;/PageContent&gt;
        &lt;/&gt;
    )
};

export default Error;</code></pre>
<pre><code class="language-jsx">import { useLoaderData } form &quot;react-router-dom&quot;;

export default function Example () {
    const loaderData = useLoaderData();

    if (loaderData.isError) {
        console.log(loaderData.message);  // Could not fetch data
    }

    return &lt;subExample data={loaderData}&gt;
};

export async function loader () {
    const response = await fetch(&quot;API URL&quot;);

    if (!response.ok) {
        throw new Response(JSON.stringify({message:  &quot;Could not fetch data&quot;}),
        {status: 500}
        )
    } else {
        const result = await response.json();
        return result
    }
};</code></pre>
<h3 id="json-유틸리티-함수">Json() 유틸리티 함수</h3>
<p>new Response를 사용하는 대신 json 유틸리티 함수를 사용하면 코드도 줄어들고 가독성이 좋아집니다.</p>
<pre><code class="language-jsx">import { useLoaderData, json } form &quot;react-router-dom&quot;;

export default function Example () {
    const loaderData = useLoaderData();

    if (loaderData.isError) {
        console.log(loaderData.message);  // Could not fetch data
    }

    return &lt;subExample data={loaderData}&gt;
};

export async function loader () {
    const response = await fetch(&quot;API URL&quot;);

    if (!response.ok) {
        return json({message: &quot;데이터 통신 오류&quot;},{status: 500})
    } else {
        const result = await response.json();
        return result
    }
};</code></pre>
<pre><code class="language-jsx">    const error =    useRouteError();
    //  json 객체 처리하기 
    if (error.status === 500) {
        const message = error.data.message 
    };</code></pre>
<h2 id="동적-라우트와-loader">동적 라우트와 loader()</h2>
<p>loader는 파라미터로 객체를 전달하는데, 객체에서 request와 params에 접근할 수 있습니다.  request 값을 통해 url 등의 값에 접근할 수 있고, params로 동적 라우트 파라미터에 접근할 수 있습니다. </p>
<pre><code class="language-jsx">function loader ({ request, params}) {
    const id = params.eventId;

    const res = fetch(&quot;BASE URL&quot; + id);

    if(!res.ok){
        return json({message: &quot;NO DATA&quot;},{status: 500})
    } else {
        return res
    }
};</code></pre>
<blockquote>
<p><em>가장 일반적인 사용 사례는 <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL">URL을</a> 생성하고 여기에서 <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams">URLSearchParams를</a> 읽는 것입니다 .</em></p>
<pre><code class="language-jsx">function loader({ request }) {
const url = new URL(request.url);
const searchTerm = url.searchParams.get(&quot;q&quot;);
return searchProducts(searchTerm);
}</code></pre>
<p><em>여기의 API는 React Router에 특화된 것이 아니라 표준 웹 객체( <a href="https://developer.mozilla.org/en-US/docs/Web/API/Request">Request</a> , <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL">URL</a> , <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams">URLSearchParams</a> )입니다 .</em></p>
</blockquote>
<h2 id="userouteloaderdata">useRouteLoaderData</h2>
<blockquote>
<p><em>이 후크는 <strong>현재 렌더링된 경로의 데이터를 트리의 어디에서나 사용</strong>할 수 있게 합니다. 이는 트리의 깊은 곳에서 훨씬 더 위쪽 경로의 데이터가 필요한 구성 요소와 트리의 더 깊은 곳에 있는 자식 경로의 데이터가 필요한 부모 경로에 유용합니다.</em></p>
</blockquote>
<p>useLoaderData는 해당 컴포넌트가 특정 라우트에 매핑될 때, 이 라우트에 정의된 loader 함수가 데이터를 반환하고, useLoaderData를 사용하여 이 데이터를 접근할 수 있습니다.</p>
<p>하지만, 현재 라우트가 아닌 다른 라우트의 loader 데이터도 접근하기 위해서는 useRouteLoaderData 사용해야 합니다.</p>
<p>이 부모 라우트의 데이터를 사용하려면 부모 라우트에 id 프로퍼티를 추가해야 합니다. 이 훅은 useLoaderData와 거의 비슷하게 작동하지만, routeId를 인자로 받습니다.</p>
<pre><code class="language-jsx">createBrowserRouter([
  {
    path: &quot;/&quot;,
    loader: () =&gt; fetchUser(),
    element: &lt;Root /&gt;,
    id: &quot;root&quot;,
    children: [
      {
        path: &quot;jobs/:jobId&quot;,
        loader: loadJob,
        element: &lt;JobListing /&gt;,
      },
    ],
  },
]);</code></pre>
<pre><code class="language-jsx">const user = useRouteLoaderData(&quot;root&quot;);</code></pre>
<blockquote>
<p><em>사용 가능한 유일한 데이터는 현재 렌더링된 경로입니다. 현재 렌더링되지 않은 경로에서 데이터를 요청하면 후크가 <code>undefined</code>을 반환합니다</em></p>
</blockquote>
<h2 id="참고">참고</h2>
<p><a href="https://reactrouter.com/en/main/route/loader">[react-router-dom 공식문서]</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Lighthouse Performance 성능 79% 개선 (번들, 이미지 최적화)]]></title>
            <link>https://velog.io/@good_sang/Sweet-Home-Lighthouse-Performance-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-1</link>
            <guid>https://velog.io/@good_sang/Sweet-Home-Lighthouse-Performance-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0-1</guid>
            <pubDate>Sat, 27 Jul 2024 15:02:53 GMT</pubDate>
            <description><![CDATA[<h2 id="lighthouse-performance-개선하기">Lighthouse Performance 개선하기</h2>
<p>프로젝트 <strong>&#39;Sweet home&#39;의 성능 개선 과정</strong>을 공유해보고자 합니다. 개선점이 무엇이고, 어떻게 최적화했는지, 최적화 후 결과를 정리하였습니다. </p>
<h2 id="어떤-프로젝트인가요">어떤 프로젝트인가요?</h2>
<p><strong>&#39;Sweet Home&#39;</strong>는 인증/인가, 가상 계좌 거래, 결제, 상품 등록 등의 20개 이상의 API를 사용해서 <strong>가상의 쇼핑몰을 개발</strong>한 프로젝트입니다. </p>
<p>라이프 스타일 편집샵을 컨셉으로 생활용품과 가구 등을 구매할 수 있는 온라인 쇼핑몰을 구현하였습니다. </p>
<h2 id="성능-개선-목표">성능 개선 목표</h2>
<p><strong>상품 리스트 페이지</strong>는 쇼핑몰에서 <strong>사용자가 가장 많이 이용하는 페이지</strong>이기 때문에 이 페이지의 성능을 가장 먼저 개선하기로 했습니다. <strong>LightHouse</strong>로 성능을 측정해보니, 상품 리스트 페이지의 <strong>Performance 49점으로 성능이 &#39;나쁨&#39; 상태</strong>였습니다. </p>
<p>Performance 카테고리의 세부 측정 항목 수치를 고려해서 <strong>Performance 점수 80점 이상을 목표</strong>로 성능 개선을 진행했습니다. </p>
<blockquote>
<p><strong><a href="https://developer.chrome.com/docs/lighthouse/performance/performance-scoring?hl=ko">Performance 카테고리</a></strong></p>
</blockquote>
<ol>
<li>FCP(First Contentful Paint): 🔺 <strong>6.7s</strong></li>
<li>LCP(Lagest Contentful Paint): 🔺 <strong>14.7s</strong></li>
<li>TBT(Total Blocking Time): 🟢 <strong>0ms</strong></li>
<li>CLS(Cumulative Layout Shift): 🟨 <strong>0.152</strong></li>
<li>Speed Index: 🔺 <strong>6.9s</strong></li>
</ol>
<center><img src="https://velog.velcdn.com/images/good_sang/post/2efa704f-1339-4186-acc3-dd643a3cfb5b/image.png" width="500" alt="최적화 전 성능 이미지"><center/>


<h2 id="번들-크기-최적화">번들 크기 최적화</h2>
<p><strong>FCP(First contentful paint) 최적화</strong>하기 위해서 가장 먼저 <strong>번들 크기를 분석</strong>해서 초기 로딩 속도를 개선해 보기로 했습니다. </p>
<p>vite는 내부적으로 rollup 번들러를 사용해서 <strong><a href="https://www.npmjs.com/package/rollup-plugin-visualizer">rollup-plugin-visualizer</a></strong>를 통해 번들 크기를 시각적으로 분석했습니다. 분석 결과, 모든 리소스가 하나의 청크에 묶여 <strong>초기 로딩 시 리소스가 전부 로드</strong>되었습니다. </p>
  <center><img src="https://velog.velcdn.com/images/good_sang/post/1a818411-7f4b-49ce-b99d-a585d28cdc9d/image.png" width="500" alt="최적화 전 번들 이미지"><center/>




<p><strong>청크를 분리해서 크기를 줄이거나 필요한 시점에 청크를 동적으로 불러와서 페이지 로드 시간 단축</strong>했습니다. 다만, 청크들이 너무 세분화돼서 많아지면 성능에 부정적인 영향을 미칠 수 있기 때문에 적절하게 나눠야 했습니다.</p>
<h3 id="manualchunks로-라이브러리-청크-분리">manualChunks로 라이브러리 청크 분리</h3>
<p>vite.confing.js에 <strong>manualChunks 옵션</strong>을 적용했습니다. 이 옵션을 통해 <strong>특정 모듈에서 사용하는 외부 라이브러리를 메인 청크에서 분리하여 Lazy Load</strong>할 수 있었습니다. rollupOptions.output.manualChunks에 특정 라이브러리나 모듈을 명시적으로 지정해 별도의 청크로 분리합니다. </p>
<pre><code class="language-js">    rollupOptions: {
      output: {
        manualChunks: {
          react: [&quot;react&quot;, &quot;react-dom&quot;],
          reactRouter: [&quot;react-router-dom&quot;],
          swiperBundle: [&quot;swiper&quot;]
        }
      }
  }</code></pre>
<h4 id="manualchunks-적용-후-번들-모습">manualChunks 적용 후 번들 모습</h4>
<center><img src="https://velog.velcdn.com/images/good_sang/post/32e80027-f65d-4688-86c9-5a9f1ae9606b/image.png" width="500" alt="최적화 후 번들 사이즈"><center/>


<h3 id="page-단위로-code-splitting-적용">Page 단위로 Code Splitting 적용</h3>
<p>Page 단위로 Code Splitting 적용해서 사용자가 앱에 처음 접근했을 때, <strong>필요한 최소한의 코드만 로드</strong>되고 이후 사용자 동작에 따라 추가 코드가 로드되도록 개선했습니다. <strong>초기 로드 시간 및 파일 크기 감소에 도움이 되었습니다.</strong></p>
<h4 id="reactlazy">React.lazy</h4>
<p>React.lazy 함수를 사용하면 <strong>동적 import를 사용해서 컴포넌트를 렌더링</strong>할 수 있습니다. <strong>lazy 컴포넌트는 Suspense 컴포넌트 하위에서 렌더링</strong> 되어야 하며, Suspense는 lazy 컴포넌트가 로드되길 기다리는 동안 로딩 화면과 같은 컨텐츠를 보여줍니다.</p>
<p>페이지 전환 시 웹 페이지 로드 시간이 발생하며 대부분 페이지를 한 번에 렌더링하기 때문에 <strong>라우트 기반 코드 분할을 설정</strong>했습니다. </p>
<p>홈페이지는 Lazy Loading를 적용하지 않습니다. 그 이유는 사용자가 처음 홈페이지에 접속했을 때, 컨텐츠가 아직 로드되지 않을 수 있기 때문입니다. 만약 홈페이지의 하단 사이드바에 위치한 광고나 추가 콘텐츠가 있다면, 그 부분에는 lazy loading을 적용하는 것이 좋을 것 같습니다.</p>
<pre><code class="language-js">import { lazy, Suspense, type ReactElement } from &quot;react&quot;;
import { createBrowserRouter } from &quot;react-router-dom&quot;;
import Loading from &quot;~/components/common/Loading&quot;;
import App from &quot;~/App&quot;;
import Home from &quot;~/routes/Home/Home&quot;;
const About = lazy(() =&gt; import(&quot;~/routes/About/About&quot;));
const Shop = lazy(() =&gt; import(&quot;~/routes/Shop/Shop&quot;));
const ShopDetail = lazy(() =&gt; import(&quot;~/routes/Shop/ShopDetail&quot;));
const MyPage = lazy(() =&gt; import(&quot;~/routes/MyPage/MyPage&quot;));

  //중략

const SuspenseWrapper = ({ element }: { element: ReactElement }) =&gt; (
  &lt;Suspense fallback={&lt;Loading /&gt;}&gt;{element}&lt;/Suspense&gt;
);

export default createBrowserRouter([
  {
    path: &quot;/&quot;,
    element: &lt;App /&gt;,
    children: [
      {
        path: &quot;/&quot;,
        element: &lt;Home /&gt;
      },
      {
        path: &quot;/about&quot;,
        element: &lt;SuspenseWrapper element={&lt;About /&gt;} /&gt;
      },
      {
        path: &quot;/shop&quot;,
        element: &lt;SuspenseWrapper element={&lt;Shop /&gt;} /&gt;
      },
      {
        path: &quot;/shop/:id&quot;,
        element: &lt;SuspenseWrapper element={&lt;ShopDetail /&gt;} /&gt;
      },
      {
        path: &quot;/mypage&quot;,
        element: &lt;SuspenseWrapper element={&lt;MyPage /&gt;} /&gt;
      },

     // 하략</code></pre>
<h4 id="manualchunks와-code-splitting-적용-후-번들-모습">manualChunks와 Code Splitting 적용 후 번들 모습</h4>
<p><img src="https://velog.velcdn.com/images/good_sang/post/088c66c8-5104-4aaf-8289-3024a5fcdfbe/image.png" alt=""></p>
<h2 id="이미지-최적화">이미지 최적화</h2>
<p>상품 목록 페이지는 상품의 이미지가 많이 사용되기 때문에 해당 페이지의 이미지를 한 번에 불러오면 웹 페이지 로딩 속도가 느려질 수밖에 없습니다. <strong>화면에 보여지는 이미지만 로드해서 주요 컨텐츠를 빠르게 보여주는 방법을 적용했습니다.</strong></p>
<h3 id="intersection-observer-api-사용">Intersection Observer API 사용</h3>
<blockquote>
<p><strong><em>Intersection observer는 브라우저 뷰포트(Viewport)와 설정한 요소(Element)의 교차점을 관찰하며, 요소가 뷰포트에 포함되는지 포함되지 않는지 구별하는 기능을 제공합니다.</em></strong></p>
</blockquote>
<p>또한, <strong>비동기적으로 실행</strong>되기 때문에, scroll 같은 이벤트 기반의 요소 관찰에서 발생하는 <strong>렌더링 성능이나 이벤트 연속 호출 같은 문제 없이 사용</strong>할 수 있습니다. </p>
<h4 id="uselazyimageobserver-커스텀-훅">useLazyImageObserver 커스텀 훅</h4>
<ol>
<li>imageRef가 truthy하고 imageSrc가 falsy할 때, <strong>IntersectionObserver를 생성</strong>합니다.</li>
<li>IntersectionObserver는 <strong>이미지가 뷰포트에 10% 이상(threshold: [0.1])</strong> 보이게 되면(entry.isIntersecting) imageSrc를 src로 설정합니다.</li>
<li>이미지가 로드되면, 옵저버는 <strong>해당 이미지 관찰을 중지(unobserve)</strong>합니다.</li>
<li>useEffect 클린업 함수는 <strong>옵저버를 해제(disconnect)</strong>합니다.</li>
</ol>
<pre><code class="language-js">import { useEffect, useRef, useState } from &quot;react&quot;;

function useLazyImageObserver(src: string) {
  const [imageSrc, setImageSrc] = useState(&quot;&quot;);
  const imageRef = useRef(null);

  useEffect(() =&gt; {
    if (imageRef &amp;&amp; !imageSrc) {
      const observer = new IntersectionObserver(
        ([entry]) =&gt; {
          if (entry.isIntersecting) {
            setImageSrc(src);
            if (imageRef.current) {
              observer.unobserve(imageRef.current);
            }
          }
        },
        { threshold: [0.1] }
      );
      if (imageRef.current) {
        observer.observe(imageRef.current);
      }

      return () =&gt; {
        observer &amp;&amp; observer.disconnect();
      };
    }
  }, [imageSrc, imageRef]);

  return { imageRef, imageSrc };
}

export default useLazyImageObserver;
</code></pre>
<h4 id="lazyimage-컴포넌트에-uselazyimageobserver-훅-적용한-예시">LazyImage 컴포넌트에 useLazyImageObserver 훅 적용한 예시</h4>
<pre><code class="language-js">import useLazyImageObserver from &#39;./useLazyImageObserver&#39;;

const LazyImage = ({ src }) =&gt; {
  const { imageRef, imageSrc } = useLazyImageObserver(src);

  return  &lt;img ref={imageRef} src={imageSrc}  alt=&quot;상품 이미지&quot;/&gt;;
};

export default LazyImage;</code></pre>
<h4 id="비교">비교</h4>
<ul>
<li><strong>최적화 전</strong>
상품 리스트 페이지에서 사용하는 이미지를 모두 불러옵니다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/26e9ad30-807b-45ee-a2c0-328da39bc966/image.png" alt=""></p>
<ul>
<li><strong>최적화 후</strong>
뷰포트에 보이는 이미지만 먼저 불러옵니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/77a7da40-6c5e-408f-b5db-f29941e6ab5c/image.png" alt=""></p>
<h4 id="placeholder-제공">placeholder 제공</h4>
<p>뷰포트에 보이지만 임계점을 넘지 않아 이미지가 로드되지 않는 경우가 발생할 수 있습니다. <strong>이미지가 로드되지 않아 엑박이 뜨거나 CLS가 발생하는 것을 방지하기 위해서 기본 이미지를 설정</strong>했습니다. 기본 이미지는 용량이 작은 이미지를 사용했습니다.</p>
<ul>
<li><p><strong>임계점이 10% 넘지 않았을 때 엑박 발생</strong></p>
<center><img src="https://velog.velcdn.com/images/good_sang/post/983d7f6e-812c-466f-ab7c-8e91dab6bc53/image.png" width="500" alt="최적화 전 성능 이미지"></center>
</li>
<li><p><strong>기본 이미지 설정</strong></p>
<center><img src="https://velog.velcdn.com/images/good_sang/post/f18b5356-9a41-4284-9ec7-76467d595425/image.png" width="500" alt="최적화 전 성능 이미지"></center>


</li>
</ul>
<h3 id="imagemin을-사용해-이미지-압축">imagemin을 사용해 이미지 압축</h3>
<p>Lighthouse에서 <strong>&#39;Efficiently encode images&#39;</strong>부분 개선이 필요했습니다. <strong>압축되지 않은 이미지는 이미지의 리소스 로드 기간이 늘어나 LCP 시간이 길어지게 합니다.</strong> imagemin을 사용해서 이미지를 압축하여 이미지 용량을 줄였습니다.</p>
<center><img src="https://velog.velcdn.com/images/good_sang/post/06c88f00-4f4b-40ef-83db-bc54bf8255c7/image.png" width="500" alt="최적화 전 성능 이미지"></center>

<h4 id="imagemin">imagemin</h4>
<blockquote>
<p><strong><em>Imagemin은 다양한 이미지 형식을 지원하고 빌드 스크립트 및 빌드 도구와 쉽게 통합되므로 이미지 압축에 탁월한 선택입니다.</em></strong></p>
</blockquote>
<p>Imagemin은 CLI 및 npm 모듈로 사용할 수 있지만, 일반적으로 더 많은 구성 옵션을 제공하는 npm 모듈을 사용했습니다. </p>
<p>프로젝트에서 사용하는 <strong>특정 이미지 형식을 압축하는 npm 패키지를 설치</strong>해서 사용합니다. (예: &#39;mozjpeg&#39;는 JPEG를 압축, &#39;pngquant&#39;는 png 압축). </p>
<p><strong>플러그인을 선택할 때</strong> <strong>&#39;손실&#39;를 선택할 것인지, &#39;무손실&#39;을 선택할 것인지</strong>를 고려해야 합니다. 손실 압축은 이미지 품질이 저하될 수 있지만, 파일 크기를 줄일 수 있습니다.</p>
<p><strong>파일 크기를 크게 절약할 수 있고, 필요에 맞게 압축 수준을 맞춤 설정할 수 있어서 손실(lossy) 플러그인을 선택했습니다.</strong></p>
<h4 id="사용한-imagemin-플러그인">사용한 imagemin 플러그인</h4>
<pre><code class="language-js">    rollupOptions: {
      plugins: [
        visualizer({
          filename: &quot;./dist/report.html&quot;,
          open: true,
          gzipSize: true,
          brotliSize: true
        }) as PluginOption,
        viteImagemin({
          plugins: {
            jpg: imageminMozjpeg({ quality: 50 }),
            png: imageminPngQuant({ quality: [0.6, 0.8] })
          },
          makeWebp: {
            plugins: {
              jpg: imageminWebp(),
              png: imageminWebp()
            }
          }
        })
      ],
      ...
}</code></pre>
<ul>
<li><p><a href="https://www.npmjs.com/package/@vheemstra/vite-plugin-imagemin">@vheemstra/vite-plugin-imagemin</a>
Vite 빌드 과정에서 이미지 파일을 최적화하기 위해 vite-plugin-imagemin을 사용했습니다.</p>
</li>
<li><p><a href="https://www.npmjs.com/package/imagemin-mozjpeg">imagemin-mozjpeg</a>
imageminMozjpeg를 설정해 JPEG 이미지를 품질 50으로 압축했습니다.</p>
</li>
<li><p><a href="https://www.npmjs.com/package/imagemin-pngquant">imagemin-pngquant</a>
imageminPngQuant를 사용하여 PNG 이미지를 품질 0.6에서 0.8 사이로 압축했습니다.</p>
</li>
<li><p><a href="">imagemin-webp</a>
makeWebp에 imageminWebp를 사용하여 JPEG, png 이미지를 WebP 형식으로 변환합니다.</p>
</li>
</ul>
<h4 id="측정">측정</h4>
<p>다음 사진은 프로젝트에서 이미지 포맷을 jpeg를 사용해서 jpeg 최적화 적용된 부분만 측정한 이미지입니다. <strong>전체 이미지 용량이 65.51% 감소</strong>되었습니다. </p>
<center><img src="https://velog.velcdn.com/images/good_sang/post/96cf85b1-b825-48b7-bc25-8ab041e75bbe/image.png" width="500" alt="최적화 전 성능 이미지"></center>


<h2 id="최적화-결과">최적화 결과</h2>
<p>번들 크기와 이미지 최적화를 통해 Performance 점수가 49점에서 88점으로 <strong>79.59% 개선</strong>되었습니다. 성능 개선 목표 80점 이상을 달성하였습니다. </p>
<p>최적화 후 성능이 많이 개선되었지만, LCP와 CLS 항목에 아직 개선할 부분이 남아 있습니다.
캐싱을 활용하여 서버 응답 시간 단축하거나, 중요한 리소스를 미리 로드(preload), 폰트 최적화를 통해 성능 개선하는 등 추후 90점 이상을 목표로 추가로 성능을 개선해 보겠습니다.</p>
<blockquote>
<p><strong><a href="https://developer.chrome.com/docs/lighthouse/performance/performance-scoring?hl=ko">Performance 카테고리</a></strong></p>
</blockquote>
<ol>
<li>FCP(First Contentful Paint): 🔺 6.7s → 🟢 <strong>0.6s</strong></li>
<li>LCP(Lagest Contentful Paint): 🔺 14.7s → 🟨 <strong>1.7s</strong></li>
<li>TBT(Total Blocking Time): 🟢 0ms → 🟢 <strong>0ms</strong></li>
<li>CLS(Cumulative Layout Shift): 🟨 0.152 → 🟨  <strong>0.135</strong></li>
<li>Speed Index: 🔺 6.9s → 🟢 <strong>0.8s</strong></li>
</ol>
<center><img src="https://velog.velcdn.com/images/good_sang/post/80514670-df83-47c3-95f8-ee5405c65fd7/image.png" width="500" alt="최적화 후 성능 이미지"></center>



<h2 id="참고">참고</h2>
<ul>
<li><a href="https://tech.kakao.com/posts/587">FE 성능개선기 2부: 카카오 비즈니스폼</a></li>
<li><a href="https://velog.io/@seesaw/Vite-%EC%97%90%EC%84%9C-build-chunks-%EC%82%AC%EC%9D%B4%EC%A6%88%EB%A5%BC-%EC%A4%84%EC%97%AC%EB%B3%B4%EC%9E%90">Vite 에서 build chunks 사이즈를 줄여보자 (Some chunks are larger than 500 kB after minification 경고)</a></li>
<li><a href="https://sangminnn.tistory.com/entry/Vite%EC%9D%98-manualChunks%EC%98%B5%EC%85%98%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC">Vite의 manualChunks옵션에 관하여</a></li>
<li><a href="https://ko.legacy.reactjs.org/docs/code-splitting.html">code splitting</a></li>
<li><a href="https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API">Intersection Observer API</a></li>
<li><a href="https://fe-developers.kakaoent.com/2021/211202-gpu-intersection-observer/">카카오웹툰은 하드웨어 가속과 IntersectionObserver를 어떻게 사용했을까?</a></li>
<li><a href="https://velog.io/@yejinh/Intersection-Observer%EB%A1%9C-Lazy-Image-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">Intersection Observer로 Lazy Image 구현</a></li>
<li><a href="https://web.dev/articles/use-imagemin-to-compress-images?hl=ko">Imagemin을 사용하여 이미지 압축</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[브라우저 랜더링 과정 ( + reflow, repaint)]]></title>
            <link>https://velog.io/@good_sang/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%9E%9C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95-reflow-repaint</link>
            <guid>https://velog.io/@good_sang/%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EB%9E%9C%EB%8D%94%EB%A7%81-%EA%B3%BC%EC%A0%95-reflow-repaint</guid>
            <pubDate>Thu, 18 Jul 2024 06:50:05 GMT</pubDate>
            <description><![CDATA[<h2 id="브라우저-랜더링-과정">브라우저 랜더링 과정</h2>
<h3 id="html-파싱">HTML 파싱</h3>
<p><strong>1. 바이트 스트림</strong>
웹 서버에서 브라우저로 HTML 파일이 전송될 때, 데이터는 8비트 단위로 전송된다. 이 8비트 단위의 연속이 바이트 스트림을 형성한다.</p>
<p><strong>2. HTML 마크업으로 변환</strong>
바이트 스트림이 브라우저에 도착하면, 문자(character)로 변환한다.</p>
<p><strong>3. 토큰화</strong>
HTML 마크업을 토큰(token)으로 분해한다.
예: <code>&lt;html&gt;</code> 태그는 하나의 토큰, <code>&lt;/html&gt;</code> 태그는 또 다른 토큰으로 분해</p>
<blockquote>
<p>HTML 파싱에서 <strong>토큰</strong>이란 <strong>의미를 가지는 최소 단위</strong>를 뜻한다.</p>
</blockquote>
<p><strong>4. 노드로 변환</strong>
각 토큰의 관계성을 부여하기 위해 DOM(Document Object Model) 트리를 구성하는 Node로 변환한다.</p>
<p><strong>5. DOM 트리 생성</strong>
노드를 바탕으로 DOM 트리를 생성하여 HTML 문서의 구조를 계층적으로 나타낸다. 이 과정에서 link 와 img 태그를 만나면 다운로드 진행한다. 결과적으로 브라우저가 HTML문서를 렌더링할 때 사용된다. </p>
<blockquote>
<p><strong>script 위치에 따른 파싱 차이</strong>
HTML 파싱 중에 브라우저가 <code>&lt;script&gt;</code> 태그를 만나면 <strong>DOM 생성을 멈추고 해당 스크립트를 다운로드 및 실행</strong>한다. 이는 전체 페이지 로딩 속도에 영향을 준다. </p>
</blockquote>
<ul>
<li><strong>defer 속성을 사용했을 때</strong>
브라우저는 DOM 생성이 완료된 후 스크립트를 실행한다. <pre><code>&lt;script src=&quot;example.js&quot; defer&gt;&lt;/script&gt;</code></pre><ul>
<li><strong>async 속성을 사용했을 때</strong>
스크립트를 비동기적으로 다운로드하고 실행한다. 스크립트는 다운로드 즉시 실행되며, 스크립트 실행 도중에는 DOM 생성을 중단한다.<pre><code>&lt;script src=&quot;example.js&quot; async&gt;&lt;/script&gt;</code></pre></li>
<li><strong>스크립트를 <code>&lt;body&gt;</code> 태그의 끝에 배치했을 때</strong>
DOM이 거의 생성된 상태에서 스크립트가 로드되므로 DOM 생성이 중단되지 않는다.<pre><code>&lt;body&gt;
&lt;!-- HTML Content --&gt;
&lt;script src=&quot;example.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;</code></pre></li>
</ul>
</li>
</ul>
<h3 id="css-파싱">CSS 파싱</h3>
<p>  CSS도 HTML 파싱 과정과 비슷한 과정을 거쳐 CSSOM 트리를 생성한다. </p>
<p>  <code>바이트 스트림</code> -&gt; <code>문자화</code> -&gt; <code>토큰화</code> -&gt; <code>노드로 변환</code> -&gt; <code>CSSOM 트리 생성</code></p>
<h3 id="render-tree">Render Tree</h3>
<p> ** DOM 트리와 CSSOM 트리를 결합<strong>하여 랜더 트리를 생성한다. 각 요소가 화면에 어떻게 표시되어야 하는지 보여준다. 단, 렌더 트리는 시각적 요소들만 포함하며, **display: none 속성을 가진 요소들은 포함되지 않는다.</strong> 웹 접근성 부분에서 신경써야 하는 부분이다.</p>
<h3 id="layout">Layout</h3>
<p>레이아웃 단계에서는 렌더 트리의 각 요소들이 화면의 어디에, 어느 크기로 배치될지를 계산하여 배치한다.</p>
<blockquote>
<h4 id="reflow">Reflow</h4>
<p>  레이아웃 계산이 다시 수행되는 과정. 
DOM이나 CSSOM의 변경으로 인해 발생하며, 이는 성능에 큰 영향을 줄 수 있다. 특히, 트리의 상위 요소에서 변화가 발생하면 하위 요소들도 모두 재계산된다.</p>
</blockquote>
<pre><code>  position(top, left, bottom, right), width, height, margin, padding...</code></pre><h3 id="painting">Painting</h3>
<p>페인팅 단계에서는 각 요소에 대한 시각적 표현이 픽셀 단위로 그려진다. 색상, 테두리, 그림자 등 모든 스타일이 이 단계에서 적용된다.</p>
<blockquote>
<h4 id="repaint">Repaint</h4>
<p>리페인트는 요소의 스타일이 변경되어 화면에 다시 그려지는 과정. 
레이아웃 변화 없이 시각적 속성만 변경되는 경우 발생한다.</p>
</blockquote>
<pre><code>  background, box-shadow, border-radius, color...</code></pre><blockquote>
<h4 id="reflow-repaint-발생하지-않는-속성">Reflow, RePaint 발생하지 않는 속성</h4>
<p>  다음 CSS 속성은 DOM를 조작하지 않고, GPU 가속을 통해 성능을 최적화할 수 있다.</p>
</blockquote>
<pre><code>  transform, opacity</code></pre><h3 id="compositing">Compositing</h3>
<p>최종적으로, 브라우저는 여러 레이어를 결합하여 최종 화면을 구성한다. </p>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://html.spec.whatwg.org/multipage/parsing.html">HTML</a></li>
<li><a href="https://youtu.be/z1Jj7Xg-TkU?si=Br5Tc16g8leavSHi">브라우저는 어떻게 화면을 랜더링할까?</a></li>
<li><a href="https://mystudy.tistory.com/21">웹 브라우저에 대해서(2)</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Tanstack Query v4] 무한 스크롤 구현하는 방법 알아보기 ]]></title>
            <link>https://velog.io/@good_sang/Tanstack-Query-v4-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@good_sang/Tanstack-Query-v4-%EB%AC%B4%ED%95%9C-%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 17 Jul 2024 09:12:48 GMT</pubDate>
            <description><![CDATA[<h2 id="tanstack-query를-사용해서-무한-스크롤-구현하기">Tanstack Query를 사용해서 무한 스크롤 구현하기</h2>
<p>푸디로그에서 식당 리뷰를 보여주는 피드 목록을 무한 스크롤로 어떻게 구현하였는지 분석해보았다. </p>
<blockquote>
<p><strong>무한스크롤</strong>
Tanstack Query를 사용해서 데이터 페칭하고, react-infinite-scroller를 사용해서 페이지를 스크롤할 때 추가 데이터를 불러온다.</p>
</blockquote>
<h3 id="푸디로그에서-사용했던-버전">푸디로그에서 사용했던 버전</h3>
<ul>
<li>tanstack query 4.33.0버전</li>
<li>react-infinite-scroller ^1.2.6버전</li>
</ul>
<h3 id="useinfinite-query">useInfinite Query</h3>
<h4 id="훅에서-사용한-options">훅에서 사용한 Options</h4>
<p>다음 코드는 푸디로그 피드 목록을 불러오는 쿼리를 useFeedListQuery 커스텀 훅으로 래핑한 코드. </p>
<pre><code class="language-js">import { RestaurantCategory } from &quot;@/src/types/restaurant&quot;;
import { APIFeedResponse } from &quot;@@types/apiTypes&quot;;
import { getFeedList, getFeedListByUserId } from &quot;@services/feed&quot;;
import { useInfiniteQuery } from &quot;@tanstack/react-query&quot;;

interface useFeedListQueryProps {
  userId?: number;
  singleFeedId?: number;
  category?: RestaurantCategory;
}

const useFeedListQuery = ({ userId, singleFeedId, category }: useFeedListQueryProps) =&gt; {
  return useInfiniteQuery(
    [&quot;feedList&quot;, userId, category],
    async ({ pageParam = 0 }) =&gt; {
      let response;
      if (userId) {
        response = await getFeedListByUserId(userId, pageParam);
      } else {
        response = await getFeedList(pageParam, category);
      }

      const apiResponse = response.data;
      return apiResponse;
    },
    {
      getNextPageParam: (lastPage: APIFeedResponse) =&gt; {
        const lastFeed = lastPage.response.content.at(-1);
        if (lastPage?.response?.content?.length &lt;= 15) return undefined;
        return lastFeed?.feed.feedId;
      },
      enabled: !singleFeedId || !userId,
    }
  );
};
export default useFeedListQuery;</code></pre>
<ul>
<li><p>*<em>queryKey *</em>
기본적으로 쿼리 키에 따라 쿼리 캐싱을 관리한다.
쿼리 키에 종속 변수를 추가하면 쿼리가 독립적으로 캐시되고 변수가 변경될 때마다 쿼리가 자동으로 다시 페치된다.</p>
</li>
<li><p>** Query Functions: (context: QueryFunctionContext) =&gt; Promise<TData>**
프로미스를 반환하는 함수. 데이터를 반환하거나 오류 반환한다.</p>
</li>
<li><p><strong>getNextPageParam:  (lastPage, allPages, lastPageParam, allPageParams) =&gt; TPageParam | undefined | null</strong></p>
<p>다음 페이지를 가져올 파라미터를 반환해서 계속 다음 페이지를 불러올 수 있도록 한다.</p>
<p>위 예시 코드에선 lastPage 길이가 15를 초과하면 lastFeed의 FeedId를 반환하고, 길이 15이하면 불러올 다음 페이지가 없기 때문에 undefind 반환한다.</p>
<p>반환되는 값에 따라 hasNextPage 값이 boolean으로 반환된다.</p>
</li>
</ul>
<h4 id="훅에서-사용한-retruns">훅에서 사용한 Retruns</h4>
<p>다음 코드에서 useFeedListQuery를 호출하여 데이터를 페칭하고, InfiniteScroll 컴포넌트를 사용해 무한 스크롤을 구현한다. data.pages를 순회하며 피드 데이터를 렌더링한다.</p>
<pre><code class="language-js">  &quot;use client&quot;;
import { Fragment, useEffect, useRef, useState } from &quot;react&quot;;
import { getSingleFeed } from &quot;@services/feed&quot;;
import InfiniteScroll from &quot;react-infinite-scroller&quot;;
import Feed from &quot;@components/Feed/Feed&quot;;
import { Content } from &quot;@@types/feed&quot;;
import useFeedListQuery from &quot;@hooks/queries/useFeedListQuery&quot;;

interface FeedsProps {
  id?: number;
  startingFeedId?: number;
  singleFeedId?: number;
}

const Feeds = ({ id, startingFeedId, singleFeedId }: FeedsProps) =&gt; {
  const [singleFeedData, setSingleFeedData] = useState&lt;Content | null&gt;(null);
  const feedRef = useRef&lt;{ [key: number]: HTMLDivElement | null }&gt;({});

  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useFeedListQuery({ userId: id, singleFeedId });

  return (
    &lt;div className=&quot;flex flex-col pt-5 max-w-[640px] w-full mx-auto&quot;&gt;

        &lt;InfiniteScroll pageStart={0} loadMore={loadMore} hasMore={hasNextPage &amp;&amp; !isFetchingNextPage}&gt;
          {(data?.pages || []).map((page, index) =&gt; {
            console;
            if (!Array.isArray(page.response?.content)) {
              return null;
            }
            return (
              &lt;Fragment key={index}&gt;
                {page?.response.content.map((feedData: Content, index) =&gt; {
                  const { feed } = feedData;
                  const hasFeedId = feed?.feedId !== undefined;
                  const Key = hasFeedId ? feed.feedId : index;

                  return (
                    &lt;div
                      key={Key}
                      ref={(el) =&gt; {
                        if (hasFeedId) {
                          feedRef.current[feed.feedId] = el;
                        }
                      }}
                    &gt;
                      &lt;Feed key={Key} feedData={feedData} userId={id} /&gt;
                    &lt;/div&gt;
                  );
                })}
              &lt;/Fragment&gt;
            );
          })}
        &lt;/InfiniteScroll&gt;
      )}
    &lt;/div&gt;

};

export default Feeds;</code></pre>
<ul>
<li><strong>data</strong><ul>
<li><strong>data.pages</strong>:  모든 페이지를 포함하는 배열.</li>
</ul>
</li>
<li><strong>fetchNextPage: (options?: FetchNextPageOptions) =&gt; Promise<UseInfiniteQueryResult></strong>
다음 &quot;페이지&quot;의 결과를 가져올 수 있다</li>
<li><strong>hasNextPage: boolean</strong>
가져올 다음 페이지가 있는 경우 true. (getNextPageParam 반환값에 따라 boolean 반환) </li>
<li><strong>isFetchingNextPage: boolean</strong>
fetchNextPage 로 다음 페이지를 가져오는 동안에는 true.</li>
</ul>
<h4 id="infinitescroll">InfiniteScroll</h4>
<p>  react-infinite-scroller 라이브러리의 InfiniteScroll 컴포넌트는 <strong>스크롤 이벤트를 감지</strong>하여 스크롤이 뷰포트 끝에 도달하면 <strong>loadMore 함수를 호출하여 추가 데이터를 로드</strong>하는 기능을 제공한다.</p>
<pre><code class="language-js">  &lt;InfiniteScroll pageStart={0} loadMore={loadMore} hasMore={hasNextPage &amp;&amp; !isFetchingNextPage}&gt;
</code></pre>
<ul>
<li><strong>pageStart</strong> 
  <strong>초기 페이지 번호</strong>를 설정.</li>
<li><strong>loadMore</strong>
  <strong>추가 데이터를 로드할 때 호출되는 함수</strong>. 다음 데이터를 서버로부터 가져와서 현재 리스트에 추가한다.</li>
<li><strong>hasMore</strong>
  <strong>추가로 로드할 데이터가 있는지 여부를 나타내는 boolean.</strong>
  true이면 loadMore가 호출되어 더 많은 데이터를 가져오고, false이면 더 이상 데이터를 가져오지 않는다.
<code>hasMore={hasNextPage &amp;&amp; !isFetchingNextPage}</code>
  <strong>hasMore가 false일 때,</strong> 추가 데이터 요청 중지
 ** isFetchingNextPage가 true일 때(현재 데이터가 로딩 중일 때)**,  loadMore 함수를 호출하지 않도록 하여, 중복 요청을 방지한다.</li>
</ul>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery">Tanstack-Query 공식 문서</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2024 Epson Challenge] 회고(4): 데모데이]]></title>
            <link>https://velog.io/@good_sang/2024-Epson-Challenge-%ED%9A%8C%EA%B3%A04-%EB%8D%B0%EB%AA%A8%EB%8D%B0%EC%9D%B4</link>
            <guid>https://velog.io/@good_sang/2024-Epson-Challenge-%ED%9A%8C%EA%B3%A04-%EB%8D%B0%EB%AA%A8%EB%8D%B0%EC%9D%B4</guid>
            <pubDate>Thu, 04 Jul 2024 01:23:33 GMT</pubDate>
            <description><![CDATA[<h2 id="2024-epson-innovation-challenge-in-korea">2024 Epson Innovation Challenge in Korea</h2>
<blockquote>
<p><strong>Epson Innovation Challenge</strong>
엡손은 프린터와 스캐너의 새로운 사용자 경험과 문제 해결을 위해 <strong>혁신적인 개발자들과 관심 있는 모든 분을 위한 챌린지</strong>를 개최하였다. <strong>Epson Connect API를 기반으로 서비스를 기획 및 개발하는 챌린지</strong>이다.</p>
</blockquote>
<p>드디어 6월 7일부터 6월 24일까지 17일간의 챌린지가 모두 끝이 났다. 24일 정오 12시까지 제출이라 하루 만에 발표 자료를 만들어야 해서 <strong>팀원들이 날을 새면서 최종 발표 자료를 완성</strong>했다.</p>
<p>기획 배경부터 비즈니스 모델까지 AIGOO 서비스를 통해서 <strong>Epson 프린트에 어떠한 비즈니스 솔루션을 제시할 수 있는지</strong>에 대한 내용을 알차게 담으려고 노력했다. 우리 <strong>AIGOO 팀 끝까지 최선을 다해 챌린지 참여</strong>하느라 고생이 많았다!</p>
<h2 id="데모데이">데모데이</h2>
<p>대망의 데모데이. 챌린지에 참여했던 모든 팀이 자신들의 서비스를 피칭하고 <strong>최종 10팀의 발표를 듣기 위해 한자리에 모였다</strong>. 챌린지 동안 참 순탄하지 않았지만, 최종 결과물에 자신이 있었기 때문에 기대하는 마음으로 잠실 롯데타워로 향했다. </p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/9b4b09a2-e617-4b9a-aa2f-dd4f53757abc/image.jpeg" alt=""></p>
<p><strong>한국엡손 대표이사님의 개회사</strong>를 시작으로 데모데이가 시작되었다.
<img src="https://velog.velcdn.com/images/good_sang/post/b265a687-2823-4ee7-92eb-19928fdabf92/image.jpeg" alt=""></p>
<h3 id="피칭-시간">피칭 시간</h3>
<blockquote>
<p><strong>피칭(pitching)</strong>
심사위원 앞에서 자신들의 서비스에 대한 간략한 설명을 하는 것</p>
</blockquote>
<p>오전 10시부터 오전 12시 30분까지 <strong>선착순으로 피칭 시간이 주어졌다</strong>. 데모데이에서 모든 팀이 발표할 수 있는 게 아니라, <strong>최종 선발된 10팀만 발표할 수 있었다.</strong>. 열심히 만든 발표 자료와 소스코드를 모두 발표하지 못한다는 점이 아쉬웠지만, 발표 전에 선착순으로 피칭할 기회가 있어서 다행이었다. </p>
<p>제한된 시간 때문에 모든 팀이 발표할 수 없어서 <strong>혹시나 피칭 기회를 얻지 못할까 봐 조마조마했다</strong>. 다행히 10번째로 피칭을 할 수 있었다. 한국엡손 대표님과 엡손 본사 임원, 멋쟁이 사자 대표님, 연세대 컴퓨터공학과 교수님, 소프트웨어 협회 관련 심사위원 앞에서 우리 서비스를 피칭하였다. *<em>짧은 시간 내 우리 서비스를 최대한 설명하려고 애썼다. *</em></p>
<p>피칭 후 <strong>스캔 후 AI 기술을 활용한 부분</strong>에 대한 질문을 많이 받았다. 기억나는 질문은 스캔본을 얼마나 정확하게 텍스트를 인식하고 번역하는지에 대한 질문이었다. 수치상으로 정확도가 얼마인지를 질문하셨다. 그때 &#39;회사에서는 <strong>정확한 수치로 이야기할 줄 알아야겠구나</strong>&#39;라고 다시 한번 깨달았다. </p>
<p>다행히 <strong>백엔드 팀원이 질문에 답을 잘해준 덕분에</strong> 피칭은 만족스럽게 잘 마무리했다. 그렇게 챌린지에 참여한 기념으로 팀원들과 함께 사진 촬영을 했다. 너무 좋은 우리 팀. <strong>이때 마음만은 1등 한 기분</strong>이었다. </p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/33ea3841-0e73-4253-8ef4-e9d1d588aa0c/image.jpeg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/9197e855-6e9c-482e-949b-18f3e8353b7d/image.jpeg" alt=""></p>
<p>12팀의 피칭 시간이 끝이 나고, 점심을 먹었다. </p>
<p><strong><em>엡손에서 준비해 주신 점심 도시락! 든든하게 잘 먹었습니다 👍</em></strong></p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/7dbbdef9-d101-4764-b7c2-75bd86d5e1f5/image.jpeg" alt=""></p>
<h3 id="발표-시간">발표 시간</h3>
<p>점심시간이 끝난 뒤, <strong>최종 10팀을 발표</strong>하였다. 기대와 달리 10팀에 선정되지 못했다..! 꽤 괜찮은 서비스를 만들었다고 생각했는데, 다른 팀에 비해 부족한 부분이 있었나 보다! 아쉬움이 있었지만, 다른 팀의 발표를 들을 수 있다는 것만으로도 만족했다. 열심히 듣고 인사이트를 많이 얻어 가야겠다는 마음으로 초집중해서 발표를 들었다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/1da60b54-1e31-41ff-aa15-a02a2200a159/image.jpeg" alt=""></p>
<p><strong>주제와 관련한 산업의 지표들을 제시하며</strong> 서비스가 제시할 수 있는 솔루션들을 잘 개발해 낸 부분이 인상적이었다. <strong>AI 기술</strong>을 빼놓을 수가 없었는데, <strong>새로운 기술들을 아이디어와 잘 접목해 적재적소에 잘 반영한 것</strong>을 보고 많이 배웠다. <strong>당장 시중에 내놓아도 손색없을 만큼의 퀄리티</strong>였다. </p>
<p>개발자란 단순히 코드를 작성하는 사람이 아니라, <strong>주어진 리소스를 효율적으로 활용해서 비즈니스 솔루션을 실현하는 직업</strong>이라는 것. 10팀의 발표를 통해 직접 느끼며 개발자로서의 시야가 넓어지는 경험이었다. </p>
<p><strong>메모장에 10팀의 발표 내용을 요약</strong>해가며 적다 보니, 우리 팀의 부족한 점들을 무엇이었는지 알 수 있었다. 기획의 측면에서는 <strong>K-Culture 시장 분석이 부족</strong>했다. 실제 K-Culture 시장의 지표를 바탕으로 기획했다면 더 설득력 있는 서비스가 될 수 있었을 것이다. 기능적인 측면은 <strong>손 편지를 통한 소통 기능과 학습 자료 생성 기능</strong> 중 하나에 집중해서 세부적인 기능을 추가했다면 더 좋았겠다는 생각이 들었다.</p>
<h2 id="시상">시상</h2>
<blockquote>
<p><strong>1등</strong> - 유아기 신체 두뇌 발달을 위한 색칠 놀이 앱을 개발한 &#39;Chillin&#39;팀
<strong>2등</strong> - 인생 네컷 서비스를 개발한 &#39;체리쉬&#39;
<strong>3등</strong> - 육아일기 서비스를 만든 &#39;아이맘&#39;과 ai 기반 단어 카드 서비스 &#39;뽑아보카&#39;</p>
</blockquote>
<p>수상한 팀 모두 <strong>자신이 선택한 주제 안에서 Epson 비즈니스 솔루션을 잘 제시한 팀들</strong>이었다. 멋진 서비스 덕분에 많은 인사이트를 얻었습니다. 너무 고생하셨습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2024 Epson Challenge] 회고(3): 개발과 기술 멘토링]]></title>
            <link>https://velog.io/@good_sang/2024-Epson-Challenge-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EA%B0%9C%EB%B0%9C%EA%B3%BC-%EA%B8%B0%EC%88%A0-%EB%A9%98%ED%86%A0%EB%A7%81</link>
            <guid>https://velog.io/@good_sang/2024-Epson-Challenge-3%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-%EA%B0%9C%EB%B0%9C%EA%B3%BC-%EA%B8%B0%EC%88%A0-%EB%A9%98%ED%86%A0%EB%A7%81</guid>
            <pubDate>Sun, 30 Jun 2024 22:59:32 GMT</pubDate>
            <description><![CDATA[<h2 id="2024-epson-innovation-challenge-in-korea">2024 Epson Innovation Challenge in Korea</h2>
<blockquote>
<p><strong>Epson Innovation Challenge</strong>
엡손은 프린터와 스캐너의 새로운 사용자 경험과 문제 해결을 위해 혁신적인 개발자들과 관심 있는 모든 분을 위한 챌린지를 개최하였다. <strong>Epson Connect API를 기반으로 서비스를 기획 및 개발하는 챌린지</strong>이다.</p>
</blockquote>
<h2 id="개발">개발</h2>
<p><strong>AIGOO(아이고)</strong>는 Epson Innovation Challenge에 참여하며 <strong>AI 기술과 Epson Connect API</strong>를 활용해 <strong>아티스트와 글로벌 팬 간 손 편지 소통하는 서비스</strong>를 기획하였다. 이 회고에서 프로젝트의 주요 과정을 돌아보며 배운 점과 아쉬운 점을 정리하고, 앞으로의 개선 방향을 생각해 보고자 한다.</p>
<h3 id="기술-스택-선정-및-화면-설계">기술 스택 선정 및 화면 설계</h3>
<p><strong>프론트엔드 기술 스택</strong></p>
<ul>
<li><strong>Next.js</strong>: 서버 측 렌더링 기능을 제공하여 빠른 페이지 로딩과 향상된 SEO 가능. API routes를 통해 백엔드 서버 구축 가능.</li>
<li><strong>React-query</strong>: API 데이터를 효율적으로 관리 및 캐싱.</li>
<li><strong>Zustand</strong>: 간편한 상태 관리 가능.</li>
<li><strong>Tailwind CSS</strong>: 자체 유틸리티를 사용하여 디자인 작업을 효율적으로 진행 가능.</li>
</ul>
<p><strong>화면 설계</strong>
UX/UI 주도하에 와이어 프레임과 기능 설계서를 반영하여 화면 설계를 만들었다.
많은 상의 끝에 AIGOO의 기획과 어울릴 수 있도록 <strong>&#39;손편지&#39;라는 아날로그적 감성</strong>을 느낄 수 있는 디자인으로 가면 좋겠다고 정했다. 또한, <strong>편리하고 자연스러운 사용자 경험</strong>을 제공할 수 있도록 설계하였다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/ba214e5a-fa63-4319-96e6-91bef58ef712/image.png" alt=""></p>
<h3 id="개발-과정에서-새롭게-배운-점">개발 과정에서 새롭게 배운 점</h3>
<ul>
<li><strong>Epson Connect API를 활용하여 복합기와 연동</strong></li>
</ul>
<p>API를 통해 외부 기기를 작동시키는 작업은 처음이었는데, 생각보다 크게 복잡하거나 어려운 것은 아니었다. 오픈 API와 다를 거 없이 API_KEY와 DEVICE 값 등을 설정하고 주어진 엔드포인트로 API를 호출하면 된다.</p>
<p>시간상으로 백엔드에서 해당 API의 엔드포인트를 구축하였지만, 다음에는 직접 Next.js의 API 라우팅을 사용하여 API 엔드포인트를 만들어서 적용해 보고 싶다. </p>
<ul>
<li><strong>Next API routes와 Open API를 활용하여 키워드 의미 분석</strong></li>
</ul>
<p>Next.js API Routes를 사용하여 OpenAI API를 통해 키워드 분석하는 데이터를 받아왔다. 사용자는 API URL에 쿼리 스트링으로 키워드를 전달하고, 코드는 OpenAI API를 호출하여 키워드의 정의, 동의어, 반의어, 예시 문장 등을 JSON 객체 형태로 반환한다. </p>
<p>Next.js는 프론트엔드와 백엔드를 모두 개발할 수 있는 프레임워크이다. API Routes를 통해 서버리스 함수를 쉽게 작성할 수 있다. 앞으로 풀스택 프레임워크의 기능을 적극적으로 활용해 봐야겠다고 느꼈다. </p>
<p>알고 보면 크게 어렵지 않는 기능인데, 무엇이든지 일단 해봐야 아는 것 같다. 일단 해보면 걱정했던 것보다 크게 어렵지 않다는 걸 다시 느낀다. 새로 배우는 것에 두려움보다 도전하는 마음으로 많이 시도하며 부딪혀봐야겠다.</p>
<pre><code class="language-js">//app/API/gptapi/route.ts

import OpenAI from &quot;openai&quot;;
import type { NextRequest } from &quot;next/server&quot;;

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export const POST = async (req: NextRequest) =&gt; {
  try {
    const { searchParams } = req.nextUrl;
    const keyword = searchParams.get(&quot;keyword&quot;);
    if (!keyword) {
      return new Response(JSON.stringify({ error: &quot;키워드가 필요합니다.&quot; }), { status: 400 });
    }
    const response = await openai.chat.completions.create({
      response_format: { type: &quot;json_object&quot; },
      model: &quot;gpt-4o&quot;,
      messages: [
        {
          role: &quot;system&quot;,
          content:
            &quot;keyword를 분석하여 JSON 객체로 반환해. keyword가 한글이라면 translated: keyword를 영어로 번역한 값, definition: keyword의 영어 사전적 의미, synonyms: 한글 동의어 리스트, antonyms: 한글 반의어 리스트, example: 한글로 된 예시 문장, translatedExample: example을 영어로 번역한 값. keyword가 영어라면 translated: keyword를 한국어로 번역한 값,definition: keyword의 한글 사전적 의미, synonyms: 영어 동의어 리스트, antonyms: 영어 반의어 리스트, example: 영어로 된 예시 문장, translatedExample: example을 한국어로 번역한 값.&quot;,
        },
        {
          role: &quot;user&quot;,
          content: keyword,
        },
      ],
      temperature: 0,
      max_tokens: 300,
      top_p: 0,
      frequency_penalty: 0,
      presence_penalty: 0,
    });

    const completion = response.choices[0].message.content;
    return new Response(JSON.stringify({ completion }));
  } catch (error) {
    console.error(&quot;Error in handler:&quot;, error);
    return new Response(JSON.stringify({ error: `${error}` }));
  }
};</code></pre>
<ul>
<li><strong>서비스에 사용된 인공지능 기술</strong></li>
</ul>
<p>백엔드에서 손편지 번역과 키워드 분석, AI 학습 자료 생성 기능에 활용한 AI 기술이다. 이번 서비스를 구현하면서 AI 기술의 발전을 크게 느낄 수 있었다. 서비스에서 기대한 수준의 데이터를 받아볼 수 있었고, <strong>많은 인공지능 기술을 손쉽게 서비스에 적용</strong>해 볼 수 있었다는 점이 놀라웠다. </p>
<ul>
<li><p><strong>Naver CLOVA OCR</strong>은 이미지나 스캔본 문서에 있는 텍스트를 인식하여 텍스트 데이터로 변환하는 기술이다. 방대한 텍스트 데이터와 이미지 데이터를 학습하여 높은 정확도의 텍스트 인식을 수행한다.</p>
</li>
<li><p>ETRI(과학기술정보통신부)에서 제공하는 <strong>NLU OPEN API</strong>는 한국어 문장을 입력받아 한국어 어휘 형태 및 의미와 문장의 구조 및 의미를 분석하여 언어를 이해하는 기술을 제공하여 편지 내용이 정확하게 번역될 수 있도록 한다.</p>
</li>
<li><p>Google AI의 <strong>Gemini LLM</strong> (Large Language Model)를 사용하여 텍스트를 번역하고, 저장한 키워드를 기반으로 학습자료를 생성한다.</p>
</li>
</ul>
<p>AI 기술을 활용하여 손편지 번역, 키워드 분석, 학습 자료 생성 등의 기능을 구현하면서 AI 기술의 발전을 직접 확인할 수 있었다. 아직 AI 모델의 원리와 적용 방법에 대한 이해를 높이기 위해 더 많은 학습이 필요하지만, <strong>인공지능과 자연어 처리 기술을 적용하는 방법을 이해할 수 있는 좋은 기회</strong>였다.</p>
<h2 id="기술-멘토링">기술 멘토링</h2>
<h3 id="멘토와의-만남-및-상담">멘토와의 만남 및 상담</h3>
<p>멘토와의 만남을 통해 프로젝트 관련 질문뿐만 아니라 기술적인 질문에 상세한 답변으로 많이 배울 수 있었다.</p>
<h3 id="멘토링-내용">멘토링 내용</h3>
<ol>
<li><p><strong>Epson 제품 활용 방안</strong>
우리 서비스에서 어떻게 엡손에 대한 새로운 솔루션을 제시할 수 있을지가 중요하다</p>
</li>
<li><p><strong>Epson Connect API 피드백</strong>
API 사용 과정에서 발생했던 문제점이나 개선점 등 피드백을 전달하는 것도 필요하다.</p>
</li>
<li><p><strong>웹 서비스 보안 측면</strong>
웹 서비스 개발 시 보안에 대한 신중한 고려가 필요하다. 백엔드 코드 내 사용 시에는 컴팩트하게, 외부 사용 시에는 타이트하게 조절하여 보안 취약점을 줄여야 한다.</p>
</li>
<li><p><strong>발표 자료 방향</strong>
해당 서비스를 통해 Epson과의 어떤 시너지 효과를 창출할 수 있는지 명확하게 제시한다.</p>
</li>
</ol>
<h3 id="마무리">마무리</h3>
<p>이번 <strong>Epson Innovation Challenge</strong>에 참여하면서 Epson과 AI 기반의 서비스를 개발하는 새로운 경험을 쌓을 수 있었다.</p>
<p>배운 점 중 하나는 새로운 기술에 시도해보는 것이 중요하다는 것이다. 처음 접하는 Epson Connect API나 OpenAI API를 활용해 보면서, 예상보다 어렵지 않다는 것을 알게 되었고, 그 과정에서 <strong>새로운 기술을 배우고 적용하는 데 자신감을 얻을 수 있었다.</strong> AI와 같은 새로운 기술에 대해 지속해서 학습하며, 더 완성도 높은 서비스를 개발해 보고 싶다는 마음이 들었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2024 Epson Challenge] 회고 (2): API 교육과 네트워킹 데이]]></title>
            <link>https://velog.io/@good_sang/2024-Epson-Challenge-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-API-%EA%B5%90%EC%9C%A1%EA%B3%BC-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EB%8D%B0%EC%9D%B4</link>
            <guid>https://velog.io/@good_sang/2024-Epson-Challenge-2%EC%A3%BC%EC%B0%A8-%ED%9A%8C%EA%B3%A0-API-%EA%B5%90%EC%9C%A1%EA%B3%BC-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-%EB%8D%B0%EC%9D%B4</guid>
            <pubDate>Fri, 28 Jun 2024 05:37:30 GMT</pubDate>
            <description><![CDATA[<h2 id="2024-epson-innovation-challenge-in-korea">2024 Epson Innovation Challenge in Korea</h2>
<blockquote>
<p><strong>Epson Innovation Challenge</strong>
엡손에서 프린터와 스캐너의 새로운 사용자 경험과 문제 해결을 위해 혁신적인 개발자들과 관심 있는 모든 분을 위한 챌린지를 개최하였다. <strong>Epson Connect API를 기반으로 서비스를 기획 및 개발하는 챌린지</strong>이다.</p>
</blockquote>
<p><strong>24년 5월 27일(월) 기획서를 제출</strong>하고, 기획서 심사를 거쳐 <strong>06. 05 (수) 챌린지 합격자 발표</strong>가 났다. 우리 팀도 합격하여 6월동안 챌린지에 참여하게 되었다. </p>
<p><strong>챌린지 기간</strong>이 6월 7일부터 6월 24일까지 <strong>약 17일.</strong> 합격자 발표가 나자마자 기획서를 바탕으로 기능 명세서와 와이어프레임을 작성했다. </p>
<p>해커톤은 짧은 시간 내 우리의 핵심 기능을 보여줘야 하기 때문 <strong>기존 사이드 프로젝트 기능 설계와 조금 달랐다.</strong> 이전에 사이드 프로젝트는 인증/인가부터 시작해서 정말 하나의 웹사이트를 만드는 데 세세한 부분을 많이 신경 썼었다. </p>
<p>하지만, 해커톤은 <strong>짧은 시간 동안 집중적으로 작업</strong>하여 혁신적인 아이디어나 제품을 개발해야 한다. 완전한 웹사이트가 아닌 <strong>우리만의 아이디어를 설득할 수 있는 데모</strong>를 만들기 위한 기능 명세서를 작성하였다. </p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/898ac186-d24e-4681-8ffb-8f60da26f580/image.png" alt=""></p>
<h2 id="api-교육">API 교육</h2>
<p>Epson Challenge의 특이점은 <strong>Epson Connect API</strong>를 사용해야 한다는 점이다. Epson 복합기와 연결하여 <strong>서비스에서 스캔 또는 인쇄할 수 있어야 했다.</strong> Epson에서 챌린지에 사용할 수 있게 복합기를 제공해 주었다. 또한 이틀간 Epson Connect API 교육을 통해 참가자들이 서비스 적용할 수 있도록 도와주었다. </p>
<p><strong>API 교육</strong>은 ZOOM을 통해 비대면으로 진행되었다. 첫 번째 날엔 프린트 API, 두 번째 날엔 스캔 API에 대한 교육을 해주셨다. <strong>PHP와 python 샘플 코드를 통해 직접 엡손 기기와 연결하여 테스트해 볼 수 있었다.</strong></p>
<h2 id="네트워킹-데이">네트워킹 데이</h2>
<p>Epson의 기술력과 비전에 관해 설명듣고, 엡손 챌린지 참여자들이 모여 네트워킹할 수 있는 시간이었다.</p>
<blockquote>
<p><strong>네트워킹 데이</strong>
[1부] 18:00 ~ 19:00 - 솔루션 센터 안내 및 투어
[2부] 19:00 ~ 21:00 - 행사 및 저녁식사
[3부] 21:00 ~ - 정리 및 해산</p>
</blockquote>
<h3 id="솔루션-센터-안내-및-투어">솔루션 센터 안내 및 투어</h3>
<p>Epson은 <strong>탈탄소화와 자원 재활용을 촉진하는 환경 솔루션</strong>을 개발하는 등 친환경 기술력을 제시한다는 점이 인상 깊었다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/7539559e-a4cf-422e-818d-b811dd8135e9/image.jpeg" alt=""></p>
<p>이상봉 디자이너와 엡손이 협업하여 <strong>친환경 디지털 텍스타일 프린팅 기술</strong> 활용해 제작한 옷들을 볼 수 있었다.
<img src="https://velog.velcdn.com/images/good_sang/post/4662334d-a706-46f0-aa8c-a702f6218caf/image.jpeg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/294233cb-ca83-47e1-808b-3da3a6a2a4af/image.jpeg" alt=""></p>
<h3 id="기획-멘토링">기획 멘토링</h3>
<p>행사 당일, ZOOM으로 기획 멘토링도 진행되었다. 우리 팀은 중소벤처기업진흥공단 대구·경북연수원 원장으로 계신 이명선 님께서 기획서를 바탕으로 피드백을 해주셨다. </p>
<p><strong>주요 내용</strong></p>
<ol>
<li><strong>기획서의 부족한 내용에 대한 보충 설명</strong></li>
<li><strong>서비스의 현실 가능성, 상업성을 고려한 기획</strong></li>
</ol>
<p><strong>기획서 피드백</strong>
<strong>1. 서비스의 목적 명확화</strong>
서비스의 목적을 명확히 설정하는 것이 중요하다. 외국인 팬과의 소통을 중심으로 하되, 교육자료 제공을 통합하는 형태로 구체화함으로써 서비스의 방향성을 확실히 정할 수 있었다.</p>
<p><strong>2. 시장 규모 및 확장성 수치화</strong>
K팝 시장의 규모가 매우 크고 세계 시장에서의 영향력이 크다는 것은 알지만, 수치상으로 파악하여 추후 2차적으로 사업 확장할 가능성까지 고려해야 한다.</p>
<p><strong>3. 엡손과 서비스 연결성</strong>
엡손과의 기술적 협력을 통해 서비스와 엡손 비즈니스 솔루션을 제시하며 시너지 효과를 낼 수 있을지를 고려해야 한다.</p>
<p><strong>4. 금전적 수익 모델의 다양성</strong>
다양한 수익 모델을 고려하여 서비스의 사업성을 확보해야 한다. 구독 모델, 광고 수익, 프리미엄 콘텐츠 판매 등 여러 수익 창출 방법을 제시할 수 있어야 한다.</p>
<p><strong>5. 마케팅 전략과 지속 가능한 사업성</strong>
챌린지에서 그치는 것이 아니라, 마케팅 전략을 통해 고객층을 확장하는 방식도 필요하다.
또한, 지속 가능한 사업성을 확보하기 위해 고객 피드백을 반영하고, 실질적인 수익 모델을 제시해야 한다.</p>
<p>멘토링을 통해 <strong>기획의 전반적인 피드백</strong>을 얻을 수 있었고, 더 체계적이고 성공적인 서비스를 구축할 수 있는 방향성을 배울 수 있었다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/ea3b919b-9eda-4095-9034-a9f085eb8557/image.jpeg" alt=""></p>
<h3 id="저녁-식사">저녁 식사</h3>
<p>마지막으로 맛있는 저녁 식사를 준비해 주셨다. Epson 한국 대표님의 인삿말과 함께 <strong>한국에서는 처음 진행되는 챌린지에 대한 기대감</strong>을 들을 수 있었다. 기획서를 보시고 운영진도 결과물에 대해 많이 기대하고 있다고 하셨다. 결과물에 대한 욕심과 열정이 생겨났다. 그렇게 팀원들과 함께 식사를 마지막으로 네트워킹 데이를 잘 마무리하였다.
<img src="https://velog.velcdn.com/images/good_sang/post/57e5271b-1212-4ac9-a3be-9ac62eabf017/image.jpeg" alt=""></p>
<h2 id="마무리">마무리</h2>
<p>API 교육과 네트워킹 데이를 통해 <strong>인사이트를 얻은 뜻깊은 시간</strong>이었다.
네트워킹 데이를 통해 엡손의 혁신적인 기술력과 친환경 비전을 듣고, 참가자들과 교류할 수 있어 유익한 시간이었다. 또한, <strong>지속해서 발전하고 혁신적인 서비스란 무엇인지</strong> 깊게 고민할 수 있었고, 앞으로 실제 서비스로 구현할 수 있는 아이디어를 도출하는 데 도움이 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[2024 Epson Challenge] 회고(1): 팀 빌딩 및 기획서 제출]]></title>
            <link>https://velog.io/@good_sang/2024-Epson-Challenge-%ED%9A%8C%EA%B3%A0-%ED%8C%80-%EB%B9%8C%EB%94%A9-%EB%B0%8F-%EA%B8%B0%ED%9A%8D%EC%84%9C-%EC%A0%9C%EC%B6%9C</link>
            <guid>https://velog.io/@good_sang/2024-Epson-Challenge-%ED%9A%8C%EA%B3%A0-%ED%8C%80-%EB%B9%8C%EB%94%A9-%EB%B0%8F-%EA%B8%B0%ED%9A%8D%EC%84%9C-%EC%A0%9C%EC%B6%9C</guid>
            <pubDate>Fri, 28 Jun 2024 04:43:38 GMT</pubDate>
            <description><![CDATA[<h2 id="2024-epson-innovation-challenge-in-korea">2024 Epson Innovation Challenge in Korea</h2>
<p>지인의 소개로 알게 된 Epson Challenge. <strong>이번 해커톤을 통해 새로운 분야의 솔루션을 제시하고, 해결해볼 수 있는 기회라고 생각해 도전하였다.</strong> 주제는 교육, 이커머스, K-Culture로, 제시된 주제로 다양한 서비스를 구상해 볼 수 있을 것 같았다. 또한, Epson 복합기와 연결하는 Epson Connect API를 활용해 볼 수 있는 기회여서 개발자로서 새로운 경험을 해볼 수 있는 좋은 기회였다. </p>
<blockquote>
<p><strong>Epson Innovation Challenge</strong>
엡손에서 프린터와 스캐너의 새로운 사용자 경험과 문제 해결을 위해 혁신적인 개발자들과 관심 있는 모든 분을 위한 챌린지를 개최하였다. <strong>Epson Connect API를 기반으로 서비스를 기획 및 개발하는 챌린지</strong>이다.</p>
</blockquote>
<p><strong>[ 일정 안내 ]</strong></p>
<ul>
<li><strong>팀별 참가 신청 및 기획서 제출</strong>: <strong>04. 29 (월) ~ 05. 27 (월)</strong></li>
<li><strong>기획서 심사</strong>: <strong>05. 28 (화) ~ 05. 31 (금)</strong></li>
<li><strong>합격 팀 발표</strong>: <strong>06. 05 (수)</strong></li>
<li><strong>챌린지 진행 (온라인)</strong>: <strong>06. 07 (금) ~ 06. 29 (토)</strong></li>
<li><strong>네트워킹 행사(오프라인)</strong>: <strong>06. 12 (수)</strong></li>
<li><strong>데모데이</strong>: <strong>06. 29 (토)</strong></li>
</ul>
<h2 id="팀-빌딩">팀 빌딩</h2>
<p>함께 스터디를 진행하던 프론트엔드 개발자 2분과 함께 팀을 꾸렸다. 그리고 Epson Challeng에서 제공하는 팀빌딩 디스코드를 통해서 팀원을 모집했다.</p>
<p>전에 사이드 프로젝트 팀원 구할 때 매우 많은 분이 지원해서 팀원을 모집하는 게 어렵지 않겠다고 생각했다. 하지만, 생각과 달리 연락이 많이 오지 않았다. 마감일까지 팀 신청과 함께 기획서까지 제출해야 했기에 아주 초조했다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/6d6c9560-c88d-4a56-932b-95c4383cc1e5/image.png" alt=""></p>
<p>타 사이트에도 팀원 모집 공고를 올려보았지만, 정해진 기한까지 연락이 오지 않았다. 언제까지 기다리기만 할 수 없어서 <strong>팀을 구하는 분들께 직접 연락해 보기로 했다.</strong> 팀 빌딩 디스코드에 올라온 분들에게 우리의 팀과 대략적인 방향성을 설명해 드리고 팀 조인을 요청했다. </p>
<p>우선, 경력이 많지 않더라도 적극적으로 참여가 가능한 분들께 여쭤보았다. 대부분 다른 팀과 이미 커넥트가 있어서 이 또한 팀원을 모으는 게 쉽지 않았다. 하지만, 포기하지 않고, 물어보았다.</p>
<p>그렇게 마감 일주일 정도를 남기고, 백엔드 2분과 UX/UI 1분을 팀원으로 모집할 수 있었다. 그렇게 <strong>프론트엔드 3명, 백엔드 2명, UX/UI 1명</strong>으로 팀을 구성하였다. </p>
<h2 id="기획서-작성">기획서 작성</h2>
<p>지체할 시간없이 어떤 서비스를 만들지 기획을 진행해야 했다. 주제는 <strong>K-culture</strong>로 잡았다. K-pop을 필두로 한국 문화에 대해 세계적인 관심이 점점 커지고 있다. K-Culture와 Epson을 협업하여 기존에 없었던 혁신적인 서비스를 기획해 보고자 주제를 선정했다. </p>
<p><strong>브레인스토밍</strong>을 통해 K-Culture 주제에 대해 가감 없이 이야기하여 <strong>구체적인 기획의 방향성</strong>을 정하였다. 처음에는 Epson 복합기와 연관된 주제를 생각하다 보니 다양한 주제를 내기 어려웠다. 그래서 <strong>우선 Epson 복합기를 중점에 두지 말고, 여러 가지 생각들을 던져보았다.</strong></p>
<p>K-Culture에는 K-Pop, K-Food, K-Beauty 등 세부적으로 다양하게 접근할 수 있었다. 어떤 주제들이 나왔는지 자세하게 말할 순 없지만, <strong>함께 의견을 내고 받아들이는 팀 분위기 덕분에 좋은 아이디어들을 많이 낼 수 있었다.</strong>  </p>
<blockquote>
<p><strong>심사 기준</strong>
<strong>제안 적합성(20점), 창의성(30점), 적절성(20점), 실현 및 활용 가능성(30점)</strong></p>
</blockquote>
<p>많은 아이디어 중에 심사 기준에 부합할 수 있도록 아이디어를 발전해 나갔다. </p>
<p><strong>1. 제안 적합성 (20점)</strong>
해커톤의 주제와 목적을 정확히 이해하고, 제안하는 아이디어가 주제와 밀접하게 연관되어 있는지 확인한다. 사용자 리서치나 설문조사를 통해 타깃 사용자들이 직면하고 있는 실제 문제를 반영한다.</p>
<p><strong>2. 창의성 (30점)</strong>
기존에 없던 새로운 아이디어를 제안한다. 유사한 솔루션이 있다면, 그들과의 차별성을 강조한다. AI와 같은 최신 기술을 활용도 포함된다.</p>
<p><strong>3. 적절성 (20점)</strong>
기술적, 시간적, 자원적 측면을 고려해서 제안한 솔루션이 현실적으로 구현 가능한지 평가한다. 솔루션이 실제로 시장이 필요한지, 시장에서 수용될 가능성이 있는지 고려한다. 단, 제안한 솔루션이 법적, 윤리적 문제를 일으키지 않도록 주의한다.</p>
<p><strong>4. 실현 및 활용 가능성 (30점)</strong>
솔루션을 구현하기 위한 구체적인 계획과 로드맵을 기술한다. 또한, 솔루션이 실제로 어떻게 활용될 수 있는지, 사용자들이 어떻게 사용할지 구체적인 시나리오를 작성한다.</p>
<p>위 내용을 바탕으로 <strong>AI 기술을 활용하여 K-pop 아티스트와 글로벌 팬 간 손편지로 소통하고, 편지 내용을 바탕으로 학습자료를 생성하는 서비스 &#39;AIGOO(아이고)&#39;</strong>를 기획하였다.</p>
<blockquote>
<p><strong>팀 이름: AIGOO(아이고)</strong> 
외국인 팬을 대상으로 한 서비스다 보니, 외국인들이 발음하기 쉽고, 귀엽게 느끼는 한국어인 &#39;아이고&#39;를 선정하였다. 한국에서 <strong>&quot;아이고 우리 아기&quot;</strong> 이렇게 친근함의 표현으로 사용되기도 하고, 영어 문장 <strong>&quot;I go&quot;</strong>와 비슷한 발음으로 <strong>중의적인 의미</strong>를 담고 있다.
<br/>
🔗 <a href="https://github.com/ChoEun-Sang/Epson-client"><strong>아이고 Github 바로가기</strong></a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘과 자료구조] 이진 탐색 트리 알아보기]]></title>
            <link>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%ED%8A%B8%EB%A6%AC-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%ED%8A%B8%EB%A6%AC-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 02 May 2024 17:00:11 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📖 &#39;누구나 자료구조와 알고리즘&#39;책을 공부한 내용을 담고 있습니다. </p>
</blockquote>
<h2 id="15장-이진-탐색-트리로-속도-향상">15장 이진 탐색 트리로 속도 향상</h2>
<p>데이터를 특정 순서로 정리할 때, 가장 효율적인 알고리즘은 무엇일까?
정렬 알고리즘은 아무리 빨라도 O(logN)이다. 정렬된 배열에선 삽입과 삭제할 때 O(N)이 걸린다. 
해시 테이블은 검색, 삽입, 삭제가 O(1)이지만, 순서를 유지하지 못한다.
순서를 유지하면서 빠른 검색, 삽입, 삭제가 가능한 자료구조하면 어떻게 해야할까?</p>
<h3 id="151-트리">15.1 트리</h3>
<p><strong>트리(tree)</strong>는 노드 기반 자료구조이다. 또 다른 노드 기반 자료구조인 <strong>연결 리스트</strong>는 노드와 다른 한 노드를 연결하는 링크를 포함한다. <strong>트리</strong>는 각 노드에 <strong>여러 노드의 링크</strong>를 포함할 수 있다. 
 <img src="https://velog.velcdn.com/images/good_sang/post/1da19ed9-5c45-49ee-97ec-02bdf2bff0e1/image.jpg" alt=""></p>
<p>이미지 출처: <a href="http://www.ktword.co.kr/test/view/view.php?no=4726">http://www.ktword.co.kr/test/view/view.php?no=4726</a></p>
<h4 id="트리의-구성요소">트리의 구성요소</h4>
<ul>
<li><strong>루트(Root):</strong> 가장 상위 노드 &quot;A&quot;</li>
<li><strong>부모:</strong> &quot;A&quot;는 &quot;B&quot;와 &quot;C&quot;의 부모</li>
<li><strong>자식:</strong> &quot;D&quot;와 &quot;E&quot;, &quot;F&quot;는 B의 자식</li>
<li><strong>조상과 자손:</strong> &quot;A&quot;는 나머지 노드의 조상, 나머지 노드는 자손</li>
<li><strong>레벨(Level)</strong>: 트리의 같은 줄(row)</li>
<li><strong>프로퍼티(Property):</strong> 균형 정도<ul>
<li><strong>균형 트리:</strong> 모든 노드에서 하위 트리의 노드 개수가 동일</li>
<li><strong>불균형 트리:</strong> 모든 노드에서 하위 트리의 노드 개수가 동일하지 않음<h3 id="152-이진-탐색-트리">15.2 이진 탐색 트리</h3>
</li>
</ul>
</li>
<li><em>이진 트리*</em>는 각 노드에 자식이 0개나 1개,2개다.</li>
<li><em>이진 탐색 트리*</em>는 다음 규칙이 추가된 트리다.</li>
<li>각 노드의 자식은 최대 &quot;왼쪽에&quot;에 하나, &quot;오른쪽&quot;에 하나다.</li>
<li>한 노드의 &quot;왼쪽&quot; 자손은 그 노드보다 작은 값만 포함할 수 있다. 오른쪽 자손이 그 노드보다 큰 값만 포함할 수 있다. </li>
</ul>
<h4 id="코드-구현">코드 구현</h4>
<pre><code class="language-js">class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(value) {
    const newNode = new TreeNode(value);
    if (!this.root) {
      this.root = newNode;
      return;
    }

    let currentNode = this.root;
    while (currentNode) {
      if (value &lt; currentNode.value) {
        if (!currentNode.left) {
          currentNode.left = newNode;
          return;
        }
        currentNode = currentNode.left;
      } else {
        if (!currentNode.right) {
          currentNode.right = newNode;
          return;
        }
        currentNode = currentNode.right;
      }
    }
  }
}
</code></pre>
<h3 id="153-검색">15.3 검색</h3>
<p>찾는 값이 61일 때, 루트 50보다 크기 때문에 오른쪽 하위 노드에서 값을 찾는다.
오른쪽 노드 75보다 작기 때문에 왼쪽 하위 노드 56로 내려가고, 56보다 크기 때문에 오른쪽 하위 노드에 61를 찾을 수 있다. 61를 찾는 데 총 4단계가 걸렸다.</p>
<h4 id="이진-탐색-트리-검색의-효율성">이진 탐색 트리 검색의 효율성</h4>
<p>이진 탐색 트리 검색 과정을 보면 각 단계마다 검색할 대상이 남은 노드의 반으로 줄어든다. 따라서 이진 탐색 트리 검색은 O(logN)이다. </p>
<h4 id="logn-레벨">log(N) 레벨</h4>
<p>이진 트리에 노드가 N개면 레벨이 약 logN개이다.</p>
<p>트리의 모든 노드가 채우져 있다고 가정하자. 하나의 레벨이 추가되면 트리 노드 개수가 대략 두 배로 늘어난다. 레벨 4인 노드에 레벨을 하나 추가하면, 노드 개수가 16개가 추가 되면서 트리 크기가 대략 2배로 늘어난다. log31를 하면 약 5가 된다.  따라서 노드 N개인 트리에서 모든 자리마다 노드를 두려면 logN 레벨이 필요하다. </p>
<p>이진 탐색 트리 검색은 선택한 각 수가 가능한 남은 값 중 반을 제거해서 정렬된 배열의 이진 검색과 효율성이 같다. 그래서 O(logN)의 시간 복잡도를 갖는다. </p>
<h4 id="코드구현">코드구현</h4>
<pre><code class="language-javascript">/**
기저조건: 노드가 없으면 None를, 찾고 있던 노드면 노드를 반환

elseif 찾고 있는 값이 현재 노드보다 작으면 왼쪽 노드만 재귀적으로 호출

현재 보다 크면 오른쪽 노드만 재귀적으로 호출
*/

class BinarySearchTree {
  // ... insert 메서드는 생략

  search(value) {
    let currentNode = this.root;
    while (currentNode &amp;&amp; currentNode.value !== value) {
      if (value &lt; currentNode.value) {
        currentNode = currentNode.left;
      } else {
        currentNode = currentNode.right;
      }
    }
    return currentNode;
  }
}
</code></pre>
<h3 id="154-삽입">15.4 삽입</h3>
<p>45를 삽입하려고 할때, 검색 4단계와 삽입 1단계가 걸린다. 즉 logN + 1이므로 O(logN)이다. 정렬된 배열에서의 삽입은 O(N)이 걸린다. 그래서 이진 탐색 트리는 색과 삽입 모두 O(logN)이므로 매우 효율적이다.</p>
<h4 id="코드-구현-1">코드 구현</h4>
<pre><code class="language-javascript">/**
현재 노드와 값을 비교해서 값이 작으면 왼쪽, 크면 오른쪽에 삽입해야 한다.
만약 왼쪽 자식이 없으면 그곳에 값이 들어간다. 이 코드가 기저 조건이다.
*/

class BinarySearchTree {
  // ... insert 메서드는 생략

  insert(value) {
    const newNode = new TreeNode(value);
    if (!this.root) {
      this.root = newNode;
      return;
    }

    let currentNode = this.root;
    while (currentNode) {
      if (value &lt; currentNode.value) {
        if (!currentNode.left) {
          currentNode.left = newNode;
          return;
        }
        currentNode = currentNode.left;
      } else {
        if (!currentNode.right) {
          currentNode.right = newNode;
          return;
        }
        currentNode = currentNode.right;
      }
    }
  }
}
</code></pre>
<h4 id="삽입-순서">삽입 순서</h4>
<p>무작위로 정렬된 데이터로 트리를 생성해야 균형 잡힌 트리가 생성된다.
정렬된 데이터는 불균형이 심하고 효율적이지 않다. 정렬된 데이터는 완벽히 선형이라 O(N) 걸린다. 균형 트리일 때만 검색이 대략 O(logN) 걸린다. </p>
<h3 id="155-삭제">15.5 삭제</h3>
<p>이진 탐색 트리에서 삭제 알고리즘은 다음과 같은 규칙을 따른다.</p>
<ul>
<li>삭제할 노드에 자식이 없으면 그냥 삭제한다.</li>
<li>삭제할 노드에 자식이 하나면 노드를 삭제하고 그 자식을 삭제된 노드가 있던 위치에 넣는다.</li>
</ul>
<pre><code class="language-js">class BinarySearchTree {
  // ... insert 메서드는 생략

  delete(value) {
    this.root = this.deleteRec(this.root, value);
  }

  deleteRec(root, value) {
    if (!root) {
      return root;
    }

    if (value &lt; root.value) {
      root.left = this.deleteRec(root.left, value);
    } else if (value &gt; root.value) {
      root.right = this.deleteRec(root.right, value);
    } else {
      // 노드를 찾음
      if (!root.left) {
        return root.right;
      } else if (!root.right) {
        return root.left;
      }

      root.value = this.minValue(root.right);
      root.right = this.deleteRec(root.right, root.value);
    }

    return root;
  }

  minValue(node) {
    let current = node;
    while (current.left) {
      current = current.left;
    }
    return current.value;
  }
}</code></pre>
<h4 id="자식이-둘인-노드-삭제">자식이 둘인 노드 삭제</h4>
<p>삭제된 노드 자리에 후속자 노드로 대체한다. 삭제된 노드 다음으로 큰 수가 후속자 노드다.</p>
<h4 id="후속자-노드-찾기">후속자 노드 찾기</h4>
<p>컴퓨터는 후속자 노드를 어떻게 찾을까?<br>삭제할 노드의 오른쪽 하위 트리에서 가장 작은 값을 가진 노드가 후속자 노드가 된다.</p>
<h4 id="이진-탐색-트리-삭제의-효율성">이진 탐색 트리 삭제의 효율성</h4>
<p>O(logN)이다. 삭제에는 검색 한 번과 연결이 끊긴 자식을 처리하느 단계가 추가로 필요하기 때문이다.</p>
<h3 id="156-이진-탐색-트리-다뤄보기">15.6 이진 탐색 트리 다뤄보기</h3>
<p>이진 탐색 트리는 데이터 삽입 삭제가 정렬된 배열보다 훨씬 빠르기 때문에 데이터를 자주 수정하는 경우 효율적이다. </p>
<h3 id="157-이진-탐색-트리-순회">15.7 이진 탐색 트리 순회</h3>
<p>모든 노드를 방문하는 과정을 자료 구조 순회라 부른다.
각 제목을 알파벳 순으로 출력하는 것은 중위 순회이다. </p>
<h3 id="요약">요약</h3>
<p>이진 탐색 트리는 데이터 구조에서 검색, 삽입, 삭제를 효율적으로 수행할 수 있는 구조로, 특히 데이터를 자주 수정해야 하는 경우에 매우 유리하다. 이진 탐색 트리의 주요 특성과 동작을 정리하면 다음과 같다.</p>
<ol>
<li><p><strong>이진 탐색 트리의 특성</strong></p>
<ul>
<li>각 노드의 자식은 최대 두 개.</li>
<li>왼쪽 자식 노드는 부모 노드보다 작고, 오른쪽 자식 노드는 부모 노드보다 크다.</li>
</ul>
</li>
<li><p><strong>시간 복잡도</strong></p>
<ul>
<li><strong>검색, 삽입, 삭제</strong>: 모두 평균적으로 O(logN)의 시간 복잡도를 가지며, 이는 트리가 균형을 이룰 때 성립한다.</li>
<li>최악의 경우, 즉 트리가 한쪽으로 치우친 경우(예: 정렬된 데이터를 삽입할 경우), 시간 복잡도는 O(N)까지 증가할 수 있다.</li>
</ul>
</li>
<li><p><strong>삽입과 삭제</strong></p>
<ul>
<li><strong>삽입</strong>: 새 노드를 적절한 위치에 삽입하여 트리 구조를 유지한다.</li>
<li><strong>삭제</strong>: 노드의 자식 수에 따라 다른 방법으로 처리된다. 자식이 하나인 경우 그 자식을 삭제된 노드의 위치로 이동시키고, 자식이 둘인 경우 후속자 노드를 찾아 대체한다.</li>
</ul>
</li>
<li><p><strong>트리 순회</strong></p>
<ul>
<li><strong>중위 순회(Inorder Traversal)</strong>: 왼쪽 자식, 현재 노드, 오른쪽 자식 순으로 방문하며, 이는 이진 탐색 트리에서 데이터를 정렬된 순서로 출력하는데 유용하다.</li>
</ul>
</li>
</ol>
<p>이진 탐색 트리는 정렬된 배열보다 삽입과 삭제가 빠르고, 해시 테이블보다 순서를 유지할 수 있다는 장점이 있다. 다만, 균형 잡힌 트리를 유지하기 위해서는 균형 트리 알고리즘(예: AVL 트리, 레드-블랙 트리 등)을 적용하는 것이 좋다.</p>
<pre><code class="language-javascript">// 이진 탐색 트리 구현 예시
class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(value) {
    const newNode = new TreeNode(value);
    if (!this.root) {
      this.root = newNode;
      return;
    }

    let currentNode = this.root;
    while (currentNode) {
      if (value &lt; currentNode.value) {
        if (!currentNode.left) {
          currentNode.left = newNode;
          return;
        }
        currentNode = currentNode.left;
      } else {
        if (!currentNode.right) {
          currentNode.right = newNode;
          return;
        }
        currentNode = currentNode.right;
      }
    }
  }

  search(value) {
    let currentNode = this.root;
    while (currentNode &amp;&amp; currentNode.value !== value) {
      if (value &lt; currentNode.value) {
        currentNode = currentNode.left;
      } else {
        currentNode = currentNode.right;
      }
    }
    return currentNode;
  }

  delete(value) {
    this.root = this.deleteRec(this.root, value);
  }

  deleteRec(root, value) {
    if (!root) {
      return root;
    }

    if (value &lt; root.value) {
      root.left = this.deleteRec(root.left, value);
    } else if (value &gt; root.value) {
      root.right = this.deleteRec(root.right, value);
    } else {
      if (!root.left) {
        return root.right;
      } else if (!root.right) {
        return root.left;
      }

      root.value = this.minValue(root.right);
      root.right = this.deleteRec(root.right, root.value);
    }

    return root;
  }

  minValue(node) {
    let current = node;
    while (current.left) {
      current = current.left;
    }
    return current.value;
  }

  inorderTraversal(node = this.root, result = []) {
    if (node) {
      this.inorderTraversal(node.left, result);
      result.push(node.value);
      this.inorderTraversal(node.right, result);
    }
    return result;
  }
}</code></pre>
<p>이 코드는 이진 탐색 트리의 기본적인 기능을 포함하고 있으며, 이를 통해 효율적인 데이터 검색, 삽입, 삭제, 그리고 트리 순회를 수행할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘와 자료구조] 노드 기반 자료 구조]]></title>
            <link>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%99%80-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EB%85%B8%EB%93%9C-%EA%B8%B0%EB%B0%98-%EC%9E%90%EB%A3%8C-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%99%80-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EB%85%B8%EB%93%9C-%EA%B8%B0%EB%B0%98-%EC%9E%90%EB%A3%8C-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Thu, 25 Apr 2024 10:27:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📖 &#39;누구나 자료구조와 알고리즘&#39;책을 공부한 내용을 담고 있습니다. </p>
</blockquote>
<h2 id="14장-노드-기반-자료-구조">14장 노드 기반 자료 구조</h2>
<h3 id="141-연결-리스트">14.1 연결 리스트</h3>
<blockquote>
<p><strong>연결 리스트(Linked List)</strong>
항목의 리스트를 표현하는 자료 구조</p>
</blockquote>
<p>연결 리스트는 배열과 다르게 동작한다. 연결 리스트 내 데이터는 연속된 메모리 블록이 아니라 컴퓨터 메모리 전체에 걸쳐 여러 셀에 퍼져 있을 수 있다. 
메모리에 곳곳에 흩어진 연결된 데이터는 노드(node)라 부른다. 그렇다면, 노드가 메모리 내에 서로 인접해 있지 않다면, 같은 연결 리스트에 속하는지 어떻게 알까? </p>
<p> 연결 리스트 내 데이터 옆에는 다음 노드의 메모리 주소도 포함된다. </p>
<p>연결 리스트 [&quot;a&quot;,&quot;b&quot;,&quot;c&quot;,&quot;d&quot;]가 있다. 하지만 데이터를 저장하는 데 첫 번째 셀에는 실제 데이터가 들어 있고, 두 번째 셀에는 다음 노드의 시작 메모리 주소(링크)가 들어 있다. 마지막 노드의 링크는 null이다. 각 링크를 따라 전체 리스트를 연결하면 연결 리스트가 된다. </p>
<table>
<thead>
<tr>
<th align="center">데이터</th>
<th align="center">링크</th>
</tr>
</thead>
<tbody><tr>
<td align="center">&quot;a&quot;</td>
<td align="center">1652</td>
</tr>
</tbody></table>
<br/>

<table>
<thead>
<tr>
<th align="center">데이터</th>
<th align="center">링크</th>
</tr>
</thead>
<tbody><tr>
<td align="center">&quot;b&quot;</td>
<td align="center">1983</td>
</tr>
</tbody></table>
<br/>

<table>
<thead>
<tr>
<th align="center">데이터</th>
<th align="center">링크</th>
</tr>
</thead>
<tbody><tr>
<td align="center">&quot;c&quot;</td>
<td align="center">2001</td>
</tr>
</tbody></table>
<br/>

<table>
<thead>
<tr>
<th align="center">데이터</th>
<th align="center">링크</th>
</tr>
</thead>
<tbody><tr>
<td align="center">&quot;d&quot;</td>
<td align="center">null</td>
</tr>
<tr>
<td align="center">### 14.2 연결 리스트 구현</td>
<td align="center"></td>
</tr>
<tr>
<td align="center">연결 리스트를 코드로 구현해 보면, Node와 LinkedList라는 두 클래스로 구현한다. Node 클래스는 &quot;once&quot;, &quot;upon&quot;, &quot;a&quot;, &quot;time&quot;이라는 문자열을 포함한 네 개의 노드로 이뤄진 리스트를 생성한다. next_node는 노드의 링크 역할을 하며, 실제 메모리 주소 대신 또 다른 Node 인스턴스를 참조한다. 그래도 결과는 같다. data 매서드는 노드의 데이터를 반환하고, next_node 매서드는 다음 노드를 반환한다. Linked List 클래스는 첫 번째 노드를 추적한다. 연결 리스트는 첫 번째 노드에만 즉시 접근할 수 있다.</td>
<td align="center"></td>
</tr>
</tbody></table>
<pre><code class="language-js">class Node {
    constructor(data) {
        this.data = data;
        this.next_node = null;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
    }

    add(data) {
        const newNode = new Node(data);
        if (!this.head) {
            this.head = newNode;
        } else {
            let current = this.head;
            while (current.next_node) {
                current = current.next_node;
            }
            current.next_node = newNode;
        }
    }
}
</code></pre>
<h3 id="143-읽기">14.3 읽기</h3>
<p>연결 리스트에서 읽기의 효율성을 알아보자. 연결 리스트는 배열처럼 1단계로 바로 값을 읽을 수 없다. 프로그램은 연결 리스트의 첫 번째 노드의 메모리 주소만 안다. </p>
<p>만약 세 번째 노드를 읽기 위해서는, 첫 번째 노드부터 링크를 따라 세 번째 노드로 가야 한다. 노드가 N일 때, 리스트의 마지막 노드를 읽으려면 N 단계가 걸린다. </p>
<ul>
<li><p>코드 구현: 연결 리스트 읽기</p>
<pre><code class="language-javascript">class LinkedList {
  // 기존 코드

  read(index) {
      let current_node = this.head;
      let current_index = 0;

      while (current_node &amp;&amp; current_index &lt; index) {
          current_node = current_node.next_node;
          current_index++;
      }

      return current_node ? current_node.data : null;
  }
}
</code></pre>
</li>
</ul>
<pre><code>read 매서드에 찾는 노드의 인덱스를 넣는다. 첫 번째 노드부터 접근한다. 현재 인덱스를 기록해 언제 원하는 인덱스에 도달하는지 안다. 

while문을 통해 현재 인덱스가 찾는 인덱스보다 작은 때까지 루프를 실행한다. 
다음 노드에 접근해 그 노드를 새 current_node로 만든다. current_index도 1씩 증가한다. 

원하는 노드에 도달하면 현재 노드의 값을 반환하고, current_node가 없으면, 리스트 끝에 도달했기 때문에 null를 반환한다. 


### 14.4 검색
리스트 검색은 리스트 내 값을 찾아서 그 인덱스를 반환하는 것이다. 연결 리스트의 검색 속도는 O(N)이다. 
리스트 검색 과정은 읽기와 비슷한 과정이다. 

```javascript
class LinkedList {
    // 기존 코드

    search(value) {
        let current_node = this.head;
        let current_index = 0;

        while (current_node) {
            if (current_node.data === value) {
                return current_index;
            }
            current_node = current_node.next_node;
            current_index++;
        }

        return null;
    }
}</code></pre><h3 id="145-삽입">14.5 삽입</h3>
<p>배열에서는 값을 삽입할 때 다른 데이터를 이동해야되기 때문에 효율성이 O(N)이었다. 연결 리스트에서는 데이터를 이동하지 않아도 되기 때문에 성능 면에서 이점이 있다. 맨 앞에 값을 삽입할 때는 1단계가 걸린다. 중간 데이터에 값을 삽입할 때는 이전 데이터의 링크를 연결하기 위해서 해당 인덱스를 찾아서 삽입한다. 최악의 경우, N + 1 단계 걸린다. </p>
<p>배열과 연결 리스트의 상황에 따른 성능 차이는 정반대이다.</p>
<table>
<thead>
<tr>
<th align="center">시나리오</th>
<th align="center">배열</th>
<th align="center">연결 리스트</th>
</tr>
</thead>
<tbody><tr>
<td align="center">앞에삽입</td>
<td align="center">최악의 경우</td>
<td align="center">최선의 경우</td>
</tr>
<tr>
<td align="center">중간에삽입</td>
<td align="center">평균적인 경우</td>
<td align="center">평균적인경우</td>
</tr>
<tr>
<td align="center">끝에삽입</td>
<td align="center">최선의 경우</td>
<td align="center">최악의 경우</td>
</tr>
</tbody></table>
<ul>
<li><p>코드 구현: 삽입</p>
<pre><code class="language-javascript">class LinkedList {
  // 기존 코드

  insert(index, data) {
      const newNode = new Node(data);

      if (index === 0) {
          newNode.next_node = this.head;
          this.head = newNode;
          return;
      }

      let current_node = this.head;
      let current_index = 0;

      while (current_node &amp;&amp; current_index &lt; index - 1) {
          current_node = current_node.next_node;
          current_index++;
      }

      if (current_node) {
          newNode.next_node = current_node.next_node;
          current_node.next_node = newNode;
      }
  }
}</code></pre>
</li>
</ul>
<h3 id="146-삭제">14.6 삭제</h3>
<p>배열과  연결 리스트의 시나리오 대조는 삽입과 똑같다.</p>
<p>연결 리스트 삭제에서 첫 번째 노드 삭제할 경우, 첫 번째 노드를 두 번째 노드로 바꾸면 되고, 다른 노드를 삭제할 경우, 삭제하려는 노드의 앞의 노드에 접근해서 해당 노드의 링크를 삭제하려는 노드의 뒤 노드를 가리키도록 바꾼다. 삭제된 노드는 연결리스트에서만 제거될 뿐, 메모리에는 남게 된다. </p>
<ul>
<li>코드 구현</li>
</ul>
<pre><code class="language-js">class LinkedList {
    // 기존 코드

    delete(index) {
        if (index === 0 &amp;&amp; this.head) {
            this.head = this.head.next_node;
            return;
        }

        let current_node = this.head;
        let current_index = 0;

        while (current_node &amp;&amp; current_index &lt; index - 1) {
            current_node = current_node.next_node;
            current_index++;
        }

        if (current_node &amp;&amp; current_node.next_node) {
            current_node.next_node = current_node.next_node.next_node;
        }
    }
}
</code></pre>
<h3 id="147-연결-리스트-연산의-효율성">14.7 연결 리스트 연산의 효율성</h3>
<p>배열과 연결리스트의 시간 복잡도를 비교해보면 연결 리스트가 그렇게 효율적인진 않다. 연결 리스트를 효과적으로 쓰러면 삽입과 삭제 단계에서 O(1)이라는 점을 활요해야 한다. </p>
<h3 id="148-연결-리스트-다루기">14.8 연결 리스트 다루기</h3>
<p>연결 리스트는 삽입이나 삭제할 때 다른 데이터를 이동하지 않아도 된다는 점이 좋다. 만약 배열에서 1000개의 이메일에서 하나의 이메일을 삭제하려면, 많은 양의 데이터를 이동시켜야 한지만, 연결 리스트는 그렇지 않아도 된다는 점이 장점이다. </p>
<h3 id="149-이중-연결-리스트">14.9 이중 연결 리스트</h3>
<p>이중 연결 리스트는 연결 리스트의 변형 중 하나로, 각 노드에 2개의 링크가 있다는 특징이 있다. 한 링크는 다음 노드를, 다른 한 링크는 앞 노드를 가리킨다. 첫 노드 뿐만 아니라 마지막 노드를 알고 있어서, 마지막 노드 읽기, 삽입, 삭제할 때도 1단계 걸린다. </p>
<h4 id="앞과-뒤로-이동">앞과 뒤로 이동</h4>
<p>일반 연결 리스트는 링크에 다음 노드의 주소만 있기 때문에 앞으로만 이동 가능하다. 하지만 이중 연결 리스트는 앞과 뒤의 주소를 알고 있어서 앞 뒤로 이동할 수 있다. </p>
<h3 id="1410-이중-연결-리스트-기반-큐">14.10 이중 연결 리스트 기반 큐</h3>
<p>이중 연결 리스트는 O(1) 시간 복잡도로 데이터를 삽입/삭제할 수 있기 때문에 큐를 위한 완벽한 내부 자료구조다.</p>
<p>큐는 데이터의 끝에서 삽입할 수 있고, 앞에서만 삭제할 수 있다. 배열로 구현했을 때 삽입은 O(1)이지만, 삭제할 때 O(N)이 걸린다. 
반면, 이중 연결 리스트는 끝에서 삽입하고 앞에서 삭제하는 데 모두 O(1)이다. 따라서 큐를 구현하는데 완벽하다. </p>
<h3 id="요약">요약</h3>
<p>연결 리스트는 배열과 다른 방식으로 데이터를 저장한다. 연결 리스트는 동적 메모리 할당, 삽입, 삭제 등의 작업에서 유리하지만, 인덱스 접근 속도에서는 배열보다 느릴 수 있다. 이중 연결 리스트와 같은 변형을 통해 데이터 구조의 유연성을 높일 수 있다. 이중 연결 리스트는 큐와 같은 자료 구조를 효율적으로 구현하는 데 유용하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[푸디로그] Google Play Store에 PWA앱 등록하기]]></title>
            <link>https://velog.io/@good_sang/Google-Play-Store%EC%97%90-PWA-%EC%A0%9C%EC%B6%9C</link>
            <guid>https://velog.io/@good_sang/Google-Play-Store%EC%97%90-PWA-%EC%A0%9C%EC%B6%9C</guid>
            <pubDate>Wed, 24 Apr 2024 12:09:52 GMT</pubDate>
            <description><![CDATA[<p>사이드 프로젝트로 진행했던 <strong>푸디로그 앱</strong>을 실제 사용자에게 제공하고 피드백을 받아 앱을 개선해보기 위해서 Google Play Store에 프로덕션 신청을 해보았다. </p>
<p>PWA 앱을 안드로이드 앱 배포 형식으로 패키징하는 것부터 프로덕션 신청까지의 과정을 정리하였다.</p>
<h2 id="pwa를-구글-스토어에-등록하기">PWA를 구글 스토어에 등록하기</h2>
<p>푸디로그 앱은 <strong>next-pwa 라이브러리</strong>를 사용하여 PWA를 구성한 후, 이를 Google Play 스토어에 등록하기 위해선 먼저 PWA를 Android 앱으로 패키징해야 한다. 즉, <strong>PWA를 .aab(android app build)형식으로 빌드</strong>한다. </p>
<p>PWA를 AAB 형식으로 변환 방식은 <strong>Bubblewrap</strong>를 사용해 PWA를 <strong>TWA</strong>로 전환하고, 그 결과를 <strong>AAB파일로 패키징</strong>한다.</p>
<blockquote>
<p><strong>AAB(Android App Build)</strong>
여러 APK 파일을 하나의 번들로 합쳐 효율적으로 관리할 수 있게 하는 최신 안드로이드 앱 배포 형식.</p>
</blockquote>
<blockquote>
<p><strong>TWA(Trusted Web Activity)</strong> 
웹 앱을 안드로이드 앱처럼 동작하게 해주는 기술로, PWA를 최신 안드로이드 앱 배포 형식인 AAB으로 패키징해 구글 플레이 스토어에 등록될 수 있도록 도와준다. </p>
</blockquote>
<blockquote>
<p><strong>Bubblewrap</strong>
Google과 Microsoft가 협력하여 만든 오픈 소스 도구. PWA를 안드로이드용 Trusted Web Activity (TWA)로 쉽게 변환할 수 있도록 설계되어 있다. </p>
</blockquote>
<h2 id="bubblewrap-사용해-aab로-변환">Bubblewrap 사용해 AAB로 변환</h2>
<h3 id="0-pwa-준비">0. PWA 준비</h3>
<p>우선 next-pwa를 사용하여 PWA가 정상적으로 작동하도록 설정한다. manifest.json 파일과 서비스 워커 설정 포함한다.
PWA가 오프라인에서도 작동할 수 있도록 적절히 테스트하고 최적화한다.</p>
<h3 id="1-bubblewrap-설치-및-구성">1. Bubblewrap 설치 및 구성</h3>
<pre><code>$ npm install -g @bubblewrap/cli
</code></pre><h3 id="2-twa-프로젝트-초기화-및-구성">2. TWA 프로젝트 초기화 및 구성</h3>
<pre><code>$ bubblewrap init --manifest App-Manifest-URL
</code></pre><p>App-Manifest-URL: 웹 서버의 루트 URL 뒤에 /manifest.json를 붙이면 된다.</p>
<pre><code>tps://www.foodielog.shop/manifest.jsonient cho-eunsang$ bubblewrap init --manifest h </code></pre><h3 id="3-aab-파일-생성">3. AAB 파일 생성</h3>
<pre><code>$ bubblewrap build
</code></pre><blockquote>
<ul>
<li><strong>bubblewrap build</strong>
PWA를 구성한 설정에서 안드로이드 패키지를 빌드하는 명령어.</li>
</ul>
</blockquote>
<ul>
<li>(옵션) <strong>--skipPwaValidation</strong>
이 플래그는 PWA가 표준 PWA 기준(유효한 매니페스트 및 서비스 워커를 포함하는 등)에 부합하는지 검증하는 과정을 건너뛰라고 Bubblewrap에 지시한다. PWA가 모든 필요한 기준을 충족한다고 확신하는 경우에 빌드 과정을 가속화하고자 할 때 유용하다.</li>
</ul>
<h2 id="구글-개발자-계정-만들기">구글 개발자 계정 만들기</h2>
<p>안드로이드 앱을 Google Play Store에 등록하기 위해서 <a href="https://support.google.com/googleplay/android-developer/answer/6112435?hl=ko#zippy=%2C%EB%8B%A8%EA%B3%84-play-console-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EA%B3%84%EC%A0%95-%EB%A7%8C%EB%93%A4%EA%B8%B0%2C%EB%8B%A8%EA%B3%84-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%B0%B0%ED%8F%AC-%EA%B3%84%EC%95%BD%EC%97%90-%EB%8F%99%EC%9D%98%ED%95%98%EA%B8%B0%2C%EB%8B%A8%EA%B3%84-%EB%93%B1%EB%A1%9D-%EC%88%98%EC%88%98%EB%A3%8C-%EA%B2%B0%EC%A0%9C%ED%95%98%EA%B8%B0%2C%EB%8B%A8%EA%B3%84-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EA%B3%84%EC%A0%95-%EC%9C%A0%ED%98%95-%EC%84%A0%ED%83%9D%ED%95%98%EA%B8%B0%2C%EB%8B%A8%EA%B3%84-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%8B%A0%EC%9B%90-%EC%A0%95%EB%B3%B4-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0">Play Console</a>를 이용해야 한다. </p>
<p><a href="https://play.google.com/console/u/0/signup">Play Console 개발자 계정</a>을 만들고 설정해야 Google Play에서 Android 앱을 게시할 수 있다. 개발자 계정을 등록할 때 등록 수수료(미화 25달러)가 발생한다. 그외 개발자 신원 정보 확인 과정을 거쳐야 한다.</p>
<p>하지만 개발자 계정을 만들었다고 해서 바로 앱을 Play Store에 게시할 수 없다. </p>
<h2 id="테스트-요구사항-및-기기-인증-요구사항-충족하기">테스트 요구사항 및 기기 인증 요구사항 충족하기</h2>
<p><strong>기본 스토어 등록 정보 입력하기</strong>
앱 만들기의 첫 단계, 출시할 앱의 기본 정보를 입력해야 한다. 
이때, 앱 아이콘, 그랙픽 이미지 등 크기 기준에 맞는 이미지를 등록해야 된다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/48f51e06-9df1-46a7-b116-75f507b8e5d7/image.png" alt="">
<img src="https://velog.velcdn.com/images/good_sang/post/298c500b-86bd-4513-9199-63340de2fe56/image.png" alt="">
<img src="https://velog.velcdn.com/images/good_sang/post/73e63fa0-5aa7-4a06-a8d1-e39312d54e77/image.png" alt="">
<img src="https://velog.velcdn.com/images/good_sang/post/de90b429-8ac9-4b40-97f8-cfc9f9a31cef/image.png" alt=""></p>
<p><strong>콘텐츠 등급 확인하기</strong>
카테고리 - 설문지 - 요약 단계를 거쳐 등록할 앱에 대한 콘텐츠 등급을 받아야 한다. 
<img src="https://velog.velcdn.com/images/good_sang/post/139957f8-4da0-4a55-8f43-b85dfae4820e/image.png" alt="">
<img src="https://velog.velcdn.com/images/good_sang/post/a32f3655-f08c-47b4-82f0-8f72a19131bb/image.png" alt=""></p>
<p><strong>비공개 테스트 진행하기</strong>
최종 프로덕션을 신청하기 전 반드시 비공개 테스트를 진행해야 한다. 
2023년 11월 13일 이후에 개인 계정을 만든 개발자는 특정 테스트 요구사항을 충족해야 Google Play에 앱을 게시할 수 있다. </p>
<p>테스트 항목에 공개 테스트, 비공개 테스트, 내부 테스트가 있지만, 그 중 비공개 테스트를 필수적으로 진행해야 한다. </p>
<blockquote>
<p><strong>비공개 테스트</strong>
14일동안 테스터로 등록한 20명 이상의 테스터가 비공개 테스트에 참여해야 한다. 해당 기간동안 유의미한 테스트를 진행해야 프로덕션 신청에 통과할 수 있다. 
<img src="https://velog.velcdn.com/images/good_sang/post/1c01da9c-8073-4246-99a5-12ccd4d7a4c7/image.png" alt=""></p>
</blockquote>
<p>비공개 테스트 항목에서 테스터를 20명이상 등록해야 한다. 테스트 참여 방법에 등록된 링크를 통해 테스트에 참여할 수 있다. </p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/43d48702-e2bb-4bc3-a8b2-81e2932c6267/image.png" alt="">
<img src="https://velog.velcdn.com/images/good_sang/post/74e93797-9fbb-4df4-85d1-4cba4986408e/image.png" alt=""></p>
<p><strong>테스트 화면</strong> 
테스트 하기를 누르면 테스트가 시작된다. 구글 플레이에서 앱을 설치한 후 테스트를 진행하면 된다. 
단, 테스트 진행 기간에 프로그램 탈퇴를 하면 테스트 참여에서 제외되기 때문에 주의해야 한다. 
<img src="https://velog.velcdn.com/images/good_sang/post/eb1fdb23-5dc3-4844-bbf1-a55ba290b098/image.png" alt="">
<img src="https://velog.velcdn.com/images/good_sang/post/78e8bf97-82e7-4a58-88a7-6941c45b5fb3/image.png" alt=""></p>
<h2 id="프로덕션-신청하기">프로덕션 신청하기</h2>
<p>20명의 테스터를 모집하여 14일간 테스트를 진행했다. 드디어 구글 Play Store에 등록하기 위한 프로덕션 신청할 수 있게 되었다. </p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/e034f607-a2a8-4837-98c3-63751f64ffed/image.png" alt=""></p>
<p>하지만, 다음날 구글로부터 &#39;앱이 아직 프로덕션용으로 준비되지 않았습니다.&#39;라는 메일을 받았다. 프로덕션 반려된 이유는 다음과 같다. </p>
<ul>
<li>비공개 테스트 중 테스터 참여가 잘 되지 않았다.</li>
<li>테스터 피드백을 반영하지 않았다.</li>
<li>신청 질문에 불충분했다. </li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/a82282cd-66bc-43ba-ac17-8b09354ac5b9/image.png" alt=""></p>
<p>쉽게 통과될거라고 생각하진 않았지만, 생각보다 더 까다로운 과정이라는 것을 깨달았다. 특히, 안드로이드를 사용하는 테스터를 모집하기가 어렵고, 20명 이상의 테스터가 모두 14일간 성실하게 테스트를 참여하기가 힘들기 때문에 프로덕션 신청이 더더욱 어려울 수 있다. </p>
<p>만약 프로덕션 신청에 관심이 있다면, 카카오톡 오픈톡방에서 구글 Play Store 등록을 위한 테스터 모집하는 곳이 있으니 그 곳에서 테스터를 모아볼 수 있을 것이다.</p>
<p>Play Store에 PWA 앱을 등록하는 과정을 통해 단순한 개발을 넘어서 배포, 유지보수, 사용자 피드백 수집 등 여러 측면에서 개발자로서 역할을 경험하고 고민해볼 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘과 자료구조] 퀵 정렬에 대해 알아보기]]></title>
            <link>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%80%B5-%EC%A0%95%EB%A0%AC%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%ED%80%B5-%EC%A0%95%EB%A0%AC%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Thu, 18 Apr 2024 17:20:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📖 &#39;누구나 자료구조와 알고리즘&#39; 책을 공부한 내용을 담고 있습니다. </p>
</blockquote>
<h2 id="13장-속도를-높이는-재귀-알고리즘">13장 속도를 높이는 재귀 알고리즘</h2>
<p>실제 배열을 정렬할 때 버블 정렬과 선택 정렬, 삽입 정렬은 잘 사용하지 않는다. 컴퓨터 언어의 내장 함수를 사용해 시간과 노력을 아껴준다. 그 중 퀵 정렬(Quicksort)은 정렬 알고리즘으로 많이 쓰인다. </p>
<p>퀵 정렬 동작 방식을 통해 재귀가 어떻게 알고리즘의 속도를 크게 향상시키는지 배워보자.</p>
<h3 id="131-분할">13.1 분할</h3>
<p>배열을 분할(partition)한다는 것은 배열로부터 임의 수를 가져와(피벗) 피벗보다 작은 모든 수는 피벗의 왼쪽에 피벗보다 큰 모든 수는 피벗의 오른쪽에 두는 것이다. </p>
<table>
<thead>
<tr>
<th align="center">0</th>
<th align="center">5</th>
<th align="center">2</th>
<th align="center">1</th>
<th align="center">6</th>
<th align="center">3</th>
</tr>
</thead>
</table>
<ul>
<li>일관된 설명을 위해 가장 오른쪽 값을 항상 피벗으로 고른다. -&gt; 3</li>
<li>2개의 포인터는 가장 왼쪽과 오른쪽 값을 가르킨다.</li>
</ul>
<table>
<thead>
<tr>
<th align="center">0</th>
<th align="center">5</th>
<th align="center">2</th>
<th align="center">1</th>
<th align="center">6</th>
<th align="center">3</th>
</tr>
</thead>
<tbody><tr>
<td align="center">↑</td>
<td align="center"></td>
<td align="center"></td>
<td align="center"></td>
<td align="center">↑</td>
<td align="center">피벗</td>
</tr>
</tbody></table>
<p>다음과 같은 단계에 따라 실제로 분할한다.</p>
<ol>
<li><p>왼쪽 포인터를 한 셀씩 계속 오른쪽으로 옮기면서 피벗보다 크거나 같은 값에 도달하면 멈춘다.</p>
</li>
<li><p>이어서 오른쪽 포인터를 한셀씩 계속 왼쪽으로 옮기면서 피벗보다 작거나 같은 값에 도달하면 멈춘다. 또는 배열 맨 앞에 도달해도 멈춘다. </p>
</li>
<li><p>오른쪽 포인터가 멈춘 후에는 둘 중 하나를 선택해야 한다.왼쪽 포인터와 오른쪽 포인터가 가리키고 있는 값을 교환한 값을 교환한 후, 1,2,3단계를 반복한다. 왼쪽 포인터가 오른쪽 포인터에 도달했으면 (또는 넘어섰으면) 4단계로 넘어간다.</p>
</li>
<li><p>끝으로 왼쪽 포인터가 현재 가리키고 있는 값과 피벗을 교환한다.</p>
</li>
</ol>
<p>분할이 끝나면 피벗 왼쪽에 잇는 값은 모두 피벗보다 작고, 피벗 오른쪽에 있는 값은 모두 피벗보다 크다고 확실할 수 이있다. 또한, 다른 값들은 아직 완전히 정렬되지 않았지만 피벗 자체는 이제 배열 내에서 올바른 위치에 있다는 뜻이다.</p>
<ul>
<li>자바스크립트로 코드 구현<pre><code class="language-javascript"></code></pre>
</li>
</ul>
<pre><code>


### 13.2 퀵 정렬
퀵 정렬 알고리즘은 분할과 재귀로 이뤄진다. 피벗을 기준으로 왼쪽과 오른쪽 하위 배열로 나눠 분할 과정을 거친다.

|0|1|2|3|5|6|
|:---:|:---:|:---:|:---:|:---:|:---:|
||||피벗| ||

먼저, 피벗 기준 왼쪽 하위 배열을 분할해보자.

1. 왼쪽 하위 배열의 가장 마지막 값이 2를 피벗으로 한다.

|0|1|2|
|:---:|:---:|:---:|
|||피벗|

2.피벗 앞의 숫자 왼쪽, 오른쪽 포인터를 설정한다.

|0|1|2|
|:---:|:---:|:---:|
|↑|↑|피벗|

3. 왼쪽 포인터(0)와 피벗(2)을 비교한다. 0은 피벗보다 작으므로 왼쪽 포인터를 옮긴다.

|0|1|2|
|:---:|:---:|:---:|
||↑ ↑|피벗|

4. 왼쪽 포인터(1)와 피벗(2)을 비교한다. 1은 피벗보다 작으므로 왼쪽 포인터를 옮긴다.

|0|1|2|
|:---:|:---:|:---:|
||↑|↑ 피벗|

5. 왼쪽 포인터(2)와 피벗(2)가 동일하므로 왼쪽 포인터를 멈춘다. 

6. 이제 오른쪽 포인터를 동작시킨다. 오른쪽 포인터의 값(1)이 피벗보다 작으므로 그대로 둔다.

7. 왼쪽 포인터가 오른쪽 포인터를 지나쳤으므로 이번 분할에서 더이상 포인터를 이동시키지 않는다. 

8. 피벗과 왼쪽 포인터의 값을 교환해야 되지만, 동일한 값(2)을 가리키고 있기 때문에 아무런 변화가 없다.

9. 왼쪽 하위 배열의 요소가 0또는 1개 남을 때까지 분할 과정을 반복한다.

10. 오른쪽 하위 배열의 분할 과정도 동일하다. 피벗(3)를 기준으로 오른쪽 하위 배열을 분할한다.

11. 가장 끝의 값을 피벗으로 정한다. 왼쪽, 오른쪽 포인터가 모두 6를 가리킨다. 

|6|5|
|:---:|:---:|
|↑ ↑|피벗|

12. 왼쪽 포인터(6)과 피벗(5)를 비교한다. 6이 5보다 크기 때문에 포인터를 움직이지 않는다.

13. 오른쪽 포인터는 피벗보다 작은 값을 만났을 때까지 이동해야 되지만, 더이상 셀이 없으므로 오른쪽 포인터의 이동을 뭄춘다. 

14. 두 포인터가 멈췄으므로 왼쪽 포인터와 피벗의 값을 교환한다. 피벗(5)가 올바른 위치에 놓여졌다.

15. 5의 오른쪽 하위 배열을 재귀적으로 분할해야 한다. 하지만, 원소가 하나(6) 이기 때문에 기저 조건을 충족해서 재귀를 종료한다. 

### 13.3 퀵 정렬의 효율성
퀵 정렬의 효율성을 알아내려면 한 번 분할할 때의 효율성을 밝혀야 한다.
분할에 필요한 단계를 분류해 보면 2단계로 나눌 수 있다. 
- 비교: 각 값과 피벗을 비교한다.
- 교환: 왼쪽, 오른쪽 포인터의 값을 교환한다.

비교 횟수는 각 분할마다 배열 내 각 원소를 피벗과 비교하므로 최소 N번 비교한다. 

교환 횟수는 데이터가 어떻게 정렬되어 있는냐에 따라 다르다. 모든 값을 교환한다 해도 한 번에 두개의 값을 교환하므로 한 분할에 최대 N/2번 교환한다. 평균적으로 N/4번 정도 교환한다. 

한 번 분할할 때의 효율성은 O(N)이라고 할 수 있다. 

#### 한눈에 보는 퀵 정렬
퀵 정렬은 각 분할마다 하위 배열의 원소가 N개 일 때, 약 N단계 걸린다. 
결론적으로 모든 하위 배열의 크기를 합하면 퀵 정렬에 걸리는 총 단계 수가 나온다. 

배열의 원소가 8개일 때 약 21단계가 걸렸다. 각 분할 후 피벗이 하위 배열의 중간 부근에 놓일 것이라는 평균적인 시나리오를 가정한 것이다.

원소가 16개인 배열에 대해 퀵 정렬 단계 수를 보면 다음과 같다.

|N|퀵 정렬 단계 수(근사치)|
|:---:|:---:|
|4|8|
|8|24|
|16|64|
|32|160|

### 빅 오로 나타낸 퀵 정렬
앞서 봤던 패턴을 보면 다음의 표에 나오듯이 배열의 원소가 N개일 때 퀵 정렬에 필요한 단계 수는 약 N * logN이다.

평균적인 시나리오에서 배열을 분할할 때마다 하위 배열 두 개로 나누면 이 두 배열의 크기는 비슷하다. 크기가 N인 배열을 크기가 1이 될때까지 각 하위 배열을 반으로 나누려면 logN번 걸린다. 각 하위 배열의 분할 횟수를 합치면 N단계가 걸린다. 

하지만, O(N*logN)는 근사치이다. 정확한 단계를 나타내진 않는다.

|N|logN|N*logN|퀵 정렬 단계 수 (근사치)|
|:---:|:---:|:---:|:---:|
|4|2|8|8|
|8|3|24|24|
|32|5|160|160|


### 13.4 퀵 정렬의 최악의 시나리오
이전 가정은 평균적인 시나리오였다면, 최악의 시나리오는 피벗이 항상 배열의 끝에 있을 때다. 배열이 완전 오름차순 또는 내림차순일 때 일어날 수 있다. 

원소가 N개 일 때, N + (N-1)+ ...+ 1단계가 걸린다. N^2/2단계이므로 빅 오로 O(N^2)이다.

#### 퀵 정렬 대 삽입 정렬
||최선|평균|최악|
|:---:|:---:|:---:|:---:|
|삽입 정렬|O(N)|O(N^2)|O(N^2)|
|퀵 정렬|O(NlogN)|O(NlogN)|O(N^2)|

평균적인 상황에서는 퀵 정렬이 효율적이기 때문에 퀵 정렬을 사용해서 내장 정렬 함수를 구현하는 언어가 많다. 

### 13.5 퀵 셀렉트



### 13.6 다른 알고리즘의 핵심 역할을 하는 정렬
가장 빠른 정렬 알고리즘의 속도는 O(NlogN)이다. 배열의 중복 확인하는 문제에 정렬을 사용해서 더 효율적인 알고리즘을 만들 수 있다. 

```javascript
// 중복 확인 알고리즘</code></pre><p>정렬 (NlogN) 단계와 배열 순회 N 단계가 걸려서 결과적으로 O(NlogN)이 된다. 원래 O(N^2)걸리던 알고리즘에서 O(NlogN)으로 개선할 수 있다. 
정렬을 잘 사용하면 효율성을 크게 개선할 수 있겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Next/bundle-analyzer를 활용한 번들 사이즈 최적화]]></title>
            <link>https://velog.io/@good_sang/Nextbundle-analyze%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B2%88%EB%93%A4-%EC%82%AC%EC%9D%B4%EC%A6%88-%EC%B5%9C%EC%A0%81%ED%99%94</link>
            <guid>https://velog.io/@good_sang/Nextbundle-analyze%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B2%88%EB%93%A4-%EC%82%AC%EC%9D%B4%EC%A6%88-%EC%B5%9C%EC%A0%81%ED%99%94</guid>
            <pubDate>Sun, 14 Apr 2024 13:55:42 GMT</pubDate>
            <description><![CDATA[<h3 id="번들링이란">번들링이란?</h3>
<p>번들링은 개발 과정에서 작성된 다양한 코드 파일들을 하나 또는 여러 개의 파일로 묶는 과정을 의미한다. 이 과정은 애플리케이션의 로딩 시간을 단축시키고, 브라우저가 파일을 더욱 빠르게 해석할 수 있도록 도와준다.</p>
<h3 id="번들링의-중요성">번들링의 중요성</h3>
<p>번들링을 통해 파일 크기를 감소시키는 것은 웹 애플리케이션의 성능을 향상시키는 핵심 요소이다. 다음과 같은 방법으로 이루어진다</p>
<p>번들 사이즈가 증가하면 페이지 로딩 및 실행 시간이 증가하게 되며, 사용자 경험을 저하시키는 주된 요인 중 하나다. 디자이너는 이미지 크기를 최적화하고, 개발자는 스크립트 파일의 크기를 줄이는 데 집중해야 한다.</p>
<h3 id="번들-사이즈-최적화-방법">번들 사이즈 최적화 방법</h3>
<ol>
<li><strong>크기가 작은 라이브러리로 교체</strong>
동일/ 유사한 기능의 라이브러리 중 크기가 작은 라이브러리로 교체</li>
</ol>
<blockquote>
<p><a href="https://bundlephobia.com">Bundlephobia</a>
패키지의 번들 사이즈, 다운로드 시간, 그리고 다른 패키지와의 비교 사이트</p>
</blockquote>
<ol start="2">
<li><strong>Tree Shaking을 통한 최적화</strong></li>
</ol>
<blockquote>
<p><strong>Tree Shaking</strong>
사용되지 않는 코드(Dead Code)를 제거하여 번들의 크기를 줄이는 과정. 
이 기술은 모듈 번들러가 ES2015+의 모듈 시스템(즉, import와 export 구문)을 사용하는 코드에서 미사용 모듈을 식별하고 제거한다.</p>
</blockquote>
<h3 id="tree-shaking을-통한-최적화-비교">Tree Shaking을 통한 최적화 비교</h3>
<p>번들 사이즈 분석/시각화 도구 <strong>&#39;Bundle size analyzer&#39;</strong>를 사용해 번들 사이즈를 분석하고, Tree Shaking 전/후를 시각적으로 비교해보았다.</p>
<blockquote>
<p><a href="https://www.npmjs.com/package/@next/bundle-analyzer">Bundle size analyzer</a><br>번들 사이즈 분석 시각화 도구</p>
</blockquote>
<ul>
<li>Next.js 프로젝트용: @next/bundle-analyzer<pre><code>$npm install -D @next/bundle-analyzer</code></pre></li>
<li>next.config.js 파일 수정<ul>
<li>withBundleAnalyzer 준비</li>
<li>env 변수 ANALYZE 값에 따라 실행 결정<pre><code class="language-javascript">const withBundleAnalyzer = require(&quot;@next/bundle-analyzer&quot;)
({ enabled: process.env.ANALYZE === &quot;true&quot; });
module.exports = withBundleAnalyzer(nextConfig);</code></pre>
</li>
</ul>
</li>
<li>환경변수 설정을 위한 cross-env 설정<pre><code>$ npm install -D cross-env</code></pre></li>
<li>Package.json 내 analyzer 실행 커멘드 적용<pre><code class="language-javascript">&quot;scripts&quot;: {
  //...
  &quot;analyze&quot;: &quot;cross-env ANALYZE=true npm run build&quot;
  //...
},</code></pre>
</li>
</ul>
<h3 id="barrel-import-했을-때">Barrel import 했을 때</h3>
<pre><code class="language-javascript">export {Eye, EyeSlash} from &#39;@assets/icons&#39;;</code></pre>
<ul>
<li>70개의 아이콘을 index.ts에서 barrel import 했을 때,
Start size: 121.04KB
Parsed size: 85.12 KB
Gzipped size: 38.82 KB</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/13b68086-b7f6-4488-9257-138a4c58ed33/image.png" alt=""></p>
<ul>
<li>13개 이미지 파일을 index.ts에서 Barrel import 했을 때, 
Start size: 143.51KB
Parsed size: 113.5 KB
Gzipped size: 18.48 KB</li>
</ul>
<p><img src="https://velog.velcdn.com/images/good_sang/post/ea8a4141-5859-46dc-a8dd-3ef591683543/image.png" alt=""></p>
<h3 id="default-import로-수정했을-때">Default import로 수정했을 때</h3>
<ul>
<li><p>아이콘 파일
<img src="https://velog.velcdn.com/images/good_sang/post/c6ccfbc9-bbfc-4e63-9244-7c38f20dc06d/image.png" alt=""></p>
</li>
<li><p>이미지 파일
이미지 파일은 측정이 안 될 정도로 사이즈가 작아졌다.</p>
</li>
</ul>
<h3 id="최적화-결과">최적화 결과</h3>
<blockquote>
<p><strong>First Load JS</strong> Shared by all : <strong>188 KB *<em>에서 *</em>80.5 KB</strong>로  감소</p>
</blockquote>
<ul>
<li><p><strong>최적화 전</strong>
<img src="https://velog.velcdn.com/images/good_sang/post/bdb73b58-f0e2-404a-881e-e35977d65c4a/image.png" alt=""></p>
</li>
<li><p><strong>최적화 후</strong>
<img src="https://velog.velcdn.com/images/good_sang/post/7dece2c6-d910-4f10-be4a-b42fcb109649/image.png" alt=""></p>
</li>
</ul>
<h2 id="평가">평가</h2>
<p>바렐 파일을 사용하지 않고 각 컴포넌트를 독립적인 모듈로 분리하여 번들 사이즈를 크게 줄일 수 있었다. 추가로, 각 컴포넌트가 필요할 때만 로드하도록 next/dynamic을 사용한다면  초기 로드 시 번들 크기를 더욱 줄일 수 있을 것 같다. 추후에 next/dynamic도 적용해봐야겠다.</p>
<ul>
<li>next/dynamic 예시 코드<pre><code class="language-javascript">import dynamic from &#39;next/dynamic&#39;;
</code></pre>
</li>
</ul>
<p>const DynamicEye = dynamic(() =&gt; import(&#39;@assets/icons/Eye.svg&#39;));
const DynamicEyeSlash = dynamic(() =&gt; import(&#39;@assets/icons/EyeSlash.svg&#39;));</p>
<pre><code>
### 참고자료
- [쉽게 따라하는 프론트엔드 웹 어플리케이션 패키지 최적화](https://velog.io/@bluestragglr/쉽게-따라하는-프론트엔드-웹-어플리케이션-패키지-최적화)

- [lazy-loading](https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading)
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[실제 프로젝트에 적용해본 클린 아키텍쳐에 따르는 구조화]]></title>
            <link>https://velog.io/@good_sang/%EC%8B%A4%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B8-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EC%97%90-%EB%94%B0%EB%A5%B4%EB%8A%94-%EA%B5%AC%EC%A1%B0%ED%99%94</link>
            <guid>https://velog.io/@good_sang/%EC%8B%A4%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B8-%ED%81%B4%EB%A6%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EC%97%90-%EB%94%B0%EB%A5%B4%EB%8A%94-%EA%B5%AC%EC%A1%B0%ED%99%94</guid>
            <pubDate>Sat, 13 Apr 2024 17:43:25 GMT</pubDate>
            <description><![CDATA[<h2 id="클린-아키텍쳐에-따르는-구조화">클린 아키텍쳐에 따르는 구조화</h2>
<p>SearchUser 컴포넌트는 Next.js를 사용자 검색 기능을 구현한 예입니다. 사용자 입력에 따라 검색 결과를 동적으로 불러오고, 검색 히스토리를 보여주는 기능을 포함하고 있습니다. 이 컴포넌트를 클린 아키텍처 원칙에 따라 구조화하고 평가하기 위해 각 부분을 분석하고 개선점을 확인해보겠습니다.</p>
<pre><code class="language-javascript">const SearchUser: React.FC = () =&gt; {
  const [query, setQuery] = useState(&quot;&quot;);
  const [searchResults, setSearchResults] = useState&lt;APIUserSearchResponse[&quot;response&quot;][&quot;content&quot;] | []&gt;([]);
  const { searchHistory, setSearchHistory } = useSearchStore();

  const { toast } = useToast();

  const debouncedQuery = useDebounce(query, 500);
  const key = getKey();

  useEffect(() =&gt; {
    if (debouncedQuery === &quot;&quot;) {
      setSearchResults([]);
    } else {
      getSearches();
    }
  }, [debouncedQuery]);

  const getSearches = async () =&gt; {
    try {
      const { response } = await searchUser(debouncedQuery);
      setSearchResults(response.content);
      setSearchHistory({ id: key, keyword: debouncedQuery });
    } catch (error) {
      toast(TOAST_MESSAGES.ERROR_PLEASE_RETRY);
    }
  };

  const onChangeInputHandler = (e: React.ChangeEvent&lt;HTMLInputElement&gt;) =&gt; {
    setQuery(e.target.value);
  };

  return (
    &lt;div className=&quot;w-full flex flex-col items-center justify-between px-4 mt-3&quot;&gt;
      &lt;SearchInput query={query} setQuery={setQuery} onChangeInputHandler={onChangeInputHandler} /&gt;
      {searchResults.length ? (
        &lt;ul className=&quot;w-full mt-[16px]&quot;&gt;
          {searchResults.map((user) =&gt; (
            &lt;SearchUserList key={user.id} user={user} /&gt;
          ))}
        &lt;/ul&gt;
      ) : (
        &lt;ul className=&quot;w-full mt-[16px]&quot;&gt;
          {searchHistory.map((searchBox) =&gt; (
            &lt;SearchHistory key={searchBox.id} searchBox={searchBox} setSearchResults={setSearchResults} /&gt;
          ))}
        &lt;/ul&gt;
      )}
    &lt;/div&gt;
  );
};

export default SearchUser;
</code></pre>
<h3 id="클린-아키텍쳐에-따른-구조">클린 아키텍쳐에 따른 구조</h3>
<ol>
<li><p>Domain Layer: 플랫폼/프레임워크와 독립적으로 구성
• Model : 문제와 관련된 실제 세계의 Object
• Repository: 모델에 접근 가능한 인터페이스 제공
• UseCase: 애플리케이션의 비즈니스 로직 포함</p>
</li>
<li><p>Presentation Layer: 사용자에게 화면으로 보여지는 레이어
• SearchInput: 사용자의 검색어 입력을 받습니다.
• SearchUserList: 검색 결과를 리스트 형태로 보여줍니다.
• SearchHistory: 이전 검색어를 기반으로 히스토리를 보여줍니다.</p>
</li>
<li><p>Data Layer: 애플리케이션이 데이터를 관리
• searchUser: API를 호출하여 사용자 검색 결과를 불러옵니다.
• useState: 검색어(query), 검색 결과(searchResults), 검색 히스토리(searchHistory)의 상태를 관리합니다.
• useSearchStore: 검색 히스토리를 전역 상태로 관리하는 커스텀 훅입니다.</p>
</li>
</ol>
<h3 id="클린-아키텍처-적용을-위한-개선점">클린 아키텍처 적용을 위한 개선점</h3>
<ol>
<li><p>의존성 분리와 관리
Presentation과 Data Layer 사이의 의존성을 명확히 분리합니다. 예를 들어, searchUser 같은 API 호출 로직은 별도의 서비스 레이어나 리포지토리 레이어에 위치시켜 컴포넌트에서는 단순히 호출만 합니다.</p>
</li>
<li><p>상태 관리의 단일 책임 원칙 적용
각 상태는 그와 관련된 로직만 처리합니다. 예를 들어, 검색어 상태 관리는 SearchInput 컴포넌트 내부에서 처리하고, 검색 결과 관리는 상위 레벨에서 처리하는 등의 구분을 명확히 합니다.</p>
</li>
<li><p>에러 처리 및 사용자 경험 향상
에러 처리 로직을 강화하여 사용자에게 보다 명확한 피드백을 제공합니다. 에러가 발생했을 때 사용자가 이해하기 쉽고 직관적인 메시지를 제공합니다.</p>
</li>
<li><p>테스트 용이성 확보
컴포넌트의 테스트를 용이하게 하기 위해 의존성 주입을 활용합니다. 예를 들어, API 호출 함수를 props으로 받게 함으로써 컴포넌트가 외부에 종속되지 않고 독립적으로 테스트 가능하게 합니다.</p>
</li>
</ol>
<h3 id="평가">평가</h3>
<p>SearchUser 컴포넌트는 기능적으로 잘 구현되어 있으나, 클린 아키텍처 원칙을 적용하여 각 계층의 역할을 더 명확히 할 필요가 있다. 이를 통해 더 견고하고, 유지보수가 쉬우며, 테스트 가능한 코드 베이스를 만들 수 있겠다. 각 계층이 자신의 책임만을 수행하도록 구조를 개선해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Dependency-Cruiser를 사용해서 프로젝트 의존성 구조 파악하기]]></title>
            <link>https://velog.io/@good_sang/Dependency-Cruiser%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B5%AC%EC%A1%B0-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@good_sang/Dependency-Cruiser%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B5%AC%EC%A1%B0-%ED%8C%8C%EC%95%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 13 Apr 2024 15:20:27 GMT</pubDate>
            <description><![CDATA[<h2 id="dependency-cruiser로-의존성-분석">Dependency-cruiser로 의존성 분석</h2>
<blockquote>
<p><strong>Dependency-Cruiser</strong> 
프로젝트의 의존성 구조를 분석하고 시각화하는 도구</p>
</blockquote>
<p>Dependency-cruiser를 사용하여 프로젝트의 의존성을 분석하고, 실제 프로젝트에 적용한 내용을 바탕으로 dependency-cruiser를 활용한 소스코드 검증과 의존성 시각화하였습니다. 이를 통해 얻을 수 있는 인사이트를 설명하고자 합니다.</p>
<h3 id="dependency-cruiser-설치">Dependency-cruiser 설치</h3>
<pre><code class="language-bash">npm install -D dependency-cruiser</code></pre>
<h3 id="dependency-cruiser-초기화">Dependency-cruiser 초기화</h3>
<pre><code class="language-bash">npx depcruise --init</code></pre>
<p>초기화 후 dependency-cruiser를 설치하면  .dependency-cruiser.js 가 루프 경로에 자동으로 생성된다.</p>
<h3 id="소스코드-검증">소스코드 검증</h3>
<pre><code class="language-bash">npx depcruise src --include-only &#39;^src&#39; --config --output-type err-long</code></pre>
<p>소스 코드 검증 명렁어를 통해 다음의 내용을 확인할 수 있다.</p>
<ul>
<li><p>의존성 분석: --include-only &#39;^src&#39; 옵션은 분석 범위를 src 디렉토리로 제한하여, 해당 디렉토리 내 파일들만을 대상으로 의존성을 검사한다.</p>
</li>
<li><p>상세한 오류 출력: --output-type err-long 옵션은 의존성 분석 결과에서 문제가 발견된 경우, 문제의 상세한 정보를 출력한다.</p>
</li>
</ul>
<pre><code class="language-bash">
// 예시

$ npx depcruise src --include-only &#39;^src&#39; --config --output-type err-long

  warn no-orphans: src/app/main/settings/layout.tsx
    This is an orphan module - it&#39;s likely not used (anymore?). Either use it
    or remove it. If it&#39;s logical this module is an orphan (i.e. it&#39;s a config
    file), add an exception for it in your dependency-cruiser configuration.
    By default this rule does not scrutinize dot-files (e.g. .eslintrc.js),
    TypeScript declaration files (.d.ts), tsconfig.json and some of the babel
    and webpack configs.

  warn no-orphans: src/app/auth/kakao/layout.tsx
    This is an orphan module - it&#39;s likely not used (anymore?). Either use it
    or remove it. If it&#39;s logical this module is an orphan (i.e. it&#39;s a config
    file), add an exception for it in your dependency-cruiser configuration.
    By default this rule does not scrutinize dot-files (e.g. .eslintrc.js),
    TypeScript declaration files (.d.ts), tsconfig.json and some of the babel
    and webpack configs.

  warn no-orphans: src/app/accounts/layout.tsx
    This is an orphan module - it&#39;s likely not used (anymore?). Either use it
    or remove it. If it&#39;s logical this module is an orphan (i.e. it&#39;s a config
    file), add an exception for it in your dependency-cruiser configuration.
    By default this rule does not scrutinize dot-files (e.g. .eslintrc.js),
    TypeScript declaration files (.d.ts), tsconfig.json and some of the babel
    and webpack configs.

  warn no-circular: src/components/Restaurant/ShopFeedItem.tsx → 
      src/components/Restaurant/ShopFeedList.tsx →
      src/components/Restaurant/ShopFeedItem.tsx
    This dependency is part of a circular relationship. You might want to
    revise your solution (i.e. use dependency inversion, make sure the modules
    have a single responsibility)

  warn no-circular: src/components/PostForm/PostContent.tsx → 
      src/components/PostForm/PostSearch.tsx →
      src/components/PostForm/PostContent.tsx
    This dependency is part of a circular relationship. You might want to
    revise your solution (i.e. use dependency inversion, make sure the modules
    have a single responsibility)

✘ 5 dependency violations (0 errors, 5 warnings). 294 modules, 611 dependencies cruised.</code></pre>
<h3 id="의존성-시각화">의존성 시각화</h3>
<p>아래 이미지와 같이 프로젝트 의존성을 시각화한 이미지가 새 창으로 띄워진다.</p>
<pre><code class="language-bash">npx depcruise src --include-only &#39;^src&#39; --config --output-type dot | dot -Tsvg &gt; dependency-graph.svg &amp;&amp; open dependency-graph.svg</code></pre>
<p><img src="https://velog.velcdn.com/images/good_sang/post/7d88ba3b-5894-4183-bfe6-29ab95eb495e/image.svg" alt=""></p>
<h3 id="그외-설정">그외 설정</h3>
<ul>
<li><p>script에 명령어 추가하여 명령어를 사용한다.</p>
<pre><code class="language-bash">script: {
  &quot;depcruise:validate&quot;: &quot;npx depcruise src --include-only &#39;^src&#39; --config --output-type err-long&quot;,
  &quot;depcruise:tree&quot;:&quot;npx depcruise src --include-only &#39;^src&#39; --config --output-type dot | dot -Tsvg &gt; dependency-graph.svg &amp;&amp; open dependency-graph.svg&quot;,
}</code></pre>
</li>
<li><p>의존성 방향 allowed 설정</p>
<pre><code class="language-bash">// 예시- Presentation Layer 의존성은 Domain으로만 향하게끔 설정
</code></pre>
</li>
</ul>
<p>allowedSeverity: &quot;error&quot;,
allowed: [
    {
        from: {path: &quot;(^src/Presentation)&quot;},
        to: {path: [&quot;^src/Domain&quot;]},
    }
]</p>
<pre><code>그러나 다른 규칙들이 모두 not-in-allowed로 표시 → 모든 유효한 의존성을 표기해야 한다.

번거롭지만, 레피지토리에 새 폴더를 추가할 때마다 의존성을 설계하고 새 규칙을 추가하는 것이 좋은 습관이라고 한다.

### 리뷰

1. **의존성의 복잡성 이해**
Dependency-Cruiser를 사용하면 프로젝트 내의 각 파일과 모듈 간의 의존성을 명확하게 볼 수 있다. 프로젝트의 구조적 복잡성을 이해하는 데 좋다.

2. **잠재적 문제 식별 **
의존성 그래프를 통해 순환 의존성이나 과도한 결합 같은 구조적 문제를 쉽게 식별할 수 있다. 이러한 문제들은 프로젝트의 유지보수를 어렵게 만들 수 있기 때문에 초기에 발견하고 수정하는 데 도움을 얻을 수 있다.

3. **리팩토링 가이드 제공**
의존성 분석 결과를 바탕으로 리팩토링을 계획할 수 있다. 예를 들어, 의존성 그래프에서 특정 모듈이 과도하게 많은 의존성을 갖는지 파악하여, 모듈을 분리하거나 기능을 재조정할 수 있다.

4. **코드 베이스의 품질 향상**
의존성 규칙을 설정함으로써 코드 베이스의 일관성과 품질을 향상시킬 수 있다.</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘과 자료구조] 재귀 속도를 개선하는 방식을 알아보자]]></title>
            <link>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9E%AC%EA%B7%80-%EC%86%8D%EB%8F%84%EB%A5%BC-%EA%B0%9C%EC%84%A0%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9E%AC%EA%B7%80-%EC%86%8D%EB%8F%84%EB%A5%BC-%EA%B0%9C%EC%84%A0%ED%95%98%EB%8A%94-%EB%B0%A9%EC%8B%9D%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 11 Apr 2024 17:45:36 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📖 &#39;누구나 자료구조와 알고리즘&#39; 책을 공부한 내용을 담고 있습니다. </p>
</blockquote>
<h2 id="12장-동적-프로그래밍">12장 동적 프로그래밍</h2>
<p>12장에서 재귀 코드의 속도를 느리게 만드는 원인을 찾고, 빅 오 관점에서 어떻게 나타내는지 배워보자.</p>
<h3 id="121-불필요한-재귀-호출">12.1 불필요한 재귀 호출</h3>
<p>다음 예제는 배열에서 최댓값을 찾는 문제이다. 재귀 함수를 활용해서 배열의 최댓값을 구현해 보았다. </p>
<pre><code class="language-javascript">function max(array) {
    // 기저조건: 원소가 하나면 그 원소가 최댓값
      if (array.length === 1) return array[0];

      /** 첫 번째 원소와 나머지 배열에서 가장 큰 원소를 비교한다.
    첫 번째 원소가 더 크면 그 원소를 최댓값으로 반환한다.
    그렇지 않으면 나머지 배열의 최댓값을 반환한다. 
    */
      if (array[0] &gt; max(array.slice(1, array.length - 1))) {
        return array[0];
    // 그렇지 않으면 나머지 배열의 최댓값을 반환한다.
    } else {
        return max(array.slice(1, array.length - 1)))
    }
}</code></pre>
<p>예를 들어, 배열 [1,2,3,4]에서 최댓값을 찾는다면, 첫 번째 원소인 1과 배열 [2,3,4]의 최댓값을 비교한다. 이어서 2와 [3,4]의 최댓값을 비교하고, 3과 [4]를 비교한다. [4]로 한번 더 재귀 호출을 수행하는데 이때 기저조건에 해당한다. </p>
<h4 id="max-재귀-분석">max 재귀 분석</h4>
<p>max([4])부터 호출 사슬 위로 올라가면 max([4])는 기저 조건에 해당하므로 4를 반환한다. </p>
<pre><code class="language-javascript">      if (array.length === 1) return array[0];</code></pre>
<p>max([3,4])는 max([4])를 2번 호출한다. 이렇게 모든 재귀 함수를 호출해보면 불필요한 호출이 발생한다. max([4])는 한 번만 계산해도 충분한데, 예제에서는 8번이나 호출한다. </p>
<h3 id="122-빅-오를-위한-작은-개선">12.2 빅 오를 위한 작은 개선</h3>
<p>다행히도 불필요한 재귀 호출을 없애는 손쉬운 방법이 있다. 코드에서 max를 딱 한 번만 호출하고 그 결과를 변수에 저장하면 된다. </p>
<pre><code class="language-javascript">function max(array) {
    // 기저조건: 원소가 하나면 그 원소가 최댓값
      if (array.length === 1) return array[0];

    let max_of_remainder = max(array.slice(1, array.length - 1))
      if (array[0] &gt; max_of_remainder) {
        return array[0];
    // 그렇지 않으면 나머지 배열의 최댓값을 반환한다.
    } else {
        return max(array.slice(1, array.length - 1)))
    }
}</code></pre>
<p>이렇게 간단한 수정으로 max함수를 4번만 호출한다. </p>
<h3 id="123-재귀의-효율성">12.3 재귀의 효율성</h3>
<p>위 개선된 max 함수는 배열 내 원소 개수만큼 자신을 재귀적으로 호출한다. 이 함수의 빅 오는 O(N)이다. </p>
<p>하지만 개선되기 전 max 함수 호출 횟수를 보면, 원소 개수가 1개씩 커질 때, 호출 횟수는 약 2배씩 늘어난다. O(2^N)이다. 현저히 느린 알고리즘이다.</p>
<table>
<thead>
<tr>
<th align="center">원소 개수(N)</th>
<th align="center">호출 횟수</th>
</tr>
</thead>
<tbody><tr>
<td align="center">1</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">3</td>
</tr>
<tr>
<td align="center">3</td>
<td align="center">7</td>
</tr>
<tr>
<td align="center">4</td>
<td align="center">15</td>
</tr>
<tr>
<td align="center">5</td>
<td align="center">31</td>
</tr>
</tbody></table>
<p>두 함수의 효율성 차이는 매우 크다. 불필요한 재귀 호출을 피하는 것이 재귀 효율성을 높이는 데 핵심이다. </p>
<h3 id="124-하위-문제-중첩">12.4 하위 문제 중첩</h3>
<blockquote>
<p><strong>피보나치 수열(Fibonacci sequence)</strong>
0과 1로 시작하며 이어지는 수는 수열의 앞 두 수의 합이다.</p>
</blockquote>
<pre><code>0,1,1,2,3,5,8,13,21,34,55</code></pre><p>예를 들어, 피보나치 수열의 N번째 수를 반환하는 함수가 있다. 함수에 10을 전달하면 수열의 10번째 수가 55이므로 55를 반환한다. (0은 수열에서 0번째 수로 간주된다.)</p>
<pre><code class="language-javascript">function fib(n) {
    // 기저 조건은 수열의 처음 두 수다.
      if (n === 0 or n === 1) return n;

      // 앞의 두 피보나치 수의 합을 반환한다.
      return fib(n-2) + fib(n-1)
}</code></pre>
<p>이 함수 또한 자기 자신을 두 번씩 호출하는 문제가 있다. 시간 복잡도는 O(2^N)이다. 하지만, 피보나치 수열은 이전 예제처럼 쉽게 최적화가 쉽지 않다.</p>
<p>변수에 저장할 데이터가 하나가 아니기 때문이다. fib(n-2)와 fib(n-1) 모두 계산해야 한다. 이를 컴퓨터 과학에서 <strong>하위 문제 중첩(overlapping subproblems)</strong>이라 부른다.</p>
<p>하위 문제 중첩이 발생하는 이유는 fib(n-2)와 fib(n-1)이 결과적으로 같은 함수를 여러 번 중복 호출하기 때문이다. </p>
<h3 id="125-메모이제이션을-통한-동적-프로그래밍">12.5 메모이제이션을 통한 동적 프로그래밍</h3>
<p><strong>동적 프로그래밍</strong>을 통해 피보나치 수열의 하위 문제 중첩을 해결할 수 있다.</p>
<blockquote>
<p><strong>동적 프로그래밍(dynamic programming)</strong>을 통한 알고리즘 최적화 기법을 알아보자</p>
</blockquote>
<h4 id="알고리즘-최적화-기법-메모이제이션memoization">알고리즘 최적화 기법: 메모이제이션(memoization)</h4>
<p>메모이제이션은 먼저 계산한 함수 결과를 기억해 재귀 호출을 감소시킨다.
해시 테이블에 함수 결과를 저장해서 중복 호출을 막는다. </p>
<pre><code>피보나치 예제에서 fib(3),fib(4),fib(5),fib(6) 호출했을 때 해시테이블
{
3: 2,
4: 3,
5: 5,
6: 8
}</code></pre><blockquote>
<p>그렇다면, 재귀함수에서 해시 테이블에 어떻게 접근할까?</p>
</blockquote>
<p>피보나치 함수의 두 번째 인자로 해시 테이블을 전달하면 된다. </p>
<h4 id="메모이제이션-구현">메모이제이션 구현</h4>
<p>함수 처음 호출할 때 빈 해시 테이블을 두 번째 인자로 전달한다. </p>
<pre><code class="language-javascript">function fib(n, memo) {
    // 기저 조건은 수열의 처음 두 수다.
      if (n === 0 or n === 1) return n;

      // 해시 테이블 memo에 fib(n)의 값이 있는지 확인
      if (!memo[n]) {
        /** n이 memo에 없으면 fib(b)을 계산 후 
        결과값을 해시 테이블에 저장한다.
        */
          memo[n] = fib(n-2,memo) + fib(n-1,memo)
    }
      /** 이제 fib(n)의 값이 memo에 저장돼 있다.
    따라서, 그 값을 반환한다.
    */

      return memo[n]
}</code></pre>
<p>참고로, memo에 기본값을 할당하면 빈 해시 테이블을 명시적으로 전달하지 않아도 된다. </p>
<pre><code class="language-javascript">function fib(n, memo={}) {
    // 코드 생략
}</code></pre>
<p>그렇다면, 메모이제이션 사용한 함수의 빅 오는 무엇일까? 
N에 대해서 2N - 1 호출된다. 빅 오에서는 상수를 버리기 때문에 O(N)이 된다. O(2^N)에 비해 효율성이 크게 개선되었다.</p>
<table>
<thead>
<tr>
<th align="center">원소 개수(N)</th>
<th align="center">호출 횟수</th>
</tr>
</thead>
<tbody><tr>
<td align="center">1</td>
<td align="center">1</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">3</td>
</tr>
<tr>
<td align="center">3</td>
<td align="center">5</td>
</tr>
<tr>
<td align="center">4</td>
<td align="center">7</td>
</tr>
<tr>
<td align="center">5</td>
<td align="center">9</td>
</tr>
<tr>
<td align="center">6</td>
<td align="center">11</td>
</tr>
</tbody></table>
<h3 id="126-상향식을-통한-동적-프로그래밍">12.6 상향식을 통한 동적 프로그래밍</h3>
<blockquote>
<p>&quot;상향식&quot;으로 하위 문제 중첩을 해결해 보자</p>
</blockquote>
<p>상향식은 재귀 대신 루프 같은 다른 방식으로 해결한다는 뜻이다. </p>
<h4 id="1261-상향식-피보나치">12.6.1 상향식 피보나치</h4>
<p>상향식에서는 0, 1로 시작한다. 루프를 사용해서 수열을 만든다.</p>
<pre><code class="language-javascript">function fib(n) {
    if (n === 0) return 0;

      // a와 b는 각각 수열의 처음 두 수로 시작한다.
      let a = 0;
      let b = 1

      for (let i = 1; i &lt; n; i++) {
          /**a와 b가 다음 수가 된다.
        a는 b가 되고, b는 이전 a + 이전 b
        이전 a는 temp로 저장해서 계산한다.
        */
        let temp = a;
          a = b
          b = temp + a

      return b;
    }
}</code></pre>
<p>N까지의 간단한 루프로 N단계가 걸리므로 O(N) 시간 복잡도이다.</p>
<h4 id="메모이제이션-대-상향식">메모이제이션 대 상향식</h4>
<p>메모이제이션을 쓰더라도 재귀가 반복에 비해 오버페드가 더 든다. 재귀를 어떻게 사용하든 호출 스택에 호출을 기록해야 하므로 메모리를 소모한다. 메모이제이션도 해시테이블을 사용해서 메모리를 추가로 소모한다. </p>
<p>재귀가 매우 직관적이지 않는 이상 일반적으로 상향식을 택하는 편이 더 낫다. 재귀가 더 직관적이면 재귀를 사용하되 메모이제이션을 사용하자.</p>
<h3 id="요약">요약</h3>
<blockquote>
<ul>
<li>재귀 함수가 같은 계산을 반복하여 수행함으로써 불필요한 재귀 호출 발생</li>
</ul>
</blockquote>
<ul>
<li>불필요한 재귀 호출을 변수에 결과 저장으로 최소화</li>
<li>하위 문제 중첩: 피보나치 수열과 같은 일부 문제는 같은 하위 문제를 반복해재귀 함수의 효율성을 크게 저하</li>
<li>메모이제이션은 해시테이블에 계산된 결과를 저장하여 효율을 높임.</li>
<li>상향식 접근은 재귀 대신 반복문을 사용</li>
<li>메모이제이션은 메모리 추가 사용이 필요, 상향식이 메모리 측면에서 유리</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[알고리즘과 자료구조] 재귀함수를 사용해 다양한 문제를 접근해보자]]></title>
            <link>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-0dsg8uli</link>
            <guid>https://velog.io/@good_sang/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EA%B3%BC-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-0dsg8uli</guid>
            <pubDate>Thu, 11 Apr 2024 15:18:54 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📝 &#39;누구나 자료구조와 알고리즘&#39; 책을 공부한 내용을 담고 있습니다.</p>
</blockquote>
<h2 id="11장-재귀적으로-작성하는-법">11장 재귀적으로 작성하는 법</h2>
<p>11장에서는 재귀적 사고방식을 기르는 데 집중한다. </p>
<h3 id="111-재귀-카테고리-반복-실행">11.1 재귀 카테고리: 반복 실행</h3>
<p>여러 가지 재귀 문제를 다루면서 문제에 다양한 &quot;카테고리&quot;가 있음을 알게 됐다. 같은 카테고리에 속한 문제는 같은 기법으로 해결할 수 있다.</p>
<p>예를 들어, 반복적으로 한 작업을 실행하는 알고리즘이 있다. 우주선 카운트 다운 알고리즘이 대표적이다. 10에서부터 0까지의 수를 반복적으로 출력한다.</p>
<pre><code class="language-javascript">function countdown(number) {
    console.log(number);

    if (number === 0) { // number가 0일 때가 기저조건
        return;
    } else {
        countdown(number -1)
    }
}</code></pre>
<p>else 구문에서 재귀 함수를 다시 한 번 호출하여 반복적으로 숫자를 출력한다.</p>
<h4 id="1111-재귀-트릭-추가-인자-넘기기">11.1.1 재귀 트릭: 추가 인자 넘기기</h4>
<p>반복 실행 알고리즘 예시 중 숫자 배열을 받아 배열 내 각 숫자를 두 배로 만드는 알고리즘을 작성하려고 한다. 단 새 배열을 만드는 것이 아니라 배열 자체에서 수정한다.</p>
<p>만약 재귀 대신 루프를 사용한다면, 인덱스를 기록하는 변수를 두고 다음 코드처럼 계속해서 1씩 증가시켜야 한다.</p>
<pre><code class="language-javascript">function double_array(array){
  let index = 0;

  while (index &lt; array.length) {
      array[index] *=2;
    index += 1;
  }
}</code></pre>
<blockquote>
<p>재귀 함수를 사용한다면 어떻게 해결할까? </p>
</blockquote>
<p>index를 인수를 받아서 index가 array.length보다 작을 때까지 재귀 함수를 호출한다. index를 대신 0을 긴본값으로 받아서 사용할 수도 있다. 이렇게 함수 인자를 추가해서 재귀 함수로 문제를 해결할 수 있다. </p>
<pre><code class="language-javascript">function double_array(array, index){
  if (index &gt;= array.length) return;

  array[index] *= 2;
  double_array(array, index + 1);
}</code></pre>
<blockquote>
<p>[참고] <strong>제자리 수정(in-place)</strong>
배열 데이터를 조작하는 2가지 방법이 있다. 
첫 번째, 원래 배열은 그대로 두고 새 배열을 생성하는 방법이다.
두 번째, <strong>제자리 수정</strong> 방법으로, 원본 배열을 바꾸는 것이다.
어떤 방법을 선택하든 사용자의 몫이고, 프로젝트 상황에 따라 달라진다.</p>
</blockquote>
<h3 id="112-재귀-카테고리-계산">11.2 재귀 카테고리: 계산</h3>
<p>재귀가 유용하게 쓰이는 영역은 하위 문제의 계산에 기반해 계산할 수 있을 때다. </p>
<p>예를 들어, 6의 계승(factorial) 문제가 있다. </p>
<pre><code>6 * 5 * 4 * 3 * 2 * 1</code></pre><p>어떤 수의 계승을 계산하는 함수를 작성할 때 1부터 시작해 결과를 계산해 나가는 루프를 사용해도 된다. 즉, 1에 2를 곱하고, 그 결과에 3을 곱하고, 다시 4를 곱해서 6까지 가는 방식이다. </p>
<pre><code class="language-javascript">function factorial (number) {
    let product = 1;

  for (let i = 1; i &lt;= number; i++) {
      product *= i
  }

  retrun product;
}</code></pre>
<p>하지만, 문제에 다르게 접근해 볼 수 있다. 즉, 하위 문제에 기반해 계승을 계산하는 것이다. </p>
<p><strong>하위 문제(subproblem)</strong>의 관점에서 factorial(6)은 factorial(5)의 결과에 6을 곱한 값이다. 즉, factorial(6) = 6 * factorial(5) 이다. 코드로 구현하면 다음과 같다.</p>
<pre><code class="language-javascript">function factorial (number) {
    if (number === 1) {
      return 1;
    } else {
      return number * factorial(number - 1)
    }
}</code></pre>
<h4 id="두-가지-계산-방식">두 가지 계산 방식</h4>
<p>재귀 함수를 사용해서  &quot;상향식&quot; 또는 &quot; 하향식&quot;으로 문제를 해결할 수 있다.
단, 재귀로 상향식 전략을 구현할 때, 인자를 추가로 전달해야 한다.</p>
<p>factorial 문제를 상향식으로 풀어보면</p>
<pre><code class="language-javascript">function factorial (number, i = 1, product = 1) {
    if ( i &gt; number ) return product;
      return factorial(number, i + 1, product * i)
}</code></pre>
<p>이 함수의 인자는 3개이다. 하나는 계승 수인 number, 두 번째는 1부터 시작해 매 호출마다 1씩 증가하는 변수 i, 마지막으로 i와 계속 곱한 결과를 저장하는 변수 product이다. </p>
<p>재귀 함수를 상향식으로 쓸 수 있지만, 루프를 사용하는 것보다 낫다고 볼 수 없다. 상향식에서는 재귀나 루프나 동일한 방식이다. </p>
<p>하지만, 하향식에서는 재귀를 써야한다.</p>
<h3 id="113-하향식-재귀-새로운-사고방식">11.3 하향식 재귀: 새로운 사고방식</h3>
<p>재귀는 하향식 방식을 구현하는 데 탁월하다. 재귀적 하향식 방식은 문제를 완전히 다른 방식으로 생각하게 해준다. </p>
<h3 id="114-계단-문제">11.4 계단 문제</h3>
<blockquote>
<p>n개의 계단을 올라는 경로 문제를 풀어보자. </p>
</blockquote>
<p>계단 수가 1개일 때, 가능한 경로는 1개이다.</p>
<p>계단 수가 3개면 가능한 경로는 4개이다.</p>
<p>1,1,1
1,2
3</p>
<p>계단 수가 4개면, 가능한 경로는 7개이다.
1,1,1,1
1,1,2
1,2,1
1,3
2,1,1
2,2
3,1</p>
<blockquote>
<p>그렇다면, 모든 경로를 계산하는 코드를 어떻게 작성할까?</p>
</blockquote>
<p>&quot;하향식&quot;으로 생각하면 문제가 단순해진다.
11개 계단의 경로를 구할 때 10개 계단 경로를 알면 된다. 10번째 계단까지 가는 경로 수에 9번째 계단까지 경로 수를 더한 값이다. 따라서 N개 계단일 때 경로 수는 다음과 같다. </p>
<pre><code>number_of_paths(n-1) + number_of_paths(n-2) + number_of_paths(n-3)</code></pre><h4 id="계단-문제-기저-조건">계단 문제 기저 조건</h4>
<p>계단 문제의 기저 조건은 알아내기 까다롭다. n이 0보다 작으면 0이고, n이 1이거나 0이면 1이 된다. 이 기저조건을 추가한 코드는 다음과 같다.</p>
<pre><code class="language-javascript">function number_of_paths(n){
    if (n &lt; 0) return 0;
      if (n === 1 || n === 0) return 1;
      return number_of_paths(n-1) + number_of_paths(n-2) +number_of_paths(n-3)
}</code></pre>
<h3 id="115-애너그램-생성">11.5 애너그램 생성</h3>
<p>가장 복잡한 재귀 문제인 주어진 문자열의 모든 애너그램(anagram) 배열을 반환하는 함수를 풀어본다.</p>
<blockquote>
<p><strong>애너그램(angram)</strong>
문자열 내 모든 문자들을 재배열한 문자열</p>
</blockquote>
<p>문자열 &quot;abcd&quot;의 모든 애너그램을 구해보자. &quot;abcd&quot;의 하위 문제는 &quot;abc&quot;일 것이다. &quot;abc&quot;의 모든 애너그램을 반환하는 애너그램 함수가 있을 때, 이를 사용해서 어떻게 &quot;abcd&quot;의 모든 애너그램을 만들어낼까?</p>
<p>&quot;abc&quot;으로 가능한 문자열은 다음과 같다.</p>
<pre><code>&quot;abc&quot;
&quot;acb&quot;
&quot;bac&quot;
&quot;bca&quot;
&quot;cab&quot;
&quot;cba&quot;</code></pre><p>각 문자열의 알파벳 앞뒤에 d를 넣으면, &quot;abcd&quot;의 애너그램을 만들 수 있다. </p>
<p><strong>자바스크립트 코드를 구현</strong></p>
<pre><code class="language-javascript">function anagrams (string) {
    //string이 1개일 때
    if (string.length === 1) return [string];

   // 모든 애너그램 저장할 배열 
    let collection = []

   // 두 번째 문자부터 마지막 문자까지의 부분 문자열에서 모든 애너그램을 찾는다.
   // 예를 들어, string이 &quot;abcd&quot;면 부분 문자열 &quot;bcd&quot;이니
   // &quot;bcd&quot;의 모든 애너그램을 찾는다.
   let substring_anatrams = anagrams();

   // 각 부분 문자열을 순회한다.

  // 0부터 string 끝을 하나 지난 인덱스까지
  //부분 문자열의 각 인덱스를 순회한다.

  // 부분 문자열 애너그램의 복사본을 생성한다. 
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[회고]'푸디로그' 프로젝트를 마치고...]]></title>
            <link>https://velog.io/@good_sang/%ED%9A%8C%EA%B3%A0%ED%91%B8%EB%94%94%EB%A1%9C%EA%B7%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%A7%88%EC%B9%98%EA%B3%A0</link>
            <guid>https://velog.io/@good_sang/%ED%9A%8C%EA%B3%A0%ED%91%B8%EB%94%94%EB%A1%9C%EA%B7%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EB%A7%88%EC%B9%98%EA%B3%A0</guid>
            <pubDate>Wed, 10 Apr 2024 05:45:58 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><a href="https://sangsangisgood.notion.site/ec2adf20705a428f82c066c9c8053e70?pvs=4">📝 푸디로그 노션</a>
프로젝트 소개 및 과정, 문서 등을 보실 수 있습니다.</p>
</blockquote>
<h2 id="푸디로그-프로젝트">&#39;푸디로그&#39; 프로젝트</h2>
<p>작년 7월~10월 진행했던 &#39;푸디로그&#39;가 새롭게 리뉴얼 시도하면서 다시금 프로젝트를 재개하였다. 보통의 프로젝트는 단기간 작업 후 새로운 프로젝트를 작업하는 데 바빠 코드 리팩토링을 신경쓰지 못했었다. 하지만, 푸디로그는 사이드 프로젝트로 진행했던 프로젝트라 팀원들의 열정이 남달랐다. 기존 프론트엔드 팀원의 건강의 이유로 나가게 되어 새로운 팀원들을 뽑아 프로젝트 리뉴얼에 진행하게 되었다.</p>
<h3 id="프로젝트-목표">프로젝트 목표</h3>
<ol>
<li>기존 코드 리팩 + 웹 표준/웹 접근성 반영 (디자인 작업 기간 동안)</li>
<li>새로운 추가 기능 화면 구현 (+ 관리자 페이지)</li>
<li>전반적인 디자인 수정으로 인한 HTML, CSS 수정</li>
<li>테스트 코드 작성</li>
</ol>
<h3 id="팀원-선정">팀원 선정</h3>
<p>이전에는 백엔드와 프론트엔드가 기획,디자인까지 모두 담당하였는데, 본격적으로 앱을 리뉴얼하기 위해 디자이너 2명과 프론트엔드 3명을 뽑기로 했다. 프로젝트 팀원 공고를 올렸을 때 많은 분들이 지원해주셨다. 취준생부터 경력자까지 다양한 분들이 지원해주셔서 감사했다. 보내주신 깃허브와 포트폴리오를 세세하게 살펴보면서 프로젝트 스택과 방향성에 맞는 분을 총 3분 선정했다. 그러나, 본격적으로 프로젝트가 시작하고 일주일 뒤, 두 명의 팀원의 개인 사정으로 나가게 되셨다. 그렇게 팀원 한 분과 프로젝트를 진행하게 되었다.</p>
<p>기한 내 프로젝트를 해내기 위해선 최소 1명의 팀원이 더 필요하다고 판단했다. 부트캠프 동기 중 프로젝트 스택을 사용해본 사람을 찾아보았고, 다행히 적합한 분이 계셔서 함께 프로젝트를 진행하기로 했다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/fd2d8f48-748e-43ab-a296-6427e00ec13e/image.png" alt=""></p>
<h3 id="역할-분담">역할 분담</h3>
<p>푸디로그 기획부터 참여했기 때문에 푸디로그 앱에 대한 이해도가 높은 내가 팀장을 맡기로 했다. 리팩토링 목적으로 프로젝트르 진행해본 적이 없어서 어떻게 역할 분담을 할지 고민이 있었다. 가장 중요한 점은 &#39;부트캠프&#39;라는 테두리가 없는 사이드 프로젝트이기 때문에 팀원들이 끝까지 프로젝트에 참여하도록 이끄는 게 중요했다. 프로젝트의 참여도를 높이기 위해선 이 프로젝트를 통해서 팀원들이 얻어가고자 하는 것이 무엇인지가 중요했다. 그것은 대부분 프로젝트 경험을 통해 성과를 경험하는 것일 것이다. </p>
<p>그렇기 때문에 팀원들에게 담당 페이지를 자율적으로 선택하도록 하되, 주요한 페이지가 한 사람에게 쏠리지 않도록 분배했다. 다음으로 기존 코드 리팩토링 상황을 고려하여 서로 연관성 높은 페이지 단위로 담당을 나눴다. 처음부터 새롭게 프로젝트를 구현하는 게 아니기 때문에 누군가 작성한 코드를 이해하는 시간이 필요하고, 연관성이 높은 페이지를 맡으면 코드 이해가 더 쉽다고 판단했기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/good_sang/post/35ba1eb0-7993-4d37-b676-d05b606503d5/image.png" alt=""></p>
<h3 id="프로젝트-작업">프로젝트 작업</h3>
<ul>
<li><strong>웹 표준과 웹 접근성 반영</strong><ul>
<li>WCAG(웹 콘텐츠 접근성 가이드라인)과 같은 접근성 표준을 배우게 됐고, 장애를 가진 사용자들이 웹사이트를 사용하는 방식에 대해 깊이 있게 이해함</li>
</ul>
</li>
<li>*<em>기존의 데이터 패칭을 리액트 쿼리로 마이그레이션 *</em><ul>
<li>useQuery와 useMutation 훅을 별도의 훅 폴더에서 독립적으로 관리 
► 코드의 재사용성과 유지보수성 향상</li>
<li>팔로우 기능을 useMutation으로 구현하여 자동으로 Query 무효화 
► 적절한 시점에 자동으로 UI 업데이트 </li>
</ul>
</li>
<li><strong>커스텀 훅을 사용해서 상태 로직이나 사이드 이펙트 관리</strong><ul>
<li>useDebounce라는 커스텀 훅을 정의하여 자동완성 기능에 적용
► 사용자의 입력에 따른 서버 요청 줄임</li>
</ul>
</li>
</ul>
<h3 id="느낀점">느낀점</h3>
<ul>
<li><strong>좋았던 점</strong></li>
</ul>
<p>전부터 적용해보고 싶었던 웹 표준과 웹 접근성을 반영해볼 수 있어서 좋았다. 또한, 데이터 패칭을 React Query로 마이그레이션하면서 직접 자동화 데이터 관리를 경험해볼 수 있었다.</p>
<ul>
<li><strong>아쉬웠던 점</strong></li>
</ul>
<p>프로젝트 초기에 팀원들의 개인 사정으로 인한 인원 이탈이 있었다. 이로 인해 작업 분배와 일정 조정에 어려움을 겪었고, 프로젝트의 효율성이 다소 저하되었다.
이러한 상황으로 인해 테스트 코드를 적용해보지 못한 부분이 아쉽다. 테스트 코드 더 공부해서 프로젝트에 적용하고 싶다.</p>
<ul>
<li><strong>깨달은 점</strong></li>
</ul>
<p>팀장으로서 프로젝트의 성공적인 완수를 위해 체계적인 관리와 명확한 커뮤니케이션이 중요하다.</p>
]]></description>
        </item>
    </channel>
</rss>