<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>kyun 기술 블로그</title>
    <link>https://kyuntechblog.tistory.com/</link>
    <description>kyun 기술 블로그</description>
    <language>ko</language>
    <pubDate>Thu, 16 Apr 2026 10:14:09 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kyuuuun</managingEditor>
    <image>
      <title>kyun 기술 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/6027521/attach/e710fe3ddffc474ab7bcf08ff788a507</url>
      <link>https://kyuntechblog.tistory.com</link>
    </image>
    <item>
      <title>Promise.all 과 Promise.allSettled 직접 구현해보기</title>
      <link>https://kyuntechblog.tistory.com/76</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FbHL1/dJMb9V7AJFz/PE84lvlZRi3WJ0tizFidP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FbHL1/dJMb9V7AJFz/PE84lvlZRi3WJ0tizFidP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FbHL1/dJMb9V7AJFz/PE84lvlZRi3WJ0tizFidP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFbHL1%2FdJMb9V7AJFz%2FPE84lvlZRi3WJ0tizFidP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;225&quot; height=&quot;225&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise.all 의 익숙함의 속아 소중함을 잃지 말고 직접 구현해보기&lt;/p&gt;
&lt;pre id=&quot;code_1760946632819&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 모든 Promise가 성공하면 결과 배열을 반환하고,
// 하나라도 실패하면 즉시 reject 
const promiseAll = (promises) =&amp;gt; {
  const result = [];

  return new Promise((resolve, reject) =&amp;gt; {
    let completeLen = 0; // 완료된 Promise 개수

    for (let i = 0; i &amp;lt; promises.length; i++) {
      // Promise가 아닐 수도 있으니 안전하게 감싸기
      Promise.resolve(promises[i])
        .then((res) =&amp;gt; {
          result[i] = res;       // 인덱스 순서대로 결과 저장
          completeLen += 1;      // 완료 개수 증가

          // 모든 Promise가 성공하면 resolve
          if (completeLen === promises.length) {
            resolve(result);
          }
        })
        .catch((err) =&amp;gt; {
          // 하나라도 실패하면 즉시 reject
          reject(err);
        });
    }
  });
};

// 모든 Promise가 완료될 때까지 기다리고
// 각 결과를 { status, value/reason } 형태로 반환
const promiseAllSettled = (promises) =&amp;gt; {
  if (promises.length === 0) return Promise.resolve([]);

  const result = [];
  let completeLen = 0;

  return new Promise((resolve) =&amp;gt; {
    for (let i = 0; i &amp;lt; promises.length; i++) {
      Promise.resolve(promises[i])
        .then((res) =&amp;gt; {
          // 성공한 경우
          result[i] = { status: 'fulfilled', value: res };
          completeLen += 1;
        })
        .catch((err) =&amp;gt; {
          // 실패한 경우
          result[i] = { status: 'rejected', reason: err };
          completeLen += 1;
        })
        .finally(() =&amp;gt; {
          // 모든 Promise가 settled(성공 또는 실패) 상태가 되면 resolve
          if (completeLen === promises.length) {
            resolve(result);
          }
        });
    }
  });
};

// 테스트
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.reject('에러 발생');

const promises = [p1, p2, p3];

promiseAllSettled(promises)
  .then((res) =&amp;gt; console.log(res))
  .catch((err) =&amp;gt; console.log(err));&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Fe-Dev/JavaScript</category>
      <category>promise</category>
      <category>PromiseAll</category>
      <category>PromiseAllSettled</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/76</guid>
      <comments>https://kyuntechblog.tistory.com/76#entry76comment</comments>
      <pubDate>Mon, 20 Oct 2025 16:52:28 +0900</pubDate>
    </item>
    <item>
      <title>Next.js App Router에 FSD(Feature-Sliced Design)를 곁들인..- FSD 도입기</title>
      <link>https://kyuntechblog.tistory.com/75</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N1xMK/btsQL6O6Lpk/RceurxqPzUcvCGbrF3KCd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N1xMK/btsQL6O6Lpk/RceurxqPzUcvCGbrF3KCd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N1xMK/btsQL6O6Lpk/RceurxqPzUcvCGbrF3KCd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN1xMK%2FbtsQL6O6Lpk%2FRceurxqPzUcvCGbrF3KCd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;786&quot; height=&quot;330&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 프로젝트를 App Rotuer 로 마이그레이션하면서 아키텍처의 중요성을 다시한번 깨닫고 FSD(Feature-Sliced Design)을 고민해보았다. 기존 프로젝트 구조는 Next.js Pages Router 의 공식 Docs 를 보고 큰 고민없이 확장해나간 모놀리식 구조였다.&lt;/p&gt;
&lt;pre id=&quot;code_1758610298024&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;nextjs-keylog
 ┣ components
 ┣ config
 ┣ hooks
 ┣ pages
 ┃ ┣ [userId]
 ┃ ┃ ┣ posts
 ┃ ┣ api
 ┃ ┃ ┣ auth
 ┃ ┣ resetPassword
 ┣ public
 ┃ ┣ font
 ┃ ┣ icon
 ┣ reducer
 ┣ store
 ┣ styles
 ┣ utils&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;[기존 Pages Router 기반 구조]&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식의 구조를 구성하다보니 프로젝트가 커질수록 컴포넌트가 계속 늘어나 어디에서 쓰이는지 구분하기 어려워졌고, 비즈니스 로직도 도메인 기준이 아닌 util, hook 같은 기술 단위로만 나뉘어 뒤섞이다 보니 구조 자체가 점점 더 복잡해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FSD 란 무엇인가&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1) FSD(Feature-Sliced Design)란?&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 FSD(Feature-Sliced Design)에 대해 간략하게 설명을 해보면 프론트엔드 아키텍처 설계 방법론 중 하나로, 기능 단위로 분할 설계 하여 낮은 결합력과 높은 응집력을 달성하는것을 목표로 하는 아키텍처이다. 이를 통해 코드의 복잡성은 줄이고 유지 보수성과 확장성을 높여 팀원 간의 협업을 극대화할 수 있는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2) FSD의 계층 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 FSD는 크게 아래와 같이 Layers - Slices - Segment 계층으로 나눌수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1355&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcuMo0/btsQJXFPBRX/A1tQkyQh3JpjKjdVLMp140/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcuMo0/btsQJXFPBRX/A1tQkyQh3JpjKjdVLMp140/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcuMo0/btsQJXFPBRX/A1tQkyQh3JpjKjdVLMp140/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcuMo0%2FbtsQJXFPBRX%2FA1tQkyQh3JpjKjdVLMp140%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1355&quot; height=&quot;754&quot; data-origin-width=&quot;1355&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- Layers&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;app&lt;/b&gt;&lt;/span&gt; : 애플리케이션 전역 설정 (라우터, 프로바이더, 글로벌 스타일 등)&lt;/li&gt;
&lt;li&gt;&lt;s&gt;&lt;span&gt;&lt;b&gt;processes&lt;/b&gt;&lt;/span&gt;&lt;/s&gt; : 로그인 흐름, 결제 플로우처럼 여러 기능을 묶어 하나의 큰 프로세스를 표현(더이상 사용되지는 않는다.)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;pages&lt;/b&gt;&lt;/span&gt; : 페이지 단위. 특정 URL에 매핑되는 화면&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;widgets&lt;/b&gt;&lt;/span&gt; : 여러 features나 entities를 조합해서 화면의 큰 블록을 이루는 단위&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;features&lt;/b&gt;&lt;/span&gt; : 사용자에게 제공되는 구체적인 기능 (예: 댓글 작성, 게시글 좋아요 버튼)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;entities&lt;/b&gt;&lt;/span&gt; : 비즈니스 도메인 모델 단위 (예: User, Post, Product)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;shared&lt;/b&gt;&lt;/span&gt; : 프로젝트 전반에서 재사용 가능한 유틸, UI 컴포넌트, 훅 등&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- Slices&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;의미&lt;/span&gt;: 프로젝트를 &lt;span&gt;도메인 단위&lt;/span&gt;로 세로로 나눈 것.&lt;/li&gt;
&lt;li&gt;예를 들어 블로그 서비스라면 &lt;span&gt;Post&lt;/span&gt;, &lt;span&gt;User&lt;/span&gt;, &lt;span&gt;Comment&lt;/span&gt; 같은 도메인별 폴더가 Slice가 될 수 있다.&lt;/li&gt;
&lt;li&gt;한 슬라이스 안에는 그 도메인을 표현하는 UI, 상태관리, API 호출, 비즈니스 로직이 모두 들어갈 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- Segements&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의미&lt;span&gt;: 각 Slice 안에서 기술적 목적에 따라 세분화하여&amp;nbsp;&lt;/span&gt;역할별로 나눈 계층.&lt;/li&gt;
&lt;li&gt;Segment는 팀/프로젝트 성격에 맞게 자유롭게 구성할 수 있고, 보통은 다음과 같은 Segment를 사용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;ui&lt;/span&gt; : React 컴포넌트(UI 레이어)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;model&lt;/span&gt; : 상태관리, 비즈니스 로직 (예: Zustand, React Query, Redux 등)&lt;/li&gt;
&lt;li&gt;&lt;span&gt;api&lt;/span&gt; : 서버와의 통신 로직&lt;/li&gt;
&lt;li&gt;&lt;span&gt;lib&lt;/span&gt; : 순수 유틸/도우미 함수&lt;/li&gt;
&lt;li&gt;config : configuration files, feature flags 등 환경 기능 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;span&gt;Cross-import 금지&lt;/span&gt;: &lt;span&gt;features/*&lt;/span&gt; &amp;rarr; 다른 &lt;span&gt;features/*&lt;/span&gt; 직접 import 지양. 필요한 건 &lt;span&gt;entities/*&lt;/span&gt; 또는 &lt;span&gt;shared/*&lt;/span&gt;로 끌어올려 재사용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 상위 레이어가 하위 레이어를 의존하고, 역방향 의존은 금지한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;허용: &lt;/span&gt;features&lt;span&gt; &amp;rarr; &lt;/span&gt;entities&lt;span&gt;, &lt;/span&gt;shared&lt;/li&gt;
&lt;li&gt;&lt;span&gt;지양: &lt;/span&gt;entities&lt;span&gt; &amp;rarr; &lt;/span&gt;features&lt;span&gt;, &lt;/span&gt;widgets&lt;span&gt; &amp;rarr; &lt;/span&gt;pages&lt;span&gt; 등 상향 의존&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Next.js App Router 에서의 FSD 도입&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FSD 공식 문서를 보면 Next.js 환경에 맞춘 가이드라인이 친절하게 제공된다.(공식 Docs 가이드라인: &lt;a href=&quot;https://feature-sliced.design/docs/guides/tech/with-nextjs&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://feature-sliced.design/docs/guides/tech/with-nextjs&lt;/a&gt;) 특히 Pages Router에서의 pages 레이어와 App Router에서의 app 레이어 간 폴더명 충돌 문제에 대해 다양한 예시를 들어 설명하고 있다. 나 역시 이 부분을 고민했지만, 가이드라인을 그대로 따르기보다는 app 폴더는 유지하되 라우팅과 Pages 레이어의 역할을 동시에 수행하도록 방향을 정했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 고민은 entities 와 features 의 명확한 구분 기준에 대해서도 많은 고민을 했는데 entities 는 비즈니스 api 응답에서 받는 DTO , 즉 명사 개념으로 생각했고 features 는 사용자와의 인터랙션, 기능 위주 즉 동사 개념으로 생각해서 구분하였다.&amp;nbsp; 최종적인 디렉토리 구조는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1758614714627&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;src/
├── app/                         # App Router (route segments만 표시)
│   ├── [userId]/
│   │   └── [postId]/
│   ├── api/
│   ├── findPassword/
│   ├── resetPassword/
│   ├── login/
│   ├── signup/
│   ├── write/
│   ├── home/
│   ├── provider/
├── entities/                    # 엔티티 레이어
│   ├── article/
│   │   ├── api/
│   │   ├── component/
│   │   ├── lib/
│   │   ├── model/
│   │   └── query/
│   ├── comment/
│   │   ├── api/
│   │   ├── component/
│   │   └── model/
│   ├── hashtag/
│   │   ├── api/
│   │   ├── component/
│   │   ├── lib/
│   │   └── model/
│   ├── like/
│   │   ├── api/
│   │   ├── component/
│   │   ├── lib/
│   │   └── model/
│   ├── post/
│   │   ├── api/
│   │   ├── component/
│   │   ├── lib/
│   │   ├── model/
│   │   └── query/
│   ├── trend/
│   │   ├── api/
│   │   ├── component/
│   │   ├── hooks/
│   │   ├── lib/
│   │   ├── model/
│   │   └── query/
│   └── user/
│       ├── api/
│       ├── component/
│       └── model/
├── features/                    # 기능 레이어
│   ├── account/
│   │   ├── api/
│   │   ├── component/
│   │   ├── lib/
│   │   └── model/
│   ├── comment/
│   │   ├── api/
│   │   ├── component/
│   │   ├── hooks/
│   │   ├── lib/
│   │   └── model/
│   ├── like/
│   │   ├── api/
│   │   └── hooks/
│   ├── login/
│   │   ├── api/
│   │   ├── component/
│   │   ├── lib/
│   │   └── model/
│   ├── logout/
│   │   └── api/
│   ├── post/
│   │   ├── api/
│   │   ├── component/
│   │   ├── hooks/
│   │   ├── lib/
│   │   └── model/
│   └── signup/
│       ├── api/
│       ├── component/
│       ├── lib/
│       └── model/
├── shared/                      # 공용 레이어
│   ├── boundary/
│   ├── hooks/
│   ├── lib/
│   └── ui/
├── styles/
│   ├── fonts/
│   ├── globals.css
│   └── scss/
└── widgets/                     # 페이지 위젯
    ├── article/
    ├── footer/
    ├── header/
    └── sidebar/&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Segments에서&amp;nbsp; ui 대신 component 라는 segment를 만들어서 사용했는데 이유는 ui 라고 하기에 SSR을 사용하기위한 서버컴포넌트가 있어서 순수 UI 라 말할수는 없을 것 같아 component segment를 만들고 그 아래 순수 ui 만 렌더링하는 ui 세그먼트를 하나 더 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FSD 아키텍처를 도입해보며 느낀점&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FSD 공식 문서에서 제시하는 가이드라인을 그대로 FM처럼 도입한 것은 아니지만, 직접 적용해보니 DX가 얼마나 크게 향상되는지 체감할 수 있었다. 이전 구조에서는 components, utils, hooks 같은 기술 단위 폴더에 코드가 흩어져 있어 새로운 기능을 추가할 때마다 서로 다른 관심사의 코드들이 뒤섞이며 어디에 넣어야 할지 고민이 많았다. 반면 FSD는 기능 단위(Slice)로 구조가 나뉘어 있어 필요한 api, model, ui, lib 코드가 한 폴더 안에 모여 있으니 새로운 기능을 개발할 때 해당 Slice에만 집중하면 되고, 다른 Slice에 불필요한 영향을 주지 않아 개발 속도와 안정성 측면에서도 큰 개선을 가져올 수 있었다. 규모나 복잡도가 높은 프로젝트 일 수록 더 직관적이고, 유지보수가 쉽고, 확장성이 높은 구조의 이점이 더 확연하게 느낄 수 있는 아키텍처라는 생각이 든다.&lt;/p&gt;</description>
      <category>Fe-Dev/Architecture</category>
      <category>Architecture</category>
      <category>fsd</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/75</guid>
      <comments>https://kyuntechblog.tistory.com/75#entry75comment</comments>
      <pubDate>Tue, 23 Sep 2025 17:38:58 +0900</pubDate>
    </item>
    <item>
      <title>useInfiniteQuery를 활용한 무한 스크롤 구현과 뒤로가기 스크롤 복원</title>
      <link>https://kyuntechblog.tistory.com/74</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bANnkH/btsQHy0myVF/wjSAukDN58Pvs7LheqBVjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bANnkH/btsQHy0myVF/wjSAukDN58Pvs7LheqBVjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bANnkH/btsQHy0myVF/wjSAukDN58Pvs7LheqBVjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbANnkH%2FbtsQHy0myVF%2FwjSAukDN58Pvs7LheqBVjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;651&quot; height=&quot;407&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js v15를 기반으로 개인 프로젝트를 진행하면서 게시물들을 블러올 때 무한 스크롤을 구현할 상황이 생겨서 IntersectionObserver 를 기반으로 무한 스크롤을 구현하였다. 근데 게시물을 클릭하여 상세 페이지로 이동 후 뒤로가기를 하니 이전 스크롤 위치가 아닌 최상단으로 스크롤이 올라가는 현상이 있었다. 이런 현상은 사용자 경험에 있어 매우 좋지 않다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선하기 위해 Next.js 에서 혹시 제공해주는 기능이 있는지 공식 Docs 를 먼저 뒤져봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1758516706118&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// next.config.js
module.exports = {
  experimental: {
    scrollRestoration: true
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험적 기능으로 scrollRestoration 옵션이 있었다. 해당 기능을 켜고 테스트 해봤지만 기대하는 대로 동작하지 않았다. 정확하지 않지만 body에 overflow-y 로 스크롤을 걸었더니 정상적으로 동작은 했다. 하지만 내 개인 프로젝트는 body 외에 내부적으로&amp;nbsp;스크롤 기능이 필요한 컴포넌트들이 있어 직접 구현하기로 결정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Session Storage 를 활용한 스크롤 복원&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 세션스토리지를 사용한 이유는 단순하고 휘발성이 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬스토리지처럼 탭을 닫고나서도 기억하고 있을 필요도 없고 쿠키보다 상대적으로 구현하기 편리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  스크롤 위치 저장시점은 언제 ?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크롤 위치를 저장하는 시점에 대한 고민이 많았다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. beforeunload 이벤트&lt;/h3&gt;
&lt;pre id=&quot;code_1758522193796&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;window.addEventListener(&quot;beforeunload&quot;, handler)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 단순하게 생각해보면 페이지를 나가기 직전에 저장하는게 맞으니까 제일 먼저 떠오른 방법이지만 React는 Single Page Application 이다. 내부적으로 감지할수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. next/router 이벤트(구 pages router)&lt;/h3&gt;
&lt;pre id=&quot;code_1758522472696&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import Router from 'next/router';

useEffect(() =&amp;gt; {
  const onStart = () =&amp;gt; saveScroll(); // routeChangeStart 시 저장
  Router.events.on('routeChangeStart', onStart);
  return () =&amp;gt; Router.events.off('routeChangeStart', onStart);
}, []);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 next/router 이벤트를 활용해서 route 가 변하는 시점에 저장하는 방법인데 app router를 사용하고 있어서 패스했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app router에서 사용하는 next/navigation 의 useRotuer 는 저런 이벤트를 제공하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 라우트 전환 감지 (usePathname)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useEffect에&amp;nbsp; usePathname 에서 가져온 pathname 을 의존성으로 걸고 해당 컴포넌트가 unmount 되는 시점을 잡아보려고도 해봤지만&amp;nbsp;그 시점은 이미 새로운 페이지로 진입한 이후에 잡혀서 이것도 실패했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. onClick 이벤트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 선택한 방법은 새로운 페이지로 이동하기 위해 next/link 를 클릭할 때 onClick 이벤트를 활용하여 저장하는 걸로 결정했다. 물론 스크롤 유지가 필요한 컴포넌트에 모두 걸어줘야 하는 단점이 존재하긴 하지만 스크롤 유지가 필요한 리스트페이지가 개인프로젝트 특성상 그렇게 많지는 않아서 선택했다. 커스텀 훅으로 구현 코드는 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758518182277&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client';

import { usePathname } from 'next/navigation';
import { useSearchParams } from 'next/navigation';

interface Props {
  scrollElementId: string;
  extendQueryParams?: boolean;
}

export const useScrollRestoration = ({ scrollElementId, extendQueryParams = false }: Props) =&amp;gt; {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  const query = searchParams.toString();
  const key = createScrollKey(pathname, query, extendQueryParams);

  return {
    saveScrollPos: () =&amp;gt; saveScrollPos(scrollElementId, key),
    restoreScrollPos: () =&amp;gt; restoreScrollPos(scrollElementId, key),
  };
};

const createScrollKey = (pathname: string, query: string, extendQueryParams?: boolean) =&amp;gt; {
  return `scrollPos_${pathname}${extendQueryParams &amp;amp;&amp;amp; query ? `_${query}` : ''}`;
};

const saveScrollPos = (scrollElementId: string, key: string) =&amp;gt; {
  const el = document.getElementById(scrollElementId);
  if (!el) return;
  sessionStorage.setItem(key, el.scrollTop.toString());
};

const restoreScrollPos = (scrollElementId: string, key: string) =&amp;gt; {
  const el = document.getElementById(scrollElementId);
  const scrollTop = sessionStorage.getItem(key);
  if (!el || !scrollTop) return;
  requestAnimationFrame(() =&amp;gt; {
    el.scrollTop = parseInt(scrollTop, 10);
  });
  sessionStorage.removeItem(key);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터로 스크롤을 유지하고 싶은 element의 ID 를 넘겨주고 url path 를 키값으로 sessionStorage 에 스크롤 위치를 저장해줬다. saveScrollPos 를 onClick 에 걸고 스크롤 복원은 restoreScrollPos 로 렌더링이 완료되는 시점에 복원해줬다. extendQueryParams 는 쿼리스트링까지 키값으로 활용하고 싶은 경우에 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1758523674549&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;'use client'

import { useEffect } from 'react'
import Link from 'next/link'
import { useScrollRestoration } from '@/shared/hooks'

export function BlogPostItem() {
  const { saveScrollPos, restoreScrollPos } = useScrollRestoration({
    scrollElementId: 'article',
    extendQueryParams: true,
  })

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

  return (
    &amp;lt;Link href=&quot;/somewhere&quot; onClick={saveScrollPos}&amp;gt;
      {..중략...}
    &amp;lt;/Link&amp;gt;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 컴포넌트에서의 사용방법은 위와 같다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;useInfiniteQuery 를 사용한 무한 스크롤 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스크롤 복원을 위한 커스텀 훅을 만들었지만 기존 IntersectionObserver 만을 사용해서 구현한 무한 스크롤에서는 정상적으로 동작하지 않는다. 설정한 perPage 개수 만큼 한페이지에 게시물들을 불러오고 마지막 게시물이 뷰포트에 관측되면 다음 페이지 게시물들을 요청하여 아래에 뿌리는데 2페이지 이상만 넘어가도 뒤로가기시 default 로 불러오는 1페이지까지의 게시물 개수만큼만 있기 때문에 스크롤이 더 아래로 내려갈데가 없다.. 스크롤을 내린 이후 다음 데이터 패칭이 일어나기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 아래와 같은 flow 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1) 3페이지까지 스크롤을 내림&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) 게시물 상세 페이지 이동&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) 뒤로가기&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) 1페이지 게시물만 있음&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) 스크롤 복원으로 1페이지 끝까지 스크롤이 내려감(더 내려가야하지만 스크롤이 끝남)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6) 다음 페이지 데이터패칭&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7) 스크롤 늘어남&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 해결은 단순하게 생각하면 뒤로 갔을때 이전 페이지 개수만큼 게시물이 있으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useInfiniteQuery를 사용하면 이 문제가 명쾌하게 해결된다.&lt;/p&gt;
&lt;pre id=&quot;code_1758524764730&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { Post } from '../model';
import { getPosts } from '../api';
import { queryKey } from '@/app/provider/query/lib';
import { useInfiniteQuery, InfiniteData, QueryKey } from '@tanstack/react-query';

interface UseInfinitePostsQueryProps {
  searchWord?: string;
  initialPage?: Post[];
}

export const useInfinitePostsQuery = ({ searchWord = '', initialPage }: UseInfinitePostsQueryProps) =&amp;gt; {
  // 초기 데이터
  const initialData: InfiniteData&amp;lt;Post[], number&amp;gt; | undefined = initialPage
    ? { pages: [initialPage], pageParams: [1] }
    : undefined;

  return useInfiniteQuery&amp;lt;Post[], Error, InfiniteData&amp;lt;Post[], number&amp;gt;, QueryKey, number&amp;gt;({
    queryKey: queryKey().post().postSearch({ searchWord }),
    initialPageParam: 1,
    queryFn: async ({ pageParam }) =&amp;gt; await getPosts({
        searchWord,
        currPageNum: pageParam,
      });
    },
    // 다음페이지 반환 값
    getNextPageParam: (lastPage, _allPages, lastPageParam) =&amp;gt; {
      const totalItems = lastPage[0]?.totalItems;
      const lastPageItemIndex = Number(lastPage[lastPage.length - 1]?.pageIndx);
      const hasMorePages = totalItems &amp;amp;&amp;amp; lastPageItemIndex !== totalItems;
      return hasMorePages ? lastPageParam + 1 : undefined;
    },
    initialData,
  });
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- queryKey : 명시한 queryKey로 데이터 페칭을 통해 가져온 데이터를 누적하여 캐싱한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- initialPageParam: 초기 페이지 번호&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- queryFn:&amp;nbsp; 데이터를 페치해오기 위한 함수이다. pageParam은 useInfiniteQuery가 내부적으로 관리하고 있는 페이지 번호로 같은 키로 관리되는 리스트의 다음 조각을 불러오기 위한 제어 값이라고 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- getNextPageParam: 무한 스크롤에서 다음 페이지를 무엇으로 요청할지 결정하는 함수이다. 로직을 직접 구현해야하는데 반환값이 다음 요청의 pageParam이 되고 undefined 를 반환하면 더이상 불러오지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 모든 게시물 데이터의 raw 응답에 totalItems 컬럼이 포함되어 있어, 마지막 게시물의 페이지 인덱스와 비교하여 이후에 더 페이지가 존재하는지 판별하고 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1758527785259&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching } = useInfinitePostsQuery({
  searchWord,
  initialPage: initPosts,
  });

// 마지막 게시물이 viewport 에 노출되면 fetchNextPage
const { setTarget } = useIntersectionObserver({
    onShow: () =&amp;gt; {
      if (hasNextPage &amp;amp;&amp;amp; !isFetchingNextPage &amp;amp;&amp;amp; !isFetching) {
        fetchNextPage();
      }
    },
    once: true,
  });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용은 위와 같이 IntersectionObserver 기반의 커스텀 훅에서 마지막 게시물이 노출 될 때 useInfiniteQuery 가 다음 페이지 데이터를 페치 해오도록 설정하였다. getNextPageParam 의 반환값이 undefined 가 아니면 hasNextPage의 값은 true 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;useInfinitQuery 가 반환하는 프로퍼티는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- data: 누적 페이지 데이터들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- fetchNextPage: 다음 페이지 트리거&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- hasNextPage: 다음 페이지 존재 여부&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- isFetchingNextPage: 다음 페이지 로딩중 여부&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- isFetching: 이 쿼리로 네트워크 요청중 여부&lt;/p&gt;</description>
      <category>Fe-Dev/React.js</category>
      <category>useInfiniteQuery</category>
      <category>무한스크롤</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/74</guid>
      <comments>https://kyuntechblog.tistory.com/74#entry74comment</comments>
      <pubDate>Mon, 22 Sep 2025 17:08:04 +0900</pubDate>
    </item>
    <item>
      <title>Next.js Pages Router에서 App Router로: 데이터 패칭 흐름 개선기</title>
      <link>https://kyuntechblog.tistory.com/73</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cq0JnC/btsQIPAbZGE/dkeW8PXgr47yAfylvRkbKk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cq0JnC/btsQIPAbZGE/dkeW8PXgr47yAfylvRkbKk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cq0JnC/btsQIPAbZGE/dkeW8PXgr47yAfylvRkbKk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcq0JnC%2FbtsQIPAbZGE%2FdkeW8PXgr47yAfylvRkbKk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;420&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 next.js pages router 로 진행하였던 개인 프로젝트(&lt;a title=&quot;개인 프로젝트&quot; href=&quot;https://github.com/alkalisummer/nextjs-keylog&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/alkalisummer/nextjs-keylog)&lt;/a&gt;를 v15.3으로 버전을 올리면서 app router 로 마이그레이션을 진행하였다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 직장에서 nuxt.js 를 next.js(v15.2.5)의 app router 로 마이그레이션 해본 경험을 토대로 진행하며 개선점을 정리해보려한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크게 와닿았던 개선점은 app router에서 서버 컴포넌트와 스트리밍 SSR이 가능해지면서 data fetch가 끝나고 준비된 컴포넌트부터 순차적으로 렌더링하여 바로 사용자에게 보여줄 수 있다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pages Router - 기존 Data Fetch 패턴의 한계점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pages router에서의 SSR 은 getServerSideProps 에서 해당 page에 진입할 때 필요한 데이터를 한번에 fetch 하여 클라이언트 컴포넌트로 내려주는 구조다. 예시로 아래의 sudo 코드를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1758187856572&quot; class=&quot;javascript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;const Blog =({ ...props }) =&amp;gt; {
  const {authorInfo, recentPosts, popularPosts, recentComments, hashtags} = props 
  return (
    &amp;lt;&amp;gt;
      &amp;lt;AuthorInfo authorInfo={authorInfo}/&amp;gt;
      &amp;lt;RecentPosts recentPosts={recentPosts}/&amp;gt;
      &amp;lt;PopularPosts popularPosts={popularPosts}/&amp;gt;
      &amp;lt;RecentComments recentComments={recentComments}/&amp;gt;
      &amp;lt;hashtags hashtags={hashtags}/&amp;gt;
    &amp;lt;/&amp;gt;
  )
}
export default Blog

export const getServerSideProps = (ctx) =&amp;gt; {
 
  const session = await fetchSession(ctx)
  const authorId = ctx.query.auhorId
  
  // Blog page 에 필요한 데이터를 병렬로 fetch
  if (userId) {
    const [
      authorInfo,
      recentPosts,
      popularPosts,
      recentComments,
      hashtags,
    ] = await Promise.all([
      fetchData(&quot;getAuthor&quot;,      { id: authorId }),
      fetchData(&quot;getRecentPosts&quot;,  { id: authorId }),
      fetchData(&quot;getPopularPosts&quot;, { id: authorId }),
      fetchData(&quot;getRecentComments&quot;,{ id: authorId }),
      fetchData(&quot;getAuthorHashtags&quot;,{ id: authorId }),
    ])
  }

  return {
    props: {
      authorInfo,
      recentPosts,
      popularPosts,
      recentComments,
      hashtags,
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 getServerSideProps 를 통해 server-side 에서 이 페이지에서 필요한 블로그 저자의 정보, 최근게시물, 인기 게시물, 최근 댓글등의 데이터를&amp;nbsp; SSR로 fetch 해오는데 병렬로 실행된다고 하더라도 어느 하나가 늦게 도착하면 Promise All의 특성상 꼼짝없이 기다려야 한다. 위 구조를 app router의 서버컴포넌트 + 스트리밍 SSR 로 개선한 구조는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;App Router &amp;ndash; 요청은 한 번만, 먼저 온 놈 먼저&lt;/h2&gt;
&lt;pre id=&quot;code_1758509400898&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import { PropsWithChildren, ReactNode, Suspense } from 'react';
import { ErrorBoundary } from './ErrorBoundary';

interface AsyncBoundaryProps {
  pending: ReactNode | null;
  error: ReactNode | null;
}

// loading 상태에 따른 fallback과 error를 처리
export const AsyncBoundary = ({ pending, error, children }: PropsWithChildren&amp;lt;AsyncBoundaryProps&amp;gt;) =&amp;gt; {
  return (
    &amp;lt;ErrorBoundary fallback={error}&amp;gt;
      &amp;lt;Suspense fallback={pending}&amp;gt;{children}&amp;lt;/Suspense&amp;gt;
    &amp;lt;/ErrorBoundary&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1758507996163&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Props {
  authorId: string;
}

export const Sidebar = async ({ authorId }: Props) =&amp;gt; {
  const authorInfo = getAuthor(authorId);
  const recentPosts = getRecentPosts(authorId);
  const popularPosts = getPopularPosts(authorId);
  const recentComments = getRecentComments(authorId);
  const hashtags = getAuthorHashtags(authorId);
  
  //promise 객체를 그대로 내려줌
  return (
    &amp;lt;aside className={css.module}&amp;gt;
      &amp;lt;div className={css.leftArea}&amp;gt;
        &amp;lt;AsyncBoundary pending={&amp;lt;BoxSkeleton height={166.5} /&amp;gt;} error={&amp;lt;BoxError height={150} /&amp;gt;}&amp;gt;
          &amp;lt;UserInfo promise={{ authorInfo }} authorId={authorId} /&amp;gt;
        &amp;lt;/AsyncBoundary&amp;gt;
        &amp;lt;AsyncBoundary pending={&amp;lt;BoxSkeleton height={166.5} /&amp;gt;} error={&amp;lt;BoxError height={150} /&amp;gt;}&amp;gt;
          &amp;lt;RecentPosts promise={{ recentPosts }} authorId={authorId} /&amp;gt;
        &amp;lt;/AsyncBoundary&amp;gt;
        &amp;lt;AsyncBoundary pending={&amp;lt;BoxSkeleton height={166.5} /&amp;gt;} error={&amp;lt;BoxError height={150} /&amp;gt;}&amp;gt;
          &amp;lt;PopularPosts promise={{ popularPosts }} authorId={authorId} /&amp;gt;
        &amp;lt;/AsyncBoundary&amp;gt;
        &amp;lt;AsyncBoundary pending={&amp;lt;BoxSkeleton height={166.5} /&amp;gt;} error={&amp;lt;BoxError height={150} /&amp;gt;}&amp;gt;
          &amp;lt;RecentComments promise={{ recentComments }} authorId={authorId} /&amp;gt;
        &amp;lt;/AsyncBoundary&amp;gt;
        &amp;lt;AsyncBoundary pending={&amp;lt;BoxSkeleton height={150} /&amp;gt;} error={&amp;lt;BoxError height={150} /&amp;gt;}&amp;gt;
          &amp;lt;SidebarHashtags promise={{ hashtags }} authorId={authorId} /&amp;gt;
        &amp;lt;/AsyncBoundary&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/aside&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 서버컴포넌트인 Sidebar 컴포넌트에서 필요한 데이터를 fetch 해오는 함수들은 모두 Promise 객체이다. 중요한 점은 Sidebar 컴포넌트에서 await 으로 데이터를 fetch 하여 응답을 기다리는게 아니라 그대로 해당 데이터가 필요한 컴포넌트에 props 로 내려주는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 패턴으로 next.js의 스트리밍 SSR을 활용하여 먼저 응답이 도착한 컴포넌트 부터 순차적으로 렌더링하여 사용자에게 보여줄 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 의문이 생길 수 있다. 특정 데이터 패칭 함수를 여러 컴포넌트에서 공통으로 사용한다면 호출이 중복되지 않을까 하는 점이다. 그러나 이 함수들은 내부적으로 Next.js의 fetch를 사용하고 있으며, Next.js는 한 렌더 사이클 내에서 동일한 URL 요청을 한 번만 수행하고 결과를 재사용한다. 따라서 불필요한 중복 네트워크 요청에 대해서는 걱정하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Async Boundary 컴포넌트는 React의 Suspense와 Error Boundary를 활용하여, Promise를 반환하는 데이터 패칭 함수의 로딩 상태는 fallback으로, 에러는 에러 핸들러로 처리하도록 책임을 분리해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 패칭을 위한 promise 를 props로 받은 이후에는 react query 로 아래와 같이 작성하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1758511614908&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;export const RecentPosts = async ({ promise, authorId }: Props) =&amp;gt; {
  const queryClient = new QueryClient();

  const recentPostsQueryOptions = {
    queryKey: queryKey().post().recentPost(authorId),
    queryFn: () =&amp;gt; promise.recentPosts,
  };
  await queryClient.prefetchQuery(recentPostsQueryOptions);

  const dehydratedState = dehydrate(queryClient);

  return (
    &amp;lt;HydrateClient state={dehydratedState}&amp;gt;
      &amp;lt;View/&amp;gt;
    &amp;lt;/HydrateClient&amp;gt;
  );
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;View 컴포넌트는 UI 를 렌더링하는 담긴 클라이언트 컴포넌트이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 컴포넌트에서 데이터를 가져오는 동안 클라이언트 컴포넌트는 Suspense의 fallback UI를 노출하고, 패칭 완료 시 queryKey 기반으로 캐싱된 데이터를 useQuery로 즉시 불러와 렌더링하도록 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 패턴으로 화면 진입 시 즉시 보여야 하는 데이터나 사용자와의 인터랙션이 필요 없는 정적 데이터는, 스트리밍 SSR을 활용해 서버 컴포넌트에서 미리 패치해 두고 응답이 도착한 컴포넌트부터 순차적으로 렌더링하는 방식이 특히 유용했다.&lt;/p&gt;</description>
      <category>Fe-Dev/Next.js</category>
      <category>App Router</category>
      <category>Next.js</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/73</guid>
      <comments>https://kyuntechblog.tistory.com/73#entry73comment</comments>
      <pubDate>Mon, 22 Sep 2025 12:51:25 +0900</pubDate>
    </item>
    <item>
      <title>Google Trends API, npm 패키지 배포</title>
      <link>https://kyuntechblog.tistory.com/72</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Google-Trends-1.png&quot; data-origin-width=&quot;1509&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6hAWB/btsOokwGM0T/CrLedqv3lxcUfpktwiSrP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6hAWB/btsOokwGM0T/CrLedqv3lxcUfpktwiSrP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6hAWB/btsOokwGM0T/CrLedqv3lxcUfpktwiSrP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6hAWB%2FbtsOokwGM0T%2FCrLedqv3lxcUfpktwiSrP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;522&quot; height=&quot;293&quot; data-filename=&quot;Google-Trends-1.png&quot; data-origin-width=&quot;1509&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;개요&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 개인적으로 진행하던 사이드프로젝트를 리팩토링 해볼겸 로컬에서 서버를 올려봤는데 ..&amp;nbsp;동적으로 Google Trends 데이터를 받아오기 위해 사용하던 npm 패키지(&lt;a href=&quot;https://www.npmjs.com/package/google-trends-api&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/google-trends-api&lt;/a&gt;)가 어느순간 먹통이 되어 디버깅을 해보니 response가 왠 404 에러가 .. 뭐지 .. 하는 기분으로 goole trends 에 먼저 접속해봤더니 UI가 대폭 업데이트 되었다.. npm 에서 설치하여 사용하던 패키지는 Google 에서 공식적으로 제공해주는 라이브러리가 아니기에 해당 소스 repository의 github issue 탭을 들어가보니 아니나 다를까 UI 가 업데이트 되면서 Google Trends 자체적으로 clientside쪽에서 호출하는 여러 API들의 endpoint도 같이 변경된 것으로 보였고 나와 같은 고통을 겪는 다른 개발자들의 이슈 글들이 보였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2314&quot; data-origin-height=&quot;1806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lEMJ6/btsOojKWshG/1CzJO3DAkfLfGR0JKQD7m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lEMJ6/btsOojKWshG/1CzJO3DAkfLfGR0JKQD7m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lEMJ6/btsOojKWshG/1CzJO3DAkfLfGR0JKQD7m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlEMJ6%2FbtsOojKWshG%2F1CzJO3DAkfLfGR0JKQD7m0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;511&quot; height=&quot;399&quot; data-origin-width=&quot;2314&quot; data-origin-height=&quot;1806&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2월달부터 게시된 이슈글이 아직 해결되지 않은 것을 보니 해당 패키지는 더이상 관리되지 않는 것으로 보였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 디깅을 해보다가 다른 개발자가 타입스크립트로 개발해놓은 패키지를 발견하였지만 기존 패키지보다 다소 기능이 많이 부족하여 해당 레포를 fork떠서 기존 기능들을 직접 추가하고 npm 에 배포해보기로 했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;NPM 배포&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발 방식은 네트워크 탭에 들어가서 각 API들 request 및 payload를 분석하고 response 응답을 parsing 하여 만들었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qvHp5/btsOopdcHst/mySC02lkyjFxNNhrH3PRk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qvHp5/btsOopdcHst/mySC02lkyjFxNNhrH3PRk0/img.png&quot; data-origin-width=&quot;2398&quot; data-origin-height=&quot;1040&quot; data-is-animation=&quot;false&quot; width=&quot;598&quot; height=&quot;259&quot; style=&quot;width: 49.2459%; margin-right: 10px;&quot; data-widthpercent=&quot;49.83&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qvHp5/btsOopdcHst/mySC02lkyjFxNNhrH3PRk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqvHp5%2FbtsOopdcHst%2FmySC02lkyjFxNNhrH3PRk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2398&quot; height=&quot;1040&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/saCil/btsOpIo8HT5/7aqeM4NH9FSv6kZkoApK1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/saCil/btsOpIo8HT5/7aqeM4NH9FSv6kZkoApK1k/img.png&quot; data-origin-width=&quot;2582&quot; data-origin-height=&quot;1112&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.5913%;&quot; data-widthpercent=&quot;50.17&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/saCil/btsOpIo8HT5/7aqeM4NH9FSv6kZkoApK1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsaCil%2FbtsOpIo8HT5%2F7aqeM4NH9FSv6kZkoApK1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2582&quot; height=&quot;1112&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;[request payload 및 response 분석]&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Google trends 에서 동적으로 클릭해서 요청이 발생하면 payload에 담기는 파라미터 값들이 패턴이 있어서 엄청 어렵지는 않았습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- package.json&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1749014627865&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;@alkalisummer/google-trends-js&quot;,
  &quot;version&quot;: &quot;0.1.0&quot;,
  &quot;description&quot;: &quot;Google Trends API for Node.js&quot;,
  &quot;main&quot;: &quot;lib/index.js&quot;,
  &quot;types&quot;: &quot;lib/index.d.ts&quot;,
  &quot;scripts&quot;: {
    &quot;build&quot;: &quot;tsc&quot;,
    &quot;format&quot;: &quot;prettier --write \&quot;src/**/*.(js|ts)\&quot;&quot;,
    &quot;lint&quot;: &quot;eslint src --ext .js,.ts&quot;,
    &quot;lint:fix&quot;: &quot;eslint src --fix --ext .js,.ts&quot;,
    &quot;test&quot;: &quot;jest --config jest.config.js&quot;,
    &quot;prepare&quot;: &quot;npm run build&quot;,
    &quot;prepublishOnly&quot;: &quot;npm test &amp;amp;&amp;amp; npm run lint&quot;,
    &quot;preversion&quot;: &quot;npm run lint&quot;,
    &quot;version&quot;: &quot;npm run format &amp;amp;&amp;amp; git add -A src&quot;,
    &quot;postversion&quot;: &quot;git push &amp;amp;&amp;amp; git push --tags&quot;
  },
  &quot;repository&quot;: {
    &quot;type&quot;: &quot;git&quot;,
    &quot;url&quot;: &quot;git+https://github.com/alkalisummer/google-trends-js.git&quot;
  },
  &quot;keywords&quot;: [
    &quot;google trends&quot;,
    &quot;keyword&quot;,
    &quot;google&quot;,
    &quot;trends&quot;,
    &quot;API&quot;,
    &quot;search&quot;,
    &quot;typescript&quot;,
    &quot;npm&quot;,
    &quot;module&quot;
  ],
  &quot;author&quot;: &quot;alkalisummer&quot;,
  &quot;private&quot;: false,
  &quot;publishConfig&quot;: {
    &quot;access&quot;: &quot;public&quot;
  },
  &quot;license&quot;: &quot;MIT&quot;,
  &quot;devDependencies&quot;: {
    &quot;@types/jest&quot;: &quot;29.5.14&quot;,
    &quot;@typescript-eslint/eslint-plugin&quot;: &quot;5.62.0&quot;,
    &quot;@typescript-eslint/parser&quot;: &quot;5.62.0&quot;,
    &quot;eslint&quot;: &quot;8.57.1&quot;,
    &quot;eslint-plugin-jest&quot;: &quot;27.9.0&quot;,
    &quot;jest&quot;: &quot;29.7.0&quot;,
    &quot;prettier&quot;: &quot;2.8.8&quot;,
    &quot;ts-jest&quot;: &quot;29.2.5&quot;,
    &quot;typescript&quot;: &quot;4.9.5&quot;
  },
  &quot;files&quot;: [
    &quot;lib/**/*&quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발을 다하고 나면 package.json을 위와 같이 정보에 맞게 수정후&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(새로 배포할 때마다 버전은 무조건 올려줘야 합니다.)&lt;/p&gt;
&lt;pre id=&quot;code_1749014586163&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm publish --access public&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어로 npm 에 배포할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NXwRV/btsOor3pdwz/6AkrpWlryUaqseLK8VaTpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NXwRV/btsOor3pdwz/6AkrpWlryUaqseLK8VaTpK/img.png&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;848&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.4399%; margin-right: 10px;&quot; data-widthpercent=&quot;51.03&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NXwRV/btsOor3pdwz/6AkrpWlryUaqseLK8VaTpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNXwRV%2FbtsOor3pdwz%2F6AkrpWlryUaqseLK8VaTpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1248&quot; height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xv2pb/btsOpdXFmSV/rrR6T8gVrii6jXfcK5iYV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xv2pb/btsOpdXFmSV/rrR6T8gVrii6jXfcK5iYV0/img.png&quot; data-origin-width=&quot;2474&quot; data-origin-height=&quot;1752&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.3973%;&quot; data-widthpercent=&quot;48.97&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xv2pb/btsOpdXFmSV/rrR6T8gVrii6jXfcK5iYV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxv2pb%2FbtsOpdXFmSV%2FrrR6T8gVrii6jXfcK5iYV0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2474&quot; height=&quot;1752&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 배포가 완료되면 npm에서 배포된 패키지 모듈을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능에 관한 설명은 readme에 정리해놨습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포된 npm 패키지 url&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/@alkalisummer/google-trends-js&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.npmjs.com/package/@alkalisummer/google-trends-js&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749110578263&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;@alkalisummer/google-trends-js&quot; data-og-description=&quot;Google Trends API for Node.js. Latest version: 0.1.0, last published: an hour ago. Start using @alkalisummer/google-trends-js in your project by running &amp;#96;npm i @alkalisummer/google-trends-js&amp;#96;. There are no other projects in the npm registry using @alkalisu&quot; data-og-host=&quot;www.npmjs.com&quot; data-og-source-url=&quot;https://www.npmjs.com/package/@alkalisummer/google-trends-js&quot; data-og-url=&quot;https://www.npmjs.com/package/@alkalisummer/google-trends-js&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bSNvWH/hyY4dkZPdf/odELBksid1nfZPNUiyt6GK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://www.npmjs.com/package/@alkalisummer/google-trends-js&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.npmjs.com/package/@alkalisummer/google-trends-js&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bSNvWH/hyY4dkZPdf/odELBksid1nfZPNUiyt6GK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;@alkalisummer/google-trends-js&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Google Trends API for Node.js. Latest version: 0.1.0, last published: an hour ago. Start using @alkalisummer/google-trends-js in your project by running `npm i @alkalisummer/google-trends-js`. There are no other projects in the npm registry using @alkalisu&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.npmjs.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Node.js</category>
      <category>Google Trends</category>
      <category>google trends api</category>
      <category>npm</category>
      <category>npm 패키지 배포</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/72</guid>
      <comments>https://kyuntechblog.tistory.com/72#entry72comment</comments>
      <pubDate>Wed, 4 Jun 2025 14:31:54 +0900</pubDate>
    </item>
    <item>
      <title>AWS API Gateway - Lambda 를 활용한 동적 이미지 프로세싱</title>
      <link>https://kyuntechblog.tistory.com/71</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;근래 사내 프로젝트로 AWS Lambda 를 활용하여 on-the-fly 방식으로 동적 이미지 프로세싱 프로젝트를 진행하게 되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하며 겪었던 이슈와 과정들을 회고하며 간략하게 정리해보려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;배경&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희 회사는 이커머스 업계 특성상 굉장히 많은 상품 이미지를 AWS S3로 관리하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 이미 동적 이미지 프로세싱 및 캐싱 기능의 역할을 하는 외부 솔루션을 도입하여 Image 서버가 구축되어 있었고 월마다 고정 비용이 나가고 있었는데요. 클라이언트에서 이미지 요청의 경우 flow 를 자세히 보니 조금 의아한 부분이 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;534&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l0v1J/btsNyyBMD9t/WnEDYLN6t503rLyYwZrZP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l0v1J/btsNyyBMD9t/WnEDYLN6t503rLyYwZrZP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l0v1J/btsNyyBMD9t/WnEDYLN6t503rLyYwZrZP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl0v1J%2FbtsNyyBMD9t%2FWnEDYLN6t503rLyYwZrZP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1848&quot; height=&quot;534&quot; data-origin-width=&quot;1848&quot; data-origin-height=&quot;534&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 이미지 요청 flow 를 간략하게 도식화한 이미지입니다. 이미 AWS CloudFront가 제일 앞 단에서 url 을 key 값으로 캐싱을 잘해주고 있었고 실제 Image Server 의 캐싱 히트율은 2-3%대에 지나지 않았습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이미지 프로세싱만 잘 구현하면 굳이 Image Server 솔루션 사용할 이유가 있을까 ? 에서 시작된 PoC 였습니다. 초기에는 nest.js를 기반으로 sharp 라이브러리를 활용하여 Image Server 를 대체할 서버를 구축하려 했으나 구글링을 해보니 AWS Lambda 를 활용하여 비교적 더 간단한게 구현한 선례들이 많아 API Gateway + Lambda로 우회하게 되었습니다. Lambda에 대해 간략하게 설명하면 AWS 에서 제공하는 serverless 함수 기반 서비스로 작성된 코드를 aws 가 알아서 실행해주는 저렴한 가격의 훌륭한 서비스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리하여 AWS API Gateway + Lambda 를 도입한 이미지 요청 flow 는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciHkNN/btsNyTFnpj5/kunJpWiYK4feX3wrpPCA7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciHkNN/btsNyTFnpj5/kunJpWiYK4feX3wrpPCA7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciHkNN/btsNyTFnpj5/kunJpWiYK4feX3wrpPCA7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciHkNN%2FbtsNyTFnpj5%2FkunJpWiYK4feX3wrpPCA7k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1760&quot; height=&quot;790&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;790&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;왜 CloudFront 가 바라보는 origin(원본)이 두개일까?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 Lambda의 응답 용량 한계에 있습니다. 처음에는 AWS Lamda@Edge 를 생각했지만 response limit 이 1mb 밖에 되지 않아 고민하였습니다. 실제 사용자들이 올리는 리뷰 이미지의 경우는 스마트폰의 카메라 기술 발전에 따라 굉장히 고화질이라 1mb 가 넘는 이미지가 수두룩하였기 때문입니다... 이미지를 리사이징하여 용량을 낮추면 되지 않나 ? 라고 생각하면 또 그럴수 있겠지만 원본이미지 요청인 경우 1mb가 넘어가면 불필요한 이미지 리사이징이 필요하기에 용량 제한이 6mb 인 일반 Lambda로 변경 하였고 이미지 프로세싱이 필요없는 원본 이미지 요청은 바로 S3를 찌르도록 구성하였습니다. API Gateway는 CloudFront에 직접 붙일수 있는 Lambda@Edge 와 다르게 일반 Lambda는 직접 붙일수가 없어 CloudFront에서 Lambdad를 사용하기위해서는 API Gateway를 엔드포인트처럼 함께 사용하여야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Lambda 배포 방법&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사내에서는 여러 Lambda 함수를 Serverless 프레임워크를 통해 관리 및 배포를 하고 있었습니다. serverless 프레임워크는 serverless.yml 파일 하나로 Lambda 함수, API Gateway, S3, IAM 권한 등을 손쉽게 관리하고 배포할수 있는 툴로 생각하시면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 권한만 있다면 serverless deploy 명령어 하나로 serverless.yml에 작성된 설정대로 AWS 에 바로 배포되니 정말 편리하긴 합니다. 저 같은 경우는 사내 보안상? 모든 권한을 다 받지는 못했습니다.. 그리 하여 코드 수정이 필요할때는 수동 배포를 진행했는데serverless deploy 에 비해 조금 번거롭긴 하지만 나름 적응되면 또 괜찮습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법은 아래와 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1745506034091&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;serverless package --stage dev // 개발 환경으로 패키징
serverless package --stage prod // 운영 환경으로 패키징&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZqBd6/btsNyBZBUkE/yCUbElJhxmLCKi3ZSKOShK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZqBd6/btsNyBZBUkE/yCUbElJhxmLCKi3ZSKOShK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZqBd6/btsNyBZBUkE/yCUbElJhxmLCKi3ZSKOShK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZqBd6%2FbtsNyBZBUkE%2FyCUbElJhxmLCKi3ZSKOShK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;99&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어들을 실행하면 local에 설치된 node_mouduls 와 코드를 가지고 패키징을 하여 위 이미지처럼 .serverless 폴더 아래에 zip 파일을 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;* 여기서 정말 주의 해야하는 점은 AWS Lambda는 linux 환경에서 실행되기 때문에 반드시 linux 환경에서 호환이되는 모듈을 설치해야 합니다.&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저의 경우 Lambda 런타임 환경이 리눅스, 아키텍처가 arm64로 아래 명령어를 통해 sharp 를 설치하였습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1745506695702&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install sharp --platform=linux --arch=arm64&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 zip 파일을 가지고 AWS Lambda 콘솔로 이동합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2780&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4e5WV/btsNxUFnqVV/3pet44sVbQY6XxUHkZo1Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4e5WV/btsNxUFnqVV/3pet44sVbQY6XxUHkZo1Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4e5WV/btsNxUFnqVV/3pet44sVbQY6XxUHkZo1Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4e5WV%2FbtsNxUFnqVV%2F3pet44sVbQY6XxUHkZo1Kk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2780&quot; height=&quot;1114&quot; data-origin-width=&quot;2780&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔의 코드탭에서 코드 소스를 zip파일로 업로드하거나 S3 버킷에 올리고 버킷주소를 통해 업로드할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;업로드 후에는 버전 탭으로 이동해서 새버전 발행으로 배포를 진행하면 완료입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;AWSAPIGatewayLambda를활용한동적이미지프로세싱가이드-AWSLambdaConsole테스트&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AWS Lambda Console 테스트&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3212&quot; data-origin-height=&quot;1126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zdVRu/btsNyQvaDDp/kZeblq2jooB56uk8TPhy9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zdVRu/btsNyQvaDDp/kZeblq2jooB56uk8TPhy9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zdVRu/btsNyQvaDDp/kZeblq2jooB56uk8TPhy9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzdVRu%2FbtsNyQvaDDp%2FkZeblq2jooB56uk8TPhy9k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3212&quot; height=&quot;1126&quot; data-origin-width=&quot;3212&quot; data-origin-height=&quot;1126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 업로드 후에는 테스트 탭에서 업로드된 코드로 AWS Lambda 콘솔에서 테스트를 진행해볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #172b4d; text-align: start;&quot;&gt;테스트 탭에서 저장버튼을 클릭시 테스트했던 이벤트 Json을 테스트 이름과 같이 저장하고 추후에도 계속 세팅하여 테스트 할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #172b4d; text-align: start;&quot;&gt;이미지 프로세싱의 경우 대게 request url에 원본 파일경로나 어떻게 이미지를 가공할 것인지에 대한 옵션 정보 값들이 있을텐데 저 역시 request url 이 필요했고 &lt;span style=&quot;color: #172b4d; text-align: start;&quot;&gt;Lambda 함수 실행시 진입점인 handler 함수에서는 event 객체를 인자로 받아 path 에서 request url 에서 꺼내서 사용하기 때문에 이벤트 JSON 에 위와 같이 cloudfront url을 제외한 나머지 경로를 path 프로퍼티에 넣어 테스트를 진행하면 아래와 같이 테스트 결과를 확인할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3230&quot; data-origin-height=&quot;1512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boqrzB/btsNzN5yFHe/digmnWl5Nvr4w06zWMqal0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boqrzB/btsNzN5yFHe/digmnWl5Nvr4w06zWMqal0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boqrzB/btsNzN5yFHe/digmnWl5Nvr4w06zWMqal0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboqrzB%2FbtsNzN5yFHe%2FdigmnWl5Nvr4w06zWMqal0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3230&quot; height=&quot;1512&quot; data-origin-width=&quot;3230&quot; data-origin-height=&quot;1512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;lambda 함수 내에서 찍은 log도 로그 출력에서 확인이 가능합니다. &lt;/span&gt;&lt;/span&gt;물론 CloudWatch에서도 해당 lambda의 로그를 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 id=&quot;AWSAPIGatewayLambda를활용한동적이미지프로세싱가이드-AWSCloudFront설정&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;AWS CloudFront 설정&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 lambda 를 만들었으니 API Gateway에 연결 후 마지막으로 CloudFront의 원본으로 API Gateway 설정하면 완료입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Gateway는 serverless.yml에 작성된 설정대로 배포하면 잘 배포되지만 CloudFront 설정에서 한가지 주의해야하는 점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 CloudFront로 이동하여 원본탭으로 가보면 아래와 같은 화면이 나오는데&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xIoKJ/btsNzOpRtMM/XbH3DqJIGY98YciXMtfsz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xIoKJ/btsNzOpRtMM/XbH3DqJIGY98YciXMtfsz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xIoKJ/btsNzOpRtMM/XbH3DqJIGY98YciXMtfsz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxIoKJ%2FbtsNzOpRtMM%2FXbH3DqJIGY98YciXMtfsz0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2290&quot; height=&quot;698&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 탭에서는 cloudfront 의 캐시가 miss 났을때 바라볼 원본 origin 들을 설정할 수 있습니다. 도식에서 설명드렸던것과 같이 원본유형이 API Gateway, S3&amp;nbsp; 두개가 있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 탭에서는 크게 볼건 없고 람다 함수를 실행시키기 위한 API Gateway 도메인 이름과 원본 이미지가 저장된 S3 버킷 이름 정도만 알고 넘어가면 될 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2296&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yMPfJ/btsNxveMsDL/TkdC4cyQFWCvutKH5girm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yMPfJ/btsNxveMsDL/TkdC4cyQFWCvutKH5girm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yMPfJ/btsNxveMsDL/TkdC4cyQFWCvutKH5girm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyMPfJ%2FbtsNxveMsDL%2FTkdC4cyQFWCvutKH5girm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2296&quot; height=&quot;676&quot; data-origin-width=&quot;2296&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 동작탭으로 이동해보면 request url 에서 패턴에 따라 어떤 원본(origin)을 가져올지 설정할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;처음 요청이 들어오면 우선순위가 높은 순으로 먼저 패턴 체크를 하여 저희는 image processing 이 필요한 이미지의 경우 특정 예약어가 url 에 붙기때문에 해당 예약어가 url 포함되어 있으면 API Gateway 를 통해 람다를 실행시킵니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;0번째 동작이 정상적인 응답을 반환했다면 다음 우선순위는 실행되지 않습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;1190&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/58rIQ/btsNyGmeZmo/aa3MIF3qI4b4nALwboE2M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/58rIQ/btsNyGmeZmo/aa3MIF3qI4b4nALwboE2M1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/58rIQ/btsNyGmeZmo/aa3MIF3qI4b4nALwboE2M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F58rIQ%2FbtsNyGmeZmo%2Faa3MIF3qI4b4nALwboE2M1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2250&quot; height=&quot;1190&quot; data-origin-width=&quot;2250&quot; data-origin-height=&quot;1190&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작을 새로 생성하거나 편집 버튼을 통해 수정하면&amp;nbsp; 해당 동작의 캐시 키와 응답 헤더 정책 등을 설정할 수 있는데 cloudfront 원본(origin)을 기본적으로 헤더에 넣어주지는 않습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;origin 이 빠진 정책으로 설정하면 CORS 이슈가 발생하여 정상적으로 이미지가 불러와지지 않기에 CORS 에러 발생을 막기위해서는 반드시 요청정책과 응답 헤더 정책에 origin 이 포함된 정책을 넣어야합니다. 위 이미지의 요청정책과 응답헤더 정책은 보기 클릭시 origin이 포함되어 있습니다. (응답헤더는 보안상 가리긴했지만)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기까지 모든 설정이 완료되었습니다. 현재는 운영에 배포되어 기존 이미지 서버는 걷어내고 Lambda를 통해 정상적으로 이미지 프로세싱이 되고 있습니다. 아무래도 AWS 인프라 설정은 Front end 개발자로서 자주 접해볼 기회가 많지 않아 매우 유익한 시간이였고 CloudFront와 이미지 캐싱 및 프로세싱 모두 Front end 측면에서는 알아두면 매우 좋은 경험이 될 것 같아 회고해보면서 뿌듯했습니다 ㅎㅎ..&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;긴 글 읽어주셔서 감사합니다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AWS</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/71</guid>
      <comments>https://kyuntechblog.tistory.com/71#entry71comment</comments>
      <pubDate>Fri, 25 Apr 2025 00:30:32 +0900</pubDate>
    </item>
    <item>
      <title>나 볼려고 만든 PHP 기초 문법(echo, var_dump, 변수 사용법, 형변환, $_GET, $_POST, 배열, 연관배열)</title>
      <link>https://kyuntechblog.tistory.com/70</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;php.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;648&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czBIx4/btsELpyOO6w/moG1Q3lWNWUKUesHbNHFmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czBIx4/btsELpyOO6w/moG1Q3lWNWUKUesHbNHFmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czBIx4/btsELpyOO6w/moG1Q3lWNWUKUesHbNHFmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczBIx4%2FbtsELpyOO6w%2FmoG1Q3lWNWUKUesHbNHFmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;602&quot; height=&quot;325&quot; data-filename=&quot;php.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;648&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 필요할때 볼려고 만든 PHP 기초 문법 정리 글이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 화면 출력&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo 문을 사용하여 화면에 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;','를 구분자로 여러 인자를 출력 가능&lt;/p&gt;
&lt;pre id=&quot;code_1707628421737&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
  echo &quot;Hello World!&quot;, &quot; &quot;, &quot;Hello&quot;, &quot; &quot;, &quot;World!&quot;; //Hello World! Hello World!
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 변수 선언&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$ 표시를 변수 명 앞에 붙여서 변수를 선언&lt;/p&gt;
&lt;pre id=&quot;code_1707628949274&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
  $num = 1;
  $str = &quot;string&quot;;
  $test;
  
  //1
  //string
  //Warning: Undefined variable $test2 in /Users/kihoon/myPhp/test.php on line 9
  echo $num;
  echo '&amp;lt;br/&amp;gt;';
  echo $str;
  echo $test;
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 변수 정보 확인&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;var_dump 함수를 사용하여 확인 가능.&lt;/p&gt;
&lt;pre id=&quot;code_1707629430207&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
  $num = 1;
  $str = &quot;string&quot;;
  $arr = array(1, 2);
  $bool = true;

/*
int(1)
string(6) &quot;string&quot;
array(2) { [0]=&amp;gt; int(1) [1]=&amp;gt; int(2) }
bool(true)
*/
  echo var_dump($num);
  echo '&amp;lt;br/&amp;gt;';
  echo var_dump($str);
  echo '&amp;lt;br/&amp;gt;';
  echo var_dump($arr);
  echo '&amp;lt;br/&amp;gt;';
  echo var_dump($bool);
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 문자열 붙이기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'.' 을 사용하여 문자열끼리 붙여서 표출할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1707630056892&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;?php 
      //coding everybody
      $first = &quot;coding&quot;;
      echo $first.&quot; everybody&quot;;
    ?&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 상수 선언&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;define 함수를 사용하여 상수를 선언할수 있다. 첫번째 인자로 변수명, 두번째 인자로 값을 전달받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 정의된 상수를 재선언시 오류 발생&lt;/p&gt;
&lt;pre id=&quot;code_1707630504945&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;?php
      /*
      PHP Tutorial
      Warning: Constant TITLE already defined in /Users/kihoon/myPhp/variable/variable4.php on line 7
      PHP Tutorial
      */
      define('TITLE', 'PHP Tutorial');
      echo TITLE;
      define('TITLE', 'JAVA Tutorial');
      echo TITLE;
    ?&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 형변환&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;settype 을 사용하여 변수 값 자체를 형변환 하거나&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(int), (string), (array), (object), (float)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등을 사용하여 일시적으로 형변환을 할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707632673960&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;?php
      /*
      integer
      double
      string(3) &quot;100&quot;
      array(1) { [0]=&amp;gt; float(100) }
      object(stdClass)#1 (1) { [&quot;scalar&quot;]=&amp;gt; float(100) }
      float(100)
      */
      $a = 100;
      echo gettype($a);
      settype($a, 'float');
      echo '&amp;lt;br/&amp;gt;';
      echo gettype($a);
      echo '&amp;lt;br/&amp;gt;';
      echo gettype((string)$a);
      echo '&amp;lt;br/&amp;gt;';
      echo gettype((array)$a);
      echo var_dump((array)$a);
      echo '&amp;lt;br/&amp;gt;';
      echo gettype((object)$a);
      echo '&amp;lt;br/&amp;gt;';
      echo gettype((float)$a);
      echo '&amp;lt;br/&amp;gt;';
    ?&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7. 가변 변수&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;title이라는 변수에 'subject'라는 값을 담고 $title에 $를 하나 더붙여서 사용시 $subject로 값을 할당하거나 사용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1707632950722&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;?php
      #가변변수
      // PHP tutorial
      $title = 'subject';
      $$title = 'PHP tutorial';
      echo $subject;
    ?&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8. form I/O&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;form 에서 submit으로 전송한 value를 각 메서드에 맞게&amp;nbsp; $_GET[name속성 값] , $_POST[name 속성 값] 으로 받을수 있다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707633185926&quot; class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;//formTest.html
&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;form method=&quot;POST&quot; action=&quot;postTest.php&quot;&amp;gt;
      id : &amp;lt;input type=&quot;text&quot; name=&quot;id&quot; /&amp;gt; pw : &amp;lt;input type=&quot;text&quot; name=&quot;password&quot; /&amp;gt;
      &amp;lt;button type=&quot;submit&quot;&amp;gt;전송&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1707633336027&quot; class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;//postTest.php
&amp;lt;?php
echo $_POST['id'].','.$_POST['password']
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서는 form 메서드로 input value를&amp;nbsp; POST 로 보내고 $_POST로 데이터를 받았지만 GET 메서드로 보내고 $_GET으로 데이터를 받을수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;9. function&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1707633808418&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
  $test = &quot;테스트&quot;;
  function numbering($param = 0){ // param 변수의 초기값을 0으로 설정
    global $test;
    while($param &amp;lt; 10){
      echo $param;
      $param += 1;
    }
    echo $test;
  }
  numbering(1);//123456789테스트
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;php에서의 함수사용 방법은 javascript와 큰 차이는 없지만 전역변수의 값을 함수내에서 사용시 global을 사용하여 함수 내에서 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;10.&amp;nbsp; 배열&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1707640338383&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?php
function get_members(){
    return ['egoing', 'k8805', 'sorialgi'];
}

$members = get_members();
for($i=0; $i&amp;lt;count($members); $i++){
    echo ucfirst($members[$i]).'&amp;lt;br/&amp;gt;'; // ucfirst : 첫번째 문자만 대문자로 변환
}

/*
초기화 방법 종류
$li = [];
$li = array();
$li = array(1,2);
*/
$li = ['b', 'c', 'a', 'g', 'e'];

array_push($li, 'f'); // 제일 뒤에 'f'를 삽입 
array_unshift($li, 'd');// 제일 앞에 'd'를 삽입

array_shift($li);// 제일 앞 원소를 제거
array_pop($li);// 제일 뒤 원소를 제거 

sort($li); // 정렬
rsort($li);// 역정렬
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;11. 연관배열&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1707641143603&quot; class=&quot;php&quot; data-ke-language=&quot;php&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/*
key: egoing value:10
key: k8805 value:6
key: sorialgi value:80
*/
&amp;lt;?php
  $grades = array('egoing'=&amp;gt;10, 'k8805'=&amp;gt;6, 'sorialgi'=&amp;gt;80);
  foreach($grades as $key =&amp;gt; $value){
      echo &quot;key: {$key} value:{$value} &amp;lt;br /&amp;gt;&quot;;
  }
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;</description>
      <category>PHP</category>
      <category>php</category>
      <category>php문법</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/70</guid>
      <comments>https://kyuntechblog.tistory.com/70#entry70comment</comments>
      <pubDate>Sun, 11 Feb 2024 15:48:00 +0900</pubDate>
    </item>
    <item>
      <title>[Git] Conflict(충돌)시 해결방법</title>
      <link>https://kyuntechblog.tistory.com/69</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;gitImg.png&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1xwQ5/btsDJV0Sv2j/Pb9Q317hkafL9RRc9XgfdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1xwQ5/btsDJV0Sv2j/Pb9Q317hkafL9RRc9XgfdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1xwQ5/btsDJV0Sv2j/Pb9Q317hkafL9RRc9XgfdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1xwQ5%2FbtsDJV0Sv2j%2FPb9Q317hkafL9RRc9XgfdK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;282&quot; height=&quot;118&quot; data-filename=&quot;gitImg.png&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;145&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git 에서 브랜치간 merge(병합)시 confilct(충돌)이 발생했을때 해결하는 방법에 대해 간단하게 정리하는 글이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 예시를 위해 main 브랜치와 o2 브랜치에서 같은 파일을 수정 후 merge 하여 일부러 충돌을 발생시킬 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccitrl/btsDHo3OZMi/uBOQ0Eqd6zWY4gEmXpmbQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccitrl/btsDHo3OZMi/uBOQ0Eqd6zWY4gEmXpmbQK/img.png&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;380&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.5395%; margin-right: 10px;&quot; data-widthpercent=&quot;48.1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccitrl/btsDHo3OZMi/uBOQ0Eqd6zWY4gEmXpmbQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fccitrl%2FbtsDHo3OZMi%2FuBOQ0Eqd6zWY4gEmXpmbQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1078&quot; height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mgjPf/btsDJ01SEDD/tGHWq85XJgxfHq9mc92HXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mgjPf/btsDJ01SEDD/tGHWq85XJgxfHq9mc92HXK/img.png&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;360&quot; data-is-animation=&quot;false&quot; style=&quot;width: 51.2977%;&quot; data-widthpercent=&quot;51.9&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mgjPf/btsDJ01SEDD/tGHWq85XJgxfHq9mc92HXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmgjPf%2FbtsDJ01SEDD%2FtGHWq85XJgxfHq9mc92HXK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1102&quot; height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;main 브랜치의 work.txt(좌) / o2 브랜치의 work.txt(우)&amp;nbsp;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhHnFH/btsDIvu3gvD/oeWLcBsx2sfZcjePoABv9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhHnFH/btsDIvu3gvD/oeWLcBsx2sfZcjePoABv9K/img.png&quot; data-alt=&quot;merge -&amp;amp;gt; conflict 발생&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhHnFH/btsDIvu3gvD/oeWLcBsx2sfZcjePoABv9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhHnFH%2FbtsDIvu3gvD%2FoeWLcBsx2sfZcjePoABv9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;466&quot; height=&quot;228&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;546&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;merge -&amp;gt; conflict 발생&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;======= 를 구분선으로&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 HEAD(main 브랜치)의 파일과 o2 브랜치의 파일 에서 같은 라인에 어디가 충돌 났는지 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 직접 수동으로&amp;nbsp; 두 브랜치의 반영사항을 모두 반영할 지 한쪽만 반영할 지 정하고 수정한 뒤 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/di3pFK/btsDGJNPaK1/emb8WbZvmaiYrwX5iJ9rWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/di3pFK/btsDGJNPaK1/emb8WbZvmaiYrwX5iJ9rWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/di3pFK/btsDGJNPaK1/emb8WbZvmaiYrwX5iJ9rWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdi3pFK%2FbtsDGJNPaK1%2Femb8WbZvmaiYrwX5iJ9rWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;307&quot; height=&quot;317&quot; data-origin-width=&quot;382&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두개다 반영하기 위해 위와 같이 수정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 아래 순서와 같이 충돌이 발생한 파일을 add 하고 status로 상태를 확인한 뒤 commit 으로 merge를 완료한다.&lt;/p&gt;
&lt;pre id=&quot;code_1705846408359&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git add 충돌이 발생한 파일
git status 
git commit -m &quot;커밋메시지&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;1512&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n3cFK/btsDKLcmozK/nm6hBMfxxrD2pN948JSLq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n3cFK/btsDKLcmozK/nm6hBMfxxrD2pN948JSLq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n3cFK/btsDKLcmozK/nm6hBMfxxrD2pN948JSLq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn3cFK%2FbtsDKLcmozK%2Fnm6hBMfxxrD2pN948JSLq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;591&quot; height=&quot;694&quot; data-origin-width=&quot;1288&quot; data-origin-height=&quot;1512&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git log 확인시 정상적으로 merge 가 된 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Git</category>
      <category>git</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/69</guid>
      <comments>https://kyuntechblog.tistory.com/69#entry69comment</comments>
      <pubDate>Sun, 21 Jan 2024 23:17:44 +0900</pubDate>
    </item>
    <item>
      <title>[Git] CLI 명령어 모음(init, status, add, commit, log, diff, reset, checkout, revert)</title>
      <link>https://kyuntechblog.tistory.com/68</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;gitImg.png&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MH5Zu/btsDCUIu5vd/SFEfkTWhGsNYJKLLbLHxU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MH5Zu/btsDCUIu5vd/SFEfkTWhGsNYJKLLbLHxU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MH5Zu/btsDCUIu5vd/SFEfkTWhGsNYJKLLbLHxU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMH5Zu%2FbtsDCUIu5vd%2FSFEfkTWhGsNYJKLLbLHxU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;347&quot; height=&quot;145&quot; data-filename=&quot;gitImg.png&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;145&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Git Repository 생성 및 초기화&lt;/h3&gt;
&lt;pre id=&quot;code_1705588285356&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git init&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 명령어를 사용한 경로에서 git을 사용할 수 있도록 지역 저장소를 생성 및 초기화한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pBZIl/btsDBu38Qy1/GbqU6ULAxy0VYwpjtNQiR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pBZIl/btsDBu38Qy1/GbqU6ULAxy0VYwpjtNQiR1/img.png&quot; data-alt=&quot;git init으로 repository 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pBZIl/btsDBu38Qy1/GbqU6ULAxy0VYwpjtNQiR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpBZIl%2FbtsDBu38Qy1%2FGbqU6ULAxy0VYwpjtNQiR1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;665&quot; height=&quot;119&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;git init으로 repository 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 현재 상태 확인&lt;/h3&gt;
&lt;pre id=&quot;code_1705588681347&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git status&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 명령어를 사용해서 현재 상태와 버전이 달라진게 없는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하위 이미지를 보면 Untracked files에 hello1.txt 파일이 있는 것을 확인할 수 있는데 이것은 현재 git 해당 파일을 추적하고 있지 않다는 의미이다. add 명령어를 사용하여 stagingArea에 올려주면 그 이후로는 git 이 해당파일을 추적한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br2eWu/btsDAQTTBs2/pzCvkrp7vYbbc2ueBbbKeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br2eWu/btsDAQTTBs2/pzCvkrp7vYbbc2ueBbbKeK/img.png&quot; data-alt=&quot;git status 로 상태 확인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br2eWu/btsDAQTTBs2/pzCvkrp7vYbbc2ueBbbKeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr2eWu%2FbtsDAQTTBs2%2FpzCvkrp7vYbbc2ueBbbKeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;580&quot; height=&quot;155&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;git status 로 상태 확인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Staging Area 에 올리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Working tree, Staging Area, Repository 의 개념부터 알아야할 필요가 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git init 으로 생성된 레파지토리 내에서 파일을 생성하거나 수정하게 되면 git status 명령어를 통해 커밋할수 있는 파일이나 커밋을 해야할 파일 , 추적중이지 않은 파일들을 확인할 수 있다. 지역 저장소로 버전관리를 하는 flow를 간략하게 나타내면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MichO/btsDHmQLjNq/Xd1kOFKlAuAbk8mVK2KXkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MichO/btsDHmQLjNq/Xd1kOFKlAuAbk8mVK2KXkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MichO/btsDHmQLjNq/Xd1kOFKlAuAbk8mVK2KXkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMichO%2FbtsDHmQLjNq%2FXd1kOFKlAuAbk8mVK2KXkK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1986&quot; height=&quot;280&quot; data-origin-width=&quot;1986&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 파일을 수정 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. git add 명령어를 통해 Working tree 에서 변경사항들을 Staging Area에 올린다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. git commit 명령어를 통해 Staging Area 에 올라온 사항들을 Repository에 반영한다.&lt;/p&gt;
&lt;pre id=&quot;code_1705589029692&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 파일명1 파일명2 파일명3을 stage에 올림
git add 파일명1 파일명2 파일명3 

# 현재 디렉토리의 모든 파일을 staging Area에 올림
git add .&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p5XdX/btsDHpmpFRb/MHn7m9GbQKSjVP8K7Ftflk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p5XdX/btsDHpmpFRb/MHn7m9GbQKSjVP8K7Ftflk/img.png&quot; data-alt=&quot;git add 명령어를 사용하여 스테이지에 올림&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p5XdX/btsDHpmpFRb/MHn7m9GbQKSjVP8K7Ftflk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp5XdX%2FbtsDHpmpFRb%2FMHn7m9GbQKSjVP8K7Ftflk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;542&quot; height=&quot;160&quot; data-origin-width=&quot;894&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;git add 명령어를 사용하여 스테이지에 올림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Commit&lt;/h3&gt;
&lt;pre id=&quot;code_1705589401670&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# stagig area에 올라온 모든 것을 커밋
git commit -m &quot;커밋 메세지&quot;

# stagig area에 올라온 것중에 파일명1을 커밋
git commit 파일명1 -m &quot;커밋 메세지&quot;

# add를 생략하고 바로 커밋(untracked 파일은 무시됨)
git commit -am &quot;커밋 메세지&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1KIh1/btsDCTW7EFT/0xdpKdWwsyzk9dDXxJOVyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1KIh1/btsDCTW7EFT/0xdpKdWwsyzk9dDXxJOVyk/img.png&quot; data-alt=&quot;git commit 으로 stage에 올라온 변경사항을 지역 저장소(repository)에 commit\&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1KIh1/btsDCTW7EFT/0xdpKdWwsyzk9dDXxJOVyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1KIh1%2FbtsDCTW7EFT%2F0xdpKdWwsyzk9dDXxJOVyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;643&quot; height=&quot;133&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;206&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;git commit 으로 stage에 올라온 변경사항을 지역 저장소(repository)에 commit\&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-m 명령어를 사용하지 않고 git commit 만 실행하게 되면 default 로 설정된 에디터가 켜지면서 여러 줄에 걸쳐 커밋 메세지를 작성할 수 있다. default 로 설정된 에디터를 변경하고 싶다면 아래의 명령어를 사용하여 에디터를 변경하자.&lt;/p&gt;
&lt;pre id=&quot;code_1705630684665&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git config --global core.editor &quot;vim&quot; #원하는 에디터 ex) nano, vim&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. log 확인&lt;/h3&gt;
&lt;pre id=&quot;code_1705589774127&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# commit 로그를 최신 순으로 표출
git log

# commit 로그와 변경사항을 최신 순으로 표출 
git log -p&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blY60G/btsDDYcwoxr/gHqADrUK12bVQ8WXdSmBP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blY60G/btsDDYcwoxr/gHqADrUK12bVQ8WXdSmBP0/img.png&quot; data-origin-width=&quot;882&quot; data-origin-height=&quot;848&quot; data-is-animation=&quot;false&quot; width=&quot;406&quot; height=&quot;390&quot; style=&quot;width: 66.3327%; margin-right: 10px;&quot; data-widthpercent=&quot;67.11&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blY60G/btsDDYcwoxr/gHqADrUK12bVQ8WXdSmBP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblY60G%2FbtsDDYcwoxr%2FgHqADrUK12bVQ8WXdSmBP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;882&quot; height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBAsie/btsDGnilb90/pV8bNDBM3JOHbWa9O7KNt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBAsie/btsDGnilb90/pV8bNDBM3JOHbWa9O7KNt1/img.png&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;1758&quot; data-is-animation=&quot;false&quot; style=&quot;width: 32.5045%;&quot; data-widthpercent=&quot;32.89&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBAsie/btsDGnilb90/pV8bNDBM3JOHbWa9O7KNt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBAsie%2FbtsDGnilb90%2FpV8bNDBM3JOHbWa9O7KNt1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;896&quot; height=&quot;1758&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;git log / git log -p 의 차이점&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 변경사항 확인&lt;/h3&gt;
&lt;pre id=&quot;code_1705590109561&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# working directory 와 staging area 사이의 차이 확인
git diff

# working directory, staging area의 변경사항 모두를 repository head commit과 비교 
git diff HEAD

# 브랜치1 과 브랜치2 간 변경사항 확인 
git diff branch1 branch2
git diff branch1..branch2

# commit1과 commit2 간 변경사항 확인
git diff commitId1..commitId2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blkZOl/btsDCWsLN9T/72ZvJPPJfZvwQF0k9HLttK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blkZOl/btsDCWsLN9T/72ZvJPPJfZvwQF0k9HLttK/img.png&quot; data-origin-width=&quot;748&quot; data-origin-height=&quot;286&quot; data-is-animation=&quot;false&quot; width=&quot;337&quot; height=&quot;129&quot; style=&quot;width: 51.3161%; margin-right: 10px;&quot; data-widthpercent=&quot;51.92&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blkZOl/btsDCWsLN9T/72ZvJPPJfZvwQF0k9HLttK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblkZOl%2FbtsDCWsLN9T%2F72ZvJPPJfZvwQF0k9HLttK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;748&quot; height=&quot;286&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8d9ma/btsDC7HAY0h/fZVfKGnFGAfJ1KQJrxOdDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8d9ma/btsDC7HAY0h/fZVfKGnFGAfJ1KQJrxOdDk/img.png&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;346&quot; data-is-animation=&quot;false&quot; style=&quot;width: 47.5211%;&quot; data-widthpercent=&quot;48.08&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8d9ma/btsDC7HAY0h/fZVfKGnFGAfJ1KQJrxOdDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8d9ma%2FbtsDC7HAY0h%2FfZVfKGnFGAfJ1KQJrxOdDk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;838&quot; height=&quot;346&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;git diff / git diff HEAD 차이점&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7.&amp;nbsp; Reset을 사용하여 commit 되돌리기&lt;/h3&gt;
&lt;pre id=&quot;code_1705591454953&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# HEAD를 이전 커밋으로 이동하면서 staging area와 working directory 변경내용은 그대로 유지
git reset --soft

# HEAD를 이전 커밋으로 이동하면서 staging area는 초기화되지만 working directory의 변경내용은 그대로 유지됨(기본값)
git reset --mixed

# HEAD를 이전 커밋으로 이동하면서 staging area와 working directory 변경내용을 모두 초기화
git reset --hard

# HEAD를 특정 커밋 ID로 이동하면서 staging area와 working directory 변경내용을 모두 초기화
git reset --hard 커밋ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤에 붙이는 모드에 따라 staging area 와 working directory 의 변경내용 무시 여부를 설정하고 staging area에 올라간 커밋을 되돌릴 수 있다. 주의 할점은 HEAD가 가르키던 브랜치가 특정 커밋을 가르키도록 변경되기 때문에 특정 커밋 ID로 리셋시 그 이후 커밋으로 되돌아갈수 없으니 주의해서 사용해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bssJ1M/btsDCQzGnQW/i6lS6kApoSQBEsfafZ9lgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bssJ1M/btsDCQzGnQW/i6lS6kApoSQBEsfafZ9lgk/img.png&quot; data-alt=&quot;git reset 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bssJ1M/btsDCQzGnQW/i6lS6kApoSQBEsfafZ9lgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbssJ1M%2FbtsDCQzGnQW%2Fi6lS6kApoSQBEsfafZ9lgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;676&quot; height=&quot;426&quot; data-origin-width=&quot;1442&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;git reset 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reset 실행후 Message 3 에 해당하는 커밋은 사라지고 HEAD가 가르키던 main branch의 최근 커밋이 Message 2로 변경된 것을 확인할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. 특정 버전으로 돌아가기&lt;/h3&gt;
&lt;pre id=&quot;code_1705629446036&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 특정 커밋 ID로 HEAD룰 이동
git checkout 커밋ID

# main branch 로 HEAD를 이동
git checkout main&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dq2h9/btsDHsXSXfO/F56kLMmkFO6wmOYdp2AbV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dq2h9/btsDHsXSXfO/F56kLMmkFO6wmOYdp2AbV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dq2h9/btsDHsXSXfO/F56kLMmkFO6wmOYdp2AbV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDq2h9%2FbtsDHsXSXfO%2FF56kLMmkFO6wmOYdp2AbV1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;686&quot; height=&quot;213&quot; data-origin-width=&quot;1374&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 명령어를 이용해서 특정 커밋 ID나 브랜치로 HEAD를 이동시킬 수 있다. 커밋 ID 는 git log 를 통해 확인가능하다. 특정 커밋 ID로 체크아웃을 받으면 브랜치는 별개로 HEAD의 위치만 바꿀 수 있으므로 브랜치의 최근 커밋으로 다시 이동할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. Revert를 사용한 되돌리기&lt;/h3&gt;
&lt;pre id=&quot;code_1705641288828&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git revert 커밋ID&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;revert 를 사용하여 되돌리는 방법도 있지만 reset과는 원리가 다르다. 아래의 예시를 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwMwjr/btsDGIf0eRS/RmBHhRkz3YHzHh5qtC1EMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwMwjr/btsDGIf0eRS/RmBHhRkz3YHzHh5qtC1EMK/img.png&quot; data-alt=&quot;현재의 git log&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwMwjr/btsDGIf0eRS/RmBHhRkz3YHzHh5qtC1EMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwMwjr%2FbtsDGIf0eRS%2FRmBHhRkz3YHzHh5qtC1EMK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;569&quot; height=&quot;434&quot; data-origin-width=&quot;896&quot; data-origin-height=&quot;684&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;현재의 git log&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 git log에서 R4 커밋메세지에 해당하는 커밋을 revert 하면 아래와 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;996&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhpg1m/btsDDbp0gFo/EYhyiRkUtuxA4jqEtRTGw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhpg1m/btsDDbp0gFo/EYhyiRkUtuxA4jqEtRTGw0/img.png&quot; data-alt=&quot;git revert 실행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhpg1m/btsDDbp0gFo/EYhyiRkUtuxA4jqEtRTGw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhpg1m%2FbtsDDbp0gFo%2FEYhyiRkUtuxA4jqEtRTGw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;419&quot; data-origin-width=&quot;1368&quot; data-origin-height=&quot;996&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;git revert 실행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git reset 처럼 HEAD가 가르키던 브랜치의 최근 커밋이 R3가 된것이 아니라 R4 커밋은 그대로 있고 새로운 커밋이 생긴 것을 확인할 수 있다. 파일의 내용을 열어서 확인해보면 R3의 작업 내역이 들어있는 것을 확인할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;10. 제일 최근 커밋 메시지 수정&lt;/h3&gt;
&lt;pre id=&quot;code_1705738301056&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# default 로 설정된 에디터가 열리면서 메시지 수정
git commit --amend

# 에디터를 열지 않고 간단하게 메시지 수정
git commit --amend -m &quot;커밋 메시지 수정&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Git</category>
      <category>git</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/68</guid>
      <comments>https://kyuntechblog.tistory.com/68#entry68comment</comments>
      <pubDate>Fri, 19 Jan 2024 00:29:45 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js] BOJ - 캠프가는 영식</title>
      <link>https://kyuntechblog.tistory.com/67</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;백준.png&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAkmqm/btsDsXZhITw/6ljZkzFt9oqYSwsYRpjGD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAkmqm/btsDsXZhITw/6ljZkzFt9oqYSwsYRpjGD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAkmqm/btsDsXZhITw/6ljZkzFt9oqYSwsYRpjGD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAkmqm%2FbtsDsXZhITw%2F6ljZkzFt9oqYSwsYRpjGD1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;397&quot; height=&quot;207&quot; data-filename=&quot;백준.png&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백준 링크 :&amp;nbsp;&lt;a href=&quot;https://www.acmicpc.net/problem/1590&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1590&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1705362044353&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;1590번: 캠프가는 영식&quot; data-og-description=&quot;예제 1의 경우 150분, 200분, 250분, ..., 600분에 한 대씩 버스가 출발한다. 따라서 영식이는 300분에 버스를 타면 된다.&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/1590&quot; data-og-url=&quot;https://www.acmicpc.net/problem/1590&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HlTNx/hyU5UiV50J/3m9B2ISHNKTNt6bomLrAH0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1590&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/1590&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HlTNx/hyU5UiV50J/3m9B2ISHNKTNt6bomLrAH0/img.png?width=2834&amp;amp;height=1480&amp;amp;face=0_0_2834_1480');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;1590번: 캠프가는 영식&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;예제 1의 경우 150분, 200분, 250분, ..., 600분에 한 대씩 버스가 출발한다. 따라서 영식이는 300분에 버스를 타면 된다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영식이는 민식이와 함게 고속버스를 타고 캠프를 가야 하지만, 민식이는 영식이를 깨우지 않고 혼자 버스를 타고 캠프에 가버렸다.&lt;br /&gt;&lt;br /&gt;영식이는&amp;nbsp;혼자&amp;nbsp;고속버스터미널까지&amp;nbsp;가서&amp;nbsp;캠프에&amp;nbsp;오려고&amp;nbsp;한다.&amp;nbsp;터미널에는&amp;nbsp;캠프&amp;nbsp;장소까지&amp;nbsp;운행하는&amp;nbsp;N가지의&amp;nbsp;버스가&amp;nbsp;있다.&amp;nbsp;각각의&amp;nbsp;버스는&amp;nbsp;시작&amp;nbsp;시각,&amp;nbsp;간격,&amp;nbsp;대수의&amp;nbsp;정보를&amp;nbsp;가지고&amp;nbsp;있다.&amp;nbsp;예를&amp;nbsp;들어,&amp;nbsp;어떤&amp;nbsp;버스의&amp;nbsp;시작&amp;nbsp;시각이&amp;nbsp;특점&amp;nbsp;시점을&amp;nbsp;기준으로&amp;nbsp;10분&amp;nbsp;후이고,&amp;nbsp;간격은&amp;nbsp;10분이고,&amp;nbsp;대수가&amp;nbsp;5대이면,&amp;nbsp;이&amp;nbsp;버스는&amp;nbsp;10분,&amp;nbsp;20분,&amp;nbsp;30분,&amp;nbsp;40분,&amp;nbsp;50분에&amp;nbsp;한&amp;nbsp;대씩&amp;nbsp;출발한다.&lt;br /&gt;&lt;br /&gt;영식이는&amp;nbsp;버스터미널에&amp;nbsp;T분에&amp;nbsp;도착했다.&amp;nbsp;영식이가&amp;nbsp;버스를&amp;nbsp;타려면&amp;nbsp;최소&amp;nbsp;몇&amp;nbsp;분을&amp;nbsp;더&amp;nbsp;기다려야&amp;nbsp;하는지&amp;nbsp;구하는&amp;nbsp;프로그램을&amp;nbsp;작성하시오.&lt;br /&gt;&lt;br /&gt;입력&lt;br /&gt;첫째&amp;nbsp;줄에&amp;nbsp;버스의&amp;nbsp;개수&amp;nbsp;N과&amp;nbsp;영식이가&amp;nbsp;버스터미널에&amp;nbsp;도착하는&amp;nbsp;시간&amp;nbsp;T가&amp;nbsp;주어진다.&amp;nbsp;둘째&amp;nbsp;줄부터&amp;nbsp;총&amp;nbsp;N개의&amp;nbsp;줄에&amp;nbsp;각&amp;nbsp;버스의&amp;nbsp;시작&amp;nbsp;시각&amp;nbsp;Si,&amp;nbsp;간격&amp;nbsp;Ii,&amp;nbsp;대수&amp;nbsp;Ci가&amp;nbsp;공백을&amp;nbsp;사이에&amp;nbsp;두고&amp;nbsp;주어진다.&lt;br /&gt;&lt;br /&gt;출력&lt;br /&gt;첫째&amp;nbsp;줄에&amp;nbsp;영식이가&amp;nbsp;기다려야&amp;nbsp;하는&amp;nbsp;시간을&amp;nbsp;출력한다.&amp;nbsp;영식이가&amp;nbsp;도착하는&amp;nbsp;동시에&amp;nbsp;버스가&amp;nbsp;출발하면&amp;nbsp;정답은&amp;nbsp;0이다.&amp;nbsp;만약&amp;nbsp;버스가&amp;nbsp;없어서&amp;nbsp;캠프에&amp;nbsp;갈&amp;nbsp;수&amp;nbsp;없으면&amp;nbsp;-1을&amp;nbsp;출력한다.&amp;nbsp;정답은&amp;nbsp;231보다&amp;nbsp;작다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;풀이&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분탐색 알고리즘을 적용하면 해결할 수 있는 문제로 버스의 시작 시각 S와 간격 I, 대수 C 로 각 버스별 시간정보를 담은 배열을 생성하고 반복문을 돌리면서 각 버스별로 이분탐색 알고리즘을 적용하여 해결할 수 있다. 단, 만약 버스 시각이 목표 시각 T 보다 크고&amp;nbsp; 이전 목표시각이 T보다 작은 경우도 답이 될수 있기때문에 while 문에서 탈출해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;코드&lt;/b&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1705363394010&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const fs = require('fs');
const input = fs.readFileSync('/dev/stdin').toString().trim().split('\n');

// n : 버스 종류 개수, t: 영식이가 버스터미널에 도착하는 시간
const [n, t] = input[0].split(' ').map(Number);

let res = [];
let min = 0;
for (let i = 1; i &amp;lt;= n; i++) {
  // s : 버스 시작 시각, l : 간격, C : 대수
  let [s, l, c] = input[i].split(' ').map(Number);
  let start = 0;
  let busArr = [];
  for (let i = 0; i &amp;lt; c; i++) {
    busArr.push(s + l * i);
  }
  let end = busArr.length;
  if (busArr[start] &amp;gt; t) {
    res.push(busArr[start]);
  } else {
    while (start &amp;lt;= end) {
      let mid = parseInt((start + end) / 2);
      if (busArr[mid] == t) { // 버스시각이 목표시각과 같다면 탈출
        res.push(busArr[mid]);
        break;
      } else if (busArr[mid] &amp;gt; t) { // 버스시각이 목표시각 보다 크고 
        if (busArr[mid - 1] &amp;lt; t) { // 이전 버스시각이 목표시각보다 작다면 탈출
          res.push(busArr[mid]);
          break;
        } else {
          end = mid - 1;
        }
      } else start = mid + 1;
    }
  }
}
if (res.length &amp;gt; 0) {
  min = Math.min(...res);
}
console.log(min ? min - t : -1);&lt;/code&gt;&lt;/pre&gt;</description>
      <category>BOJ - JS</category>
      <category>binarysearch</category>
      <category>BOJ</category>
      <author>kyuuuun</author>
      <guid isPermaLink="true">https://kyuntechblog.tistory.com/67</guid>
      <comments>https://kyuntechblog.tistory.com/67#entry67comment</comments>
      <pubDate>Tue, 16 Jan 2024 09:04:23 +0900</pubDate>
    </item>
  </channel>
</rss>