<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>QC's Devlog</title>
    <link>https://quickchabun.tistory.com/</link>
    <description>Web Developer  </description>
    <language>ko</language>
    <pubDate>Wed, 20 May 2026 07:26:33 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>퀵차분</managingEditor>
    <image>
      <title>QC's Devlog</title>
      <url>https://tistory1.daumcdn.net/tistory/6444859/attach/09fbce2806774b3da331cbec4469fcdb</url>
      <link>https://quickchabun.tistory.com</link>
    </image>
    <item>
      <title>Fedify와 NestJS로 개인 사이트를 연합우주에 연결하기</title>
      <link>https://quickchabun.tistory.com/195</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 오픈소스 기여했던 프레임워크를 실제로 사용해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 컨트리뷰션 아카데미의 Fedify 팀에 참여해 오픈소스 기여를 진행했었다. 컨트리뷰션 아카데미를 수료한 후, 내가 기여한 Fedify 프레임워크를 제대로 사용해본적이 없는 거 같다는 생각을 하게 되었다. 이 프레임워크를 어떻게 활용해볼까 생각해보다가, 내 개인 사이트에 Fedify를 도입해서 연합우주 계정을 생성하고 다른 연합우주 SNS와 소통을 해보기로 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. ActivityPub, 연합우주(Fediverse)와 Fedify&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ActivityPub은 World Wide Web Consortium(W3C)에서 표준화한 개방형 분산 소셜 네트워크 프로토콜이다.&lt;br /&gt;(자세한 설명: &lt;a href=&quot;https://hackernoon.com/lang/ko/%EC%95%A1%ED%8B%B0%EB%B9%84%ED%8B%B0%ED%8E%8D%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B0%84%EB%9E%B5%ED%95%9C-%EC%86%8C%EA%B0%9C-%EC%86%8C%EC%85%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EC%9D%98-%EB%AF%B8%EB%9E%98&quot;&gt;https://hackernoon.com/lang/ko/액티비티펍에-대한-간략한-소개-소셜-네트워크의-미래&lt;/a&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;ActivityPub 프로토콜을 기반으로 만들어진 SNS들은 '연합우주(Fediverse)'에 속해 있다고 표현한다. 이 프로토콜 덕분에 연합우주 내의 서로 다른 SNS 사용자들끼리도 계정을 팔로우하거나 댓글을 남기는 등 자유롭게 상호작용할 수 있다. 그리고 'Fedify' 프레임워크를 활용하면 ActivityPub 기반 SNS를 더욱 쉽고 편리하게 개발할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 데이터베이스 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 웹 사이트에 ActivityPub 프로토콜을 구현하기 위해 아래와 같이 ERD를 구성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;874&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Sfc3o/dJMcahXmQRY/YxsWzfBwcmZe18QuxurOik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Sfc3o/dJMcahXmQRY/YxsWzfBwcmZe18QuxurOik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Sfc3o/dJMcahXmQRY/YxsWzfBwcmZe18QuxurOik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSfc3o%2FdJMcahXmQRY%2FYxsWzfBwcmZe18QuxurOik%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;600&quot; height=&quot;321&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;874&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;actors - 사용자 정보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연합우주의 모든 사용자를 나타내는 테이블이다. 각 Actor는 고유 식별자(id)와 사용자명(username), 화면에 표시될 이름(display_name)을 가진다. inbox_url은 이 사용자가 다른 서버로부터 메시지를 받을 주소이며, shared_inbox_url은 서버 전체가 공유하는 inbox 주소이다. is_local 필드로 우리 서버의 사용자인지 다른 서버 사용자인지를 구분할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;key_pairs - 암호화 키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 네트워크에서 메시지의 진위를 검증하기 위한 암호화 키 쌍을 저장한다. private_key로 메시지에 서명하고, public_key로 다른 사람이 그 서명을 검증한다. 이를 통해 메시지가 정말 해당 사용자로부터 온 것인지 확인할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;micro_posts - 게시물&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 작성한 게시물을 저장한다. content에는 원본 텍스트가, content_html에는 HTML로 변환된 내용이 담긴다. visibility 필드로 게시물의 공개 범위를 설정할 수 있는데, public은 모두에게 공개, followers는 팔로워에게만 공개하는 방식이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;follows - 팔로우 관계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;follower_id는 팔로우하는 사람, following_id는 팔로우 받는 사람을 나타낸다. status 필드로 팔로우 요청이 아직 승인 대기(pending) 상태인지, 이미 승인(accepted)되었는지 추적한다. 이는 비공개 계정의 팔로우 승인 기능에 사용된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;inbox_activities - 받은 활동 기록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 서버로부터 받은 모든 활동을 기록한다. 누군가 나를 팔로우하거나 내 게시물에 좋아요를 누르면 해당 활동이 ActivityPub 프로토콜을 통해 전달되어 이 테이블에 저장된다. raw_data에는 원본 JSON 데이터가, processed 필드에는 처리 여부가 기록되어 중복 처리를 방지한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Nest.js를 활용하여 백엔드 구축하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest.js를 활용하여 백엔드를 구축했다. 구조는 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yYSeW/dJMcagxmsuf/8kW2CdWYqi6GC2F6LUo2NK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yYSeW/dJMcagxmsuf/8kW2CdWYqi6GC2F6LUo2NK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yYSeW/dJMcagxmsuf/8kW2CdWYqi6GC2F6LUo2NK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyYSeW%2FdJMcagxmsuf%2F8kW2CdWYqi6GC2F6LUo2NK%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;600&quot; height=&quot;285&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;776&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fedify.service.ts&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ActivityPub 프로토콜의 중심이다. Federation 인스턴스를 주입받아(@fedify/nestjs 활용) Actor Dispatcher, Object Dispatcher, Inbox Listeners, Outbox를 초기화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Actor Dispatcher는 사용자 정보를 제공하고, Object Dispatcher는 게시물을 제공한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;inbox.service.ts&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 연합우주 서버로부터 받은 활동을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Follow 요청을 받으면 원격 사용자 정보를 저장하고 팔로우 관계를 데이터베이스에 기록한 후, Accept 응답을 보낸다. Undo 활동이 오면 언팔로우를 처리한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;outbox.service.ts&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 서버에서 다른 서버로 활동을 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Accept 활동으로 팔로우 요청을 수락하고, Create 활동으로 새 게시물을 팔로워들에게 전달한다. Federation의 &amp;lsquo;sendActivity&amp;rsquo; 메서드를 사용해 HTTP 서명된 요청으로 전송한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;actor.service.ts&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 정보를 관리한다. 로컬 사용자는 &amp;lsquo;createLocalActor&amp;rsquo;로 생성하며, 원격 사용자는 &amp;lsquo;createOrUpdateRemoteActor&amp;rsquo;로 다른 서버에서 받은 정보를 저장한다. 모든 사용자는 고유한 ActivityPub ID(URI)를 가진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;keypair.service.ts&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 사용자의 암호화 키 쌍을 관리한다. RSA와 Ed25519 두 가지 타입의 키를 생성하고 JWK 형식으로 저장한다. 이 키들은 ActivityPub 메시지에 HTTP 서명을 하여 메시지의 신뢰성을 보장한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;following.service.ts&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팔로우 로직을 처리한다. WebFinger 프로토콜로 원격 사용자를 찾고, Follow 활동을 전송한다. 또한 팔로잉 중인 사용자들의 최신 활동을 수집해 타임라인을 생성한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;following.controller.ts&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팔로우 요청을 받고, 팔로워 목록을 조회하며, 타임라인을 제공하는 API를 생성했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;micro-posts.service.ts&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시물 작성을 담당한다. 새 게시물을 ActivityPub의 Note 객체로 변환하고, Create 활동으로 감싸서 모든 팔로워에게 전송한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Nginx 리버스 프록시와 도메인 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션 환경에서는 프론트엔드와 백엔드를 별도 도메인으로 분리했다. 메인 도메인은 Next.js 프론트엔드를 서빙하고, API 서브도메인은 NestJS 백엔드를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 서버는 EC2에서 포트 3001로 실행되며, Nginx가 API 서브도메인의 모든 요청을 localhost:3001로 프록시한다. 환경변수는 FEDIVERSE_BACKEND_URL=https://another.domain.com로 설정했다. (이 글에서는 보안을 위해 API 서브도메인을 another.domain.com으로 표기한다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nginx 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ec2 ssh에서 /etc/nginx/conf.d/another.domain.com.conf 파일에 다음과 같이 설정했다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server {
  server_name another.domain.com;
  location / {
    proxy_pass http://127.0.0.1:3001;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }

  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/another.domain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/another.domain.com/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

server {
  if ($host = another.domain.com) {
    return 301 https://$host$request_uri;
  }

  listen 80;
  server_name another.domain.com;
  return 404;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let's Encrypt를 통해 SSL 인증서를 발급받아 HTTPS를 지원하며, 모든 HTTP 요청은 자동으로 HTTPS로 리다이렉트된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ActivityPub을 위한 프록시 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ActivityPub 프로토콜이 제대로 작동하려면 특정 경로들이 백엔드로 라우팅되어야 한다. Next.js의 rewrites 기능을 활용하여 /.well-known/&lt;i&gt;, /users/&lt;/i&gt;, /inbox 같은 경로를 API 서브도메인으로 프록시한다.&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;가장 중요한 것은 WebFinger 프로토콜이다. 다른 연합우주 서버가 @username@mydomain.com 형식으로 사용자를 찾을 때, &amp;lsquo;/.well-known/webfinger?resource=acct:username@mydomain.com&amp;rsquo; 경로로 요청을 보낸다. 이 요청은 Next.js rewrites를 통해 https://another.domain.com/.well-known/webfinger로 전달되고, Nginx는 이를 다시 백엔드(포트 3001)로 프록시한다. 백엔드는 Fedify가 자동으로 생성한 WebFinger 응답을 반환하여 해당 사용자의 ActivityPub 프로필 주소를 알려준다.&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;이러한 프록시 체인 덕분에 연합우주 핸들이 정상적으로 작동하며, HackersPub이나 Mastodon 같은 다른 연합우주 SNS에서 이 주소로 검색하면 내 계정을 찾고 팔로우할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 구현 후 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현을 완료했으니 이제 실제로 연합우주에서 작동하는지 테스트해보자. 기존에 연합우주 플랫폼인 Hackers.pub에 가입되어 있었기 때문에, 이를 활용해 상호작용을 테스트했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;계정 검색 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 해커스펍에서 내가 만든 계정인 @crohasang@crohasang.com을 검색해보았다.&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;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZbvvo/dJMcaiopjyC/njRXtRyIp3UKhYWgnidWe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZbvvo/dJMcaiopjyC/njRXtRyIp3UKhYWgnidWe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZbvvo/dJMcaiopjyC/njRXtRyIp3UKhYWgnidWe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZbvvo%2FdJMcaiopjyC%2FnjRXtRyIp3UKhYWgnidWe0%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;600&quot; height=&quot;95&quot; data-origin-width=&quot;1760&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;계정 검색이 정상적으로 되는 것을 확인할 수 있었다. 이는 WebFinger 프로토콜이 제대로 작동하고 있으며, ActivityPub 프로필이 연합우주에 올바르게 등록되었다는 뜻이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;팔로우 관계 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 실제로 팔로우 기능이 작동하는지 확인해보았다. 내 웹 사이트 계정(@crohasang@crohasang.com)과 해커스펍 계정(@crohasang@hackers.pub)을 서로 맞팔로우했다. 그리고 내 웹 사이트에서 팔로워와 팔로잉 목록이 제대로 표시되는지 확인해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AOTXj/dJMcaiopjBV/X4wZtQi41ZmJgZfLf8K2Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AOTXj/dJMcaiopjBV/X4wZtQi41ZmJgZfLf8K2Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AOTXj/dJMcaiopjBV/X4wZtQi41ZmJgZfLf8K2Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAOTXj%2FdJMcaiopjBV%2FX4wZtQi41ZmJgZfLf8K2Wk%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;600&quot; height=&quot;248&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팔로잉과 팔로워가 모두 정상적으로 표시되는 것을 확인할 수 있었다. 이는 팔로우 요청을 보낼 때 outbox.service.ts가 Follow 액티비티를 전송하고, 상대방 서버로부터 받은 Accept 응답을 inbox.service.ts가 처리하여 데이터베이스에 저장했다는 뜻이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타임라인 연동 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 타임라인 기능을 테스트했다. 해커스펍 계정으로 글을 작성한 후, 내 웹 사이트의 타임라인에 해당 글이 나타나는지 확인해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mjNig/dJMcahpwznc/869tuKoHc7kJsSOAdacsWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mjNig/dJMcahpwznc/869tuKoHc7kJsSOAdacsWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mjNig/dJMcahpwznc/869tuKoHc7kJsSOAdacsWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmjNig%2FdJMcahpwznc%2F869tuKoHc7kJsSOAdacsWK%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;600&quot; height=&quot;195&quot; data-origin-width=&quot;1754&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해커스펍에서 작성한 글이 웹 사이트 타임라인에서 정상적으로 표시되는 것을 확인할 수 있었다! 이는 following.service.ts가 팔로잉 중인 계정들의 최신 액티비티를 수집하고, 이를 통합 타임라인으로 제공하는 기능이 제대로 작동한다는 뜻이다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 Fedify와 NestJS를 활용해 개인 사이트를 연합우주에 연결했다.&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;이번 작업을 통해 ActivityPub 프로토콜의 핵심 개념들을 이해할 수 있었고, Fedify 프레임워크를 실제 프로젝트에 적용하는 경험을 할 수 있었다. 또한 NestJS에서 기능별로 모듈과 서비스를 나누고, TypeORM으로 데이터베이스 마이그레이션을 관리하는 방법도 익힐 수 있었다. 아직 좋아요 같은 구현하지 못한 기능들이 남아있지만, 앞으로 천천히 추가해볼 예정이다.&lt;/p&gt;</description>
      <category>Project/crohasang_page</category>
      <category>ActivityPub</category>
      <category>fedify</category>
      <category>fediverse</category>
      <category>nestjs</category>
      <category>연합우주</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/195</guid>
      <comments>https://quickchabun.tistory.com/195#entry195comment</comments>
      <pubDate>Sun, 28 Dec 2025 16:11:31 +0900</pubDate>
    </item>
    <item>
      <title>토스 Frontend Fundamentals 1회 모의고사 후기</title>
      <link>https://quickchabun.tistory.com/194</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 웹 프론트엔드 모의고사?&lt;/h2&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;/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;1166&quot; data-origin-height=&quot;1184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sTKCD/dJMb99Y7FDz/czs2u8OHokSvz1HzKcjtk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sTKCD/dJMb99Y7FDz/czs2u8OHokSvz1HzKcjtk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sTKCD/dJMb99Y7FDz/czs2u8OHokSvz1HzKcjtk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsTKCD%2FdJMb99Y7FDz%2Fczs2u8OHokSvz1HzKcjtk0%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;305&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;1184&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;글을 자세히 읽어보니 모두가 모의고사를 볼 수 있는게 아니라 선착순 50명만 모의고사를 볼 수 있다고 써져있었다. 수강신청만 했다하면 번번히 망했던 내게 좋은 소식은 아니었다. 그래도 한 번 시도는 해봐야지 생각하며, 2시가 되자마자 트위터를 계속 새로고침하다가 뜬 초대 링크를 클릭했다. 이번에는 다행히 선착순에 성공할 수 있었고, 주말에 시간을 내어 2시간 동안 모의고사에 응시할 수 있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 얼마만에 듣는 해설강의인지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해설강의는 화요일 8시에 라이브로 진행되었다. 6평이나 9평이 끝나면 인강 사이트에 접속해서 시험지를 펼치고 인강 선생님들의 해설강의를 듣고는 했었다. 나한테는 많이 어려웠는데 선생님들은 모든 걸 안다는 듯이 척척 풀곤해서 현타가 꽤 많이 왔었던 기억이 난다. 아무튼 8시가 되어 폰을 키고 해설강의 링크에 접속했다. 과연 주말에 내가 열심히 만든 코드는 퀄리티가 괜찮았을까. 과연 토스 개발자들은 어떻게 코드를 짜셨을까. 호기심을 가지고 개발자님의 설명에 집중하기 시작했다.&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.md를 안썼거나, 테스트 코드를 짜지 않아서 떨어졌다는 소문이 퍼졌다고 하는데, 사실 그런 부분이 중요한 건 아니라고 하셨다. 그렇다면 과제를 구현할 때 중요한 점은 과연 무엇일까? 바로 &lt;b&gt;&amp;lsquo;코드를 어떻게 설계하는지&amp;rsquo;&lt;/b&gt;에 대한 것이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코드 구조와 UI 구조를 1대1 매칭하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자분께서는 먼저 코드를 읽기 전에, 파일을 새로 생성하시고 제공된 UI를 보며 어떻게 구조를 짤까, 이상적인 인터페이스는 무엇일까 고민하시면서 마크업을 구성하셨다. 개발자분께서 작성한 코드는 어떤 UI를 나타내는지 바로 알 수 있을 정도로 직관적이었다. 왜냐하면 코드의 구조와 UI의 구조가 &lt;b&gt;1:1&lt;/b&gt;로 매핑이 되어있었기 때문이다. 특히 title과 같은 텍스트가 해당 코드가 어느 부분인지 알려주는 역할을 해주었다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 컴포넌트를 만들 때 일반적인 표준을 따르자 &amp;rarr; 코드의 사회성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Input이라는 접미사가 붙은 컴포넌트를 만든다고 가정하자. 그렇다면 그 컴포넌트는 일반적인 Input과 유사한 구조를 가지고 있어야 다른 사람들이 이해하기 쉽다.&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;lsquo;EmptyResultBoundary&amp;rsquo;라는 이름의 컴포넌트 코드를 보여주시면서, 이 코드를 왜 우리한테 보여주는지를 질문해주셨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 보통 &amp;lsquo;Boundary&amp;rsquo;라는 이름이 들어가게 되면 ErrorBoundary를 떠올리게 되고, 그러면 Error나 Promise가 던져지는걸 예상하게 되는데 이 코드에서는 그렇지 않았다는 점이 아쉬웠다는 설명을 해주셨다.&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;lsquo;언어의 사회성&amp;rsquo;처럼 &lt;b&gt;&amp;lsquo;코드의 사회성&amp;rsquo;&lt;/b&gt;을 지킬 수록 우리의 소통은 더욱 간편해진다. 이를 지키기 위해서, 더 많은 코드를 보면서 패턴에 익숙해질 필요가 있다는 사실을 알게 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 추상화 !== 추출 &amp;rarr; Monster Hook의 출현을 막아라&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번에서 언급했듯이, 우리는 파일을 깔끔하게 분리하는 것에만 집중하면 안된다. 이번에 해설을 진행해주신 재엽님의 블로그에는 추상화와 추출의 차이점이 작성되어있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jbee.io/articles/etc/%EC%A2%8B%EC%9D%80%20%EC%BD%94%EB%93%9C%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jbee.io/articles/etc/%EC%A2%8B%EC%9D%80%20%EC%BD%94%EB%93%9C%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1764167994330&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;좋은 코드란 무엇일까&quot; data-og-description=&quot;성선설에 기반하면 모든 개발자는 좋은 코드를 작성하고 싶으리라 생각한다. 누구나 관심 있어 하는 주제인 만큼 나 또한 여러 고민을 거듭해왔고 여태까지의 생각을 정리해보려고 한다. 어떤 &quot; data-og-host=&quot;jbee.io&quot; data-og-source-url=&quot;https://jbee.io/articles/etc/%EC%A2%8B%EC%9D%80%20%EC%BD%94%EB%93%9C%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C&quot; data-og-url=&quot;https://jbee.io/articles/etc/좋은 코드란 무엇일까&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cuWWAI/hyZOxb7Bmy/JI5Jfbvf3l1XxSCckVbagK/img.png?width=1179&amp;amp;height=749&amp;amp;face=0_0_1179_749,https://scrap.kakaocdn.net/dn/8dhug/hyZOpyoFee/d3zWGVNKljaFnOK7gD3vKK/img.png?width=1179&amp;amp;height=749&amp;amp;face=0_0_1179_749&quot;&gt;&lt;a href=&quot;https://jbee.io/articles/etc/%EC%A2%8B%EC%9D%80%20%EC%BD%94%EB%93%9C%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://jbee.io/articles/etc/%EC%A2%8B%EC%9D%80%20%EC%BD%94%EB%93%9C%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cuWWAI/hyZOxb7Bmy/JI5Jfbvf3l1XxSCckVbagK/img.png?width=1179&amp;amp;height=749&amp;amp;face=0_0_1179_749,https://scrap.kakaocdn.net/dn/8dhug/hyZOpyoFee/d3zWGVNKljaFnOK7gD3vKK/img.png?width=1179&amp;amp;height=749&amp;amp;face=0_0_1179_749');&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;좋은 코드란 무엇일까&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;성선설에 기반하면 모든 개발자는 좋은 코드를 작성하고 싶으리라 생각한다. 누구나 관심 있어 하는 주제인 만큼 나 또한 여러 고민을 거듭해왔고 여태까지의 생각을 정리해보려고 한다. 어떤&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;jbee.io&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;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;&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;단순히 깔끔하게 보이기 위한 분리는 재사용하기 어렵고, 역할을 파악하기 힘든 괴물을 만들 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 이번 모의고사에서 깔끔하게 보이기 위해 내가 만든 훅을 보자.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;const {
    isLoading,
    errorMessage,
    goalAmount,
    monthlyAmount,
    term,
    setGoalAmount,
    setMonthlyAmount,
    setTerm,
    selectedProductId,
    handleSelectProduct,
    selectedProduct,
    finalAmount,
    diffFromGoal,
    recommendedMonthly,
    recommendedProducts,
    filteredProducts,
  } = useSavingsCalculator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무려 16개의 반환값이 있는 기괴한 훅을 만들었다. 이런 훅을 &lt;b&gt;Monster Hook&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;단순히 분리만 한 훅은 아무런 의미가 없다. 우리는 &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;같은 의미로 개발자분들께서는 FSD와 같이 어떻게 폴더를 쪼개야되는지를 설명해주는 방법론에 대해서 회의적인 시각을 가지고 계셨다. FSD가 유명해진 이유는 코드의 라인수가 줄어들면 깔끔해보이는데, 코드의 라인수를 줄이게 하기위해 끄집어내고 집어넣는 방법을 FSD가 알려주기 때문이지 않을까하는 견해를 밝혀 주셨다. 오히려 코드들은 물리적으로 가까이 있는 것이 직관적이고 좋지 않을까하는 생각을 말씀해주셨다. 즉, &lt;b&gt;폴더 구조에 너무 집착하지 말자!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결론은 직관적인 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 적은 내용 외에도,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Global State를 쓰는 이유가 props drilling이어서는 안된다,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;isLoading 같은 우리의 관심사가 아닌 요소에 시선을 빼앗기기 위해 useSuspenseQuery를 사용하면 좋다,&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 마지막으로 아래와 같이 3줄 요약을 해주셨다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;코드를 보기 전에 요구사항 문서를 보고 먼저 이상적인 형태를 떠올려보자.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UI 형태와 코드 형태가 1:1로 매칭되면 유지보수가 쉽다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;책임 단위로 추상화를 하면 재사용성은 따라오는 것.&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해설강의를 통해 알려주신 내용들은 결국 누구나 알아보기 쉬운 직관적인 코드를 만들기 위한 방법이었다. 지금까지 내가 어떻게 구조를 짜왔고 코딩을 해왔는지 되돌아보았다. 그저 깔끔해보이기 위한 의미없는 분리의 반복. 내가 만든 컴포넌트가 무슨 역할을 하기 위한 것인지 확인하기 위해 주석을 달고 drilling을 해왔던 것을 떠올려봤을 때, 나의 코드는 분명 직관적이지 않았다.&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;이번 과제 레포의 discussion 탭에서 다양한 토론이 이루어졌는데 글을 읽어보며 접근성이나 구조 설계 등 여러 주제에 대한 정보를 익힐 수 있었다. 이뿐만 아니라 이번에 모의고사를 주최한 Frontend Fundamentals 홈 페이지에서 코드퀄리티, 번들링 등에 대한 내용들이 상세하게 적혀있어 앞으로 자주 접속하려고 한다. (사실 최근에 홈페이지 최적화를 할 때 이 페이지의 번들링 탭을 자주 참고했었다!)&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;정말 오랜만의 모의고사와 해설강의. 옛날로 돌아간 기분과 함께 얻어가는 게 많았던 귀중한 시간이었다. 이런 모의고사를 위해 고생해주신 Frontend Fundamentals 개발자분들께 감사드리며, 다음에 이런 기회가 또 생긴다면 참여해야겠다는 다짐을 하게 되었다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>frontend fundamentals</category>
      <category>Frontend Fundamentals 모의고사</category>
      <category>Web Frontend</category>
      <category>웹 프론트엔드</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/194</guid>
      <comments>https://quickchabun.tistory.com/194#entry194comment</comments>
      <pubDate>Wed, 26 Nov 2025 23:43:14 +0900</pubDate>
    </item>
    <item>
      <title>NestJS 백엔드와 EC2 인스턴스로 개인 사이트에 RDS 연결하기</title>
      <link>https://quickchabun.tistory.com/193</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난 주, 나는 개인 사이트에 게시글을 올리기 위해 MySQL RDS를 생성하고 Next.js에서 라우팅을 설정했었다. 하지만 Vercel은 고정 IP를 제공하지 않아 RDS의 인바운드 규칙을 설정할 수 없는 문제가 발생했고, 결국 현재 구조로는 RDS에 접근하지 못한다는 사실을 알게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이전 작성 글: &lt;a href=&quot;https://quickchabun.tistory.com/191&quot;&gt;https://quickchabun.tistory.com/191&lt;/a&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;어떻게 하면 Vercel로 배포하면서 RDS에 접근할 수 있을까?&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Vercel의 Static IP 기능을 활성화해서 고정 IP를 생성한다. (구매 필요)&lt;/li&gt;
&lt;li&gt;AWS Lambda를 VPC 내부 DB 프록시로 두고 HTTP로 호출한다.&lt;/li&gt;
&lt;li&gt;데이터베이스를 AWS RDS에서 고정 IP 없이 접근이 가능한 데이터베이스로 마이그레이션한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 방법을 쓸지 고민하다가, 생각해보니 직접 RDS에 접근하는게 아니라, EC2 인스턴스를 생성해서 백엔드를 구축하고 Vercel에서 EC2에 연결하면 되지 않을까? 하는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;038f5935-1.png&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MIi7P/dJMcaaXXqeC/G8Ive8jmiqzFSqxAV5L3E0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MIi7P/dJMcaaXXqeC/G8Ive8jmiqzFSqxAV5L3E0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MIi7P/dJMcaaXXqeC/G8Ive8jmiqzFSqxAV5L3E0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMIi7P%2FdJMcaaXXqeC%2FG8Ive8jmiqzFSqxAV5L3E0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;634&quot; height=&quot;390&quot; data-filename=&quot;038f5935-1.png&quot; data-origin-width=&quot;1396&quot; data-origin-height=&quot;858&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 예전에 내 개인 페이지에 백엔드를 도입하고 싶어서 모노레포를 도입해 프론트엔드(NextJS) 레포와 백엔드(NestJS) 레포로 분리해놨었다. 만들기만 해놓고 지금까지 아무 작업을 하지 않았었는데, 이번 기회에 백엔드를 구축해보면 좋을 것 같다는 생각이 들었다. 그러면 시작해보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. NestJS로 게시글 조회 API를 만들자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백엔드 레포에 게시글을 조회할 수 있게 API 서버를 만들어야한다. ORM은 prisma를 쓸까 typeorm을 쓸까 고민했었는데 저번에 인프런 NestJS 강의를 들었을 때 TypeORM을 사용했었기 때문에 더 익숙한 TypeORM을 선택했다.&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;먼저 posts 디렉토리를 생성하고 API 구축에 필요한 파일들을 생성해주었다.&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;post.entity.ts를 생성해서 데이터베이스의 테이블 구조를 정의해주었다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('posts')
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', length: 255 })
  title: string;

  @Column({ type: 'text' })
  body: string;

  @CreateDateColumn({ name: 'created_at' })
  created_at: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updated_at: Date;
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;posts.service.ts를 생성해서 게시물 목록과 게시물을 조회하는 비즈니스 로직을 구현했다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './post.entity';

@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post)
    private postsRepository: Repository&amp;lt;Post&amp;gt;,
  ) {}

  async findAll() {
    const posts = await this.postsRepository.find({
      order: { created_at: 'DESC' },
    });

    // displayId 추가
    const postsWithDisplayId = posts.map((post, index) =&amp;gt; ({
      ...post,
      displayId: posts.length - index,
    }));

    return postsWithDisplayId;
  }

  async findByDisplayId(displayId: number) {
    const posts = await this.postsRepository.find({
      order: { created_at: 'DESC' },
    });

    const postIndex = posts.length - displayId;

    if (postIndex &amp;lt; 0 || postIndex &amp;gt;= posts.length) {
      throw new NotFoundException('Post not found');
    }

    return posts[postIndex];
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시글을 조회할 때 displayId 속성을 추가해줬는데, 전체 게시글 수에서 자신의 인덱스를 빼주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 글이 10개일 때, 게시글이 가장 오래된 게시글의 인덱스는 10 - 9 = 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;그리고 posts.controller.ts에서 요청이 들어오면 posts.service.ts 로직이 실행될 수 있게 연결해주었다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { PostsService } from './posts.service';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Get()
  findAll() {
    return this.postsService.findAll();
  }

  @Get(':displayId')
  findOne(@Param('displayId', ParseIntPipe) displayId: number) {
    return this.postsService.findByDisplayId(displayId);
  }
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 posts.module.ts에서 entity, service, controller를 하나의 모듈로 묶고 NestJS에 등록해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그리고 이 모듈은 app.module.ts에 import 해준다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsController } from './posts.controller';
import { PostsService } from './posts.service';
import { Post } from './post.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Post])],
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. EC2 인스턴스를 생성하고 RDS와 연결해보자.&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/posts api 엔드포인트를 만들었으니 이제 AWS EC2 인스턴스를 생성하고 연결을 하면 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;a. AWS EC2 콘솔로 이동해서 EC2 인스턴스를 생성해주었다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 EC2 인스턴스를 생성하는 건 이번이 처음이어서 AMI를 Amazon Linux를 선택할지 Ubuntu를 선택할지와 같은 선택의 기로에서 많이 헷갈렸었는데 LLM의 도움을 받아 하나씩 선택하면서 진행할 수 있었다. (참고로 AMI는 Amazon Linux를 선택했다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;b. 인스턴스를 생성하고 EC2 Instance Connect로 브라우저 터미널로 접속했다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 지금까지는 내장 터미널에서 ssh에 접속했었는데, 브라우저에서 터미널을 접속할 수 있다는 사실이 신기했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;c. 접속 후 &amp;lsquo;sudo dnf update -y&amp;rsquo; 명령어를 입력해 시스템을 업데이트해주었다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Node.js, pnpm, git을 설치하고 개인 블로그 레포에서 내가 작업 중인 브랜치를 clone했다.&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;clone이 끝나고 백엔드 디렉토리로 이동해 pnpm install 명령어를 입력하여 의존성 패키지를 설치하려고 했는데 보안 권한 때문인지 설치가 안되는 패키지들이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &amp;lsquo;pnpm approve-builds&amp;rsquo; 명령어를 입력하고 다시 설치하면 된다.&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;lsquo;nano .env&amp;rsquo; 명령어를 입력해 .env 파일을 생성 후 환경변수들을 입력해주면 어느정도 설정은 마무리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;d. RDS와 EC2의 보안 그룹 인바운드 규칙을 업데이트했다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDS 보안 그룹에 EC2 인스턴스 보안 그룹 ID (launch-wizard-1)를 소스로 추가해줘서 EC2에서 RDS에 접근이 가능하도록 했다.&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;그리고 EC2 보안그룹의 인바운드 규칙에 SSH(22), HTTP(80)를 추가했는데 이상하게 백엔드 통신이 되지 않았다. 생각해보니 백엔드는 3001번 포트에서 실행이 되고 있었고, Custom TCP의 3001번 포트도 인바운드 규칙에 허용해주니 그 때부터 통신을 할 수 있었다 (모노레포를 사용하고 있어서 프론트엔드는 3000번 포트, 백엔드 포트는 3001번 포트를 사용하고 있었다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;e. 다시 터미널로 돌아와서 PM2 라이브러리를 설치해주었다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM2는 Node.js 애플리케이션을 백그라운드에서 실행하고, 손쉽게 관리할 수 있게 해주는 프로세스 매니저다. 일반적으로 터미널에서 Node.js 서버를 실행하면 탭을 닫거나 세션이 종료되면 서버도 함께 꺼지지만, PM2로 구동하면 서버가 계속 백그라운드에서 안정적으로 돌아갑니다.&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;먼저 &amp;lsquo;pm2 start dist/main.js --name 내 프로젝트 이름&amp;rsquo; 명령어를 입력해 PM2로 백엔드를 실행시켜주고, &amp;lsquo;pm2 status&amp;rsquo; 명령어를 입력해 잘 실행되고 있는지 확인했다. 그리고 &amp;lsquo;pm2 logs 내 프로젝트 이름&amp;rsquo; &amp;mdash;line 20&amp;rsquo; 명령어를 입력해서 로그를 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 잘 켜진 것을 확인했으니 이제 startup 명령어를 입력해서 운영체제의 부팅 시스템에 PM2가 자동으로 실행되도록 등록해야 한다.&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;lsquo;pm2 startup&amp;rsquo; 명령어를 실행하면 서버의 운영체제에 맞는 자동 실행용 스크립트 명령어가 출력된다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/nodejs18/lib/node_modules/pm2/bin/pm2 startup systemd -u ec2-user --hp /home/ec2-user
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저 명령어를 복사 후 실행하면 운영체제에 PM2가 등록되어서 서버를 재부팅해도 PM2가 자동으로 켜진다. 마지막으로 &amp;lsquo;pm2 save&amp;rsquo;를 입력해서 현재 앱 목록을 저장했다.&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Next.js에서 라우팅을 RDS &amp;rarr; EC2로 변경하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 프론트엔드에서 RDS에서 EC2로 라우팅시키면 된다. 바꾸는 과정은 어렵지 않다. 지금까지는 커넥션 풀을 생성해서 RDS 주소로 연결했었는데, 이제 그 주소를 EC2 인스턴스 IP 주소로 변경하면 된다.&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;rarr; 즉, 환경 변수를 새로 만들고 바꾸면 된다. .env 파일만 변경할게 아니라 vercel의 환경변수도 변경해야 배포 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS API 구현 + Next.js 라우팅 작업 Pull Request: &lt;a href=&quot;https://github.com/crohasang/crohasang_page/pull/20&quot;&gt;https://github.com/crohasang/crohasang_page/pull/20&lt;/a&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;프론트엔드 코드를 push하고 배포했더니, 지금까지 접속이 안되었던 &amp;lsquo;/post&amp;rsquo;에 접근할 수 있게 되었다. 이번에 EC2 인스턴스를 직접 생성하고, NestJS API 서버를 구현하면서 그동안 막연하게만 느껴졌던 AWS와 백엔드에 대해서 한 걸음 더 나아갈 수 있었다. 저번 달에 RDS 고정 IP 비용이 청구되었기에 (천원정도) 갑작스럽게 청구서가 날아오지 않을까 살짝 두렵긴 하지만, 그래도 아직은 프리티어가 적용되니까 크게 걱정하지 않기로 했다.&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;이제 DB에 저장되어 있는 게시물을 볼 수 있는 건 구현했으니, 이제 구조를 더 확장할 차례이다. 좋아요와 댓글을 구현해도 되고, 백오피스를 프론트엔드로 구현해서 게시물 업로드를 개인 웹 사이트에서 직접 해도 재밌을 것 같다. 차례차례 해보자!&lt;/p&gt;</description>
      <category>Project/crohasang_page</category>
      <category>EC2</category>
      <category>nestjs</category>
      <category>rds</category>
      <category>vercel</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/193</guid>
      <comments>https://quickchabun.tistory.com/193#entry193comment</comments>
      <pubDate>Sat, 15 Nov 2025 17:03:17 +0900</pubDate>
    </item>
    <item>
      <title>딥링크를 활용하여 앱 설치 유무를 인식하는 앱 배너 구현하기</title>
      <link>https://quickchabun.tistory.com/192</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;모바일에서 클릭했을 때, 사용자의 기기에서 앱이 깔려있으면 앱으로 이동하고, 앱이 깔려있지 않으면 앱스토어로 이동하게하는 배너를 최근 구현했다. iOS, 안드로이드마다 적용해야되는 로직이 다르기 때문에 각자 다르게 설정을 해줘야하고 또 여러 가지 방법으로 구현할 수 있기 때문에 글로 정리해보려 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. iOS&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;a. Smart App Banner - 사파리에서만 가능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간편한 방법은 &amp;lsquo;스마트 앱 배너&amp;rsquo;인데, 사용자의 기기에 해당 앱이 깔려있으면 사파리에서 해당 앱이 해당하는 도메인에 접속하면 웹 사이트 상단에 자동으로 배너를 띄워준다. 구현 방법도 간단하다. HTML 헤더에다 다음과 같이 추가하면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;meta name=&quot;apple-itunes-app&quot; content=&quot;app-id=myAppStoreID, app-argument=myURL&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현이 쉽고 간단하지만, 사파리에서만 볼 수 있다는 점과 커스터마이징을 할 수 없다는 단점을 가지고 있다. 더 자세한 내용은 아래 링크에서 살펴볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mine-it-record.tistory.com/710&quot;&gt;https://mine-it-record.tistory.com/710&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762585602909&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Smart App Banner란?&quot; data-og-description=&quot;스마트 앱 배너란? 스마트 앱 배너는 애플 앱스토어에서 앱을 홍보하는 표준 방식을 의미한다. iOS에서 공식지원하는 Smart App Banner는 웹 페이지의 최상단에 위치하게 된다. 안타깝게도 안드로이&quot; data-og-host=&quot;mine-it-record.tistory.com&quot; data-og-source-url=&quot;https://mine-it-record.tistory.com/710&quot; data-og-url=&quot;https://mine-it-record.tistory.com/710&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/beVKMR/hyZMuf7GtK/wgMszld3JsQKksAMAG0hU0/img.jpg?width=665&amp;amp;height=1440&amp;amp;face=0_0_665_1440,https://scrap.kakaocdn.net/dn/i4VxH/hyZM0TVqtI/BbTZJt7Qzpsxg7nOGgK1zK/img.jpg?width=665&amp;amp;height=1440&amp;amp;face=0_0_665_1440,https://scrap.kakaocdn.net/dn/bif8K9/hyZMuAoyVB/7cPAK6Qm2qrUkzsUhDWlJK/img.png?width=1214&amp;amp;height=951&amp;amp;face=0_0_1214_951&quot;&gt;&lt;a href=&quot;https://mine-it-record.tistory.com/710&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mine-it-record.tistory.com/710&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/beVKMR/hyZMuf7GtK/wgMszld3JsQKksAMAG0hU0/img.jpg?width=665&amp;amp;height=1440&amp;amp;face=0_0_665_1440,https://scrap.kakaocdn.net/dn/i4VxH/hyZM0TVqtI/BbTZJt7Qzpsxg7nOGgK1zK/img.jpg?width=665&amp;amp;height=1440&amp;amp;face=0_0_665_1440,https://scrap.kakaocdn.net/dn/bif8K9/hyZMuAoyVB/7cPAK6Qm2qrUkzsUhDWlJK/img.png?width=1214&amp;amp;height=951&amp;amp;face=0_0_1214_951');&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;Smart App Banner란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;스마트 앱 배너란? 스마트 앱 배너는 애플 앱스토어에서 앱을 홍보하는 표준 방식을 의미한다. iOS에서 공식지원하는 Smart App Banner는 웹 페이지의 최상단에 위치하게 된다. 안타깝게도 안드로이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mine-it-record.tistory.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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;b. Universal Link&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유니버설 링크는 iOS에서 도메인 주소를 활용한 딥링크를 구현하는 방법이다. 유니버설 링크를 활용하면 해당 링크를 클릭했을 때, 기기 시스템에서 해당 앱이 기기에 깔려있는지 확인하고, 앱이 존재한다면 앱이 켜지게 된다.&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;특정 도메인을 클릭했을 때 시스템에서 앱과 연결되어 있는 도메인인지 판별할 수 있도록 도메인의 웹 서버에 apple-app-site-association(줄여서 AASA) 파일을 .well-known 디렉토리나 루트 디렉토리에 추가해야 한다.&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;AASA 파일 형식&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
	&quot;applinks&quot;: {
		&quot;apps&quot;: [],
		&quot;details&quot;: [
			{
				&quot;appID&quot;: &quot;appId&quot;,
				&quot;paths&quot;: [
					&quot;NOT /앱에서 지원하지 않는 경로&quot;,
					&quot;/앱에서 지원하는 경로&quot;,
				]
			}
		]
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;paths 경로를 통해 특정 페이지에서는 유니버설 링크가 동작하지 않게 만들 수 있다. 또한, &amp;lsquo;/*&amp;rsquo;를 통해 사이트의 도메인의 모든 페이지를 앱에서 지원하는 경로로 설정할 수 있다.&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;웹 서버에 AASA 파일을 배포하면, Apple CDN에서 해당 웹 서버의 AASA 파일을 수집한 뒤, 해당 도메인의 앱이 설치되어있는 기기의 시스템에 AASA 파일의 설정을 삽입한다. 즉, 도메인 링크를 클릭했을 때 앱으로 넘어가는 동작은 기기의 시스템에서 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Apple CDN에서 언제 도메인의 AASA 파일을 수집하는지는 예측할 수 없다. (24시간 ~ 48시간 정도라고 한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 잘 작동하는지 테스트를 하려면 해당 도메인 링크를 메모 앱에 적고 클릭해보면 된다. 참고로, 도메인을 직접 입력해서 접속하면 앱이 켜지지 않는다. (사용자가 해당 링크로 직접 이동하고 싶다고 시스템이 인식하기 때문)&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;아니면 터미널에 다음과 같은 curl 요청을 날려보자&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;curl -v &amp;lt;https://app-site-association.cdn-apple.com/a/v1/{your-domain}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;200 응답과 함께 AASA 파일 JSON이 출력된다면 Apple CDN에 제대로 반영이 된것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 Not Found가 뜬다면, 아직 Apple CDN에서 반영이 안된 것이므로 조금만 더 기다려보자.&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;또한, 본인의 도메인에서 Apple CDN이 AASA 파일을 수집할 수 있는지 확인하려면 아래 명령어를 입력하자.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;curl -v https://{your-domain}/.well-known/apple-app-site-association
&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고1)&lt;/h4&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;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;해당 도메인의 웹 서버(nginx)에서 .well-known/apple-app-site-association의 접근을 막는다.&lt;/li&gt;
&lt;li&gt;새로운 공유용 전문 도메인을 파고, 해당 도메인의 웹 서버에 AASA 파일을 추가한다.&lt;/li&gt;
&lt;/ol&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;참고2)&lt;/h4&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;배너를 클릭했을 때, 현재 탭에서 바로 공유용 URL이 켜지게 한다면 앱으로 이동하시겠습니까? 라는 팝업이 뜨게된다. 만약에 이 팝업이 뜨지 않고 바로 앱으로 이동하게 만들고 싶다면, 공유용 URL을 새 탭으로 키면 (window.open 사용) 팝업을 뜨지 않고 바로 앱으로 이동할 수 있다. (앱스토어 이동 팝업은 그대로 뜬다)&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;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고3)&lt;/h4&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;왜냐하면 기기의 시스템이 사용자의 이전 선택을 기억하기 때문인데, 만약 사용자가 배너를 클릭해서 다시 앱으로 이동하고 싶다면 배너를 꾹 눌러서 &amp;lsquo;앱으로 열기&amp;rsquo; 옵션을 클릭하면 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 안드로이드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;a. App Link&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS의 Universal Link와 같은 역할을 한다. iOS에서는 AASA 파일을 사용했듯이 안드로이드에서는 .well-known/assetlinks.json을 사용한다.&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;iOS는 AASA 파일이 Apple CDN에 반영되기까지 시간이 좀 걸렸지만, 안드로이드는 iOS에 비해 웹 서버 파일 변경이 반영되는 시간이 더 빠르다는 장점이 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[
  {
    &quot;relation&quot;: [&quot;delegate_permission/common.handle_all_urls&quot;],
    &quot;target&quot;: {
      &quot;namespace&quot;: &quot;android_app&quot;,
      &quot;package_name&quot;: &quot;com.example&quot;,
      &quot;sha256_cert_fingerprints&quot;: [
        &quot;AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90&quot;
      ]
    }
  }
]&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;sha256_cert_fingerprints에는 앱 서명 키의 SHA-256 해시 값을 입력하면 된다. 안드로이드 개발자에게 해시 값이 무엇인지 물어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 링크에 대한 더 자세한 내용은 아래 링크로 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.nachocode.io/docs/guide/deep-link/app-link&quot;&gt;https://developer.nachocode.io/docs/guide/deep-link/app-link&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762585602965&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;앱 링크 (App Link) | nachocode Developer&quot; data-og-description=&quot;Google Android의 표준 딥링크 방식인 앱 링크 (App Link)에 대한 개념과 nachocode에서 앱링크를 설정하는 방법, 활용법을 안내합니다.&quot; data-og-host=&quot;developer.nachocode.io&quot; data-og-source-url=&quot;https://developer.nachocode.io/docs/guide/deep-link/app-link&quot; data-og-url=&quot;https://developer.nachocode.io/docs/guide/deep-link/app-link&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cbcRTU/hyZM2YufHA/GlZgysEt4zNElvbl5XQ3E1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/GYp9g/hyZNgHA61i/tVFzJZRoL7FrctuJW6ex3k/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://developer.nachocode.io/docs/guide/deep-link/app-link&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.nachocode.io/docs/guide/deep-link/app-link&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cbcRTU/hyZM2YufHA/GlZgysEt4zNElvbl5XQ3E1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/GYp9g/hyZNgHA61i/tVFzJZRoL7FrctuJW6ex3k/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;앱 링크 (App Link) | nachocode Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Google Android의 표준 딥링크 방식인 앱 링크 (App Link)에 대한 개념과 nachocode에서 앱링크를 설정하는 방법, 활용법을 안내합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.nachocode.io&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;b. Intent Scheme&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텐트 스킴 딥링크를 활용하면 사용자가 클릭한 링크를 앱의 인텐트로 연결할 수 있다. 인텐트 스킴은 사용자의 기기에 앱이 깔려있지 않을 경우 이동할 URL을 지정할 수 있어서 구현이 편리했다. (iOS의 커스텀 스킴은 fallback URL을 제공하지 않았다.)&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;intent://open
		scheme=(...)
    package=(...)
    S.browser_fallback_url=(...)
    end;
&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;package에 열고자하는 앱의 패키지 이름을,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;S.browser_fallback_url에서 앱이 설치않았을 때 대신 이동할 URL을 입력하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인텐트 스킴에 대한 더 자세한 내용은 아래 링크에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.nachocode.io/docs/guide/deep-link/intent-scheme&quot;&gt;https://developer.nachocode.io/docs/guide/deep-link/intent-scheme&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1762585597821&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;인텐트 스킴 (Intent Scheme) | nachocode Developer&quot; data-og-description=&quot;Android 플랫폼에서 제공하는 인텐트 스킴 (Intent Scheme)의 개념과 nachocode에서의 인텐트 스킴 활용법을 안내합니다.&quot; data-og-host=&quot;developer.nachocode.io&quot; data-og-source-url=&quot;https://developer.nachocode.io/docs/guide/deep-link/intent-scheme&quot; data-og-url=&quot;https://developer.nachocode.io/docs/guide/deep-link/intent-scheme&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/k6zTZ/hyZM9XAvkj/dmjR3bCyyTk5516kmVK0HK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/J9Rtk/hyZMwdVVv4/WwBA4YGmyCz89KqKqEfAc0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bJ3fpo/hyZMw54EYY/BLFsUP5yOpcgqYrMpuxLYK/img.png?width=2480&amp;amp;height=2306&amp;amp;face=0_0_2480_2306&quot;&gt;&lt;a href=&quot;https://developer.nachocode.io/docs/guide/deep-link/intent-scheme&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developer.nachocode.io/docs/guide/deep-link/intent-scheme&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/k6zTZ/hyZM9XAvkj/dmjR3bCyyTk5516kmVK0HK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/J9Rtk/hyZMwdVVv4/WwBA4YGmyCz89KqKqEfAc0/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/bJ3fpo/hyZMw54EYY/BLFsUP5yOpcgqYrMpuxLYK/img.png?width=2480&amp;amp;height=2306&amp;amp;face=0_0_2480_2306');&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;인텐트 스킴 (Intent Scheme) | nachocode Developer&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Android 플랫폼에서 제공하는 인텐트 스킴 (Intent Scheme)의 개념과 nachocode에서의 인텐트 스킴 활용법을 안내합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developer.nachocode.io&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 커스텀 앱 배너를 구현하면서 각 OS별 딥링크에 대해 익힐 수 있었다. 도메인별로 AASA 파일의 접근 여부를 제어해야 했기에 ssh에 접속해 nginx 코드를 계속 만졌는데, 실수로 서비스 접속이 아예 안되게 만들 수도 있었기에 꽤 긴장을 했던 거 같다. 코드 반영을 위해 nginx 서버를 재시동할 때마다 꽤 긴장을 했었다.&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;현재 앱 배너를 클릭할 때 iOS는 유니버설 링크, 안드로이드는 인텐트 스킴을 활용하여 앱/앱스토어 이동을 구현했다. 앱스토어 이동 로직을 통일시키기 위해 안드로이드도 앱 링크를 활용하도록 바꾸는게 더 좋았을까하는 생각도 드는데, 이건 안드로이드 개발자분과의 협의가 필요하니 좀 더 고민을 해보기로 했다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>AASA</category>
      <category>apple-app-site-association</category>
      <category>universal link</category>
      <category>딥링크</category>
      <category>앱링크</category>
      <category>유니버설 링크</category>
      <category>인텐트</category>
      <category>인텐트 스킴</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/192</guid>
      <comments>https://quickchabun.tistory.com/192#entry192comment</comments>
      <pubDate>Sat, 8 Nov 2025 16:07:40 +0900</pubDate>
    </item>
    <item>
      <title>Next.js + MySQL을 활용해서 개인 웹 사이트에 글 포스팅하기</title>
      <link>https://quickchabun.tistory.com/191</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개인 웹 사이트를 다시 디자인해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 웹 사이트에 접속하면 맨 처음에 내 사진과 자기소개가 뜬다. 그리고 프로젝트와 내가 좋아하는 노래들을 소개하는 페이지가 있다. 사이트를 둘러보다가, 문득 전부 갈아엎어야겠다는 다짐을 했다. 일단 무엇보다 처음에 들어가자마자 내 사진이 뜨는게 좀 부담스러웠고, 블로그에 올린 글을 개인 사이트에도 보여주고 싶었다. 그리고 데이터들을 HTML에 하드코딩해서 표시하지 않고, 데이터베이스에 저장해서 API를 통해서 보여주고 싶었다.&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를 활용해서 구현했다. 서버 컴포넌트와 SSG를 활용하긴 했지만 Next.js의 라우팅 기능을 활용한 data fetching은 활용하지 않았기 때문에, 이번 기회에 데이터베이스를 만들어서 data fetching을 해보기로 했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. MySQL DB를 AWS와 연결하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 MySQL과 MySQL WorkBench를 설치하고 SQL문을 입력해 사용법을 익혔다. 그리고 웹 사이트에서 데이터베이스를 접근해야 하니 AWS RDS에 MySQL DB 인스턴스를 생성했다.&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;그리고 MySQL WorkBench에서 AWS RDS 인스턴스에 접근을 할 수 있어야하므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS 홈페이지의 &amp;lsquo;Aurora and RDS&amp;rsquo; &amp;rarr; DB 인스턴스 클릭 &amp;rarr; 연결 및 보안에서 &amp;lsquo;VPC 보안 그룹&amp;rsquo; 클릭 (EC2 페이지의 보안그룹으로 접속) &amp;rarr; 보안 그룹 ID 클릭 &amp;rarr; 인바운드 규칙에서 &amp;lsquo;인바운드 규칙 편집&amp;rsquo;을 클릭 &amp;rarr; 소스 유형을 &amp;lsquo;내 IP&amp;rsquo;로 선택하고 규칙 저장 클릭의 과정을 진행해주었다.&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;장소가 바뀌고, 와이파이가 바뀌어 IP 주소가 바뀔 때마다 위 과정을 다시 해줘야 MySQL WorkBench에 접속이 가능했다. 이 과정이 조금 귀찮아서 그냥 소스를 0.0.0.0/0으로 설정하면 되지 않을까? 고민을 했었는데, 지인들한테 물어보니 그러면 해킹 당할 수 있는 위험성이 있다고 해서 그냥 위 과정을 반복해주었다.&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에서 먼저 lib/db.js를 생성해 mysql2 library를 활용하여 pool을 생성해주었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import mysql from 'mysql2/promise';

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  port: process.env.DB_PORT,
});

export default pool;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(당연히 환경변수를 활용해야한다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급했듯이 개인 웹 사이트에 글을 포스팅하고 싶었으므로 /post와 /post/:id에 해당하는 page.tsx를 생성해주었다. (Next.js 15를 활용하므로 App Routing 방식을 적용했다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 app/api/posts/route.ts를 생성해서 posts 테이블에서 데이터를 가져오고,&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import { NextRequest, NextResponse } from 'next/server';
import pool from '@/lib/db';

export async function GET(req: NextRequest) {
  try {
    const connection = await pool.getConnection();
    const [rows] = await connection.query('SELECT * FROM posts');
    connection.release();
    return NextResponse.json(rows);
  } catch (error) {
    console.error('DB 에러:', error);
    const errorMessage = error instanceof Error ? error.message : String(error);
    return NextResponse.json({ error: errorMessage }, { status: 500 });
  }
}
&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;app/api/posts/[id]/route.ts를 생성해서 해당 글을 클릭했을 때 해당 id에 해당하는 정보를 data fetching 하도록 했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import { NextRequest, NextResponse } from 'next/server';
import pool from '@/lib/db';
import { RowDataPacket } from 'mysql2/promise';

export async function GET(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  try {
    const connection = await pool.getConnection();
    const [rows] = await connection.query&amp;lt;RowDataPacket[]&amp;gt;(
      'SELECT * FROM posts WHERE id = ?',
      [params.id]
    );
    connection.release();
    
    if (rows.length === 0) {
      return NextResponse.json({ error: 'Post not found' }, { status: 404 });
    }
    
    return NextResponse.json(rows[0]);
  } catch (error) {
    console.error('DB 에러:', error);
    const errorMessage = error instanceof Error ? error.message : String(error);
    return NextResponse.json({ error: errorMessage }, { status: 500 });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 스크립트를 활용하여 .md 파일의 내용을 업로드하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 블로그에 올린 글들을 DB에 저장해보자. 나는 글을 티스토리에 올렸었는데, 안타깝게도 티스토리의 open API 서비스는 종료되었다. 다행히도 나는 글을 전부 노션에 작성하고 티스토리에 옮겨왔고, 지금까지 작성된 글들이 노션에 저장되어 있었다. 그러면 노션에서 md 파일을 export하고, 그 파일을 올리면 되겠구나.&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;그래서 프로젝트의 data/posts에 md 파일을 업로드하고, scripts/importPosts.cjs를 생성해 해당 스크립트를 통해 md 파일의 내용들을 DB에 업로드하기로 했다. (당연히 이 파일들은 git에서 Tracking하지 않는다.)&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;문제는 이미지였다. md 파일 안에는 이미지가 있었고, 이 이미지들은 로컬에 저장되어 있었으므로 클라이언트에서 이미지를 인식하지 못하는 문제가 생겼다. 결국 md 파일 안에 포함되어 있는 이미지들을 CDN에 올려서 링크로 변환해야 하는데, 글을 올릴 때마다 이러면 너무 귀찮았기 때문에 어떻게 해야 이 과정을 자동화할 수 있을지 고민하였고, 스크립트에 아래 내용을 추가해서 문자를 해결했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const mysql = require('mysql2/promise');
const dotenv = require('dotenv');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

dotenv.config({ path: '.env.local' });

const postsDir = path.join(__dirname, '../data/posts');

// UUID 간단 버전 생성 (처음 8자리)
function generateShortId() {
  return crypto.randomBytes(4).toString('hex');
}

(...)

// 마크다운에서 이미지 경로를 CloudFront URL로 변환
function transformImageUrls(mdContent, imageId) {
  let imageCount = 0;
  
  // ![alt](경로) 패턴 찾기
  return mdContent.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, () =&amp;gt; {
    imageCount++;
    const cloudFrontUrl = `https://(...).cloudfront.net/posts/${imageId}-${imageCount}.png`;
    return `![image ${imageCount}](${cloudFrontUrl})`;
  });
}

async function importPostsFromMarkdown() {
  try {
    const files = fs.readdirSync(postsDir).filter(f =&amp;gt; f.endsWith('.md'));

    for (const file of files) {
      const filePath = path.join(postsDir, file);
      const mdContent = fs.readFileSync(filePath, 'utf-8');
      
      // UUID 기반 이미지 ID 생성
      const imageId = generateShortId();
      
      (...)
      
      // 이미지 URL 변환 (UUID 기반)
      body = transformImageUrls(body, imageId);
      
      if (body.length &amp;gt; 0) {
        await insertPost(title, body);
        console.log(`   이미지 파일명: ${imageId}-1.png, ${imageId}-2.png 등`);
      }
    }
    
    (...)    
    console.log('파일명 형식: {UUID}-{순서번호}.png');
    console.log('   예: a1b2c3d4-1.png, a1b2c3d4-2.png');
  } catch (error) {
    console.error('❌ 임포트 실패:', error.message);
  }
}

importPostsFromMarkdown();
&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;먼저 스크립트가 시작되면 먼저 UUID 기반 랜덤 문자열을 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이미지를 발견하면 해당 이미지의 링크를 &amp;lsquo;나의 cloudfront의 링크/posts/랜덤 문자열-{이미지 인덱스}.png&amp;rsquo;로 생성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 변환이 완료하면 터미널에 어떤 랜덤 문자열이 생성되었는지 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 나는 해당 랜덤 문자열을 확인하고 이미지들을 s3에 랜덤 문자열-1.png, 랜덤 문자열-2.png 순으로 업로드한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 이미지를 s3에 업로드하는 과정이 필요하긴 했지만 꽤 간편하게 md 파일의 이미지들을 링크로 변환하고 매칭할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. created_at 기반 순서 인덱스는 백엔드에서 생성한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블로그에서 글을 옮길 때, 노션 파일에는 블로그 업로드 시간이 기록되어 있지 않으므로 일단 DB에 업로드하고 블로그에서 업로드 시간을 확인하고 created_at 칼럼을 직접 업데이트해주었다. 그러면 DB의 id는 데이터를 삽입한 순으로 증가하지만(auto_increment), 나는 created_at을 기준으로 정렬을 해주고 싶었다. 그런데 프론트엔드에서는 id를 기준으로 정렬하였기 때문에, 처음에는 SQL문을 통하여 id를 created_at을 기준으로 정렬해주었다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;// 1. 임시로 새 테이블 생성
CREATE TABLE posts_new LIKE posts;

// 2. 새 테이블에 기존 테이블 내용을 삽입하고 created_at 기준으로 정렬
INSERT INTO posts_new (title, body, tags, created_at, updated_at)
SELECT title, body, tags, created_at, updated_at
FROM posts
ORDER BY created_at ASC;

// 3. 기존 테이블을 삭제하고 새 테이블 이름을 기존 테이블로 변경
DROP TABLE posts;
RENAME TABLE posts_new TO posts;`
&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;id 속성을 created_at 정렬 기준으로 정렬할 수는 있을까? 방법을 찾아보니 별로 권장되는 방법은 아니었다. 그래서 Next.js에서 데이터를 불러올 때 created_at을 정렬해서 데이터를 불러왔다. 그리고 정렬된 데이터들에 새로운 인덱스를 부여해서 정렬을 해주었다.&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;이러면 생기는 문제가, 기존 글들을 /post/1처럼 id 기반으로 라우팅을 해주었는데, 우리는 새로 받아온 데이터들을 created_at을 기준으로 정렬해주었기 때문에 맨 밑에 있는 글의 id가 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;먼저, app/api/posts/route.ts의 data fetching SQL문에 created_at 기준 정렬 문구를 삽입하고, 백엔드에서 displayId라는 새로운 인덱스를 생성해준다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { NextRequest, NextResponse } from 'next/server';
import pool from '@/lib/db';
import { RowDataPacket } from 'mysql2/promise';

export async function GET(req: NextRequest) {
  try {
    const connection = await pool.getConnection();
    const [rows] = await connection.query&amp;lt;RowDataPacket[]&amp;gt;(
      'SELECT * FROM posts ORDER BY created_at DESC'
    );
    connection.release();
    
    // displayId 추가
    const postsWithDisplayId = (rows as any[]).map((post, index) =&amp;gt; ({
      ...post,
      displayId: rows.length - index
    }));
    
    return NextResponse.json(postsWithDisplayId);
  } catch (error) {
    console.error('DB 에러:', error);
    const errorMessage = error instanceof Error ? error.message : String(error);
    return NextResponse.json({ error: errorMessage }, { status: 500 });
  }
}
&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;그리고 app/api/posts/[id]/route.ts를 app/api/posts/[displayId]/route.ts로 변경하고, params도 id에서 displayId로 수정해준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import { NextRequest, NextResponse } from 'next/server';
import pool from '@/lib/db';
import { RowDataPacket } from 'mysql2/promise';

export async function GET(
  req: NextRequest,
  { params }: { params: { displayId: string } }
) {
  try {
    const connection = await pool.getConnection();
    const [rows] = await connection.query&amp;lt;RowDataPacket[]&amp;gt;(
      'SELECT * FROM posts ORDER BY created_at DESC'
    );
    connection.release();
    
    const displayId = parseInt(params.displayId);
    const postIndex = rows.length - displayId;
    
    if (postIndex &amp;lt; 0 || postIndex &amp;gt;= rows.length) {
      return NextResponse.json({ error: 'Post not found' }, { status: 404 });
    }
    
    return NextResponse.json(rows[postIndex]);
  } catch (error) {
    console.error('DB 에러:', error);
    const errorMessage = error instanceof Error ? error.message : String(error);
    return NextResponse.json({ error: errorMessage }, { status: 500 });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 page도 [id]/page.tsx에서 [displayId]/page.tsx로 변경하고, 표시되는 숫자도 id에서 displayId로 수정해주면 된다.&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;이렇게 created_at을 기준으로 글들을 정렬해서 표시할 수 있었고, 정렬 후 백엔드에서 생성한 인덱스를 기준으로 라우팅을 구현할 수 있었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 그런데 배포에 실패했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글을 다쓰고 DB에 위 내용을 업로드했다. 그리고 vercel에 환경변수를 넣고 배포를 했는데, 이상하게 /post로 넘어가지 못하고 에러가 발생했다. 생각해보니 배포 후 RDS 인바운드 규칙에 ip 주소를 업데이트하지 않았다. vercel은 어떤 ip 주소를 가지고 있을까? 그런데 찾아보니 vercel은 서버리스라서 유동적인 ip 주소를 가지고 있었다. 즉, vercel의 고정 ip 주소를 넣어서 RDS와 연결할수 없다!&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;vercel 유료 버전을 결제해서 고정 ip 주소를 부여받거나,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Lambda 함수를 생성해서 API Gateway 엔드포인트 만들면 배포가 가능하다고 한다. (복잡)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;planetscale로 마이그레이션을 하는 방법도 추천받아서 회원가입까지 했는데 무료 버전이 없어서 한탄을 하기도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아니면 vercel이 아닌 다른 배포 방법을 생각해봐도 좋을 것 같다. 어떻게 해야할지 더 고민해보자.&lt;/p&gt;</description>
      <category>Project/crohasang_page</category>
      <category>mysql</category>
      <category>next.js</category>
      <category>rds</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/191</guid>
      <comments>https://quickchabun.tistory.com/191#entry191comment</comments>
      <pubDate>Sun, 26 Oct 2025 23:16:45 +0900</pubDate>
    </item>
    <item>
      <title>모노레포 'common' 프로젝트의 이전 코드가 반영되는 오류 수정 후기</title>
      <link>https://quickchabun.tistory.com/190</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 분명히 코드를 업데이트했는데&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 작업 중인 리액트 프로젝트는 pnpm의 Workspace 기능을 활용한 모노레포로 구성되어 있다. 리액트 프로젝트에서 pnpm build 명령어를 입력하면 php 프로젝트로 빌드된 js, css 파일이 올라가는 구조이다.&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;리액트 프로젝트에는 common 디렉토리가 있는데, 타입스크립트 코드를 트랜스파일링 해서 모듈로서 다른 프로젝트가 공용으로 사용할수 있도록 작동한다. 따라서 공용 모듈을 작성하고 싶다면 common 에 가서 작성후 export 한후 해당 디렉토리 내부에서 pnpm build 명령어를 실행하면 된다.&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;(프로젝트 루트의 package.json에는 다음과 같이 명령어가 설정되어 있었다.)&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;@something/root&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;build:common&quot;: &quot;pnpm --filter @something/common build&quot;,
    &quot;start:desktop-one&quot;: &quot;pnpm --filter @something/desktop-one start&quot;,
    &quot;build:desktop-one&quot;: &quot;pnpm --filter @something/desktop-one build&quot;,
    &quot;start:desktop-two&quot;: &quot;pnpm --filter @something/desktop-two start&quot;,
    &quot;build:desktop-two&quot;: &quot;pnpm --filter @something/desktop-two build&quot;,
    ...
    &quot;build:full&quot;: &quot;pnpm -F @something/common -F @something/desktop-one -F (...) build&quot;
  },
  &quot;workspaces&quot;: [
    &quot;packages/*&quot;
  ],

(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 발생 경위&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;common 프로젝트의 코드를 업데이트하고 pnpm run build:full 명령어를 입력하여 다른 프로젝트의 코드들에 업데이트된 common 프로젝트의 코드가 반영되게 하였다. (브랜치 A에서 작업)&lt;/li&gt;
&lt;li&gt;그리고 common 프로젝트의 코드가 업데이트 되기 전의 브랜치 B로 바꿔서 다른 작업을 실행했다.&lt;/li&gt;
&lt;li&gt;다시 브랜치 A로 돌아와서, common이 아닌 프로젝트 &amp;lsquo;ㄱ&amp;rsquo;에서 작업 후 프로젝트 &amp;lsquo;ㄱ&amp;rsquo;을 빌드했다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 확인을 해보니 common에 1번 이전 코드가 반영되어 있었다. 왜 갑자기 common의 이전 코드가 반영이 된거지싶어서 롤백을 하면서 확인해보니 프로젝트 &amp;lsquo;ㄱ&amp;rsquo;을 빌드하고 나서부터 common 프로젝트의 옛 버전의 코드가 반영이 되었다. 왜 갑자기 이런 문제가 생긴 것일까? 브라우저 캐싱 문제인가? 문제를 재현해보기 위해서 새로 브랜치를 파서 1번 시점으로 롤백한다음, 바로 프로젝트 &amp;lsquo;ㄱ&amp;rsquo;만 빌드를 해보았다. 하지만 이 때에는 common 프로젝트의 최신 코드가 제대로 반영이 되었다. 그러면 무엇이 문제였을까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 원인: dist는 깃에서 tracking하지 않는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 프로젝트 &amp;lsquo;ㄱ&amp;rsquo;을 빌드하기 전에 브랜치(common 프로젝트 코드를 업데이트 하기 전)를 바꿔서 작업을 했었다. 그리고 확실하지는 않지만, 그 때 full 빌드를 해서 common 프로젝트의 코드들이 빌드되었을 것이다. 그래서 dist 폴더에 이전 코드들이 저장이 되었을 것이다.&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;그리고 나는 브랜치를 다시 바꾸었다. dist에 있는 코드들은 깃에서 무시되기 때문에 브랜치를 바꾸었음에도 변경되지 않고 그대로 유지된다. 그리고 나는 프로젝트 &amp;lsquo;ㄱ&amp;rsquo;을 빌드했다. 이 때, dist에 있는 common 이전 코드들이 프로젝트 &amp;lsquo;ㄱ&amp;rsquo;에 적용되는 부작용이 발생한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고: common 코드가 프로젝트 &amp;lsquo;ㄱ&amp;rsquo;에 반영되는 과정 요약&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Workspace 의존성 선언&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;// packages/'ㄱ'/package.json
&quot;dependencies&quot;: {
  &quot;@something/common&quot;: &quot;workspace:^0.0.0&quot;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;workspace: 프로토콜로 로컬 패키지 사용 선언&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;2. pnpm의 심볼릭 링크 생성&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;node_modules/@something/common &amp;rarr; ../../common

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pnpm install 시 심볼릭 링크 자동 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간으로 packages/common 디렉토리 참조&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;3. Common 패키지의 진입점&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// packages/common/package.json
{
  &quot;main&quot;: &quot;dist/index.js&quot;,
  &quot;module&quot;: &quot;dist/index.js&quot;,
  &quot;typings&quot;: &quot;dist/index.d.ts&quot;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;import 시 항상 dist/ 폴더의 빌드된 파일 참조&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;4. 빌드 프로세스&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// 'ㄱ'/src/App.tsx
import TopNav from '@something/common/dist/components/common/desktop/TopNav';

&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Webpack이 @something/common import 발견&lt;/li&gt;
&lt;li&gt;node_modules/@something/common 심볼릭 링크 추적&lt;/li&gt;
&lt;li&gt;packages/common 디렉토리 도달&lt;/li&gt;
&lt;li&gt;package.json의 main 필드 확인 &amp;rarr; dist/index.js&lt;/li&gt;
&lt;li&gt;packages/common/dist/ 폴더의 파일 읽기&lt;/li&gt;
&lt;li&gt;해당 코드를 &amp;lsquo;ㄱ&amp;rsquo; 번들에 포함&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 해결책: 빌드 시 항상 common 프로젝트를 먼저 빌드한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하자면, 다른 브랜치에서 작업된 common 코드가 dist/에 반영되는데, 브랜치를 바꿀 때 이 내용들이 바뀌지 않고 그대로 따라와서 생기는 문제이다. 브랜치를 바꾸었을 때 common 프로젝트 (&amp;rsquo;src/&amp;rsquo;에 존재하는) 코드들은 깃에 tracking되기 때문에 common 프로젝트를 빌드한다면 dist/의 코드들이 최신 코드들로 바뀌게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 루트의 package.json 스크립트를 수정하자&lt;/p&gt;
&lt;pre class=&quot;perl&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;@something/root&quot;,
  &quot;private&quot;: true,
  &quot;scripts&quot;: {
    &quot;build:common&quot;: &quot;pnpm --filter @something/common build&quot;,
    &quot;start:desktop-one&quot;: &quot;pnpm --filter @something/desktop-one start&quot;,
    &quot;build:desktop-one&quot;: &quot;pnpm -F @something/common build &amp;amp;&amp;amp; pnpm -F @something/desktop-one build&quot;,
    &quot;start:desktop-two&quot;: &quot;pnpm --filter @something/desktop-two start&quot;,
    &quot;build:desktop-one&quot;: &quot;pnpm -F @something/common build &amp;amp;&amp;amp; pnpm -F @something/desktop-two build&quot;,
    ...
    &quot;build:full&quot;: &quot;pnpm -F @something/common build &amp;amp;&amp;amp; pnpm -F @something/desktop-one -F (...)&quot;
  },
  &quot;workspaces&quot;: [
    &quot;packages/*&quot;
  ],

(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 프로젝트를 빌드할 때마다 무조건 common 프로젝트를 빌드해서 dist/ 코드의 최신성을 보장했다. 이를 위해 script 앞에 &amp;ldquo;pnpm -F @something/common&amp;rdquo;을 입력하였고, common 프로젝트 빌드가 다 끝난 뒤에 다른 프로젝트 빌드를 시작해주기 위해 &amp;ldquo;&amp;amp;&amp;amp;&amp;rdquo;로 연결해주었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제가 발생했을 때, 처음에 왜 문제가 발생했는지 파악하지 못해서 꽤 애를 먹었었다. 처음에는 브라우저 캐싱이 원인이지 않을까 추측했기 때문에 엉뚱한 방향으로 디버깅을 했었다. 이번에 삽질을 하면서 common 프로젝트가 다른 프로젝트에 어떻게 반영되는지 과정을 살펴보며 이해할 수 있게 되었다. 이번 디버깅을 통해 앞으로 common dist/ 코드들의 최신성을 보장할 수 있게 되어서 다행이라는 생각이든다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>monorepo</category>
      <category>모노레포</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/190</guid>
      <comments>https://quickchabun.tistory.com/190#entry190comment</comments>
      <pubDate>Sat, 18 Oct 2025 18:03:28 +0900</pubDate>
    </item>
    <item>
      <title>[&amp;lsquo;NestJS로 배우는 백엔드 프로그래밍&amp;rsquo; 정리] Pipe, Middleware, Guard, Interceptor</title>
      <link>https://quickchabun.tistory.com/189</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;저번에는 NestJS의 Controller, Provider, Module에 대해서 정리했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://quickchabun.tistory.com/188&quot;&gt;https://quickchabun.tistory.com/188&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1760001648615&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[&amp;lsquo;NestJS로 배우는 백엔드 프로그래밍&amp;rsquo; 정리] Controller, Provider, Module&quot; data-og-description=&quot;사실 백엔드를 공부하고자 하는 의지는 예전부터 있었다. 그래서 백엔드 강의도 구매해서 70% 넘게 들었었는데, 다른 일과 겹쳐서 완강을 못했던 기억이 난다. 그리고 다가온 추석 연휴, 교보문&quot; data-og-host=&quot;quickchabun.tistory.com&quot; data-og-source-url=&quot;https://quickchabun.tistory.com/188&quot; data-og-url=&quot;https://quickchabun.tistory.com/188&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Poqbx/hyZKOdPDrQ/lbqCpUjstKK3pq986uhZtk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cUJK6b/hyZKfQX4Ge/ROigOCsw4ni8d5RTFq2k8k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://quickchabun.tistory.com/188&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://quickchabun.tistory.com/188&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Poqbx/hyZKOdPDrQ/lbqCpUjstKK3pq986uhZtk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/cUJK6b/hyZKfQX4Ge/ROigOCsw4ni8d5RTFq2k8k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&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;[&amp;lsquo;NestJS로 배우는 백엔드 프로그래밍&amp;rsquo; 정리] Controller, Provider, Module&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;사실 백엔드를 공부하고자 하는 의지는 예전부터 있었다. 그래서 백엔드 강의도 구매해서 70% 넘게 들었었는데, 다른 일과 겹쳐서 완강을 못했던 기억이 난다. 그리고 다가온 추석 연휴, 교보문&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;quickchabun.tistory.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;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 요청이 제대로 전달되었는지 유효성 검사를 하는 파이프, 요청 처리 전에 부가 기능을 수행하는 미들웨어, 권환 확인을 위한 가드, 요청과 응답을 수정하는하는 인터셉터, 예외를 처리하는 예외 필터, 그리고 이 요소들의 생명 주기에 대해 정리해보려 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(한용재님의 책 &amp;lsquo;NestJS로 배우는 백엔드 프로그래밍&amp;rsquo;을 읽고 정리한 글입니다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Pipe&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프는 요청이 라우트 핸들러로 전달되기 전에 요청 객체를 변환할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(route handler: 사용자의 요청을 처리하는 엔드포인트마다 동작을 수행하는 컴포넌트)&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;ParseIntPipe, ParseBoolPipe, ParseArrayPipe, ParseUUIDPipe: 전달된 인수 타입 검사 용도&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;@Param 데코레이터의 두 번째 인수로 파이프를 넘겨 현재 실행 컨텍스트에 바인딩할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
	return this.users.Service.findOne(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;파이프 객체를 직접 생성하여 전달도 가능하다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Get(':id')
findOne(@Param('id', new ParseIntPipe({ errorHTTPStatusCode: 
HttpStatus.NOT_ACCEPTABLE})) id: number) {
	return this.usersService.findOne(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;DefaultValuePipe: 인수의 값에 기본값 설정 시 사용&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Get()
findAll(
	@Query('offset' , new DefaultValuePipe(0), ParseIntPipe) offset: number,
	@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
} {
	console.log(offset, limit);
	
	(...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 파이프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PipeTransform 인터페이스를 상속받은 클래스에 @Injectable 데코레이터를 붙여주면 된다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;@Injectable()
export class ValidationPipe implements PipeTransform {
	transform(value: any, metadata: ArgumentMetadata) {
		console.log(metadata);
		return value;
	}
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PipeTransform과 ArgumentMetadata에 대해서 더 살펴보자&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// value: 현재 파이프에 전달된 인수, metadata: 현재 파이프에 전달된 인수의 메타데이터
export interface PipeTransform&amp;lt;T = any, R = any&amp;gt; {
	transform(value : T, metadata: ArgumentMetadata): R;
}

// type: 파이프에 전달된 인수가 본문인지, 쿼리 매개변수인지, 경로 매개변수인지, 커스텀 매개변수인지
// metatype: 라우트 핸들러에 정의된 인수의 타입
// data: 데코레이터에 전달된 문자열 (매개변수의 이름)

export interface ArgumentMetadata {
	readonly type: Paramtype;
	readonly metatype?: Type&amp;lt;any&amp;gt; | undefined;
	readonly data?: string | undefined;
}

export declare type Paramtype = 'body' | 'query' | 'param' | 'custom';

&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;class-validator, class-transformer&lt;/b&gt; 라이브러리를 활용하여 유효성 검사 파이프를 만들 수 있다. (joi보다 간편)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import { IsString, MinLength, MaxLenght, IsEmail } from 'class-validator';

export class CreateUserDto {
	@IsString()
	@MinLength(1)
	@MaxLength(20)
	name: string;
	
	@IsEmail()
	email: string;
}
	
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 dto 객체를 받아서 유효성 검사를 하는 파이프(ValidataionPipe)를 구현한다면&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Injectable()
export class ValidationPipe implements PipeTransform&amp;lt;any&amp;gt; {
	async transform(value: any, { metatype }: ArgumentMetadata) {
		if (!metatype || !this.toValidate(metatype)) {
			return value;
		}
		
		// class-transformer의 plainToClass 함수 -&amp;gt; 순수 자바스크립트 객체를 클래스 객체로 변환
		const object = plainToClass(metatype, value);
		const errors = await validate(object);
		if (errors.length &amp;gt; 0) {
			throw new BadRequestException('Validation  failed');
		}
		return value;
	}
	
	private toValidate(metatype: Function): boolean {
		const types: Function[] = [String, Boolean, Number, Array, Object];
		return !types.includes(metatype);
	}
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 ValidationPipe를 적용한다면&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Post()
create(@Body(ValidationPipe) createUserDto: CreateUserDto) {
	return this.usersService.create(createUserDto);
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역으로 설정하려면 부트스트랩 과정에서 적용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;async function bootstrap() {
	const app = await NestFactory.create(AppModule);
	app.useGlobalPipes(new ValidationPIpe())
	await app.listen(3000);
}
bootstrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Middleware&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;Nest의 미들웨어 = Express의 미들웨어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 형태의 코드라도 수행 가능&lt;/li&gt;
&lt;li&gt;요청과 응답에 변형을 가할 수 있음&lt;/li&gt;
&lt;li&gt;요청/응답 주기를 끝낼 수 있음&lt;/li&gt;
&lt;li&gt;여러 개의 미들웨어를 사용한다면 next()로 호출 스택 상 다음 미들웨어에 제어권 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미들웨어를 활용하여 쿠키 파싱, 세션 관리, 인증/인가, 본문 파싱을 할 수 있다. (단, 인가는 가드 사용 권장)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest에서는 미들웨어를 함수로 작성하거나 NestMiddleware 인터페이스를 구현한 클래스로 작성 가능&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
	use(req: Request, res: Response, next: NextFunction) {
		console.log('Request');
		next();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미들웨어를 모듈에 포함시키려면 NestModule 인터페이스를 구현해야한다. NestModule에 선언된 configure 함수를 통해 미들웨어를 설정한다.&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;app.module.ts에서&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;@Module({
	imports: [UsersModule],
})
export class AppModule implements NestModule {
	configure(consumer: MiddlewareConsumer): any {
		consumer
			.apply(LoggerMiddleware)
			.forRoutes('/users');
		
	}
}
	
&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;만약 미들웨어 2개를 적용한다면 apply에 콤마로 나열하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; .apply(LoggerMiddleware, Logger2Middleware)&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;&amp;rarr; app.use(logger);&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Guard&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 가드를 활용해 인가를 구현하자&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;가드는 CanActivate 인터페이스를 구현해야&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
	canActivate(
		context: ExecutionContext,
	): boolean | Promise&amp;lt;boolean&amp;gt; | Observable&amp;lt;boolean&amp;gt; {
		const request = context.switchToHttp().getRequest();
		return this.validateRequest(request);
	}
	
	private validateRequest(request: any) {
		return 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;canActivate 함수는 ExecutionContext 인스턴스를 인수로 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExecutionContext는 요청과 응답에 대한 정보를 가지고 있는 ArgumentHost를 상속 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; HTTP로 기능을 제공하고 있으므로 switchToHttp() 함수를 활용하여 원하는 정보를 가져올 수 있다.&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;가드를 사용하려면 @UseGuards(AuthGuard)와 같이 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@UseGuards(AuthGuard)
@Controller()
export class AppController {
	constructor(private readonly appService: AppService) { }
	
	@UseGuards(AuthGuard)
	@Get()
	getHello(): string {
		return this.appService.getHello();
	}
}

// 전역으로 적용하려면 부트스트랩 과정에서 useGlobalGuards 적용
async function bootstrap() {
	const app = await NestFactory.create(AppModule);
	app.useGlobalGuards(new AuthGuard());
	await app.listen(3000);
}
bootstrap();
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가드에 종속성 주입을 사용해서 다른 프로바이더를 주입해서 사용하고 싶다면 커스텀 프로바이더로 선언해야 한다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
	providers: [
		{
			provide: APP_GUARD,
			useClass: AuthGuard,
		},
	],
})
export class AppModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회원 가입&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원 가입 요청에 포함된 이메일에 다음 링크가 포함되어 있다고 가정하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;const url = ${baseUrl}/users/email-varify?signupVerifyToken=${signupVerifyToken};&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;async verifyEmail(signupVerifyToken: string): Promise&amp;lt;string&amp;gt; {
	const user = await this.usersRepository.findOne({
		where: { signupVerifyToken }
	});
	
	if (!user) {
		throw new NotFoundException('User is no exist');
	}
	
	return this.authService.login({
		id: user.id,
		name: user.name,
		email: user.email,
	});
}
		
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthService에서 로그인 처리를 하고, 응답으로 JWT 토큰을 생성해서 돌려준다&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import * as jwt from 'jsonwebtoken';
import { Inject, Injectable } from '@nestjs/common';
import authConfig from 'src/config/authConfig';
import { ConfigType } from '@nestjs/config';

interface User {
	id: string;
	name: string;
	email: string;
}

@Injectable()
export class AuthService {
	constructor(
		@Inject(authConfig.KEY) private config: ConfigType&amp;lt;typeof authConfig&amp;gt;,
	) { }
	
	login(user: User) {
		const payload = { ...user };
		
		return jwt.sign(payload, this.config.jwtSecret, {
			expiresIn: '1d',
			audience: 'example.com',
			issuer: 'example.com',
		});
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일과 패스워드로 유저를 찾고, 유저가 있다면 JWT를 발급하면 된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;async login(email: string, password: string): Promise&amp;lt;string&amp;gt; {
	const user = await this.usersRepository.findOne({
		where: { email, password }
	});
	
	if (!user) {
		throw new NotFoundException('User is not Exist');
	}
	
	return this.authService.login({
		id: user.id,
		name: user.name,
		email: user.email,
	});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT 인증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인한 유저 본인의 정보를 조회하는 API를 만들어보자. (GET /users/:id, Authorization: Bearer &amp;lt;token&amp;gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bearer 방식 인증을 사용하기 위해 헤더에 키를 Authorization, 값을 Bearer &amp;lt;token&amp;gt;으로 구성한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt; import { Headers } from '@nestjs/common';
 
 @Controller('users')
 export class UsersController {
	 constructor(
		 private usersService: UsersService,
		 private authService: AuthService,
	) { }
	
	(...)
	
	@Get(':id')
	async getUserInfo(@Headers() headers: any, @Param('id') userId: string): 
	Promise&amp;lt;UserInfo&amp;gt; {
		const jwtString = headers.authorization.split('Bearer ')[1];
		this.authService.verify(jwtString);
		return this.usersService.getUserInfo(userId)
	}
}
		
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthService에서는 JWT 토큰을 검증하는 로직을 작성한다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;export class AuthService {
	
	(...)
	verify(jwtString: string) {
		try {
			const payload = jwt.verify(jwtString, this.config.jwtSecret) as 
			(jwt.JwtPayload | string) &amp;amp; User;
			
			const { id, email } = payload;
			
			return {
				userId: id,
				email,
			}
			
		} catch (e) {
			throw new UnauthorizedException()
		}
	}
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UsersService에서는 데이터베이스에서 정보를 가져온다.&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;export class UsersService {
	
	(...)
	async getUserInfo(userId: string): Promise&amp;lt;UserInfo&amp;gt; {
		const user = await this.usersRepository.findOne({
			where: { id: userId }
		});
		
		if (!user) {
			throw new NotFoundException('User is not exist');
		}
		
		return {
			id: user.id,
			name: user.name,
			email: user.email,
		};
	}
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구현 방식은 헤더에 포함된 JWT 토큰의 유효성을 검사하는 로직을 모든 엔드포인트에 중복 구현해야 하는데, 비효율적이고 DRY 원칙에도 위배된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 가드를 활용하여 핸들러 코드에서 분리하자&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Injectable()
export class AuthGuard implements CanActivate {
	constructor(private authService: AuthService) { }
	
	canActivate(
		context: ExecutionContext,
	): boolean | Promise&amp;lt;boolean&amp;gt; | Observable&amp;lt;boolean&amp;gt; {
		const request = context.switchToHttp().getRequest();
		return this.validateRequest(request);
	}
	
	private validateRequest(request: Request) {
		const jwtString = request.headers.authorization.split('Bearer ')[1];
		this.authService.verify(jwtString);
		return 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 회원 조회 엔드포인트에만 AuthGuard를 적용해보자&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@UseGuards(AuthGuard)
@Get(':id')
async getUserInfo(@Headers() headers: any, @Param('id') userId: string):
Promsie&amp;lt;UserInfo&amp;gt; {
	return this.usersService.getUserInfo(userId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Interceptor&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청과 응답을 가로채서 변형을 가할 수 있는 컴포넌트&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메서드 실행 전/후 추가 로직을 바인딩&lt;/li&gt;
&lt;li&gt;함수에서 반환된 결과를 변환&lt;/li&gt;
&lt;li&gt;함수에서 던져진 예외를 변환&lt;/li&gt;
&lt;li&gt;기본 기능의 동작을 확장&lt;/li&gt;
&lt;li&gt;특정 조건에 따라 기능을 완전히 재정의(캐싱 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;(또한 미들웨어는 여러개의 미들웨어 조합도 가능)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;export interface Response&amp;lt;T&amp;gt; {
	data: T;
}

@Injectable()
export class TransformInterceptor&amp;lt;T&amp;gt; implements NestInterceptor&amp;lt;T, Response&amp;lt;T&amp;gt;&amp;gt; {
	intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;Response&amp;lt;T&amp;gt;&amp;gt; {
		return next
			.handle()
			.pipe(map(data =&amp;gt; {
				return { data }
			}));
	}
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestInterceptor 구조&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export interface NestInterceptor&amp;lt;T = any, R = any&amp;gt; {
	intercept(context: ExecutionContext, next: CallHandler&amp;lt;T&amp;gt;): Observable&amp;lt;R&amp;gt; |
Promise&amp;lt;Observable&amp;lt;R&amp;gt;&amp;gt;;
}

export interface CallHandler&amp;lt;T = any&amp;gt; {
	handle(): Observable&amp;lt;T&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;두 번째 인수인 CallHandler는 handle() 메서드를 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 라우트 핸들러에서 전달된 응답 스트림을 돌려주고 RxJS의 Observable로 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; handle()을 호출하고 Observable을 수신한 후 응답 스트림에 추가 작업을 수행할 수 있다.&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;예시로 LoggingInterceptor를 활용하여 요청과 응답을 로그로 남긴다면&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;@Injectable()
export class LoggingInterceptor implements NestInterceptor {
	constructor(private logger: Logger) { }
	
	intercept(context: ExecutionContext, next: CallHandler): Observable&amp;lt;any&amp;gt; {
		const { method, url, body } = context.getArgByIndex(0);
		this.logger.log(`Request to ${method} ${url}`);
		
		return next
			.handle()
			.pipe(
				tap(data =&amp;gt; this.logger.log(`Response from ${method} ${url} \\n
				response: ${JSON.stringify(data)}`))
			);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 예외 필터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest에서 제공하는 전역 예외 필터 외에 직접 예외 필터 레이어를 둬서 원하는 대로 예외를 다룰 수 있다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
	catch(exception: Error, host: ArgumentsHost) {
		const ctx = host.switchToHttp();
		const res = ctx.getResponse&amp;lt;Response&amp;gt;();
		const req = ctx.getRequest&amp;lt;Request&amp;gt;();
		
		// HttpException이 아닌 예외는 InternalServerErrorException 처리
		if(!(exception instance HttpException)) {
			exception = new InternalServerErrorException();
		}
		
		const response = (exception as HttpException).getResponse();
		
		const log = {
			timestamp: new Date(),
			url: req.url,
			response,
		}
		
		console.log(log);
		
		res
		.status((exception as HttpException).getStatus())
		.json(response);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외 필터는 @UseFilter 데코레이터로 컨트롤러에 직접 적용하거나 전역으로 적용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(예외 필터는 전역 필터를 하나만 가지도록 하는 것이 일반적)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부트스트랩 과정에서 전역 필터를 적용하는 방식은 필터에 의존성을 주입할 수 없다는 제약이 있다 (예외 필터의 수행이 예외가 발생한 모듈 외부에서 이루어지기 때문)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 의존성 주입을 받으려면 예외 필터를 커스텀 프로바이더로 등록하자&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';

@Module({
	providers: [
		{
			provide: APP_FILTER,
			useClass: HttpExceptionFilter,
		},
	],
})
export class AppModule {}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 HttpExceptionFilter는 다른 프로바이더를 주입받아 사용 가능&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export class HttpExceptionFilter implements ExceptionFilter {
	constructor(private logger: Logger) {}

	(...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 요청 생명주기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;미들웨어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역으로 바인딩된 미들웨어 실행, 이후에는 모듈에 바인딩되는 순서대로 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(다른 모듈에 바인딩되어 있는 미들웨어들이 있으면 먼저 루트 모듈에 바인딩된 미들웨어 실행 후 imports에 정의된 순서대로 실행)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역으로 바인딩된 가드 시작 &amp;rarr; 컨트롤러에 정의된 순서대로 실행&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인터셉터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터셉터는 RxJS의 Observable 객체를 반환하는데 요청의 실행 순서와 반대 순서로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청: 전역 &amp;rarr; 컨트롤러 &amp;rarr; 라우터 순&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답: 라우터 &amp;rarr; 컨트롤러 &amp;rarr; 전역 순&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파이프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프가 여러 레벨에 적용되어 있다면 순서대로 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이프가 적용된 라우터의 매개변수가 여러 개 있을 때는 정의한 순서의 역순으로 적용&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예외 필터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우터 &amp;rarr; 컨트롤러 &amp;rarr; 전역으로 바인딩된 순서대로 동작&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서를 정리하자면,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;요청&lt;/li&gt;
&lt;li&gt;미들웨어(전역 미들웨어 &amp;rarr; 모듈 미들웨어)&lt;/li&gt;
&lt;li&gt;가드(전역 가드 &amp;rarr; 컨트롤러 가드 &amp;rarr; 라우터 가드)&lt;/li&gt;
&lt;li&gt;인터셉터(전역 인터셉터 &amp;rarr; 컨트롤러 인터셉터 &amp;rarr; 라우터 인터셉터)&lt;/li&gt;
&lt;li&gt;파이프(전역 파이프 &amp;rarr; 컨트롤러 파이프 &amp;rarr; 라우터 파이프)&lt;/li&gt;
&lt;li&gt;컨트롤러&lt;/li&gt;
&lt;li&gt;인터셉터(전역 인터셉터 &amp;rarr; 컨트롤러 인터셉터 &amp;rarr; 라우터 인터셉터)&lt;/li&gt;
&lt;li&gt;예외 필터(전역 인터셉터 &amp;rarr; 컨트롤러 인터셉터 &amp;rarr; 라우터 인터셉터)&lt;/li&gt;
&lt;li&gt;응답&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>Study/NestJS</category>
      <category>nestjs</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/189</guid>
      <comments>https://quickchabun.tistory.com/189#entry189comment</comments>
      <pubDate>Thu, 9 Oct 2025 18:21:00 +0900</pubDate>
    </item>
    <item>
      <title>[&amp;lsquo;NestJS로 배우는 백엔드 프로그래밍&amp;rsquo; 정리] Controller, Provider, Module</title>
      <link>https://quickchabun.tistory.com/188</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사실 백엔드를 공부하고자 하는 의지는 예전부터 있었다. 그래서 백엔드 강의도 구매해서 70% 넘게 들었었는데, 다른 일과 겹쳐서 완강을 못했던 기억이 난다. 그리고 다가온 추석 연휴, 교보문고의 개발자 코너에서 무슨 책을 읽을까 고민하다가 &amp;lsquo;&lt;b&gt;NestJS로 배우는 백엔드 프로그래밍&lt;/b&gt;&amp;rsquo; 책이 눈에 들어왔다. 이번 연휴 때 이 책 하나만 다 읽고 정리해도 보람찬 연휴를 보낸 거라 자부할 수 있지 않을까. 그런 생각이 들어 책을 구매하고 본가에 내려갔다. 그리고 며칠에 걸쳐 틈틈이 책을 읽었고 머지않아 책의 끝 페이지까지 다다를 수 있었다. 이제 읽은 내용을 정리해보며 배운 내용을 복기해보려한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Node.js 특징&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest는 Node.js를 기반으로 하는데, Node.js는 단일 스레드에서 구동되는 논블로킹 I/O 이벤트 기반 비동기 방식이다. 이 방식은 서버의 자원에 크게 부하를 가하지 않고, 하나의 스레드로 동작하는 것처럼 (비동기 I/O 라이브러리 libuv가 스레드 풀을 관리) 코드를 작성할 수 있다는 장점이 있다. 하지만 컴파일러 언어의 처리 속도에 비해 성능이 떨어지고, 하나의 스레드에 문제가 생기면 애플리케이션 전체가 오류를 일으킬 위험이 있다는 단점도 존재한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 루프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS가 단일 스레드 기반임에도 논블로킹 I/O 작업을 수행할 수 있도록 해주는 기능&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;rarr; 대기 콜백 단계 &amp;rarr; 유휴 준비 단계 &amp;rarr; 폴 단계 &amp;rarr; 체크 단계 &amp;rarr; 종료 콜백 단계&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 데코레이터&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;데코레이터에는 여러 종류가 있는데,&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;rarr; 클래스의 생성자에 적용되어 클래스를 읽거나 수정&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;@reportableClassDecorator
class ButReport {
	type = &quot;report&quot;;
	title: string;
	
	(...)
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 데코레이터 &amp;rarr; 메서드의 속성 설명자에 적용되고 메서드의 정의를 읽거나 수정&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;class Greeter {
	@HandleError()
	hello() {
		throw new Error('error occurred');
	}
	
	(...)
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근자 데코레이터 &amp;rarr; 접근자의 속성 설명자 적용되고 접근자의 정의를 읽거나 수정&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Person {
	constructor(private name: string) {}
	
	@Enumerable(true)
	get getName() {
		return this.name;
	}
	
	(...)
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 데코레이터 &amp;rarr; 속성의 정의를 읽음&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;function format(formatString: string) {
	return function (target: any, propertyKey: string): any {
		let value = target[propertyKey];
		
		function getter() {
			return `${formatString} ${value}`;
			
			(...)
		}
		
		
		(...)
	
class Greeter {
	@format('Hello')
	greeting: string;
}
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수 데코레이터 &amp;rarr; 매개변수의 정의를 읽음&lt;/p&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;function MinLength(min: numebr) {
	return function (target: any, propertyKey: string, parameterIndex: number) {
		target.validators = {
			minLength: function (args: string[]) {
				return args[parameterIndex].length &amp;gt;= min;
			}
		}
	}
}

(...)

class User {
	private name: string;
	
	@Validate
	setName(@MinLength(3) name: string) {
		this.name = name;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Controller&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;들어오는 요청을 받고 처리된 결과를 응답으로 돌려주는 인터페이스 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;nest g controller [name]&amp;rsquo; 명령어로 컨트롤러를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(CRUD 보일러플레이트 코드를 한 번에 생성하려면 &amp;lsquo;nest g resource [name]&amp;rsquo; 입력)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅 경로는 @Get 데커레이터의 인수로 관리 가능한데, @Controller 데코레이터에도 인수를 전달해서 라우팅 경로의 접두어(prefix)를 지정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Controller('app')
export class AppController {
	constructor(private readonly appService: AppService) {}
	
	@Get('/hello')
	getHello(): string {
		return this.appService.getHello();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드가 나오면, &lt;a href=&quot;http://localhost:3000/app/hello&quot;&gt;http://localhost:3000/app/hello&lt;/a&gt; 경로로 접근해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(라우팅 패스는 와일드 카드를 이용하여 작성 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; @Get(&amp;rsquo;he*llo&amp;rsquo;)라면 helo, hello, he__lo 경로로 모두 접근이 가능하다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Req&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nest는 요청과 함께 전달되는 데이터를 핸들러가 다룰 수 있는 객체로 변환하는데, 이렇게 변환된 객체는 @Req 데코레이터를 이용하여 다룰 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Get()
getHello(@Req() req: Request): string {
	console.log(req);
	return this.appService.getHello();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Header&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답에 커스텀 헤더를 추가하고 싶으면 @Header 데코레이터를 사용하면 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import { Header } from '@nestjs/common';

@Header('Custom', 'Test Header')
@Get(':id')
findOneWithHeader(@Param('id') id: string) {
	return this.usersService.findOne(+id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;@Redirect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 요청을 처리한 후 요청을 보낸 클라이언트를 다른 페이지로 이동시키고 싶을 때 @Redirect 데코레이터를 사용해서 구현할 수 있다. (두 번째 인수는 상태코드)&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;import { Redirect } form '@nestjs/common';

@Redirect('&amp;lt;https://nestjs.com&amp;gt;', 301)
@Get(':id')
findOne(@Param('id') id: string) {
	return this.usersService.findOne(+id);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우트 매개변수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우트 매개변수를 전달받으려면, 객체로 한번에 받거나 라우팅 매개변수를 따로 받으면 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// 객체로 한 번에 받는 방법
@Delete(':/userId/memo/:memoId')
deleteUserMemo(@Param() params: { [key: string]: string}) {
	return 'userId: ${params.userId}, memoId: ${params.memoId}';
}

// 라우팅 매개변수를 따로 받는 방법
@Delete(':userId/memo/:memoId')
deleteUserMemo(
	@Param('userId') userId: string,
	@Param('memoId') memoId: string,
) {
	return 'userId: $userId}, memoID: ${memoId}';
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하위 도메인 라우팅 기법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 회사가 사용하는 도메인은 &lt;a href=&quot;http://example.com&quot;&gt;example.com&lt;/a&gt;, API 요청은 api.example.com으로 받는다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 app.controller.ts에서 ApiController가 먼저 처리되도록 순서를 수정&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;@Module({
	controllers: [ApiController, AppController],
	(...)
})
export class AppModule { }
&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;@Controller 데코레이터는 ControllerOptions 객체를 인수로 받는데, host 속성에 하위 도메인을 작성&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller({ host: 'api.example.com' }) // 하위 도메인 요청 처리 설정
export class ApiController {
	@Get() // 같은 루트 경로
	index(): string {
		return 'Hello, API'; // 다른 응답
	}
}
&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;@HostParam 데코레이터를 이용하면 서브 도메인을 변수로 받을 수 있는데, 이 방법으로 API를 버전별로 분리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller({ host: ':version.api.localhost' })
export class ApiController {
	@Get()
	index(@HostParam('version') version: string): string {
		return 'Hello, API ${version}';
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DTO&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST, PUT, PATCH 요청은 처리에 필요한 데이터를 함께 실어 보내는데, 이 페이로드를 본문(body)라고 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; NestJS는 DTO(Data Transfer Object, 데이터 전송 객체)가 구현되어 있어 본문을 다루기 쉬움&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;export class CreateUserDTO {
	name: string;
	email: string;
}

@Post()
create(@Body() createUserDto: CreateUserDto) {
	const { name, email } = createUserDto;
	
	return '${name}, ${email}';
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Provider&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 제공하고자 하는 핵심 기능(비즈니스 로직)을 수행하는 역할&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Service, Repository, Factory, Helper와 같은 형태로 구현 가능&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 주입(dependency injection, DI)&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Controller('users')
export class UsersController {
	constructor(private readonly userService: UsersService) {}
		...
		
	@Delete(':id')
	remove(@Param('id') id: string) {
		return this.usersService.remove(+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;컨트롤러에 비즈니스 로직을 직접 수행하지 않고, 컨트롤러에 연결된 UsersService 클래스에서 수행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; UsersService는 UsersController의 생성자에서 주입받아, UsersService라는 객체 멤버 변수에 할당되어 사용&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;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
	(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; UsersService 클래스에 @Injectable 데코레이터를 선언함으로써 다른 어떤 Nest 컴포넌트에서도 주입할 수 있는 Provider가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;@Module({
	...
	providers: [UsersService]
})
export class UsersModule {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Provider 인스턴스 역시 모듈에서 사용할 수 있도록 등록을 해줘야한다.&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;rarr; 부모 클래스에서 필요한 provider를 super()를 통해 전달해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 아니면 속성 기반 provider를 사용하면 된다&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;export class BaseService {
	@Inject(ServiceA) private readonly serviceA: ServiceA;
	(...)
	
	doSomeFuncFromA(): string {
		return this.serviceA.getHello();
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스코프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 싱글턴 인스턴스를 사용하는 것은 안전한 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 요청으로 들어오는 모든 정보를 공유할 수 있다&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;GraphQL 애플리케이션의 요청별 캐싱을 한다던가, 요청을 추적하거나, 멀티테넌시를 지원하려면 요청 기반으로 생명주기를 제한 해야함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; controller와 provider에 스코프 옵션을 주어 생명주기를 지정하는 방법이 있음&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;DEFAULT: 싱글턴 인스턴스가 전체 애플리케이션에서 공유.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 인스턴스 수명 = 애플리케이션 생명주기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 애플리케이션이 부트스트랩 과정을 마치면 모든 싱글턴 provider의 인스턴스 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 따로 선언하지 않으면 DEFAULT 적용&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;REQUEST: 들어오는 요청마다 별도의 인스턴스 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 요청 처리 후 인스턴스는 garbage collected 됨&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;TRANSIENT: 이 스코프를 지정한 인스턴스는 공유되지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이 provider를 주입하는 각 컴포넌트는 새로 생성된 전용 인스턴스를 주입 받게 됨&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;가능하면 DEFAULT 스코프를 사용하는 것 권장&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// provider에 스코프 적용
@Injectable({ scope: Scope.REQUEST })

// controller에 스코프 적용
export declare function Controller(options: ControllerOptions): ClassDecorator;

export interface ControllerOptions extends ScopeOptions, VersionOptions {
	path?: string | string[];
	host?: string | RegExp | Array&amp;lt;string | RegExp&amp;gt;;
}

export interface ScopeOptions {
	scope?: Scope;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 프로바이더&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Nest 프레임워크가 만들어주는 인스턴스나 캐시된 인스턴스 대신 인스턴스를 직접 생성하고 싶은 경우&lt;/li&gt;
&lt;li&gt;여러 클래스가 의존관계에 있을 때 이미 존재하는 클래스를 재사용하고자 할 때&lt;/li&gt;
&lt;li&gt;테스트를 위해 모의 버전으로 프로바이더를 재정의하려는 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 커스텀 프로바이더를 사용하면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;밸류 프로바이더: provide와 useValue 속성 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 프로바이더: useClass 속성 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팩터리 프로바이더: useFactory 속성 사용&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Module&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 컴포넌트를 조합하여 좀 더 큰 작업을 수행할 수 있게 하는 단위&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NestJS는 하나의 루트 모듈이 존재하고 이 루트 모듈은 다른 모듈들로 구성됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Module 데코레이터를 사용하고, 인수로 ModuleMetaData를 받음&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export declare function Module(metadata: ModuleMetadata): ClassDecorator;

export interface ModuleMetaData {
	imports?: Array&amp;lt;Type&amp;lt;any&amp;gt; | DynamicModule | 
	Promise&amp;lt;DynamicModule&amp;gt; | ForwardReference&amp;gt;;
	controllers?: Type&amp;lt;any&amp;gt;[];
	providers?: Provider[];
	exports?: Array&amp;lt;DynamicModule | Promise&amp;lt;DynamicModule&amp;gt; | stirng 
	| symbol | Provider | ForwardReference | Abstract&amp;lt;any&amp;gt; | Function&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;만약 A 모듈에서 B 모듈을 가져오고, C 모듈이 A 모듈을 가져왔다고 가정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; C 모듈이 B 모듈을 사용하도록 하고 싶다면 가져온 모듈을 내보내야 함&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;만약 전역 모듈을 만들고 싶으면, @Global 데코레이터만 선언하면 된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Global()
@Module({
	providers: [CommonService],
	exports: [CommonService],
})
export class CommonModule { }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 동적 모듈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈이 생성될 때 동적으로 정해지는 변수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 호스트 모듈(provider, controller와 같은 컴포넌트를 제공하는 모듈)을 가져다 쓰는 소비 모듈에서 호스트 모듈을 생성할 때 값을 설정하는 방식&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;ConfigModule: 실행 환경에 따라 서버에 설정되는 환경 변수를 관리하는 모듈&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;dotenv: 각 환경 변수를 .env 확장자를 가진 파일에 저장해두고 서버가 구동될 때 해당 파일을 읽어 환경 변수로 설정해주는 라이브러리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(AWS Secret Manager를 활용하면 프로비저닝 과정에서 환경 변수를 넣어줄 수 있다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@nestjs/config 패키지를 활용해서 ConfigModule을 동적으로 생성 할 수 있다&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import { ConfigModule } from '@nestjs/config';

@Module({
	imports: [ConfigModule.forRoot()],
	...
})
export classs AppModule { }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forRoot 메서드: DynamicModule을 리턴하는 정적 메서드&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커스텀 Config 파일 작성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DatabaseConfig, EmailConfig와 같이 의미 있는 단위로 묶어서 처리하고 싶다면 @nestjs/config 패키지에서 제공하는 ConfigModule을 이용하여 구현하면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import { registerAs } from &quot;@nestjs/config&quot;;

export default registerAs('email', () =&amp;gt; ({
	service: processs.env.EMAIL_SERVICE,
	auth: {
		user: process.env.EMAIL_AUTH_USER,
		pass: process.env.EMAIL_AUTH_PASSWORD,
		},
		baseUrl: process.env.EMAIL_BASE_URL,
	}));
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동적 ConfigModule 등록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.env 파일을 루트 경로가 아니라 src/config/env 디렉토리에 모아서 관리하면 되는데, Nest 기본 빌드 옵션은 .ts 파일 외의 에셋은 제외하도록 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; .env 파일을 out 디렉터리(dist 디렉터리)에 복사할 수 있도록 nest-cli.json에서 옵션을 바꿔줘야 한다.&lt;/p&gt;
&lt;pre class=&quot;clojure&quot;&gt;&lt;code&gt;{
	..
	&quot;compiloerOptions&quot;: {
		&quot;assets&quot;: [
			{
				&quot;include&quot;: &quot;./config/env/*.env&quot;,
				&quot;outDir&quot;: &quot;./dist&quot;
			}
		]
	}
}
&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;AppModule에 ConfigModule을 동적 모듈로 등록하기&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Module({
	imports: [
		UsersModule,
		ConfigModule.forRoot({
			envFilePath: ['${__dirname}/config/env/.${process.env.NODE_ENV}.env'],
			load: [emailConfig],
			isGlobal: true,
			validationSchema,
		}),
	],
	controllers: [],
	providers: [],
})
export class AppModule {}
&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;참고로 validationSchema는 유효성 검사 라이브러리 &amp;lsquo;joi&amp;rsquo;를 활용하면 된다&lt;/p&gt;
&lt;pre class=&quot;nimrod&quot;&gt;&lt;code&gt;import * as Joi from 'joi';

export const validationSchema = Joi.object({
	EMAIL_SERVICE: Joi.string()
		.required(),
	(...)
	
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Study/NestJS</category>
      <category>nestjs</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/188</guid>
      <comments>https://quickchabun.tistory.com/188#entry188comment</comments>
      <pubDate>Thu, 9 Oct 2025 15:53:08 +0900</pubDate>
    </item>
    <item>
      <title>[Fedify] 오픈소스 기여 - Deprecated된 Loader API 제거하기</title>
      <link>https://quickchabun.tistory.com/187</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Issue  &amp;nbsp;&lt;a href=&quot;https://github.com/fedify-dev/fedify/issues/376&quot;&gt;https://github.com/fedify-dev/fedify/issues/376&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1759165858772&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;Remove deprecated APIs for Fedify 2.0 &amp;middot; Issue #376 &amp;middot; fedify-dev/fedify&quot; data-og-description=&quot;Overview This issue tracks the removal of all deprecated APIs that need to be removed in Fedify 2.0.0 as part of the major version cleanup. APIs to Remove 1. Federation Configuration Remove documen...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/fedify-dev/fedify/issues/376&quot; data-og-url=&quot;https://github.com/fedify-dev/fedify/issues/376&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Tei89/hyZKmHO1Pl/velbaGaminKFH3pRWbj0C0/img.png?width=1200&amp;amp;height=600&amp;amp;face=999_138_1045_188,https://scrap.kakaocdn.net/dn/kENOy/hyZJVygJh0/lwX9E8F9jmudj4kMiZKWlk/img.png?width=1200&amp;amp;height=600&amp;amp;face=999_138_1045_188&quot;&gt;&lt;a href=&quot;https://github.com/fedify-dev/fedify/issues/376&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/fedify-dev/fedify/issues/376&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Tei89/hyZKmHO1Pl/velbaGaminKFH3pRWbj0C0/img.png?width=1200&amp;amp;height=600&amp;amp;face=999_138_1045_188,https://scrap.kakaocdn.net/dn/kENOy/hyZJVygJh0/lwX9E8F9jmudj4kMiZKWlk/img.png?width=1200&amp;amp;height=600&amp;amp;face=999_138_1045_188');&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;Remove deprecated APIs for Fedify 2.0 &amp;middot; Issue #376 &amp;middot; fedify-dev/fedify&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Overview This issue tracks the removal of all deprecated APIs that need to be removed in Fedify 2.0.0 as part of the major version cleanup. APIs to Remove 1. Federation Configuration Remove documen...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fedify 2.0을 대비해 사용하지 않는 API들을 정리하는 이슈가 올라왔다. 해당 이슈를 확인해보면 없애야할 API 리스트가 있는데, 처음에는 API들을 전부 다 없애고 PR을 올릴까 했지만 멘토님의 조언에 따라 API를 하나씩 삭제하고 PR을 올리기로 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. documentLoader property 삭제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;documentLoader 삭제 PR  &amp;nbsp;&lt;a href=&quot;https://github.com/fedify-dev/fedify/pull/393&quot;&gt;https://github.com/fedify-dev/fedify/pull/393&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1759165779619&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;fix: Remove documentLoader property from FederationOptions interface by crohasang &amp;middot; Pull Request #393 &amp;middot; fedify-dev/fedify&quot; data-og-description=&quot;Summary Remove documentLoader property from FederationOptions interface (use documentLoaderFactory instead) Related Issue Implemented Remove deprecated APIs for Fedify 2.0&amp;nbsp;#376 Changes In fedif...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/fedify-dev/fedify/pull/393&quot; data-og-url=&quot;https://github.com/fedify-dev/fedify/pull/393&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bm3lQ2/hyZJHAtfQI/7rNsVGBO6HdkhCLKOKWIY0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bYv8rv/hyZJWxdEgj/KkjrBCVvozGeVkXOiLXSKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/fedify-dev/fedify/pull/393&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/fedify-dev/fedify/pull/393&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bm3lQ2/hyZJHAtfQI/7rNsVGBO6HdkhCLKOKWIY0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bYv8rv/hyZJWxdEgj/KkjrBCVvozGeVkXOiLXSKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;fix: Remove documentLoader property from FederationOptions interface by crohasang &amp;middot; Pull Request #393 &amp;middot; fedify-dev/fedify&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Summary Remove documentLoader property from FederationOptions interface (use documentLoaderFactory instead) Related Issue Implemented Remove deprecated APIs for Fedify 2.0&amp;nbsp;#376 Changes In fedif...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, &amp;lsquo;FederationOptions&amp;rsquo; interface에 있는 &amp;lsquo;documentLoader&amp;rsquo; property를 없애기로 했다. 해당 인터페이스에서는 이미 &amp;lsquo;documentLoader&amp;rsquo;를 대신하는 &amp;lsquo;documentLoaderFactory&amp;rsquo;가 선언되어있었다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;  /**
   * A custom JSON-LD document loader.  By default, this uses the built-in
   * cache-backed loader that fetches remote documents over HTTP(S).
   * @deprecated Use {@link documentLoaderFactory} instead.
   */
  documentLoader?: DocumentLoader;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;documentLoader는 ActivityPub 객체나 다른 데이터를 나타내는 JSON-LD 문서를 원격 URL에서 가져오는 역할을 한다. 기본적으로는 내장된 캐시 기반 로더를 사용하지만, 사용자가 직접 만든 커스텀 로더를 제공하여 문서 로딩 방식을 변경할 수 있다.&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;JSON-LD 문서가 뭐지?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; JSON-LD(JSON for Linked Data)는 간단히 말해 '의미가 부여된 JSON'이다. 일반적인 JSON 데이터에 '이것은 사람 이름이야', '이것은 주소야' 와 같이 데이터의 의미와 관계를 명확하게 설명하는 정보를 추가하는 표준 방식&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;pre class=&quot;json&quot;&gt;&lt;code&gt;{
   &quot;@context&quot;: &quot;&amp;lt;https://schema.org/&amp;gt;&quot;,
   &quot;@type&quot;: &quot;Person&quot;,
   &quot;name&quot;: &quot;홍길동&quot;,
   &quot;homepage&quot;: &quot;[&amp;lt;https://example.com&amp;gt;](&amp;lt;https://example.com/&amp;gt;)&quot;,
   &quot;image&quot;: &quot;&amp;lt;https://example.com/profile.jpg&amp;gt;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@context : &quot;이 문서에서 사용하는 용어들은 &lt;a href=&quot;https://schema.org에&quot;&gt;https://schema.org에&lt;/a&gt; 정의된 의미를 따를 거야&quot;라고 선언하는 것 &amp;rarr; schema.org는 웹에서 사용되는 다양한 용어(사람, 장소, 이벤트 등)를 표준화한 일종의 '어휘 사전'&lt;/li&gt;
&lt;li&gt;@type: &quot;Person&quot;: 이 데이터 덩어리는 '사람(Person)'에 대한 정보라고 명확히 알려줌&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 컴퓨터는 이 데이터를 보고 다음과 같이 이해한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;아, 이건 &lt;a href=&quot;http://schema.org&quot;&gt;schema.org&lt;/a&gt; 표준에 정의된 '사람(Person)'에 대한 정보구나. name은 그 사람의 이름이고, homepage는 웹사이트구나.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그러면 왜 documentLoader에서 documentLoaderFactory로 바꾸는 걸까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;documentLoader는 정적인 객체인 반면, documentLoaderFactory는 동적인 객체를 생성하는 함수&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 1.25em; letter-spacing: -1px; color: #000000; background-color: #ffffff;&quot;&gt;documentLoader&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; color: #000000; background-color: #ffffff;&quot;&gt;documentLoader는 Federation 객체가 처음 만들어질 때 단 한 번 생성되는 고정된 로더 객체다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; color: #000000; background-color: #ffffff;&quot;&gt;&amp;rarr; 한번 만들어지면, 모든 JSON-LD 문서를 가져오는 요청은 항상 이 동일한 로더를 사용해야한다.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; font-size: 1.25em; letter-spacing: -1px; color: #000000; background-color: #ffffff;&quot;&gt;documentLoaderFactory&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; color: #000000; background-color: #ffffff;&quot;&gt;그에 비해 documentLoaderFactory는 JSON-LD 문서를 가져와야 할 필요가 생길 때마다, 그 상황에 맞는 &amp;lsquo;DocumentLoader&amp;rsquo; 객체를 즉석에서 생성하는 공장(Factory) 역할을 하는 함수이다.&lt;/span&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;27ce1f4d-1a3d-8079-ab16-cf7e2ad7e48e&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;27ce1f4d-1a3d-807d-b1c5-db865297392b&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;방식&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;27ce1f4d-1a3d-80e9-9e3f-dd3b548dfd92&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div style=&quot;text-align: start;&quot; contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. JSON-LD 문서를 가져와야 하는 상황이 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;2. Fedify는 documentLoaderFactory 함수를 호출&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;3. 현재 요청에 대한 정보(예: &quot;이 요청은 'user-A'의 권한으로 보내는 거야&quot;)와 같은 컨텍스트를 Factory 함수에 전달 가능&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;4. Factory 함수는 이 컨텍스트를 바탕으로 인증 정보가 포함된 맞춤형 DocumentLoader 객체를 새로 생성하여 반환&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;5. Fedify는 방금 만들어진 이 맞춤형 로더를 사용해 문서를 안전하게 가져옴&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;즉, documentLoader를 documentLoaderFactory로 바꾼 이유는 정적인 '하나의 도구'를 계속 사용하는 방식에서, 필요할 때마다 상황에 맞는 '맞춤형 도구'를 즉석에서 만들어 사용하는 훨씬 더 유연하고 강력한 방식으로 전환하기 위해서이다.&lt;/span&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;코드 변경&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;27ce1f4d-1a3d-8034-999f-fb8221742f55&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, fedify/src/federation/federation.ts에서 documentLoader 속성을 없앴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot;&gt;그리고 cli/src/inbox.tsx에서 createFederation에 documentLoader를 직접 전달하는 대신, getDocumentLoader() 함수로 로더를 딱 한번 만들고 계속 동일한 로더를 재사용하게 만들었다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5Zwgi/btsQU1m3nSv/klvzPEK48tSrSmDQKGl3g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5Zwgi/btsQU1m3nSv/klvzPEK48tSrSmDQKGl3g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5Zwgi/btsQU1m3nSv/klvzPEK48tSrSmDQKGl3g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5Zwgi%2FbtsQU1m3nSv%2FklvzPEK48tSrSmDQKGl3g1%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;400&quot; height=&quot;134&quot; data-origin-width=&quot;1088&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&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;fedify/src/federation/middleware.ts에서는 documentLoader를 documentLoaderFactory 방식으로 변경하면서, allowPrivateAddress와 userAgent 검사 코드를 제거했다. 왜냐하면, documentLoaderFactory을 통해 해당 속성들을 지정할 수 있게 되었기 때문이다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;1552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2DZAw/btsQWqzprWV/QWP5TuTvvr1MbDggwXxhV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2DZAw/btsQWqzprWV/QWP5TuTvvr1MbDggwXxhV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2DZAw/btsQWqzprWV/QWP5TuTvvr1MbDggwXxhV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2DZAw%2FbtsQWqzprWV%2FQWP5TuTvvr1MbDggwXxhV1%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;400&quot; height=&quot;423&quot; data-origin-width=&quot;1468&quot; data-origin-height=&quot;1552&quot;/&gt;&lt;/span&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;마지막으로, fedify/src/federation/middleware.test.ts에서는 변경된 기능에 맞춰 테스트 코드를 documentLoaderFactory 방식으로 수정했다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;1326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/53aEr/btsQWLXBvrS/34MgGcMhefs9CKYzXn3l7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/53aEr/btsQWLXBvrS/34MgGcMhefs9CKYzXn3l7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/53aEr/btsQWLXBvrS/34MgGcMhefs9CKYzXn3l7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F53aEr%2FbtsQWLXBvrS%2F34MgGcMhefs9CKYzXn3l7k%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;400&quot; height=&quot;409&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;1326&quot;/&gt;&lt;/span&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;또한, 테스트 간의 충돌을 방지하고 역할을 명확히 하고자 이전에는 하나로 사용하던 테스트용 가짜 서버 주소를 일반 객체 조회용(/object)과 인증 확인용(/auth-check)으로 나누어 분리했다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;858&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rZDJI/btsQUMDr3Oq/6jRsumEIrjiz25nzn52Wxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rZDJI/btsQUMDr3Oq/6jRsumEIrjiz25nzn52Wxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rZDJI/btsQUMDr3Oq/6jRsumEIrjiz25nzn52Wxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrZDJI%2FbtsQUMDr3Oq%2F6jRsumEIrjiz25nzn52Wxk%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;400&quot; height=&quot;293&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;858&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. contextLoader property 삭제&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;27de1f4d-1a3d-80bc-a745-ced08185a1b9&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;contextLoader 삭제 PR  &amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/fedify-dev/fedify/pull/393&quot;&gt;https://github.com/fedify-dev/fedify/pull/393&lt;/a&gt;&lt;span style=&quot;color: #2c2c2b; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;figure id=&quot;og_1759165850323&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;fix: Remove documentLoader property from FederationOptions interface by crohasang &amp;middot; Pull Request #393 &amp;middot; fedify-dev/fedify&quot; data-og-description=&quot;Summary Remove documentLoader property from FederationOptions interface (use documentLoaderFactory instead) Related Issue Implemented Remove deprecated APIs for Fedify 2.0&amp;nbsp;#376 Changes In fedif...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/fedify-dev/fedify/pull/393&quot; data-og-url=&quot;https://github.com/fedify-dev/fedify/pull/393&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bm3lQ2/hyZJHAtfQI/7rNsVGBO6HdkhCLKOKWIY0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bYv8rv/hyZJWxdEgj/KkjrBCVvozGeVkXOiLXSKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/fedify-dev/fedify/pull/393&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/fedify-dev/fedify/pull/393&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bm3lQ2/hyZJHAtfQI/7rNsVGBO6HdkhCLKOKWIY0/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bYv8rv/hyZJWxdEgj/KkjrBCVvozGeVkXOiLXSKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;fix: Remove documentLoader property from FederationOptions interface by crohasang &amp;middot; Pull Request #393 &amp;middot; fedify-dev/fedify&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Summary Remove documentLoader property from FederationOptions interface (use documentLoaderFactory instead) Related Issue Implemented Remove deprecated APIs for Fedify 2.0&amp;nbsp;#376 Changes In fedif...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이제 contextLoader property를 삭제해보자. 위에서 documentLoader는 ActivityPub 객체나 다른 데이터를 나타내는 JSON-LD 문서를 원격 URL에서 가져오는 역할을 한다라고 했는데, 그러면 contextLoader는 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; &lt;b&gt;contextLoader는 JSON-LD 문서의 의미를 해석하는 데 필요한 @context를 가져오는 역할을 한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;27de1f4d-1a3d-807f-84bd-e8a79fa218ed&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;ActivityPub에서 사용하는 데이터 형식인 JSON-LD는 @context라는 필드를 사용한다. 이 @context 필드에는 주로 &amp;lsquo;어휘 사전&amp;rsquo; 파일의&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;위치를 가리키는 URL이 들어간다. 이 사전을 참조함으로써, 컴퓨터는 &quot;이 문서에서 Person은 '사용자 프로필'을, Note는 '게시물'을 의미한다&quot;와 같이 각 용어의 정확한 의미를 해석할 수 있게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&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;먼저, fedify/src/federation/federation.ts에서 contextLoader 속성을 없앴다.&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;그리고 fedify/src/federation/middleware.ts에서는 allowPrivateAddress 같은 보안 옵션을 켰다면, documentLoaderFactory와 contextLoaderFactory를 사용하지 못하도록 하는 코드를 추가했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;1404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdTlcR/btsQUibIyXL/vFp8vrBwqmvgyGnjOU5VMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdTlcR/btsQUibIyXL/vFp8vrBwqmvgyGnjOU5VMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdTlcR/btsQUibIyXL/vFp8vrBwqmvgyGnjOU5VMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdTlcR%2FbtsQUibIyXL%2FvFp8vrBwqmvgyGnjOU5VMK%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;400&quot; height=&quot;357&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;1404&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;fedify/src/federation/middleware.test.ts에서는 contextLoader를 contextLoaderFactory로 교체해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;1602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MCVDs/btsQWBnibZl/9YlbGnF1vfbEXd2TJd5iBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MCVDs/btsQWBnibZl/9YlbGnF1vfbEXd2TJd5iBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MCVDs/btsQWBnibZl/9YlbGnF1vfbEXd2TJd5iBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMCVDs%2FbtsQWBnibZl%2F9YlbGnF1vfbEXd2TJd5iBk%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;400&quot; height=&quot;362&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;1602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;27de1f4d-1a3d-80b1-b3dc-c9f39cf4a4ca&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 documentLoader와 contextLoader, 두 개의 API를 정리했다. 이번 작업을 통해 documentLoader와 contextLoader가 Fedify에서 무슨 역할을 하는지 알 수 있었다. 이 Loader들을 Factory 함수들로 대체함으로써 단일 로더 방식에서 벗어나 상황에 맞는 맞춤형 로더를 동적으로 생성하는 유연한 구조를 갖출 수 있게 되었다.&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;위에 언급했던 이슈로 다시 돌아가보면 Loader들 이외에도 CreateFederationOptions, fetchDocumentLoader 등 정리해야될 API가 여러개 존재한다. 이 API들을 삭제하는 작업을 앞으로도 계속 한다면 Fedify가 어떻게 돌아가는지 더 깊게 이해할 수 있을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Fedify</category>
      <category>fedify</category>
      <category>오픈소스</category>
      <category>오픈소스 컨트리뷰션</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/187</guid>
      <comments>https://quickchabun.tistory.com/187#entry187comment</comments>
      <pubDate>Tue, 30 Sep 2025 02:13:00 +0900</pubDate>
    </item>
    <item>
      <title>GSAP 라이브러리를 활용하여 랜딩페이지 제작하기</title>
      <link>https://quickchabun.tistory.com/186</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;KUIT 6기 랜딩페이지   &lt;a href=&quot;https://www.konkuk-kuit.com/6/introduce&quot;&gt;https://www.konkuk-kuit.com/6/introduce&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. GSAP를 써보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 2학기를 맞이하여, 운영진으로 참여했던 KUIT(건국대학교 기획/개발 동아리)도 이제 6기를 맞아 부원들을 모집해야되는 시기가 다가왔다. 저번 4기와 5기 부원 모집 때에는 framer-motion 라이브러리를 활용하여 풀 페이지 스크롤 애니메이션 랜딩페이지를 구현했었는데, 이번 6기 모집 때 똑같은 템플릿을 활용할지 아니면 새로운 인터랙션을 구현할지 고민이 되었다.&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;4기 랜딩페이지   &lt;a href=&quot;https://www.konkuk-kuit.com/4/introduce&quot;&gt;https://www.konkuk-kuit.com/4/introduce&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀페이지 스크롤 애니메이션 구현기   &lt;a href=&quot;https://quickchabun.tistory.com/131&quot;&gt;https://quickchabun.tistory.com/131&lt;/a&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;lsquo;GSAP 라이브러리&amp;rsquo;였다.&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;먼저 공식페이지에 들어가 GSAP를 활용한 인터랙션 예시를 살펴보았는데, css와 바닐라 JS로 구현하려면 매우 어려워보이는 애니메이션들이 부드럽고 멋지게 구현된 모습에 매료되었다. 아직 써보지는 않았지만, LLM과 같이라면 구현할 수 있겠지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 KUIT 랜딩페이지 프로젝트에 새로운 브랜치를 파서 작업을 시작해보았다.&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;GSAP 공식페이지   &lt;a href=&quot;https://gsap.com/&quot;&gt;https://gsap.com/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GSAP 쇼케이스 모음   &lt;a href=&quot;https://gsap.com/showcase/&quot;&gt;https://gsap.com/showcase/&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 구조&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt; introduce
 ┣  _components
 ┃ ┣  BodyClassManager.tsx
 ┃ ┣  CountdownTimer.tsx
 ┃ ┣  FirstInteraction.tsx
 ┃ ┣  IntroduceAnimationContainer.tsx
 ┃ ┣  MarqueeItem.tsx
 ┃ ┣  SecondInteraction.tsx
 ┃ ┣  TechStack.tsx
 ┃ ┗  ThirdInteraction.tsx
 ┣  _constants
 ┃ ┗  staff.ts
 ┣  constants
 ┃ ┗  animationConfig.ts
 ┗  page.tsx&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IntroduceAnimationContainer 컴포넌트에 애니메이션에 사용될 모든 DOM 요소의 ref를 선언했고, GSAP를 활용한 스크롤 애니메이션 로직 전체가 이 컴포넌트 안에 존재한다.&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;이 컴포넌트에는 맨 처음 사이트에 접속하면 진행되는 인트로 애니메이션과, 그 뒤에 스크롤을 시작하면 진행되는 스크롤 애니메이션을 관리하는 useEffect 훅 두 개가 존재한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 인트로 애니메이션&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;1028&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf4Rk0/btsQIHPn1Nh/nbksRan7rMBzOf7gVzZbwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf4Rk0/btsQIHPn1Nh/nbksRan7rMBzOf7gVzZbwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf4Rk0/btsQIHPn1Nh/nbksRan7rMBzOf7gVzZbwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf4Rk0%2FbtsQIHPn1Nh%2FnbksRan7rMBzOf7gVzZbwK%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;209&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;1028&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;lsquo;KUIT&amp;rsquo;, 두 번째 줄에 &amp;lsquo;SIXTH&amp;rsquo;가 있고 두 &amp;lsquo;I&amp;rsquo;가 내려오면서 합쳐지는 인트로 애니메이션이 구현된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;  useEffect(() =&amp;gt; {
    const ctx = gsap.context(() =&amp;gt; {
      gsap.set(lettersRef.current, { autoAlpha: 0, y: 10 });
      gsap.set(iLineRef.current, { scaleY: 0, transformOrigin: 'top center' });
      gsap.set(secondInteractionRef.current, { autoAlpha: 0 });
      gsap.set(thirdInteractionRef.current, { autoAlpha: 0 });
      gsap.set(scrollDownRef.current, { autoAlpha: 0 });

      const introTl = gsap.timeline({
        onComplete: () =&amp;gt; {
          setIsIntroFinished(true);
          gsap.to(scrollDownRef.current, {
            autoAlpha: 1,
            duration: 1.5, 
            ease: 'power1.inOut',
          });
        },
      });

      introTl
        .to(lettersRef.current, {
          autoAlpha: 1,
          y: 0,
          duration: 0.5,
          ease: 'power2.out',
          stagger: TEXT_ANIMATION_CONFIG.intro.letterStagger,
        })
        .to(iLineRef.current, {
          scaleY: () =&amp;gt; {
            if (!iLineRef.current) return 1;
            const rect = iLineRef.current.getBoundingClientRect();
            const viewportHeight = window.innerHeight;
            const distanceToBottom = viewportHeight - rect.top;
            const initialHeight = iLineRef.current.offsetHeight;
            if (initialHeight === 0) return 1;
            return distanceToBottom / initialHeight;
          },
          duration: TEXT_ANIMATION_CONFIG.intro.lineScaleDuration,
          ease: 'expo.inOut',
        });
    }, containerRef);
    return () =&amp;gt; ctx.revert();
  }, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;275e1f4d-1a3d-80ae-90df-e313dbdf3913&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;&lt;span data-token-index=&quot;1&quot;&gt;gsap.set()&lt;/span&gt;&lt;/b&gt;을 활용해 요소들의 초기 상태를 설정한다. 여기서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;lsquo;autoAlpha: 0&amp;rsquo; - 요소들을 투명하게 만듦,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;lsquo;scaleY: 0&amp;rsquo; - 요소의 높이를 0으로 만들어 화면에서 안보이게 만듦&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;lsquo;transformOrigin &amp;lsquo;top center&amp;rsquo;&amp;rsquo;: 윗변의 중앙을 기준으로 효과가 시작&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이라는 뜻을 가지고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;275e1f4d-1a3d-80c7-ac40-c0707e9ee561&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음&lt;span data-token-index=&quot;1&quot;&gt;, &lt;b&gt;gasp.timeline()&lt;/b&gt;&lt;/span&gt;을 사용해 여러 애니메이션을 순서대로 제어할 타임라인(introTl)을 생성한다. 코드에서 확인할 수 있듯이 to 메서드에 첫 번째 매개변수에 애니메이션 ref를 작성하고, 두 번째 매개변수에 애니메이션 로직을 작성한다. .to 체이닝을 통하여 애니메이션이 차례대로 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;275e1f4d-1a3d-80bc-8d32-c839d8ec13f8&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;lettersRef&lt;/span&gt;&lt;/b&gt;는 처음 글자들(I 제외)이 출력되는 애니메이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;ease: &amp;lsquo;power2.out&amp;rsquo; - 애니메이션이 빠르게 시작해서 점차 부드럽게 감속하며 끝나는 효과 (t^2),&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;275e1f4d-1a3d-8086-a76c-c84e3821afa7&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;iLineRef&lt;/span&gt;&lt;/b&gt;는 알파벳 I가 생성되면서 밑으로 내려가는 애니메이션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 때, scaleY에는 선이 화면 하단까지 닿게 동적으로 계산하는 로직이 들어갔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;ease: &amp;lsquo;expo.inOut&amp;rsquo; - 천천히 시작 &amp;rarr; 빠르게 &amp;rarr; 천천히 마무리 하는 효과 (2^10(t-1)),&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;275e1f4d-1a3d-805a-af38-d491b967b0e2&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;introTl의 &lt;b&gt;onComplete&lt;/b&gt; 콜백은 애니메이션이 종료된 후 실행되는데, setIsIntroFinished 상태를 true로 만들어 스크롤 애니메이션이 실행되게 만들고, scrollDownRef가 가리키는 '아래로 스크롤하세요' 문구를 화면에 나타나게 한다.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 스크롤 애니메이션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인트로 애니메이션이 끝나고 setIsIntroFinished가 true가 되면 IntroduceAnimationContainer의 두 번째 useEffect 훅이 실행된다. 인터랙션 컴포넌트는 세 개로 구분했지만, 처음부터 마지막까지 계속 아래로 스크롤만 하면 되기 때문에, 하나의 scrollTl timeline에 .to 메서드로 쭉 연결하는 단순한 구조가 되었다. timeline이 완성한 후, timeline과 containerRef를 ScrollTrigger.create 메서드에 연결하면 스크롤 애니메이션이 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  if (!isIntroFinished) return;

  const ctx = gsap.context(() =&amp;gt; {
    // 각 요소의 초기 상태를 설정
    gsap.set(projectGroupRef.current, { autoAlpha: 1 });
    (...)
    techStackRefs.forEach(ref =&amp;gt; {
      gsap.set(ref.current, { autoAlpha: 0, xPercent: 100 });
    });

    // 하나의 긴 타임라인을 생성
    const scrollTl = gsap.timeline();
    
    // 타임라인에 .to()를 체이닝하여 애니메이션을 순서대로 추가
    scrollTl
    
      // FirstInteraction이 사라지는 애니메이션
      .to(scrollDownRef.current, { autoAlpha: 0, ... })
      .to(textContainerRef.current, { scale: 15, ... })
      .to(firstInteractionRef.current, { autoAlpha: 0, ... })
      
      // SecondInteraction이 나타나고, 숫자 모핑/확대되는 애니메이션
      .to(secondInteractionRef.current, { autoAlpha: 1, ... })
      
      // (34 -&amp;gt; 49 숫자 모핑 및 관련 요소 애니메이션)
      .to(digit9Ref.current, { scale: 200, ... })

      // ThirdInteraction이 나타나는 애니메이션
      .to(thirdInteractionRef.current, { autoAlpha: 1, ... });

    // ThirdInteraction 내부의 각 팀 소개 애니메이션을 루프로 추가
    techStackRefs.forEach(ref =&amp;gt; {
      scrollTl
        .to(ref.current, { autoAlpha: 1, xPercent: 0, ... })
        .to({}, { duration: 1.5 })
        .to(ref.current, { autoAlpha: 0, xPercent: -100, ... });
    });

    // 마지막 '지원하기' 섹션이 나타나는 애니메이션 추가
    scrollTl.to(applySectionRef.current, { autoAlpha: 1, ... });

    // 완성된 타임라인을 ScrollTrigger에 연결하여 스크롤과 동기화
    ScrollTrigger.create({
      animation: scrollTl,
      trigger: containerRef.current,
      scrub: 1,
      pin: true,
      // ...
    });

  }, containerRef);

  return () =&amp;gt; {
    // ...
  };
}, [isIntroFinished]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. FirstInteraction ('KUIT SIXTH' 확대)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blkkPG/btsQJchj21O/QBpXkcKRsFKS7tH1d2aWL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blkkPG/btsQJchj21O/QBpXkcKRsFKS7tH1d2aWL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blkkPG/btsQJchj21O/QBpXkcKRsFKS7tH1d2aWL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblkkPG%2FbtsQJchj21O%2FQBpXkcKRsFKS7tH1d2aWL0%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;166&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1133&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;스크롤이 시작되면 textContainerRef의 scale 값을 크게 키워 'KUIT SIXTH' 텍스트가 화면을 채우듯 확대되고, 이어서 firstInteractionRef 전체가 autoAlpha: 0으로 부드럽게 사라지며 다음 장면으로 전환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;scrollTl
  // ... scrollDownRef 사라지는 부분 ...
  .to(textContainerRef.current, {
    scale: TEXT_ANIMATION_CONFIG.scroll.containerFinalScale,
    ease: 'power2.in',
  })
  .to(firstInteractionRef.current, {
    autoAlpha: 0,
    duration: 0.5,
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. SecondInteraction ('34' -&amp;gt; '49'에서 4 이동, 9 확대)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNfAZk/btsQGnxZgGc/CAecQQ6byPKKmu6MjqEFY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNfAZk/btsQGnxZgGc/CAecQQ6byPKKmu6MjqEFY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNfAZk/btsQGnxZgGc/CAecQQ6byPKKmu6MjqEFY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNfAZk%2FbtsQGnxZgGc%2FCAecQQ6byPKKmu6MjqEFY0%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;159&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJGGO9/btsQH02Psmk/Mqc8BVYdfkeH3OWilQ5QU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJGGO9/btsQH02Psmk/Mqc8BVYdfkeH3OWilQ5QU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJGGO9/btsQH02Psmk/Mqc8BVYdfkeH3OWilQ5QU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJGGO9%2FbtsQH02Psmk%2FMqc8BVYdfkeH3OWilQ5QU0%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;162&quot; data-origin-width=&quot;1658&quot; data-origin-height=&quot;894&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;FirstInteraction이 사라진 후 secondInteractionRef가 나타나며 숫자 '34'를 보여준다.&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;스크롤이 계속되면 '3'이 사라지고, digit4Ref의 x 속성을 동적으로 계산하여 '4'가 왼쪽으로 자연스럽게 이동해 '49'를 완성한다. 이어서 digit9Ref의 scale 값을 200으로 설정 흰색 숫자 9가 확대되면서 동시에 화면도 검은색에서 흰색으로 전환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;scrollTl
  // ...
  .to(secondInteractionRef.current, {
    autoAlpha: 1,
    duration: 0.5,
  })
  // ... (관련 링크 나타나고 사라지는 부분)
  .to(
    digit4Ref.current,
    {
      x: () =&amp;gt; (digit3Ref.current ? -digit3Ref.current.offsetWidth : 0),
      duration: 1,
      ease: 'power3.inOut',
    },
    '&amp;lt;',
  )
  // ... (숫자 9와 관련 링크 나타나고 사라지는 부분)
  .to(
    digit9Ref.current,
    {
      scale: 200,
      duration: 1.5,
      ease: 'expo.in',
      transformOrigin: '75% 25%',
    },
    '&amp;lt;',
  )
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. ThirdInteraction (상하단 띠, 운영진 우-&amp;gt;좌 이동)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1137&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mnscV/btsQJ70SB6B/l6SLjJHh5FZtiN9r18CKV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mnscV/btsQJ70SB6B/l6SLjJHh5FZtiN9r18CKV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mnscV/btsQJ70SB6B/l6SLjJHh5FZtiN9r18CKV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmnscV%2FbtsQJ70SB6B%2Fl6SLjJHh5FZtiN9r18CKV0%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;167&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1137&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;화면이 흰색으로 바뀌고, thirdInteractionRef가 나타나며 상하단의 Marquee 띠가 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이어서 techStackRefs 배열에 담긴 운영진 및 각 파트의 ref들을 forEach 루프로 순회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 요소는 xPercent 값을 100에서 0으로 변경하여 오른쪽에서 부드럽게 등장하고, 잠시 멈춘 후 -100으로 변경되어 왼쪽으로 사라지는 애니메이션이 스크롤에 맞춰 순차적으로 실행된다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;scrollTl
  // ...
  .to(
    thirdInteractionRef.current,
    {
      autoAlpha: 1,
      duration: 0.7,
      ease: 'power2.out',
    },
    '-=0.3',
  );

techStackRefs.forEach(ref =&amp;gt; {
  scrollTl
    .to(ref.current, { autoAlpha: 1, xPercent: 0, duration: 1, ease: 'power2.out' })
    .to({}, { duration: 1.5 })
    .to(ref.current, { autoAlpha: 0, xPercent: -100, duration: 1, ease: 'power2.in' });
});
&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;참고로 좌측 상단에 보이는 애니메이션에는 Lottiefiles 포맷을 적용했다. 최근 FEConf에서 Lottie 관련 강연을 들어봤는데 계기로 이번 프로젝트에 도입해 보았는데, 파일 용량이 작고 적용하기도 간편해서 만족스러웠다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;div style=&quot;background-color: #ffffff; color: #2c2c2b; text-align: start;&quot; data-block-id=&quot;275e1f4d-1a3d-80de-8785-fb097972cddf&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 기회를 통해 GSAP 라이브러리를 활용해 스크롤 애니메이션을 구현하는 법을 배울 수 있었다. 만약 라이브러리 없이 직접 DOM 요소들을 조작하려 했다면 구현하기 훨씬 어려웠을 것 같다. 이번 프로젝트는 처음부터 끝까지 아래로 스크롤만 하면 되었기 때문에 때문에 한 파일에 로직을 전부 구현할 수 있었는데, 만약 컴포넌트마다 다른 애니메이션을 구현해야 했다면 구조를 어떻게 짜면 좋았을 지 고민을 했을 것 같다.&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;GSAP를 통하여 스크롤 애니메이션 대신 다른 애니메이션들도 구현할 수 있지 않을까. 사용자들이 보고 만족하는 인터랙션에는 스크롤뿐만이 아닌 다른 요소들도 많을 것 같다. 이를 위해 다른 사이트들은 어떤 인터랙션을 구현했는지 레퍼런스들을 많이 찾아보고, 또 어떻게 구현했는지 분석을 열심히 해야겠다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>KUIT</category>
      <category>gsap</category>
      <category>KUIT</category>
      <category>랜딩페이지</category>
      <category>스크롤 애니메이션</category>
      <category>인터랙션</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/186</guid>
      <comments>https://quickchabun.tistory.com/186#entry186comment</comments>
      <pubDate>Sun, 21 Sep 2025 12:42:57 +0900</pubDate>
    </item>
    <item>
      <title>[Fedify] 오픈소스 기여 - 터미널이 아닌 환경에서는 색상을 빼보자</title>
      <link>https://quickchabun.tistory.com/185</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 터미널이 아닌 곳에서는 색상을 빼야하는 이유는?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈 링크: &lt;a href=&quot;https://github.com/fedify-dev/fedify/issues/257&quot;&gt;https://github.com/fedify-dev/fedify/issues/257&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR 링크: &lt;a href=&quot;https://github.com/fedify-dev/fedify/pull/341&quot;&gt;https://github.com/fedify-dev/fedify/pull/341&lt;/a&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;1660&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vmUou/btsPHNcyzlE/REnbjkOt1EYKE05JM6HJw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vmUou/btsPHNcyzlE/REnbjkOt1EYKE05JM6HJw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vmUou/btsPHNcyzlE/REnbjkOt1EYKE05JM6HJw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvmUou%2FbtsPHNcyzlE%2FREnbjkOt1EYKE05JM6HJw1%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;400&quot; height=&quot;128&quot; data-origin-width=&quot;1660&quot; data-origin-height=&quot;530&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;lsquo;Color and TTYs&amp;rsquo;라는 글을 읽어보기를 권한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 색상코드를 출력하는 프로그램을 만들 때, 표준출력이 &lt;b&gt;TTY(=Terminal)에 연결되어있을 때에만&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;Fedify CLI에서는 Deno를 활용하는데, Deno에서는 &lt;b&gt;&amp;lsquo;Deno.stdout.isTerminal()&amp;rsquo;&lt;/b&gt; 명령어를 활용하여 터미널인지 파악할 수 있다고 한다. 이 명령어를 활용하여 현재 사용자가 터미널에 연결되어있는지 확인하고, 아니라면 색상 출력을 비활성화 해보자!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 첫 번째 시도&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 이슈를 가져가고 싶다는 댓글을 달 때, 나는 다음과 같은 계획을 말씀드렸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MQitF/btsPGHKZpAb/cKRKcUKd4EMhuh4VKXkklk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MQitF/btsPGHKZpAb/cKRKcUKd4EMhuh4VKXkklk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MQitF/btsPGHKZpAb/cKRKcUKd4EMhuh4VKXkklk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMQitF%2FbtsPGHKZpAb%2FcKRKcUKd4EMhuh4VKXkklk%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;400&quot; height=&quot;122&quot; data-origin-width=&quot;1484&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 언급한대로 init.ts에서 터미널인지 체크하고, setColorEnabled를 활용하여 색상을 조절했음에도, 터미널이 아닌 환경에서 계속 색상이 출력되었다.&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;init.ts의 맨 윗부분에서 색상을 수정했었는데, 알고 보니 init.ts는 CLI의 시작점이 아니라 명령어 &amp;lsquo;init&amp;rsquo;이 실행되는 파일이었다.&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;CLI의 시작점은 init.ts가 아닌 mod.ts였고, 그리고 CLI의 시작점을 찾는 것이 중요한 것이 아니라, 색상을 제어하는 함수를 만들고, 각 명령어가 실행되는 파일들에서 그 함수를 적용하면 되는 일이었다.&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;@std/fmt/colors의 setColorEnabled()를 통해서 색상 제어를 해줘야했기 때문에 @cliffy/ansi를 import해서 쓰고 있는 colors들을 전부 @std/fmt/colors로 수정해주었다.&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;import { colors } from &quot;@cliffy/ansi&quot;; &amp;rarr; import * as colors from &amp;ldquo;@std/fmt/colors&amp;rdquo;;&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;@cliffy/ansi에서는 색상 적용을 ${colors.bold.green(&quot;deno task start&quot;)} 와 같이 . 체이닝을 적용해줬었는데, @std/fmt/colors를 적용해줌에 따라 ${colors.bold(colors.green(&quot;deno task start&quot;))} 다음과 같이 괄호로 감싸주는 형태로 바꿔주었다.&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;코드를 적용하니 inbox 명령어에서는 터미널이 아닐 때 색상이 빠져서 출력이 되었다. 하지만, lookup 명령어에서는 여전히 색상이 포함되어서 출력이 되었다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;deno task cli inbox&amp;rsquo; 명령어 실행 시&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mHUVz/btsPIYRPD6r/JoqecerNqcWBvY675LTUl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mHUVz/btsPIYRPD6r/JoqecerNqcWBvY675LTUl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mHUVz/btsPIYRPD6r/JoqecerNqcWBvY675LTUl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmHUVz%2FbtsPIYRPD6r%2FJoqecerNqcWBvY675LTUl1%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;400&quot; height=&quot;78&quot; data-origin-width=&quot;1460&quot; data-origin-height=&quot;284&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;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;deno task cli inbox &amp;gt; inbox.txt&amp;rsquo; 실행 시&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjEcjf/btsPJDmp4rn/kaJTnTRTnW5oViHZcLK8xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjEcjf/btsPJDmp4rn/kaJTnTRTnW5oViHZcLK8xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjEcjf/btsPJDmp4rn/kaJTnTRTnW5oViHZcLK8xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjEcjf%2FbtsPJDmp4rn%2FkaJTnTRTnW5oViHZcLK8xk%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;400&quot; height=&quot;142&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;324&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;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;deno task cli lookup (handle) &amp;gt; lookup.txt&amp;rsquo; 실행&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;894&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l0SSX/btsPF13ytbq/IJf6KPcGMo7NuQkzGMqwnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l0SSX/btsPF13ytbq/IJf6KPcGMo7NuQkzGMqwnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l0SSX/btsPF13ytbq/IJf6KPcGMo7NuQkzGMqwnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl0SSX%2FbtsPF13ytbq%2FIJf6KPcGMo7NuQkzGMqwnK%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;400&quot; height=&quot;237&quot; data-origin-width=&quot;1508&quot; data-origin-height=&quot;894&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;@std/fmt/colors의 setColorEnabled()를 활용하여 색상을 제어했지만, Deno의 console.log 자체에서 색상을 조절하는 것은 제어에 실패한 것 같았다. 해결점을 찾지 못한채, 지금까지의 변경점을 토대로 PR을 작성하고, push했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 피드백을 바탕으로 한 두 번째 시도&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MjrGQ/btsPIenqsm8/WEpzbUW6DKZWcDgk9s5Wb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MjrGQ/btsPIenqsm8/WEpzbUW6DKZWcDgk9s5Wb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MjrGQ/btsPIenqsm8/WEpzbUW6DKZWcDgk9s5Wb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMjrGQ%2FbtsPIenqsm8%2FWEpzbUW6DKZWcDgk9s5Wb0%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;400&quot; height=&quot;68&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;274&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;PR을 올리자 멘토님께서는 Deno.inspect로 console.log의 색상을 조절할 수 있다고 말씀해주셨다. 말씀해주신 내용을 바탕으로 헬퍼 함수를 만들었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;export function formatObjectForOutput(obj: unknown): string {
  if (colorEnabled) {
    return Deno.inspect(obj, { colors: true });
  } else {
    return Deno.inspect(obj, { colors: false });
  }
}
&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;colorEnabled 여부에 따라 Deno.inspect를 활용하여 색상 출력 여부를 결정했다.&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;그리고 감사하게도 다른 멘티분께서 리뷰를 해주셨는데, 터미널에서 뿐만 아니라 lookup.ts에서 &lt;b&gt;&amp;lsquo;-o&amp;rsquo; 명령어&lt;/b&gt;를 통해서 options.output이 실행되었을 때에도 색상을 빼야한다는 사실을 알려주셨다.&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;1682&quot; data-origin-height=&quot;1514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBiY9I/btsPJaSjMj2/K6N7rWd0vQ501aVSD5WE90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBiY9I/btsPJaSjMj2/K6N7rWd0vQ501aVSD5WE90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBiY9I/btsPJaSjMj2/K6N7rWd0vQ501aVSD5WE90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBiY9I%2FbtsPJaSjMj2%2FK6N7rWd0vQ501aVSD5WE90%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;400&quot; height=&quot;360&quot; data-origin-width=&quot;1682&quot; data-origin-height=&quot;1514&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;기존 lookup.ts에서는 output option이 선택되었는지에 따라 색상을 제어했었는데, 거기에 colorEnabled 여부도 같이 고려해서 헬퍼 함수를 사용했다.&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;1734&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biogh6/btsPJtKSDKV/m636gY9Jg0YRGOBZ6akQsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biogh6/btsPJtKSDKV/m636gY9Jg0YRGOBZ6akQsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biogh6/btsPJtKSDKV/m636gY9Jg0YRGOBZ6akQsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbiogh6%2FbtsPJtKSDKV%2Fm636gY9Jg0YRGOBZ6akQsK%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;400&quot; height=&quot;50&quot; data-origin-width=&quot;1734&quot; data-origin-height=&quot;216&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;lookup에서 output 옵션 선택 여부를 헬퍼 함수에 전달하기 위해 파라미터에 colors를 받을 수 있도록 구조를 다음과 같이 변경했다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export const colorEnabled: boolean = Deno.stdout.isTerminal() &amp;amp;&amp;amp;
  !Deno.env.has(&quot;NO_COLOR&quot;);

export function formatCliObjectOutputWithColor(
  obj: unknown,
  colors?: boolean,
): string {
  const enableColors = colors ?? colorEnabled;
  return Deno.inspect(obj, { colors: enableColors });
}
&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;참고로, colorEnabled와 헬퍼 함수는 CLI의 시작점인 mod.ts에 위치하고 있었는데 테스트를 돌려보니 circular dependency 문제가 발생해서 utils.ts로 코드들을 옮겼다.&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;1422&quot; data-origin-height=&quot;1246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDaid/btsPIda0bIW/04EBkRd5mFb1dSTPkjkew0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDaid/btsPIda0bIW/04EBkRd5mFb1dSTPkjkew0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDaid/btsPIda0bIW/04EBkRd5mFb1dSTPkjkew0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDaid%2FbtsPIda0bIW%2F04EBkRd5mFb1dSTPkjkew0%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;400&quot; height=&quot;350&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;1246&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;해당 코드를 적용하고 다시 'deno task cli lookup (handle) &amp;gt; lookup.txt&amp;rsquo; 명령어를 적용했더니 색상이 빠진 채 출력이 되었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI의 시작점에서 사용자가 터미널인지 확인한 후 색상 옵션만 끄면 해결되리라 기대했지만, 생각보다 건드릴 부분이 많았던 이슈였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CLI에서 어떤 라이브러리를 활용하여 색상을 제어하는지, 그리고 Fedify의 각 명령어가 어떻게 작동하는지 구조를 살펴볼 수 있었던 좋은 기회였다.&lt;/p&gt;</description>
      <category>Fedify</category>
      <category>fedify</category>
      <category>오픈소스</category>
      <category>오픈소스컨트리뷰션</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/185</guid>
      <comments>https://quickchabun.tistory.com/185#entry185comment</comments>
      <pubDate>Tue, 5 Aug 2025 23:44:38 +0900</pubDate>
    </item>
    <item>
      <title>[Fedify] 첫 오픈소스 기여를 해보자</title>
      <link>https://quickchabun.tistory.com/184</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스 컨트리뷰션 아카데미 발대식이 끝나고, Fedify에 기여를 해보기로 다짐했다. Fedify의 Issue 탭에 들어가 내가 시작해볼만한 이슈가 있는지 찾아보았는데, &amp;lsquo;good first issue&amp;rsquo; 태그가 붙어있는 이슈들을 발견했다. 그 중 괜찮아 보이는 이슈를 발견해서 댓글로 이 이슈를 맡고 싶다고 했다.&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;1602&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dARvXj/btsPrhxzntl/9GHr5MlHGXi9KRzdgc8ncK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dARvXj/btsPrhxzntl/9GHr5MlHGXi9KRzdgc8ncK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dARvXj/btsPrhxzntl/9GHr5MlHGXi9KRzdgc8ncK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdARvXj%2FbtsPrhxzntl%2F9GHr5MlHGXi9KRzdgc8ncK%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;500&quot; height=&quot;173&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;554&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0zrgh/btsPrpvjmpz/iWQ62k6yQ1KHze38ry9gm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0zrgh/btsPrpvjmpz/iWQ62k6yQ1KHze38ry9gm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0zrgh/btsPrpvjmpz/iWQ62k6yQ1KHze38ry9gm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0zrgh%2FbtsPrpvjmpz%2FiWQ62k6yQ1KHze38ry9gm1%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;500&quot; height=&quot;69&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;192&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 먼저 CONTRIBUTING.md를 읽어보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발대식에서 오픈소스 기여자들을 위한 문서가 있다는 사실을 알게 되었다. 바로 CONTRIBUTING.md인데, 들어가자마자 전부 영어로 된 문서에 잠시 긴장을 하긴했지만 어렵지 않게 작성되어 있어서 다행이었다.&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;a href=&quot;https://github.com/fedify-dev/fedify/blob/main/CONTRIBUTING.md&quot;&gt;https://github.com/fedify-dev/fedify/blob/main/CONTRIBUTING.md&lt;/a&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;코드 및 문서 변경 전에는 &amp;lsquo;deno task codegen&amp;rsquo;으로 코드를 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;PR을 올리기 전에는 &amp;lsquo;deno task check-all&amp;rsquo; 명령어로 타입 체크와 포맷팅 검사를 할 수 있다.&lt;/li&gt;
&lt;li&gt;&amp;lsquo;deno task test&amp;rsquo;로 테스트로 패키지, 런타임별 테스트를 할 수 있다.&lt;/li&gt;
&lt;li&gt;&amp;lsquo;deno fmt&amp;rsquo;로 포맷팅을 할 수 있다.&lt;/li&gt;
&lt;/ol&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;h2 data-ke-size=&quot;size26&quot;&gt;2. 문제를 파악해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Issue   &lt;a href=&quot;https://github.com/fedify-dev/fedify/issues/262&quot;&gt;https://github.com/fedify-dev/fedify/issues/262&lt;/a&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;&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;1662&quot; data-origin-height=&quot;1640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FjlTY/btsPpjjp1UM/EX6shVTjwSHtvklXWwanQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FjlTY/btsPpjjp1UM/EX6shVTjwSHtvklXWwanQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FjlTY/btsPpjjp1UM/EX6shVTjwSHtvklXWwanQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFjlTY%2FbtsPpjjp1UM%2FEX6shVTjwSHtvklXWwanQk%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;500&quot; height=&quot;493&quot; data-origin-width=&quot;1662&quot; data-origin-height=&quot;1640&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;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;fedify inbox&amp;rsquo; 명령어를 입력했으르 때, actor의 이름이 &amp;lsquo;Fedify Ephemeral Inbox&amp;rsquo;로 하드코딩되어 생성된다. 사용자가 원하는 이름을 사용하고 요약을 작성할 수 있도록 cli/inbox.tsx에 &amp;lsquo;&amp;mdash;actor-name&amp;rsquo; 명령어와 &amp;lsquo;&amp;mdash;actor-summary&amp;rsquo; 명령어 옵션을 추가하면 된다. 이 때, 사용자가 아무것도 작성하지 않으면 기본 값을 제공하면 된다.&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;3. 코드를 구현하고 PR을 생성해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, cli/inbox.tsx에 &amp;lsquo;&amp;mdash;actor-name&amp;rsquo;과 &amp;lsquo;&amp;mdash;actor-summary 옵션을 생성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXnIp7/btsPqmfhzVH/Ut4qZdsEre1jIIl11DIvK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXnIp7/btsPqmfhzVH/Ut4qZdsEre1jIIl11DIvK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXnIp7/btsPqmfhzVH/Ut4qZdsEre1jIIl11DIvK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXnIp7%2FbtsPqmfhzVH%2FUt4qZdsEre1jIIl11DIvK0%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;500&quot; height=&quot;248&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;810&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;그리고 actorOptions 전역 변수를 생성하고, action 실행시 사용자가 입력한 actorName과 actorSummary를 actorOptions에 저장했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oixkI/btsPqTjoOyK/kcdk35zK5PkG8YKNeikH40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oixkI/btsPqTjoOyK/kcdk35zK5PkG8YKNeikH40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oixkI/btsPqTjoOyK/kcdk35zK5PkG8YKNeikH40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoixkI%2FbtsPqTjoOyK%2Fkcdk35zK5PkG8YKNeikH40%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;500&quot; height=&quot;290&quot; data-origin-width=&quot;1644&quot; data-origin-height=&quot;954&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;그리고, Application을 return할 때, name과 summary에 actorOptions의 값을 넣어줬다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;718&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9raGX/btsPruXIVij/h26T0PoVdr7CWZQIzhk5y1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9raGX/btsPruXIVij/h26T0PoVdr7CWZQIzhk5y1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9raGX/btsPruXIVij/h26T0PoVdr7CWZQIzhk5y1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9raGX%2FbtsPruXIVij%2Fh26T0PoVdr7CWZQIzhk5y1%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;500&quot; height=&quot;222&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;718&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;마지막으로, CHANGES.md에 업데이트 내용을 작성했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;1244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRUQtC/btsProXvO1Y/yhz8C5YbX6fg4WbZygvYz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRUQtC/btsProXvO1Y/yhz8C5YbX6fg4WbZygvYz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRUQtC/btsProXvO1Y/yhz8C5YbX6fg4WbZygvYz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRUQtC%2FbtsProXvO1Y%2Fyhz8C5YbX6fg4WbZygvYz0%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;500&quot; height=&quot;384&quot; data-origin-width=&quot;1618&quot; data-origin-height=&quot;1244&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;이제 문제는 PR을 올리는건데, 어떤 양식으로 올려야할지 몰라 다른 분들의 PR을 참고해봤는데 특별한 양식은 없는 것 같았다. 그래도 일정한 양식을 적용하는게 좋을 것 같아 다른 분의 양식을 참고했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;1580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOdLro/btsPrhj1Rol/Aqg42LaX861cn3dJrJ0mJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOdLro/btsPrhj1Rol/Aqg42LaX861cn3dJrJ0mJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOdLro/btsPrhj1Rol/Aqg42LaX861cn3dJrJ0mJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOdLro%2FbtsPrhj1Rol%2FAqg42LaX861cn3dJrJ0mJ1%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;500&quot; height=&quot;702&quot; data-origin-width=&quot;1126&quot; data-origin-height=&quot;1580&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무엇이 바뀌었고, 이슈에서 요구된 조건은 다 충족했는지, 그리고 테스트 결과는 어떤지 작성해서 올렸다. 테스트도 다 통과되고 큰 문제 없이 통과되지 않을까하는 기대를 품고, 첫 오픈소스 기여 PR을 업로드했다.&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;4. 멘토님과 Gemini의 피드백&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR을 올리자 Gemini가 먼저 피드백을 해주었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;1028&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lPC8N/btsPp1Cuexb/KwCiRPXZbHEtPQUk5dFDgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lPC8N/btsPp1Cuexb/KwCiRPXZbHEtPQUk5dFDgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lPC8N/btsPp1Cuexb/KwCiRPXZbHEtPQUk5dFDgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlPC8N%2FbtsPp1Cuexb%2FKwCiRPXZbHEtPQUk5dFDgk%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;500&quot; height=&quot;391&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;1028&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;먼저 PR이 어떤 내용인지 짧게 요약을 해주었다. PR을 AI가 요약해준다니 새삼 기술 발전이 빠르다는걸 실감했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LrKoS/btsPpzfqx4w/0GjqhwcsxCXeU52AnmkMLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LrKoS/btsPpzfqx4w/0GjqhwcsxCXeU52AnmkMLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LrKoS/btsPpzfqx4w/0GjqhwcsxCXeU52AnmkMLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLrKoS%2FbtsPpzfqx4w%2F0GjqhwcsxCXeU52AnmkMLk%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;500&quot; height=&quot;324&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Gemini는 전역변수 사용에 문제가 있음을 알려주었다. 여러 번 사용될 때 의도하지 않은 행동을 이끌 수 있다는 관점은 코드를 작성할 때 생각하지 못했었다.&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;그리고 얼마 지나지 않아 멘토님이 리뷰를 해주셨다. Gemini가 지적한 문제에 공감을 해주셨고, CHANGES.md에 이슈 링크 뿐만 아니라, PR 링크도 추가해주실 것을 요청해주셨다.&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;5. 전역변수를 없애보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역변수를 없애자는 피드백을 받았다. 전역변수를 사용하지 않는다면, 어떻게 값을 전달할 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cli/inbox.tsx의 코드를 다시 살펴보니 fedCtx라는 컨텍스트를 사용하고 있었고, 이 컨텍스트에 actorName과 actorSummary를 담아서 제공하면 되겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcXGaI/btsPrhRP3WJ/qUPpgb6dABtKbp6gAZjJOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcXGaI/btsPrhRP3WJ/qUPpgb6dABtKbp6gAZjJOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcXGaI/btsPrhRP3WJ/qUPpgb6dABtKbp6gAZjJOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcXGaI%2FbtsPrhRP3WJ%2FqUPpgb6dABtKbp6gAZjJOK%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;500&quot; height=&quot;625&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;1462&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;그런데 기존 함수들이 타입 때문에 actorName과 actorSummary 인식하지 못했고, 기존 함수를 actorName과 actorSummary를 인식할 수 있는 새로운 함수로 감싸는 function factory 방식을 사용하게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;1672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAmkUZ/btsPrbcX2mt/Xl8uEtAorbalCQKeXZK470/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAmkUZ/btsPrbcX2mt/Xl8uEtAorbalCQKeXZK470/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAmkUZ/btsPrbcX2mt/Xl8uEtAorbalCQKeXZK470/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAmkUZ%2FbtsPrbcX2mt%2FXl8uEtAorbalCQKeXZK470%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;500&quot; height=&quot;807&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;1672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 CHANGES.md를 업데이트하고, 새로 커밋을 push하게 되었다.&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;6. conflict 발생과 git rebase&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dd0hBC/btsPqJH5XMd/wwIbtFaBiJhZWdG1hS5bD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dd0hBC/btsPqJH5XMd/wwIbtFaBiJhZWdG1hS5bD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dd0hBC/btsPqJH5XMd/wwIbtFaBiJhZWdG1hS5bD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdd0hBC%2FbtsPqJH5XMd%2FwwIbtFaBiJhZWdG1hS5bD0%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;500&quot; height=&quot;90&quot; data-origin-width=&quot;1322&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PR이 열려있던 와중 main이 업데이트되었다. 그래서 conflict가 발생했고, 멘토님께서는 main 브랜치 rebase를 요청하셨다. git rebase의 개념은 알고 있었지만 지금까지는 그냥 merge를 하거나 git reset을 사용했었기에, 실제로 사용한 적이 거의 없었다. 이번 기회에 git rebase를 사용했는데 다행히 충돌이 발생한 부분이 많지 않아서 고치고 &amp;lsquo;git rebase &amp;mdash;continue&amp;rsquo; 명령어를 입력하고를 반복하다 보니 문제를 해결할 수 있었다.&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;1324&quot; data-origin-height=&quot;1040&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DyN2z/btsPpx22sDf/iaSEPlkw8jcg9dijSj3xY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DyN2z/btsPpx22sDf/iaSEPlkw8jcg9dijSj3xY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DyN2z/btsPpx22sDf/iaSEPlkw8jcg9dijSj3xY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDyN2z%2FbtsPpx22sDf%2FiaSEPlkw8jcg9dijSj3xY1%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;500&quot; height=&quot;393&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;1040&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;copilot이 지적한 또 다른 내용도 수정하고, 코드들을 force-push했다 (rebase이므로)&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;7. 드디어 첫 기여를 했다&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E8vmO/btsPpXf0SDw/WtMN11k3bTGS1PWjRVZNbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E8vmO/btsPpXf0SDw/WtMN11k3bTGS1PWjRVZNbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E8vmO/btsPpXf0SDw/WtMN11k3bTGS1PWjRVZNbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE8vmO%2FbtsPpXf0SDw%2FWtMN11k3bTGS1PWjRVZNbk%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;500&quot; height=&quot;252&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;644&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 멘토님께서 approve를 해주셨다! 조금 더 좋은 추상화가 있지 않을까라는 고민이 남긴했지만 나 또한 더 좋은 방법이 당장 떠오르지 않았다.&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;Pull Request  &amp;nbsp;&lt;a href=&quot;https://github.com/fedify-dev/fedify/pull/285&quot;&gt;https://github.com/fedify-dev/fedify/pull/285&lt;/a&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;이렇게 나의 첫 번째 오픈소스 컨트리뷰션이 마무리되었다. 드디어 나도 오픈소스 컨트리뷰터! 우여곡절이 살짝 있긴했지만 작성한 코드가 성공적으로 main에 merge될 수 있었다. 생각보다 영어의 압박이 세긴했지만, 그래도 LLM과 함께하니 큰 어려움 없이 글을 작성할 수 있었다. 시작이 반절이라고 하는데, 다행히 반절을 채웠으니 이제 남은 반절을 열심히 채워보자.&lt;/p&gt;</description>
      <category>Fedify</category>
      <category>fedify</category>
      <category>ossca</category>
      <category>오픈소스</category>
      <category>오픈소스 컨트리뷰션</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/184</guid>
      <comments>https://quickchabun.tistory.com/184#entry184comment</comments>
      <pubDate>Sun, 20 Jul 2025 17:24:32 +0900</pubDate>
    </item>
    <item>
      <title>[Fedify] 튜토리얼을 따라하며 나만의 연합우주 마이크로블로그를 만들자</title>
      <link>https://quickchabun.tistory.com/183</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;정말 감사하게도 2025 오픈소스 컨트리뷰션 참여형 프로젝트 fedify의 멘티로 참여하게 되었다. 참여가 결정된 날, 멘토님으로부터 fedify에 대한 설명이 담긴 메일을 받았다. 첨부된 링크에는 fedify에 대한 설명과 함께 fedify를 활용하여 마이크로블로그를 만드는 튜토리얼을 볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;a href=&quot;https://hackers.pub/@hongminhee/2025/fedify-tutorial-ko&quot;&gt;https://hackers.pub/@hongminhee/2025/fedify-tutorial-ko&lt;/a&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;fedify에 대한 사전지식이 많지 않았기 때문에 잘 따라갈 수 있을까 걱정했지만 설명이 매우 자세하게 써져있었기 때문에 어렵지 않게 따라갈 수 있었다. 튜토리얼을 진행하며 fedify와 연합우주 그리고 개발에 필요한 내용들을 알 수 있게 되었고, 이 지식들은 앞으로 오픈소스 컨트리뷰션을 할 때 많은 도움이 될 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜토리얼을 진행하며 새롭게 알게 된 내용들이 많은데, 그 중 일부를 짧게 정리해보았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 웹 프레임워크 Hono&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜토리얼을 진행할 때 웹 프레임워크 Hono를 활용했는데, 제너릭 타입을 선언할 때 Hono의 FC를 활용했다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;import type { FC } from &quot;hono/jsx&quot;;

export interface FollowerListProps {
  followers: Actor[];
}

export const FollowerList: FC&amp;lt;FollowerListProps&amp;gt; = ({ followers }) =&amp;gt; (
  &amp;lt;&amp;gt;
    &amp;lt;h2&amp;gt;Followers&amp;lt;/h2&amp;gt;
    &amp;lt;ul&amp;gt;
      {followers.map((follower) =&amp;gt; (
        &amp;lt;li key={follower.id}&amp;gt;
          &amp;lt;ActorLink actor={follower} /&amp;gt;
        &amp;lt;/li&amp;gt;
      ))}
    &amp;lt;/ul&amp;gt;
  &amp;lt;/&amp;gt;
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 방식으로 타입 인자들을 FC를 활용하여 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 FC를 쓰지 않았다면, 함수 인자의 props 타입을 직접 지정했을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ( { followers }: FollowerListProps) )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 프레임워크 Hono를 사용하면 좋은 점이 무엇인가 찾아보니, 다음과 같은 장점이 있다고 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;초경량, 초고속 런타임 지원 &amp;rarr; Cloudflare Workers, Deno, Bun, Node.js 등 다양한 환경에서 거의 동일한 코드를 실행 가능&lt;/li&gt;
&lt;li&gt;서버리스/엣지 친화적 설계 &amp;rarr; 서버리스 환경에 최적화되어 배포, 실행, 스케일링이 매우 쉽고 빠름&lt;/li&gt;
&lt;li&gt;미들웨어와 라우팅 &amp;rarr; Express 스타일의 미들웨어와 라우팅을 매우 가볍고 빠르게 구현 가능&lt;/li&gt;
&lt;li&gt;TypeScript 친화적&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. sqlite3&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 맥북을 사용하면서 sqlite3가 맥북에 내장되어 있는 줄은 몰랐는데, 설치를 하지 않아도 sqlite3를 사용할 수 있었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마 선언은 src/schema.sql에서 진행했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE IF NOT EXISTS users (
  id       INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
  username TEXT    NOT NULL UNIQUE      CHECK (trim(lower(username)) = username
                                               AND username &amp;lt;&amp;gt; ''
                                               AND length(username) &amp;lt;= 50)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 스키마에서 AND username &amp;lt;&amp;gt; '' 은 username 값이 빈 문자열이 아니어야 한다는 조건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&amp;lt;&amp;gt; &amp;rarr; 같지 않다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 터미널에서 &amp;lsquo;sqlite3 microblog.sqlite3 &amp;lt; src/schema.sql&amp;rsquo; 명령어를 통하여 데이터베이스 파일을 생성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 &amp;lsquo;&amp;lt; src/schema.sql&amp;rsquo;은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리다이렉션 연산자(&amp;lt;)를 사용해, src/schema.sql 파일에 저장된 SQL 명령문들을 표준 입력으로 전달 &amp;rarr; schema.sql 파일 안의 모든 SQL 명령(예: 테이블 생성, 인덱스 생성 등)이 차례로 실행된다는 뜻이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. db.pragma&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;src/db.ts에서 db.pragma 함수를 활용한다. pragma는 SQLite에서만 지원하는 특별한 명령어로, 데이터베이스의 동작 방식이나 내부 설정을 조회/변경할 때 사용한다고 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import Database from &quot;better-sqlite3&quot;;

const db = new Database(&quot;microblog.sqlite3&quot;);
db.pragma(&quot;journal_mode = WAL&quot;);
db.pragma(&quot;foreign_keys = ON&quot;);

export default db;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜토리얼에서는 다음과 같이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;journal_mode = WAL&amp;rsquo;은 로그 선행 기입 모드(데이터베이스의 변경사항을 먼저 로그 파일에 기록한 뒤, 나중에 실제 데이터 파일에 반영하는 방식)를 채택할 때 활용하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;foreign_keys = ON&amp;rsquo;은 외래 키 제약 조건을 검사할 때 활용한다고 한다.&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;rarr; 한 테이블의 특정 컬럼(외래 키)이 다른 테이블의 기본 키(또는 유니크 키)를 참조하도록 하여, 존재하지 않는 값을 참조하는 것을 방지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시(by perplexity)&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE TABLE artist (
  artistid INTEGER PRIMARY KEY,
  artistname TEXT
);

CREATE TABLE track (
  trackid INTEGER,
  trackname TEXT,
  trackartist INTEGER,
  FOREIGN KEY(trackartist) REFERENCES artist(artistid)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;track 테이블의 trackartist 칼럼은 artist 테이블의 artistId를 참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; track 테이블에 값을 넣을 때, 해당 trackartist 값이 반드시 artist 테이블에 존재해야&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. db.prepare&lt;/h2&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;db.prepare(&quot;INSERT INTO users (username) VALUES (?)&quot;).run(username);
&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;튜토리얼의 많은 부분에서 db.parpare()를 사용하는데, .prepare()가 무슨 역할을 하는지 궁금했다.&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;db.prepare()는 better-sqlite3 패키지에서 제공하는 메서드인데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.prepqre(sql)은 주어진 SQL 쿼리를 준비된 문장 객체로 컴파일하고, 이 객체는 .get(), .run(), .all(), .iterate() 같은 메서드를 통해 실행된다.&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;.get() &amp;rarr; 하나의 행 조회,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.all() &amp;rarr; 여러 행을 배열로 조회,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.run() &amp;rarr; INSERT, UPDATE, DELETE 등 쓰기,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.iterate() &amp;rarr; 반복자로 결과 처리를 수행한다.&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;prepare()는 SQL을 한 번만 파싱하고 컴파일해두어서 다음 실행 시 더 빠르게 수행이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 파라미터 바인딩을 사용하여 SQL Injection을 방지할 수 있다. 실제로 튜토리얼의 많은 부분에서 SQL에 ? 표시가 있길래 무슨 뜻인지 궁금했는데, 파라미터 바인딩을 사용한 것이다.&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;5. 파라미터 바인딩&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;users 테이블에서 username 컬럼의 값이 특정 username과 일치하는 행을 조회하는 SQL문을 작성한다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파라미터 바인딩을 쓰지 않는다면,&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;const username = &quot;'; DROP TABLE users; --&quot;;
const sql = &quot;SELECT * FROM users WHERE username = '&quot; + username + &quot;'&quot;;
// username 값이 그대로 SQL문에 들어가 위험함 (SQL Injection 공격 위험)

&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;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const sql = &quot;SELECT * FROM users WHERE username = ?&quot;;
db.prepare(sql).get(username);
// ? 위치에 username 값을 안전하게 &quot;끼워넣음&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;? 자리가 파라미터 바인딩이고, 값은 SQL문과 분리해서 DB 엔진이 따로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 파라미터 바인딩은 쿼리문에서 값이 들어갈 자리를 미리 정해놓고, 실행할 때 값을 따로 넘기는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 입력한 값으로 인해 SQL 코드로 오동작되는 걸 막을 수 있고, DB가 쿼리와 값을 분리해서 처리하므로 쿼리 구조를 망치지 않기 때문에 효율적이다.&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;6. 아무리 핸들을 검색해도 결과가 뜨지 않았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액터를 만들고 &amp;lsquo;fedify tunnel 8000&amp;rsquo; 명령어를 통해 인터넷에 로컬 서버를 노출하여 테스트할 수 있다. 그리고 &amp;lsquo;터미널에 나오는 URL/users/아이디&amp;rsquo;로 들어가면 연합우주 핸들이 나오는데, 그 핸들을 복사해서 마스토돈에 검색하면 액터가 보여야된다.&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;&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;2048&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJw8Yf/btsPrp9WSYz/1A0IXAc84A5DASosClRU11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJw8Yf/btsPrp9WSYz/1A0IXAc84A5DASosClRU11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJw8Yf/btsPrp9WSYz/1A0IXAc84A5DASosClRU11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJw8Yf%2FbtsPrp9WSYz%2F1A0IXAc84A5DASosClRU11%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;2048&quot; height=&quot;643&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;643&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;&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;684&quot; data-origin-height=&quot;84&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KRipw/btsPqSSfFFp/mjFOkrQ8V3z2odDoSzQDr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KRipw/btsPqSSfFFp/mjFOkrQ8V3z2odDoSzQDr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KRipw/btsPqSSfFFp/mjFOkrQ8V3z2odDoSzQDr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKRipw%2FbtsPqSSfFFp%2FmjFOkrQ8V3z2odDoSzQDr1%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;684&quot; height=&quot;84&quot; data-origin-width=&quot;684&quot; data-origin-height=&quot;84&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 튜토리얼을 끝마치고&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차근차근 튜토리얼을 보면서 코드를 작성한 결과 Fedify를 활용한 간단한 마이크로블로그를 만들어낼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  &lt;a href=&quot;https://github.com/crohasang/fedify-tutorial&quot;&gt;https://github.com/crohasang/fedify-tutorial&lt;/a&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;튜토리얼을 진행하며 만들어낸 마이크로블로그의 플로우를 AI를 활용하여 요약해보았다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 프로젝트 시작 및 기반 환경&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 구동: package.json 스크립트 실행 &amp;rarr; src/index.ts에서 Hono 서버 시작.&lt;/li&gt;
&lt;li&gt;DB 연결: src/db.ts에서 microblog.sqlite3 (DB 파일) 연결 및 초기화.&lt;/li&gt;
&lt;li&gt;스키마 정의: src/schema.sql로 DB 테이블(users, actors, posts, follows, keys) 구조화. src/schema.ts로 타입스크립트 인터페이스 제공.&lt;/li&gt;
&lt;li&gt;로깅: src/logging.ts에서 애플리케이션 로그 설정.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 사용자 및 웹 인터페이스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;초기 설정: 사용자 없을 시 /setup으로 리다이렉트 &amp;rarr; src/views.tsx의 SetupForm 렌더링.&lt;/li&gt;
&lt;li&gt;계정 생성 (POST /setup): src/app.tsx 처리 &amp;rarr; 사용자 이름 검증 &amp;rarr; users, actors 테이블에 정보 저장.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;src/federation.ts를 통해 ActivityPub 액터 URI 및 암호화 키 쌍(keys 테이블) 생성/저장.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메인 피드 (GET /): src/app.tsx에서 사용자 정보, 팔로잉 게시물 조회 &amp;rarr; src/views.tsx의 Home 렌더링.&lt;/li&gt;
&lt;li&gt;프로필/목록 조회: src/app.tsx의 라우트(/users/:username, /users/:username/followers, /users/:username/following, /users/:username/posts/:id) 처리 &amp;rarr; DB 조회 &amp;rarr; src/views.tsx의 관련 컴포넌트(Profile, FollowerList, FollowingList, PostPage, PostList) 렌더링.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 게시물 작성 및 Fediverse 전파&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게시물 작성 (POST /users/:username/posts): src/app.tsx 처리 &amp;rarr; 내용 검증 &amp;rarr; DB posts 테이블에 저장.&lt;/li&gt;
&lt;li&gt;ActivityPub Create: src/app.tsx에서 src/federation.ts의 fedi 인스턴스 사용 &amp;rarr; 게시물(Note)에 대한 Create 액티비티 생성 및 팔로워들에게 전송.&lt;/li&gt;
&lt;li&gt;외부 서버 수신: 외부 Fediverse 서버들이 Create 액티비티를 받아 게시물을 피드에 반영.&lt;/li&gt;
&lt;/ul&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. 팔로우/언팔로우 및 Fediverse 연동&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팔로우 요청 (POST /users/:username/following): src/app.tsx 처리 &amp;rarr; 입력된 액터 조회 &amp;rarr; src/federation.ts의 fedi 인스턴스 사용 &amp;rarr; Follow 액티비티 생성 및 대상 액터에게 전송.&lt;/li&gt;
&lt;li&gt;외부 Follow 수신: src/federation.ts 인박스 리스너 &amp;rarr; Follow 액티비티 수신 &amp;rarr; DB actors, follows 테이블에 팔로워 정보 및 관계 저장 &amp;rarr; Accept 액티비티로 응답.&lt;/li&gt;
&lt;li&gt;Undo 수신: src/federation.ts 인박스 리스너 &amp;rarr; Undo 액티비티 수신 &amp;rarr; follows 테이블에서 관계 제거.&lt;/li&gt;
&lt;li&gt;Accept 수신: src/federation.ts 인박스 리스너 &amp;rarr; Accept 액티비티 수신 &amp;rarr; follows 테이블에 관계 기록 (팔로우 수락).&lt;/li&gt;
&lt;li&gt;외부 Create 수신: src/federation.ts 인박스 리스너 &amp;rarr; Create 액티비티 수신 (게시물 Note 객체) &amp;rarr; DB posts 테이블에 게시물 저장.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 튜토리얼을 통해 ActivityPub, Fediverse, Fedify가 어떻게 동작하는지 간단하게나마 파악할 수 있었다. Fedify 프레임워크를 통해 ActivityPub 연동을 하면 간단하게 Fediverse의 일원이 될 수 있다. 튜토리얼을 따라하며 Actor를 생성하고, Dispatcher를 통해 Actor의 프로필을 외부에 제공하고, 다른 Fediverse actor의 팔로우 요청이나 게시물 활동 등을 수신하고 처리하는 흐름을 익힐 수 있었다.&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;이제 Fedify 기여를 시작할 차례다. 오픈소스에 기여하는 것은 처음이라 낯설지만, 아무 준비 없이 들이받는게 아니라 OSSCA라는 프로그램 안에서 진행하므로 크게 어렵진 않을 것 같다. 시작해보자.&lt;/p&gt;</description>
      <category>Fedify</category>
      <category>fedify</category>
      <category>fediverse</category>
      <category>ossca</category>
      <category>오픈소스컨트리뷰션</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/183</guid>
      <comments>https://quickchabun.tistory.com/183#entry183comment</comments>
      <pubDate>Sun, 20 Jul 2025 15:21:06 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js 강의 정리] Call stack, Event Loop, EventEmitter, child_process, cluster, worker_threads 등에 관하여</title>
      <link>https://quickchabun.tistory.com/182</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;얄코님의 &lt;b&gt;'얄코의 Node.js (Korean ver.)'&lt;/b&gt; 강의를 듣고 정리한 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 링크: &lt;a href=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot;&gt;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750341129713&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;얄코의 Node.js (Korean ver.) 강의 | 얄팍한 코딩사전 - 인프런&quot; data-og-description=&quot;얄팍한 코딩사전 | ,   This course is designed for Korean-speaking learners. If you speak English, Japanese, Vietnamese, or any other language, please take t&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bFl8df/hyY79qvDMI/2sQLGgCUb2Bg7LOXkwZz20/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/eNpB8/hyZcnAwnGi/zT7o2XKqFXp5gKU3CptKa0/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/bbWpks/hyY743NRKf/XFZH5IrvKqZrKEVqw6l2B0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bFl8df/hyY79qvDMI/2sQLGgCUb2Bg7LOXkwZz20/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/eNpB8/hyZcnAwnGi/zT7o2XKqFXp5gKU3CptKa0/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/bbWpks/hyY743NRKf/XFZH5IrvKqZrKEVqw6l2B0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&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;얄코의 Node.js (Korean ver.) 강의 | 얄팍한 코딩사전 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;얄팍한 코딩사전 | ,   This course is designed for Korean-speaking learners. If you speak English, Japanese, Vietnamese, or any other language, please take t&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. process&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 기본으로 제공되는 전역 객체, 현재 실행 중인 Node.js 프로세스의 정보와 제어 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pid: 현재 실행 중인 Node.js 프로세스의 ID 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ppid: 현재 프로세스를 실행시킨 부모 프로세스의 ID 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;version: 현재 사용 중인 Node.js의 버전 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;version.v8: Node.js가 사용하는 V8 JavaScript 엔진의 버전 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;arch: 현재 프로세스가 실행되는 CPU 아키텍처를 문자열로 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;platform: Node.js가 실행되는 운영체제 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;execPath: Node.js 실행 파일의 절대 경로 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cwd: 현재 작업 디렉토리의 절대 경로 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cpuUsage: 현재 프로세스의 CPU 사용량을 마이크로초 단위로 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memoryUsage: 현재 프로세스의 메모리 사용량을 바이트 단위로 반환&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 환경변수(Environment Variable)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터의 전체 시스템이나 특정 프로세스에, 실행중인 프로그램이 가져다 쓸 수 있는 값&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;process의 env 속성: 현재 실행 중인 Node.js 프로세스가 접근할 수 있는 환경 변수를 담고 있는 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 환경 변수의 이름을 키로 삼아 접근해서 값을 가져옴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널의 명령어로 설정할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;NODE_ENV = development PORT=3000 DB_HOST=localhost node index.js&lt;/b&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;환경변수들이 해당 터미널 세션에서 유지되도록 하려면 export 등을 통해 여러 줄로 작성한 명령을 입력하면 된다&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export NODE_ENV = development
export PORT = 3000
export DB_HOST = localhost
node index.js&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;터미널 세션 한정 유지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경변수를 프로젝트별로 영구 저장하려면 &lt;b&gt;.env 파일을&lt;/b&gt; 만들어서 저장하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dotenv로 불러올 수 있음(dotenv는 외부 라이브러리)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 실무에서는 관례적으로 NODE_ENV라는 환경변수가 사용됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 &amp;lsquo;node &amp;mdash;env-file=.env index.js를 불러오면 dotenv 패키지 없이도 환경변수를 가져올 수 있음(Node.js 20.6 버전 이후)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. global&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 환경에서 스크립트의 최상위 스코프에 위치한 &amp;lsquo;this&amp;rsquo;는 전역 객체인 &amp;lsquo;window&amp;rsquo;를 가리킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;window&amp;rsquo; &amp;rarr; DOM, JavaScript 전역 변수, 브라우저 API에 접근할 수 있는 인터페이스를 제공하는 객체&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;globalThis &amp;rarr; 환경에 상관없이 전역 객체를 가리키는 표준화된 속성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CommonJS 환경에서 최상위 &amp;lsquo;this&amp;rsquo;는 전역 객체 &amp;lsquo;global&amp;rsquo;을 가르키지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; module.exports 객체를 가리킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그 대신 globalThis가 global을 가리킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; CommonJS에서 var x를 선언하면, 모듈 스코프에 존재&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CommonJS 환경에서 &amp;lsquo;function&amp;rsquo; 키워드를 사용하는 일반 함수의 내부에서는 &amp;lsquo;this&amp;rsquo;가 &amp;lsquo;global&amp;rsquo;을 가리킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표 함수는 &amp;lsquo;this&amp;rsquo;를 렉시컬 스코프에서 가져옴(해당 함수가 선언된 위치의 this를 그대로 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES Module 환경에서 최상위 스코프 &amp;lsquo;this&amp;rsquo;는 &amp;lsquo;undefined&amp;rsquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES Module에서는 모듈이 캡슐화된 독립 스코프를 가지며, &amp;lsquo;this&amp;rsquo;가 특정 객체를 참조하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES Module에서도 globalThis가 global을 가리킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES Module은 기본적으로 Strict Mode로 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Strict Mode에서 일반 함수를 일반 호출로 실행하면 &amp;lsquo;this&amp;rsquo;가 undefined로 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 메서드로 호출된 함수 내부의 this는 호출 방식에 따라 동적으로 결정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;obj 객체의 메서드로 호출되면, &amp;lsquo;this&amp;rsquo;는 &amp;lsquo;obj&amp;rsquo;를 가리킨다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. console&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저와 Node.js의 console 객체는 대부분의 메서드들을 똑같이 가지고 있지만, 동작 환경이 다르기 때문에 내부적인 구현이 서로 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저의 console 객체: Web API의 일부&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 console 객체: Node.js의 core 모듈 중 하나인 &amp;lsquo;console&amp;rsquo; 모듈로 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.log에서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;%o: 객체를 간략한 형태로 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;%O: 객체를 자세히 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;%j: 객체를 JSON 문자열로 변환하여 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.error - 에러 메시지 출력 (process.stderr 스트림으로 출력)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.warn - 경고 메시지 출력 (process.stderr 스트림으로 출력)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.info - 정보성 메시지 출력(process.stdout 스트림으로 출력)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.debug - 디버그 메시지 출력 (process.stdout 스트림으로 출력)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: console.log - process.stdout 스트림으로 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.time - 인자로 주어진 문자열을 이름으로 갖는 타이머 시작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.timeEnd - 인자로 주어진 이름의 타이머를 종료하고 경과 시간 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.count - 특정 함수의 호출 횟수를 추적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.countReset - 인자로 주어진 레이블의 카운터를 0으로 초기화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.trace - 자신이 호출된 함수로부터 시작하여 호출 경로를 역순으로 표시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.assert - 첫번째 인자로 주어진 조건식이 틀렸을 때 두번째 인자로 주어진 메시지 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 앞에 ANSI 코드를 붙여 색상 적용 후 출력하면 해당색으로 표시됨&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;const colors = {
    green: '\\x1b[32m',
    (...)
}

console.log(colors.green + &amp;lsquo;Success!&amp;rsquo; + colors.reset);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 Chalk라는 라이브러리 사용&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Call Stack&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;rarr; 스택에 먼저 쌓임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스택이므로, 가장 나중에 실행되는 함수가, 가장 먼저 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Call Stack에 올라온 항목들을 각각 &amp;lsquo;&lt;b&gt;stack frame&lt;/b&gt;&amp;rsquo;이라 부르는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 함수들의 스코프(해당 함수에서 접근 가능한 데이터들의 범위)가 포함되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Console 탭에서 콜 스택을 살펴보면, 어떤 함수들이 어느 순서대로 호출되는 과정을 보며 어디에서 오류가 발생한 것인지 찾을 수 있음 &amp;rarr; &lt;b&gt;stack trace&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍 언어들에서 오류는 call stack에서 위에서 아래로 전파 &amp;rarr; error bubbling&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Event Loop&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자바스크립트 프로그램이 Node.js 환경에서 동작하는 순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. JavaScript Code&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 실행되면 V8 엔진에 의해 파싱되고, 동기 작업은 즉시 처리, 비동기 작업은 하위 계층으로 전달&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. Call Stack&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기적인 작업이 Call Stack에서 실행&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;3. Node.js API&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 관련 작업, HTTP 요청, 타이머 등의 작업들을 libuv로 넘긴다&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;4. libuv&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js가 운영체제와 소통해서 빠르게 일을 처리하게 도와주는 도구&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(다른 자바스크립트 환경들은 libuv를 쓰지 않고 자체적인 엔진 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;libuv가 파일 열기나 네트워크 요청 등을 운영체제에 요청한 다음 결과를 다시 Node.js에 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; libuv에 의해 비동기 작업들이 백그라운드에서 처리&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;Thread Pool: 멀티쓰레딩으로 비동기 작업들이 처리되는 곳&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Event loop 순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Start&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;2. Execute Synchronous Code&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;3. Process MicroTasks&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Promise의 then 메소드와, process의 nextTick 함수와 같은 마이크로태스크 처리&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;4. Timers Phase&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setTimeout이나 setInterval로 설정된 타이머가 만료되었을 때 콜백 실행&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;5. Pending Phase&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;6.Poll Phase&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;7. Check Phase&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setImmediate 콜백을 실행하여 한 사이클을 마무리한 뒤 다시 4. Timer Phase로 돌아감&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event loop는 마이크로태스크와 각 페이즈를 체크하고, 더 이상 처리할 작업이 없으면 종료&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;태스크 큐: 콜백 함수를 실행할 때를 스케줄링하기 위한 대기열&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;process.nextTick: 작업을 nextTick으로 넘겨주면, 이 작업은 동기 코드들이 모두 실행된 다음 비동기적으로 진행(즉, 비동기 작업 중 가장 먼저 처리)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; nextTick은 비동기 작업들 중 가장 최우선순위를 갖는 Microtask에 해당&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;setImmediate: 콜백을 등록하여, 이벤트 루프의 poll phase가 끝난 뒤 check phase에서 해당 콜백이 실행되도록 함&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. EventEmitter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램 안에서 무언가 중요한 일이 일어났을 때, 다른 부분에 알려주는 역할 수행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(addEventListener, dispatchEvent와 비슷한 역할)&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;내장 모듈인 events로부터 가져오고, emitter 인스턴스를 만들어서 사용&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;on: 첫번째 인자로 주어진 문자열을 이름으로 갖는 이벤트를 등록&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;once: on과 역할은 같지만, 딱 한번만 실행&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;eventNames: emitter에 등록된 이벤트 이름들을 배열로 반환&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;emit: 등록된 이벤트를 발생할 때 실행&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;listeners: 인자로 전달된 이름의 이벤트에 할당된 리스너들을 반환&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;listenerCount: 해당 리스너들의 개수를 반환&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;off: 특정 이벤트 삭제&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;setMaxListeners: 하나의 이벤트에 대한 리스너의 최대 개수를 설정할 수 있는 메서드 (강제할 수는 없고, 경고가 뜸)&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;EventEmitter의 이벤트들에는 매개변수도 사용될 수 있다&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;EventEmitter에서 &amp;lsquo;error&amp;rsquo; 이벤트에 리스너가 등록되어 있지 않으면, EventEmitter는 예외를 발생시키고 프로그램을 종료한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; emitter에 &amp;lsquo;error&amp;rsquo; 이벤트에 대한 리스너를 등록해야&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;EventEmitter를 확장한 커스텀 클래스를 만들었을 때, 이벤트 발생 시 수행할 일들은 클래스 밖에서 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 클래스의 코드에서는 주어진 상황에서 이벤트를 발생시키기만 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이벤트 발생 로직과 이벤트 처리 로직을 분리함으로써, 모듈간의 느슨한 결합과 유연성 확보 가능&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. child_process&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에서 별도의 프로세스를 생성하여 실행할 수 있도록 도와주는 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 현재의 프로그램 내에서 다른 프로그램이나 시스템 명령어를 실행할 수 있도록 해줌&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;exec: 시스템 명령어를 실행하고 결과를 받아올 수 있는 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 인자: 시스템에서 실행할 명령어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 인자: 오류 정보, 명령어의 실행 결과, 명령어 실행 중 표준 에러 출력을 갖는 콜백 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;execFile: 셸을 거치지 않고 직접 명령을 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 인자: 실행 파일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 인자: 옵션들&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;spawn: 새로운 프로세스를 생성하여 스트림 방식으로 실시간 데이터 처리&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;Node.js의 기본 process는 EventEmitter다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;exec&amp;rsquo;과 &amp;lsquo;execFile&amp;rsquo; 함수들이 콜백으로 결과를 한 번에 받는 것과 달리, &amp;lsquo;spawn&amp;rsquo;은 스트림을 통해 실시간으로 데이터 처리&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;fork: spawn이 Node.js 전용으로 특화된 버전&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체의 send 메소드를 통해 자식 프로세스에 메시지를 전달할 수 있다. (자식 프로그램에서 process의 &amp;lsquo;message&amp;rsquo; 이벤트를 통해 수신)&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;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// 부모 (index.js)

import { fork } from 'child_process';

const child = fork('child.js');

for(let i = 1; i &amp;lt;= 5; i++) {
	child.send(i) // 자식 프로세스 5개 생성
}

// 3초 후 자식 프로세스에게 exit라는 메시지를 보냄
setTimeout(() =&amp;gt; {
	child.send('exit');
	// child.kill(); 부모 쪽에서 강제로 자식 프로세스를 종료
}, 3000);

// 부모 프로세스로 자녀가 보낸 메시지가 전달되어 출력
child.on('message', (msg) =&amp;gt; {
	console.log(`Received: ${msg}`);
});

// 자식 프로세스가 종료될 때 close 이벤트 발생
child.on('close', (code) =&amp;gt; {
	console.log(`Child exited (${code})`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;// 자식 (child.js)

// 부모로부터 exit 메시지가 오면 스스로를 종료
process.on('message', (num) =&amp;gt; {
	if (num === 'exit') {
		process.exit(0);
	}
	
	// 자식 프로세스에서는 데이터로 전달된 각 숫자에 0.5를 곱한 초의 시간을 기다린 뒤 응답
	setTimeout(() =&amp;gt; {
		process.send(`Waited ${num}s`);
	}, num * 500);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. cluster&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-80f2-8bab-fa722afdd82f&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 코어를 활용하여 여러 워커 프로세스를 생성하고 관리할 수 있게 해주는 내장 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;내부적으로 child_process 모듈을 사용, fork 함수를 통해 워커 프로세스를 생성 후 통신&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;클러스터는 부모인 마스터 프로세스와 자식인 워커 프로세스의 코드를 한 곳에 작성 가능&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import cluster from 'cluster';
import { cpus } from 'os';
import http from 'http';

// 실행 중인 프로세스가 마스터 프로세스라는 의미
if(cluster.isPrimary) {
	console.log(`Master Processs ${process.pid} is running`});
	
	// cpu 코어 수만큼 워커 프로세스를 만들어냄
	for (let i = 0; i &amp;lt; cpus().length; i++) {
		cluster.fork();
	}
} else {
// 워커 프로세스가 HTTP 서버를 생성하여 요청을 처리함
	http.createServer((req, res) =&amp;gt; {
		res.writeHead(200);
		res.end('Hello from worker ' + process.pid);
	}).listen(8000);
}
	
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import clust from 'cluster';
import os from 'os';

// 병렬로 사용 가능한 스레드 또는 코어의 수 확인
const numCPUs = os.availableParallelism();

if (cluster.isPrimary) {
    // 마스터(Primary) 프로세스에서 실행
    console.log(`Primary ${process.pid} is running`);
    
    // CPU 코어 수만큼 워커 프로세스 생성
    for (let i = 0; i &amp;lt; numCPUs; i++) {
        cluster.fork();
    }
    
    // 각 워커로부터 메시지 수신 시 로그 출력
    for (const id in cluster.workers) {
        cluster.workers[id].on('message', (message) =&amp;gt; {
            console.log(`Message from worker ${id}: ${message}`);
        });
    }
} else {
    // 워커 프로세스에서 마스터로 메시지 전송
    process.send(`Hello from worker ${process.pid}`);
}
&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;실무에서는 cluster 모듈 기반으로 한 PM2 라이브러리를 사용하거나, 도커 컨테이너로 서버를 여러개 실행하는 방식 활용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. worker_threads&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JS 환경에서는 비동기 작업이 알아서 멀티스레딩 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; CPU에 부담을 줄 수 있는 작업들은 직접 다른 스레드를 만들어 실행하면 좋다(이미지 편집, 대용량 데이터 압축 등)&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// index.js
import { Worker } from 'worker_threads';

const worker = new Worker('./worker.js', { workerData: 1000000000 } );

worker.on('message', (message) =&amp;gt; {
	console.log(`Received: ${message}`);
});

(...)

-------------------------------

// worker.js

// parentPort: 메인 스레드와 통신할 수 있는 창구
// workerData: 메인 쪽에서 Worker의 생성자에 두 번째 객체를 통해 실어보낸 데이터
import { parentPort, workerData } from 'worker_threads';

function heavyCalc(num) {
	// 무거운 작업
}

const result = heavyCalc(workerData);

// 메인 쪽에서 워커의 'message' 이벤트를 통해 전달
parentPort.postMessage(result);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;// index.js
import { Worker } from 'worker_threads';
const numbers = [1, 2, 3, 4, 5];

for (const num of numbers) {
	const worker = new Worker('./worker.js');
	
	// 생성한 워커의 인스턴스에 'postMessage' 메소드를 사용하여 데이터 전달
	worker.postMessage(num * 1000000000);
	
	worker.on('message', (message) =&amp;gt; {
		console.log(`Received: ${message}`);
	});
	
	(...)
	
// worker.js
import { parentPort } from 'worker_threads';

function heavyCalc(num) {
	(...)
}

// 메인에서 전달된 데이터는 워커의 'parentPort'에서 'message' 이벤트를 통해 수신
parentPort.on('message', (num) =&amp;gt; {
	const result = heavyCalc(num);
	parentPort.postMessage(result);
	// process.exit(0);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;worker.terminate()로 메인 스레드에서 강제 종료 가능 &amp;rarr; 정상적으로 종료되지 않았다는 메시지 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워커 스레드에서 parentPort.close();로 워커 종료 가능 (주어진 작업을 마친 후)&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;Atomic: 다른 스레드로부터의 간섭 없이 데이터를 다룰 수 있게 해주는 도구&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;thread pool: 스레드들을 재활용함으로써 효율을 높임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 &amp;lsquo;Piscina&amp;rsquo;와 같은 외부 라이브러리를 통해 스레드 풀 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. package.json&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-80db-9976-fdf8a60da662&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;npm을 비롯한 패키지 관리 도구들이 이 파일을 보고 프로젝트를 어떻게 관리할지 결정&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-804e-8478-d8bce20a60e9&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-80d2-8ae0-c43362d7df2d&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;name: 프로젝트 이름&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;version: 프로젝트의 버전&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;description: 프로젝트가 뭘 하는지 간단히 설명하는 필드&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;main: 프로젝트의 진입점 지정(프로그램을 켤 때 가장 먼저 실행될 파일)&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;type: &amp;lsquo;module&amp;rsquo;로 작성되어 있으면 ESM 모드로 프로젝트가 실행&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;scripts: 단축 명령어 모음&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;keywords: 프로젝트의 해시태그(프로젝트를 검색할 때 도움이 되는 키워드)&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;author: 프로젝트 만든 사람의 정보(이름, 이메일)&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;license: 프로젝트의 사용 허가 조건 명시(MIT: 누구나 자유롭게 사용, 수정, 배포할 수 있는 오픈소스 라이선스)&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;dependencies: 프로젝트를 실행하는데 필요한 패키지들&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;devDependencies: 프로그램을 개발하는 과정 중에만 필요한 패키지들&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Semantic Versioning (SemVer) : 소프트웨어 버전을 체계적으로 관리하기 위한 규칙&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-8054-a7de-ea0fb1617486&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4.17.1 &amp;larr; 4는 메이저, 17은 마이너, 1은 패치 번호&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;메이저: 큰 변화가 있을 때 높아짐 (이전 버전과 호환되지 않는 수정이 포함될 수 있음)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;마이너: 새로운 기능이 추가됐지만, 기존 코드와 호환될 때 높아짐&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;패치 번호: 기능 추가 없이 안정성만 높아질 때 높아짐&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;^4.17.1 &amp;larr; 메이저 버전은 고정, 마이너와 패치 업데이트를 허용 (^은 캐럿이라고 부름)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; 4.17.1 이상, 5.0.0 미만의 버전이 받아짐&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;~5.12.3 &amp;larr; 메이너와 마이너 버전은 고정하고, 패치 업데이트만 허용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; 5.12.3 이상, 5.13.0 미만의 버전을 활용&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-8099-8f8a-d72df37a6adc&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5.12.3 &amp;larr; 5.12.3 버전만 사용한다는 의미&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;gt;=4.17.20 &amp;lt;5.0.0 &amp;larr; 4.17.20 이상, 5.0.0 미만의 버전 사용&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;*: 해당 패키지의 최신 버전을 사용&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;26.6.3 || 27.0.0 &amp;larr; 둘 중 하나 사용&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-809d-a49d-d2cf624d1adb&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-80c6-ab7c-fd1da9227523&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;peerDependencies: npm에 의해 자동으로 설치되지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; 어떤 패키지가 필요하다는 것을 명시만 해줌&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;optionalDependencies: 있으면 좋지만 없어도 되는 패키지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; npm install 시 설치가 시도되지만, 설치가 실패하더라도 에러 무시&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-8052-b31a-e8a6d912cae0&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;bundledDependencies: npm에 패키지를 배포할 때 여기 포함된 패키지도 같이 묶어 내보내라는 의미&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;overrides: 특정 패키지가 또 다른 패키지에 의존할 때, 그 하위 의존성의 버전을 강제로 지정할 때 사용&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. npm&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-80c8-8337-d6c72d3276ed&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;Node.js에서 기본으로 제공되는 패키지 매니저&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-805e-867e-dfb2381506b7&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-80a0-84e5-c83c754fd12f&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm -v : npm의 버전 번호 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;npm init: 질문들에 답하면 package.json 생성&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;npm init -y: 모든 질문에 yes라고 답변하고 package.json 생성&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;npmjs.com에서 Node.js 패키지들을 찾아볼 수 있다&lt;/span&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;package-lock.json: &amp;lsquo;npm install&amp;rsquo;로 설치된 모든 패키지와 그 의존성들의 정확한 버전을 기록해둔 설치 내역서&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; packages의 빈 문자열 항목은 package.json의 정보가 요약되어 있음&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; resolved에는 패키지가 어디서 다운로드되었는지 인터넷 위치가 기록됨&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; integrity는 파일 무결성을 확인하는 해시값&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;216e1f4d-1a3d-809a-acd6-c3bd7151d52d&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;&amp;lsquo;npm list -g &amp;mdash;depth=0&amp;rsquo; 명령어로 글로벌로 설치된 패키지 확인 가능&lt;/div&gt;


&lt;p data-ke-size=&quot;size16&quot;&gt;npm ci: 락파일의 내역을 정확하여 반영하여 패키지들 설치&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;npm update: 프로젝트에서 설치된 패키지를 최신 버전으로 업데이트 (뒤에 패키지 이름 붙일 수 있음)&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;npm outdated: 설치된 패키지 중 최신보다 오래된 것이 있는지 확인&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;npm uninstall: 패키지 삭제&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;(sudo) npm cache clean &amp;mdash;force: 캐시를 비워줌&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;npm doctor: npm 버전, 레지스트리 연결, 캐시 상태 등을 점검해서 문제가 있으면 알려줌&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;npm audit: 프로젝트 패키지에서 보안 취약점을 찾아줌&lt;/span&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;터미널에 &amp;lsquo;npm login&amp;rsquo; 입력&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;로그인 후 &amp;lsquo;npm info 이름&amp;rsquo; 명령어로 프로젝트와 이름이 같은 것이 있는지 확인&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;lsquo;npm publish&amp;rsquo;로 패키지 업로드&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;lsquo;npm unpublish 이름 (&amp;mdash;force)&amp;rsquo; 명령어로 npm에서 패키지 제거 가능&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;npx: 패키지를 설치하지 않고 바로 실행하거나, 로컬에 설치된 패키지를 쉽게 실행할 수 있게 해줌&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; npm install과의 차이: 캐시 등의 공간에 임시적으로 다운로드&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. testing&lt;/h2&gt;

&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js에는 내장 테스트 러너인 test module 존재&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;assert: 테스트 결과를 검증하는데 사용하는 모듈&lt;/span&gt;&lt;/p&gt;


&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;test('add(2, 3) =&amp;gt; 5', () =&amp;gt; {
	assert.strictEqual(add(2, 3), 5);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;

&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;테스트를 실행할 때 &amp;lsquo;node &amp;mdash;test&amp;rsquo; 명령어를 입력한다&lt;/div&gt;

&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;그룹으로 묶어서 테스트도 가능&lt;/div&gt;

&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;217e1f4d-1a3d-805e-bac8-d1f445734905&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;test('grouped test', async (t) =&amp;gt; {
	await t.test('add(1, 2) =&amp;gt;3', async () =&amp;gt; {
		assert.strictEqual((add(1, 2), 3);
	});
	
	await t.test('add(10, -5) =&amp;gt; 15', async () =&amp;gt; {
		assert.strictEqual(add(10, -5), 5);
	});
});
​&lt;/code&gt;&lt;/pre&gt;
​&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;217e1f4d-1a3d-80a6-81ef-d1f6970b5bd6&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;217e1f4d-1a3d-80ec-a05b-d63096bc9eee&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;test.beforeEach: 각 테스트가 실행되기 전 호출&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;test.afterEach: 테스트가 실행된 후 호출&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;test.skip: 해당 테스트의 이름만 출력하고, 테스트를 실행하지는 않음&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;test.todo: 테스트 코드 구현x, 설명만 적을 때 사용하는 함수&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;test.only: &amp;lsquo;node &amp;mdash;test &amp;mdash;test-only&amp;rsquo; 명령어를 입력하면 only 함수로 작성된 것만 실행&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;217e1f4d-1a3d-80a0-94c3-e37cb5f30dfa&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;217e1f4d-1a3d-80bf-90b4-f7fa9c34886a&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mocha: 가장 오래되고 널리 쓰이는 테스트 프레임워크 (assertion 라이브러리인 Chai와 함께 조합해서 사용, Common JS에 적합)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Jest: 올인원 테스트 솔루션(Common JS, ESM 지원, React와 잘 맞음)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Vitest: Vite 생태계를 위한 최신 테스트 러너(ESM, TypeScript에 더 강력한 호환성)&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Study/Node.js</category>
      <category>node.js</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/182</guid>
      <comments>https://quickchabun.tistory.com/182#entry182comment</comments>
      <pubDate>Thu, 19 Jun 2025 23:00:26 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js 강의 정리] 파일 시스템, TCP/UDP, HTTP, 버퍼와 스트림, 각종 모듈에 관하여</title>
      <link>https://quickchabun.tistory.com/181</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;얄코님의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'얄코의 Node.js (Korean ver.)'&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;강의를 듣고 정리한 내용입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;강의 링크:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot;&gt;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750339476883&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;얄코의 Node.js (Korean ver.) 강의 | 얄팍한 코딩사전 - 인프런&quot; data-og-description=&quot;얄팍한 코딩사전 | ,   This course is designed for Korean-speaking learners. If you speak English, Japanese, Vietnamese, or any other language, please take t&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bFl8df/hyY79qvDMI/2sQLGgCUb2Bg7LOXkwZz20/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/eNpB8/hyZcnAwnGi/zT7o2XKqFXp5gKU3CptKa0/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/bbWpks/hyY743NRKf/XFZH5IrvKqZrKEVqw6l2B0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bFl8df/hyY79qvDMI/2sQLGgCUb2Bg7LOXkwZz20/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/eNpB8/hyZcnAwnGi/zT7o2XKqFXp5gKU3CptKa0/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/bbWpks/hyY743NRKf/XFZH5IrvKqZrKEVqw6l2B0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&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;얄코의 Node.js (Korean ver.) 강의 | 얄팍한 코딩사전 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;얄팍한 코딩사전 | ,   This course is designed for Korean-speaking learners. If you speak English, Japanese, Vietnamese, or any other language, please take t&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 파일 시스템&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CommonJS에서 파일 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fs로 파일을 읽을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const fs = require('fs');

fs.readFile('파일 경로', '데이터를 읽는 데 사용할 형식(utf8)', '콜백 함수')
&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;b&gt;readFile&lt;/b&gt; 대신 &lt;b&gt;readFileSync&lt;/b&gt;를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 쓰려면 &lt;b&gt;writeFile&lt;/b&gt;(동기적으로 하고 싶으면 &lt;b&gt;writeFileSync&lt;/b&gt;)를 활용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 이어붙이고 싶으면 &lt;b&gt;appendFile&lt;/b&gt;(동기적으로 하고 싶으면 &lt;b&gt;appendFileSync&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;특정 파일의 유무를 확인하고 싶으면 &lt;b&gt;access&lt;/b&gt;를 (동기적으로 하고 싶으면 &lt;b&gt;existsSync&lt;/b&gt; - 파일 유무에 따라 boolean 값 반환) 사용하면 된다. (exists는 deprecated)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 원래 access는 파일이나 디렉토리에 특정 권한이 있는지 확인하는 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 유무를 확인하려면 access의 두 번째 인자에 fs.constants.F_OK를 넣으면 된다&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;파일을 지우고 싶으면 unlink(동기적으로 하고 싶으면 unlinkSync)를 활용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 파일이 없으면 오류 발생&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;__dirname&lt;/b&gt;: 실행되고 있는 문서의 디렉토리 경로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;__filename&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;&lt;b&gt;path 모듈&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;join: 인자로 주어진 경로 조각들을 이어붙여서 경로로 만들어줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resolve: 작업중인 디렉토리에 인자로 주어진 경로를 이어붙여서 절대 경로 생성 (해당 프로세스를 실행한 위치 기준)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;basename: 해당 경로의 마지막에 있는 파일이나 폴더의 이름을 추출하여 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dirname: 디렉토리의 경로 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;extname: 파일의 확장자를 추출하여 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parse: 해당 경로의 정보를 담은 객체 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;format: parse 함수로 만든 객체를 format 함수에 객체로 넣어주면 해당 경로의 문자열로 조합해서 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;relative: 첫 번째 인자(경로)의 파일에서 두번째 인자(경로)로 어떻게 이동해야되는지 알려줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 같은 드라이브에 위치하고 같은 기준점에서 시작해야한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ES Module에서 파일 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조 분해 할당으로 fs로부터 필요한 함수만 가져와 사용할 수 있다.&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;import { readFileSync, readFile } from &amp;lsquo;fs&amp;rsquo;;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 하지만 선호되지는 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; fs 대신 &lt;b&gt;fs/promises&lt;/b&gt;(fs의 비동기 모듈들을 프로미스 기반으로 래핑한 모듈)가 선호됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; ES Module에서는 최상위 스코프에서 await가 사용 가능하므로 동기 작업처럼 작성 가능&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;ES Module에서는 filename과 dirname이 전역 변수로 제공되지 않는다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;fileURLToPath&lt;/b&gt;(import.meta.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;readdir: 폴더 안의 들어있는 것들의 목록을 가져올 때 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stat: 해당 파일에 대한 자세한 정보를 객체로 얻을 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mkdir: 디렉토리를 만들 수 있음 (하위 폴더까지 한 번에 생성하려면 두 번째 인자에 { recursive: true}를 주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rename: 첫 번째 인자로 주어진 경로의 파일(폴더)를 찾아서 경로를 두 번째 인자로 주어진 것으로 변경한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;copyFile: 파일을 복사할 때 사용된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rmdir: 빈 디렉토리만 삭제 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rm: 내용이 있는 디렉토리도 삭제 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chmod: 파일이나 폴더의 권한 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;truncate: 파일의 크기를 특정 값으로 줄이는데 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;utimes: 파일의 접근 시간과 변경 시간을 변경&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;파일 시스템 이벤트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;readline&lt;/b&gt;: 프로그램 실행 중 터미널로부터 사용자의 키보드 입력을 받아오는데 사용되는 도구&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;import readline from &amp;lsquo;readline/promises&amp;rsquo;&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;lsquo;process&amp;rsquo;로부터 현지 실행 되고 있는 프로그램의 표준 입력 스트림, 출력 스트림&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; import { stdin as input, stdout as output } from &amp;lsquo;process&amp;rsquo;;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;readline의 createInterface 함수를 활용하여 input과 output을 연결&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;const rl = readline.createInterface({ input, output )};&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 질문을 출력하고 사용자의 입력을 받고 싶을 때는 rl.question 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 다 쓰고 나서는 rl.close(); 입력&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;watch&lt;/b&gt;: 파일이나 폴더의 상태에 변화가 있을 시 OS에서 보내주는 신호를 기다리고, 신호가 오면 그에 반응&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;&amp;rarr; 각 로그에는 이벤트가 발생한 파일 및 폴더의 이름과 이벤트의 종류가 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 파일 추가, 이동, 삭제, 이름 변경의 이벤트 타입: rename&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 파일의 내용이 수정되어 저장될 때 이벤트 타입: change&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;watchFile&lt;/b&gt;: 일정 간격을 두고 확인 (폴링 방식)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 간격에 대한 옵션을 넣지 않으면 5007ms(소수)로 파일을 확인&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;unwatchFile&lt;/b&gt; 함수를 사용하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;watch와 watchFile은 불완전하므로 일반적으로는 Chokidar 같은 외부 라이브러리를 사용&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. TCP/UDP&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TCP&lt;/b&gt;: 보낸 데이터가 확실히 도착하도록 하는 것을 속도보다 우선시하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 먼저 보낸 데이터 조각이 제대로 도착한 것을 확인한 후, 다음 조각을 보냄&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 웹 페이지 로딩, 파일 전송, 이메일 등에서 사용&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;TCP를 구현할 때에는 내장 모듈인 net 모듈 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;import net from &amp;lsquo;node:net&amp;rsquo;; (node: prefix가 있으면 혼동하지 않고 내장 net 모듈을 불러옴)&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;서버는 net의 createServer 함수로, 클라이언트는 createConnection 함수로 인스턴스를 생성해서 사용 (서버는 socket 인스턴스, 클라이언트는 clinent 인스턴스)&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;두 인스턴스 모두 on 메소드에서 &amp;lsquo;end&amp;rsquo; 문자열에 할당한 콜백으로 연결 종료시 실행할 작업 지정&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;UDP&lt;/b&gt;: 속도를 우선시하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 보내어지는 데이터 조각의 유실 여부와 관계없이 계속해서 데이터를 전송&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 영상 스트리밍, 온라인 게임 등에서 사용&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;UDP를 구현할 때에는 내장 모듈인 dgram 모듈 활용&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;UDP에서는 서버와 클라이언트의 개념이 명확히 나뉘어지지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 둘 다 createSocket으로 인스턴스를 생성&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;const client = dgram.createSocket(&amp;rsquo;udp4&amp;rsquo;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; udp4: IPv4 기반의 UDP 소켓 생성&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;bind를 호출해서 패킷을 받을 준비를 하는 쪽이 서버&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; server.bind(PORT , () &amp;rArr; { &amp;hellip;&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;send&lt;/b&gt; 메소드로 서버에 데이터 전송&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버와 클라이언트 모두 소켓 인스턴스의 &lt;b&gt;on&lt;/b&gt; 메시지를 활용하여 메시지를 받음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 socket.io, ws, gRPC, MQTT같은 라이브러리 사용&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. HTTP&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Express, NestJS, Fastify와 같은 프레임워크의 기반이 HTTP 모듈&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;createServer&lt;/b&gt;: HTTP 방식으로 동작하는 서버를 만드는 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 함수의 실행결과로 서버의 인스턴스가 만들어져서 server 함수에 할당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 함수의 매개변수에는 들어온 요청에 대한 정보를 담는 &lt;b&gt;request&lt;/b&gt; 객체와 서버의 응답을 제어하는데 사용되는 &lt;b&gt;response&lt;/b&gt; 객체가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서버 인스턴스를 listen 메서드로 실행한다(인자로는 포트 번호, 시작시 호출될 콜백 함수가 들어감)&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;writeHead&lt;/b&gt;: 응답의 상태 코드와 헤더를 인자로 넣어 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 헤더에는 Content-type을 넣음(서버가 보낼 응답이 어떤 형식의 데이터가 될지 클라이언트에게 알려주는 것)&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;end&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;Node.js에서는 URL을 모듈로서 로드해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 파일 시스템 경로로 직접 접근 가능&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import http from &quot;http&quot;;
import { URL } from &quot;url&quot;;

const server = http.createServer((req, res) =&amp;gt; {
	const { searchParams } = new URL(req.url, `http://${req.headers.host}`);
	const name = searchParams.get(&quot;name&quot;) || &quot;&quot;;
	
	(...)

&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;request 객체로부터 URL과 host 정보를 추출해서 URL 모듈의 생성자에 넣어줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 생성된 인스턴스로부터 구조 분해 할당으로 &amp;lsquo;searchParams&amp;rsquo; 항목을 추출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; get으로 name 키에 해당하는 값을 가져옴&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;HTTP 모듈로 req.method와 req.url을 조합하여 RESTful API를 제공할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import http from &quot;http&quot;;

const server = http.createServer((req, res) =&amp;gt; {
	if (req.method === &quot;GET&quot; &amp;amp;&amp;amp; req.url === &quot;/&quot;) {
		res.writeHead(200, { &quot;Content-Type&quot;: &quot;text/plain&quot; });
		res.end(&quot;Welcome~&quot;);
	}
	
	else if (req.method === &quot;POST&quot; &amp;amp;&amp;amp; req.url === &quot;/submit&quot;) {
		res.writeHEAD(200, { &quot;Content-Type&quot;: &quot;text/plain&quot; });
		res.end(&quot;Form Submitted&quot;);
	
	else {
		res.writeHead(404, { &quot;Content-Type&quot;: &quot;text/plain&quot; });
		res.end(&quot;404 Not Found);
	
});
		
&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;400번대 오류: 요청 자체에 문제가 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;500번대 오류: 서버쪽의 문제에 의한 오류&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;201: 새로운 resource 생성&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;HTTP는 클라이언트가 되어 요청을 보내는데도 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버쪽에서 브라우저가 쿠키로 저장할 데이터를 전송할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 request로부터 URL 객체를 추출해낸 다음 key와 value의 값을 가져옴&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;&lt;b&gt;Set-Cookie&lt;/b&gt; 항목을 통해 브라우저가 어떤 키와 값을 쿠키로 저장할지 알려줌&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;if (path === &quot;/set-cookie &amp;amp;&amp;amp; key &amp;amp;&amp;amp; value) {
	res.writeHead(200, {
		&quot;Set-Cookie&quot;: `${key}=${value}; HttpOnly`,
		&quot;Content-Type&quot;: &quot;text/plain&quot; });
	return res.end(`Cookie set: ${key}=${value}`);
} 
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청으로부터 쿠키 데이터를 추출할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;if (path === &quot;/get-cookie&quot; &amp;amp;&amp;amp; key) {
	const cookies = parseCookies(req.headers.cookie || &quot;&quot;);
	return res.end(cookies[key] ? `Value: ${cookes[key]` : &quot;Not Found&quot;);
}
&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;실무에서 서비스를 배포하려면 HTTPS를 사용해야함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; HTTPS 모듈을 불러와서 사용하면 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서버를 생성할 때 &amp;ldquo;key&amp;rdquo;와 &amp;ldquo;cert&amp;rdquo; 옵션으로 키와 인증서의 파일을 불러와 넣어주면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 테스트 환경에서는 직접 생성해서 사용할 수 있지만 배포를 할 때에는 공인 CA 기관으로부터 받아와야 한다&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;&lt;b&gt;REST API 참고할점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HATEOAS (Hypermedia As The Engine Of Application State)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 각 요청의 응답에, 가용한 다른 요청들의 정보를 포함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이 리소스에 관해 이런 기능들도 요청할 수 있다고 첨부하는 것&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;Stateless&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 클라이언트의 상태 정보가 서버에 저장되지 말아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 클라이언트의 요청이 얼마나 반복되든 필요한 모든 내용을 포함하고 있어야 한다. (멱등성)&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;Cacheability&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 자기가 어떤 응답을 보냈는지, 자기가 어떤 응답을 받았는지는 기억해두는게 좋다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 버퍼와 스트림&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;스트림(Stream)&lt;/b&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;버퍼(Buffer)&lt;/b&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: 바이너리 데이터를 다루기 위한 객체&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;Buffer는 전역으로 제공되는 클래스&lt;/p&gt;
&lt;pre class=&quot;glsl&quot;&gt;&lt;code&gt;// buffer에는 'Hello, Node.js'를 버퍼 형태로 바꾼 값이 담김
const buffer = Buffer.from('Hello, Node.js');

// 문자열의 각 문자가 16진수로 표현되어 나타남
console.log(buffer);

// 다시 문자열로 나타남
console.log(buffer.toString(); 

// 인자로 주어진 크기만큼의 버퍼 인스턴스 생성
// 00 00 00 00 ...
buffer = Buffer.alloc(10);

// 인자로 들어온 값을 분해
// 41 42 43 44 00 00 ...
buffer.write('ABCD');
&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;Buffer 인스턴스의 &lt;b&gt;copy&lt;/b&gt;를 사용하면 인자로 주어진 다른 인스턴스로 데이터 복사 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) bufA.copy(bufB);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 문자열을 복사하는 것보다 빠르게 동작&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;concat&lt;/b&gt;을 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) const bufZ = Buffer.concat([bufX, bufY]);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;request의 on 메서드에서 콜백함수에 매개변수로 주어지는 데이터는 버퍼 인스턴스&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;req.on('data', (chunk) =&amp;gt; {
	body += chunk;
	console.log(chunk);
}); 

// 개선 버전
let body = [];

req.on('data', (chunk) =&amp;gt; body.push(chunk));
req.on('end', () =&amp;gt; {
	const data = Buffer.concat(body).toString();
	console.log('Received:', JSON.parse(data));
	res.end('Data received')
});
&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;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { readFile} from 'fs/promises';

const filePath = '경로';

const buffer = await readFile(filePath);
const base64 = buffer.toString('base64');
&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;fs/promises의 readFile 함수를 인코딩 옵션없이 사용하면 버퍼 형태로 파일 로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;base64 문자열은 어떤 시스템에서든 데이터가 깨지지 않음&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 이미지로 만드려면 &lt;b&gt;writeFile&lt;/b&gt;을 쓰면 된다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import { writeFile } from 'fs/promises';

const decodedBuffer = Buffer.from(base64, 'base64');
await writeFile('decoded.png', decodedBuffer);

&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;Stream 모듈&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;Readable&lt;/b&gt;: 파일이나 서버 등으로부터 데이터를 읽어올 때 사용&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { Readable } from 'stream'

const readableStream = new Readable({
	read() { // 스트림이 읽히기 시작할 때 실행할 작업 작성
		this.push('Hello, '); // push로 스트림에 새 데이터를 추가
		(...)
		this.push(null); // 이 코드 없으면 스트림이 종료되지 않음
	}
});

// on 메소드로 'data' 이벤트의 콜백을 등록하면 스트림 실행
readableStream.on('data', chunk =&amp;gt; {
	(...)
});

// 스트림이 종료되었을 때 실행
readableStream.on('end', () =&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;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;createReadStream&lt;/b&gt;: 파일 읽기 전용 스트림 함수 (fs 모듈에 내장)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; encoding 옵션으로 데이터를 UTF-8 형식의 문자열로 읽게 할 수 있음&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;Writable&lt;/b&gt;: 데이터를 쓸 때 사용&lt;/p&gt;
&lt;pre class=&quot;delphi&quot;&gt;&lt;code&gt;const writableStream = new Writable({
	write(chunk, encoding, callback) {
		(...)
		callback();
	}
});

writableStream.write('Hello, ');
(...)
writablestream.end();
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;createWriteStream&lt;/b&gt;: 파일 쓰기 전용 스트림 함수 (fs 모듈에 내장)&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const writeStream = createWriteStream('*.txt');
&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;b&gt;pipe&lt;/b&gt; 메소드를 사용해서 읽기 스트림과 쓰기 스트림 연결 가능&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;readStream.pipe(writeStream);
&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;b&gt;Duplex&lt;/b&gt;: 소켓과 같이 읽기와 쓰기가 모두 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;readable의 요소, writable의 요소 둘 다 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시: net 모듈의 소켓&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;&lt;b&gt;Transform&lt;/b&gt;: 데이터를 읽은 뒤 변환해서 내보냄&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import { Transform } from 'stream'

// 전달된 문자열을 모두 대문자로 바꾸어 내보냄
const upperCaseTransform = new Transform({
	transform(chunk, encdoing, callback) {
		callbacknull, chunk.toString().toUpperCase());
	}
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transform에 Duplex의 확장이라 데이터를 쓰는 메소드와 전달된 데이터를 처리하는 메소드 모두 있음&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;lsquo;zlib&amp;rsquo; 모듈의 createGzip 함수: 데이터를 Gzip 형태로 압축하여 내보내는 Transform 스트림을 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;crypto: Node.js에서 암호화 및 해시 기능을 제공하는 내장 모듈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 각종 모듈&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;a. url 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;url 모듈은 URL 클래스의 인스턴스를 생성해서 기능들을 사용&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;const myUrl = new URL('&amp;lt;https://example.com:8080/lecture?name=John&amp;amp;age=30#section1&amp;gt;');

console.log(myUrl.protocol); // https:
console.log(myUrl.hostname); // example.com
console.log(myUrl.origin); // &amp;lt;https://example.com&amp;gt;
console.log(myUrl.port); // 8080
console.log(myUrl.pathname) // /lecture
console.log(myUrl.searchParams.get('name')); // John
console.log(myUrl.hash); // #section1
console.log(myUrl.toString()); // &amp;lt;https://example.com:8080/lecture?name=John&amp;amp;age=30#section1&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;searchParams&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;set: 매개변수 변경, 매개변수 추가&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;delete: 매개변수 삭제&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;append: 매개변수 추가(특정 매개변수가 두개 이상의 값을 갖게하려면)&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;get: 매개변수 값을 얻을 수 있음&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;getAll: 특정 매개변수의 모든 값들을 배열로 얻을 수 있음&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;has: URL에 특정 매개변수가 포함되어있는지 확인 (boolean)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;url 모듈을 사용할 때 상대 경로들을 조합할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 상위 디렉토리의 이동을 기본 URL 경로 단계보다 많이했을 때에는 기본 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;url 모듈의 fileURLToPath: 파일 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;url 모듈의 pathToFileURL: 파일 경로를 파일 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;파일 URL: 웹 브라우저나 Node.js같은 환경에서 로컬 파일 시스템의 자원에 접근할 때 사용하는 URL 형식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 경로: 운영체제에서 파일이나 디렉토리의 위치를 나타내는 문자열&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;b. dns 모듈&lt;/h3&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;lookup: 주어진 도메인 이름을 실제 IP 주소로 변환하는 함수&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;resolve4: 도메인의 IPv4 주소, 즉 A 레코드를 조회해서 배열로 반환&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;resolve6: 도메인의 IPv6 주소, 즉 AAAA 레코드를 조회해서 배열로 반환&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;reverse: IP 주소를 도메인 이름으로 변환하는 역방향 DNS 조회를 수행하는 함수&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;resolveMx: 도메인의 메일 서버, 즉 MX 레코드를 조회해서 배열로 반환하는 함수(exchange는 메일 서버 도메인, priority는 우선순위)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;c. util 모듈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;promisify&lt;/b&gt;: 인자로 전달된 콜백 기반 함수를 프로미스 기반으로 변환&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;import { promisify } from 'node:util;

const sleep = promisify(setTimeout);

await sleep(2000);
&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;inspect: 객체를 사람이 읽기 쉬운 문자열로 변환해줌(디버깅, 로깅에서 유용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 인자로 대상 객체를, 두번째 인자로 옵션 객체를 넣어줌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;color, depth, maxArrayLength, compact 등의 옵션으로 원하는 결과값을 확인할 수 있다.&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;type: typeof나 instanceof로 구분하기 어려운 특정 타입(Date의 인스턴스, 정규화 표현식, Map과 Set, 프로미스, 특정 종류의 함수들, 네이티브 오류 객체 등)을 정확히 판별할 때 유용&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;deprecate: 어떤 함수가 문제가 있거나 새 함수로 대체되어 앞으로 사용하지 않게 되는 상황이 생겼을 때, 개발자에게 경고 메시지 전달할 경우에 사용&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;d. os 모듈&lt;/h3&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;platform: 현재 운영 체제의 플랫폼을 문자열로 반환&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;arch: CPU의 아키텍처를 문자열로 반환&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;type: 운영 체제의 유형을 문자열로 반환&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;release: 운영 체제의 버전을 문자열로 반환&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;cpus: 시스템의 CPU 코어 정보를 객체 배열로 반환&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;totalmem: 시스템의 총 메모리 크기를 반환&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;freemem: 현재 사용 가능한 메모리 크기를 반환&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;userInfo: 현재 사용자의 정보를 객체로 반환(userInfo().username &amp;rarr; 사용자 이름)&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;homedir: 현재 사용자의 홈 디렉토리 경로를 문자열로 반환&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;tmpdir: 시스템의 기본 임시 디렉토리 경로(운영 체제가 임시 파일을 저장하는 데 사용하는 표준 폴더 경로)를 문자열로 반환&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;uptime: 시스템이 부팅된 이후 경과한 시간을 초 단위로 반환&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;hostname: 시스템의 호스트 이름을 문자열로 반환&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;networkInterfaces: 시스템의 네트워크 인터페이스 정보를 객체로 반환&lt;/p&gt;</description>
      <category>Study/Node.js</category>
      <category>node.js</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/181</guid>
      <comments>https://quickchabun.tistory.com/181#entry181comment</comments>
      <pubDate>Thu, 19 Jun 2025 22:40:02 +0900</pubDate>
    </item>
    <item>
      <title>[Node.js 강의 정리] Node.js, REPL, Promise, async/await, Module, Nodemon에 관하여</title>
      <link>https://quickchabun.tistory.com/180</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;얄코님의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'얄코의 Node.js (Korean ver.)'&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;강의를 듣고 정리한 내용입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;강의 링크:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750339307090&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;얄코의 Node.js (Korean ver.) 강의 | 얄팍한 코딩사전 - 인프런&quot; data-og-description=&quot;얄팍한 코딩사전 | ,   This course is designed for Korean-speaking learners. If you speak English, Japanese, Vietnamese, or any other language, please take t&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot; data-og-url=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bFl8df/hyY79qvDMI/2sQLGgCUb2Bg7LOXkwZz20/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/eNpB8/hyZcnAwnGi/zT7o2XKqFXp5gKU3CptKa0/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/bbWpks/hyY743NRKf/XFZH5IrvKqZrKEVqw6l2B0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%EC%96%84%EC%BD%94-node-js?srsltid=AfmBOopKCXPw7NK1HS79aeI2BufjFmy-iOHOM9eHCl2q_BVo1UNRMURr&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bFl8df/hyY79qvDMI/2sQLGgCUb2Bg7LOXkwZz20/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/eNpB8/hyZcnAwnGi/zT7o2XKqFXp5gKU3CptKa0/img.png?width=807&amp;amp;height=525&amp;amp;face=583_174_730_334,https://scrap.kakaocdn.net/dn/bbWpks/hyY743NRKf/XFZH5IrvKqZrKEVqw6l2B0/img.png?width=1024&amp;amp;height=1024&amp;amp;face=0_0_1024_1024');&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;얄코의 Node.js (Korean ver.) 강의 | 얄팍한 코딩사전 - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;얄팍한 코딩사전 | ,   This course is designed for Korean-speaking learners. If you speak English, Japanese, Vietnamese, or any other language, please take t&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Node.js란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js &amp;rarr; 자바스크립트란 언어를 알아듣는 프로그램&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ryan Dahl이 V8 엔진을 임베딩하고 libuv 등의 추가 부품을 제작하여 Node.js 완성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: oklch(0.99 0.004 106.471); color: oklch(0.304 0.04 213.681); text-align: start;&quot;&gt;한 번에 하나의 작업만 처리하는 싱글 스레드로 동작하지만, 시간이 오래 걸리는 파일 읽기, 네트워크 요청, 타이머 등은 백그라운드에서 처리하고, 그 결과가 준비되면 이벤트와 콜백 함수로 알려준다.&lt;/span&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;Node.js는 싱글 스레드 + 이벤트 루프 구조로, 여러 작업을 동시에 효율적으로 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 읽기, 네트워크 요청 등은 논블로킹 비동기 방식으로 처리되어, 한 작업이 끝날 때까지 다른 작업이 지연되지 않음&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;개발자는 콜백 함수만 작성하면 되고, 멀티스레딩이나 동기 처리는 Node.js가 알아서 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. REPL(Read-Eval-Print Loop)&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;20fe1f4d-1a3d-805e-bee6-da76eb05a074&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;lsquo;node&amp;rsquo; 명령어를 입력하여 REPL 세션을 열어줄 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;5+5를 입력하면 10이 반환 &amp;rarr; 5+5를 10으로 대체할 수 있다는 뜻&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;20fe1f4d-1a3d-80e4-9850-f69a705cbfc0&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&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 style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;표현식: 특정 값을 반환하는 코드 조각&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Node.js의 REPL에 표현식을 입력하면 결과로 그 반환값이 아래와 같이 출력됨&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;20fe1f4d-1a3d-8073-af2d-f6d1567c4353&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;20fe1f4d-1a3d-8051-8a8b-c61007a6a632&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;console.log(1);을 입력하면 1과 undefined가 반환됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; 콘솔의 로그 메소드는 뭔가를 반환하는 용도의 함수가 아니기 때문에, 반환값으로 undefined를 가짐&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;20fe1f4d-1a3d-8037-a0cd-e727dadeed2b&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #32302c; text-align: start;&quot; data-block-id=&quot;20fe1f4d-1a3d-80c0-aea2-e528c33eb94f&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js의 REPL에서 언더스코어(_)는 마지막 평가 결과를 참조하는데 사용된다&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;for문과 함수도 입력 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;또한 모듈도 불러올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;명령어 &amp;lsquo;.help&amp;rsquo; &amp;rarr; 명령어들의 목록과 각각에 대한 설명을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;명령어 &amp;lsquo;.editor&amp;rsquo; &amp;rarr; 텍스트 편집기와 같은 모드 실행&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;명령어 &amp;lsquo;.exit&amp;rsquo; &amp;rarr; REPL 종료&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;명령어 &amp;lsquo;.save&amp;rsquo; &amp;rarr; 저장&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;명령어 &amp;lsquo;.load&amp;rsquo; &amp;rarr; 해당 파일에 작성된 모든 코드가 입력되어 실행된 다음, 실행된 결과들이 출력되어 나타남&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;명령어 &amp;lsquo;.break&amp;rsquo;, &amp;lsquo;.clear&amp;rsquo; &amp;rarr; 코드 작성 중 취소 가능&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Promise, async/await&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수로 짜던 코드를 쉽게 작성하고 읽을 수 있게 해주는 syntactic sugar&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;어떤 함수가 일을 마치고 실행할 일을 그 안에 인자로 넣고, 또 그 함수가 일을 끝내면 할 일을 인자로 넣고를 반복&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Promise를 사용할 때:&lt;/span&gt;&lt;/p&gt;


&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;함수에 return new Promise가 있다. &amp;rarr; 시간이 걸리는 작업을 한 뒤 무언가를 할 것을 &amp;lsquo;약속&amp;rsquo;한다.&lt;/div&gt;


&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;resolve: 시간을 소모하는 일이 성공적으로 마치고 나면 수행할 일&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;lsquo;then&amp;rsquo; 메서드: 각서 작성자가 하는 일이 성공 또는 실패시 할 일을 인자로 받음&lt;/span&gt;&lt;/p&gt;


&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;catch, finally 사용&lt;/div&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;async/await를 사용할 때:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;lsquo;async&amp;rsquo;라는 키워드가 붙은 함수, 또는 Node.js의 ESM 최상위 스코프에서만 사용 가능&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;lsquo;await&amp;rsquo;은 뒤에 오는 프로미스 작업이 완료될 때까지 코드의 진행을 멈춘 다음 그 결과를 반환&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; 비동기 작업이 이뤄질 동안 다음 코드가 실행되는 것을 멈추는 역할&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Module&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍 코드를 한 곳에 작성하지 않고 기능과 목적별로 나누어 작성한 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;rarr; 코드를 여러 곳에서 재사용할 수 있고 코드의 이해, 수정, 관리도 수월해진다.&lt;/span&gt;&lt;/p&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 CommonJS가 기본 모듈 시스템으로 사용, 이후에 ECMAScript에서 ES Module을 공식적으로 표준화&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;CommonJS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본으로 제공되는 module 객체를 사용해서 모듈을 외부로 내보냄 (module.exports 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const PI = 3.14;

function add(a,b) {
	return a + b;
}

module.exports = add;
&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;&amp;lsquo;require&amp;rsquo; 함수를 사용해서 외부의 모듈을 불러옴&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;const add = require('./math.js');

console.log(add(1,2));
&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;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;pre class=&quot;monkey&quot;&gt;&lt;code&gt;const PI = 3.14;

function add(a,b) {
	(...)
}

class MathOps {
	(...)
}

module.exports = { add, MathOps, PI };
&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;pre class=&quot;processing&quot;&gt;&lt;code&gt;const math = require('./math');

const x = math.add(math.PI, 1);

const y = new math.MathOps().mul(x, 2);

(...)
&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;p data-ke-size=&quot;size16&quot;&gt;ex) const { add, PI, MathOps } = require(&amp;rsquo;./math&amp;rsquo;);&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;다른 모듈에서도 같은 이름으로 내보낸다면? 콜론과 함께 작성하자&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;const { add: addMath } = require('./math');
const { add: addVector } = require('./vector');

(...)
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;exports라는 객체에 원하는 이름으로 항목을 추가해서 내보낼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;// module.exports = {
// 	add: (a, b) =&amp;gt; a + b,
//	(...)
// }

// 를 아래와 같이 바꿀 수 있다.

exports.add = (a, b) =&amp;gt; a + b;
exports.subt = (a, b) =&amp;gt; a - b;
(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;module.exports의 exports와 exports.add의 exports는 같은 메모리상 주소를 가리킨다.&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;CommonJS는 한 번 불러온 모듈을 캐싱한다는 사실을 기억하자&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ES Module&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES Module을 사용하고 싶다면, 둘 중 하나를 설정하자&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;package.json에 &amp;ldquo;type&amp;rdquo;: &amp;ldquo;module&amp;rdquo; 입력&lt;/li&gt;
&lt;li&gt;모듈로 사용할 문서의 확장자를 js가 아닌 mjs로 지정&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES Module에서, 한 문서에서 여러 값을 내보낼 때 선언 및 할당되는 곳 앞에 export를 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 import와 from을 사용하여 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(별표를 사용해서 하나의 객체로 묶어서 가져올 수 있다. &amp;rarr; import * as math from &amp;lsquo;./math.js&amp;rsquo;&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;해당 모듈을 대표하는 값을 내보낼때는 앞에 export를 붙인다. 그리고 import 할 때는 원하는 이름으로 가져다 사용할 수 있다.&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;모듈의 대표 값(export default)은 중괄호 밖에, 다른 값들(export만)은 중괄호 안에 넣으면 된다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법1. 별표를 사용해서 각 모듈을 객체로 묶어 import&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import * as math from './math.js';
import * as vector from './vector.js';

console.log(math.add(1,2));
console.log(vector.add(3,4));
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방법2. 각각을 다른 이름으로 불러오기(as 활용)&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;import { add as addMath } from './math.js';
import { add as addVector } from './vector.js';
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES Module도 한 번 불러온 모듈을 캐싱한다는 사실을 기억하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 캐싱되지 않게 하려면 경로에 서로 다른 쿼리 스트링을 붙여주면 됨&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;import { getCount } from &quot;./counter.js?^v=1&quot;;

// 다른 파일
import { getCount } from &quot;./counter.js?^v=2&quot;;
&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;&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;ES Module에서는 각 모듈이 필요한 시점에 비동기적으로 임포트함으로써 프로그램의 초기 로딩 시간을 단축하고 효율을 높일 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import(&quot;./math.js&quot;).then((math =&amp;gt; {
	(...)
}

// or
const vector = await import(&quot;./vector.js&quot;);
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES Module은 모듈을 내부적으로 비동기 방식으로 로드한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 최상위 스코프에서 await 바로 사용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Nodemon&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node.js 환경에서의 개발을 효율적으로 만들어주는 도구&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm i -g nodemon으로 설치 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 맥에서는 사용자 권한 에러가 남&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mkdir -p ~/.npm-global (홈 디렉토리에 npm global이라는 폴더 생성)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;npm config set prefix ~/.npm-global (npm이 해당 디렉토리를 전역 설치 경로로 사용하도록 설정)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;echo &amp;lsquo;export PATH=$HOME/.npm-global/bin:$PATH&amp;rsquo; &amp;gt;&amp;gt; ~/.zshrc source ~/.zshrc (전역 패키지 실행 파일의 위치를 컴퓨터의 PATH 목록에 추가)&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;node 대신 nodemon 명령어를 사용해서 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; nodemon이 실행 중인 상태에서 해당 문서의 코드를 수정한 다음 저장하면 코드가 재실행되어 바뀐 결과가 출력&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;nodemon 명령어를 같이 실행할 파일명을 붙이지 않고 실행하면 해당 프로젝트 폴더의 index.js 파일을 실행 (바꾸고 싶으면 package.json의 &amp;ldquo;main&amp;rdquo;을 수정하자)&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;루트에 nodemon.json을 만들면 설정을 추가할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;exec: 실행할 명령어 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;watch: 감시할 디렉토리 또는 파일의 이름들을 문자열의 배열로 작성(이 곳의 코드가 수정되면 nodemon이 코드를 재실행)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ext: 어떤 확장자를 가질 파일을 감시할지 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ignore: 감시 대상에서 제외될 대상 설정&lt;/p&gt;</description>
      <category>Study/Node.js</category>
      <category>node.js</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/180</guid>
      <comments>https://quickchabun.tistory.com/180#entry180comment</comments>
      <pubDate>Thu, 19 Jun 2025 22:22:17 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js 강의 정리] 서버 액션, Parallel Route, 최적화에 관하여</title>
      <link>https://quickchabun.tistory.com/179</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이정환님의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'한 입 크기로 잘라먹는 Next.js(v15)'&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;강의를 듣고 정리한 내용입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;강의 링크:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot;&gt;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749358083080&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;한 입 크기로 잘라먹는 Next.js(v15) 강의 | 이정환 Winterlood - 인프런&quot; data-og-description=&quot;이정환 Winterlood | , [임베딩 영상]한 입 크기로 잘라먹는 Next.js | Official Trailler한입 크기로 잘라먹는 Next.js(15+)15시간의 분량으로 Page Router부터 App Router까지  Page Router란?Next.js&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot; data-og-url=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/ccz0G9/hyY37FM6Lb/zZgPHTfeILUakHKvryzrIk/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/IZbGH/hyY4aWM7mz/B1MF2ypoWvPKDbM3gOmP0K/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/SLhe2/hyY42Yp60Z/7hYfMQHilyQYGdsqmPlxX0/img.jpg?width=6000&amp;amp;height=4000&amp;amp;face=2024_759_2624_1415&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/ccz0G9/hyY37FM6Lb/zZgPHTfeILUakHKvryzrIk/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/IZbGH/hyY4aWM7mz/B1MF2ypoWvPKDbM3gOmP0K/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/SLhe2/hyY42Yp60Z/7hYfMQHilyQYGdsqmPlxX0/img.jpg?width=6000&amp;amp;height=4000&amp;amp;face=2024_759_2624_1415');&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;한 입 크기로 잘라먹는 Next.js(v15) 강의 | 이정환 Winterlood - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이정환 Winterlood | , [임베딩 영상]한 입 크기로 잘라먹는 Next.js | Official Trailler한입 크기로 잘라먹는 Next.js(15+)15시간의 분량으로 Page Router부터 App Router까지  Page Router란?Next.js&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서버 액션&lt;/h2&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;rarr; 클라이언트인 브라우저에서 특정 양식의 제출이 발생했을 때 서버 측에서만 실행되는 어떠한 함수를 실행시켜주는 기능&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;ldquo;&lt;b&gt;use server&lt;/b&gt;&amp;rdquo; 지시자를 사용해야&lt;/li&gt;
&lt;li&gt;&lt;b&gt;formData.get&lt;/b&gt;을 통해서 브라우저에서 전달받은 값을 쉽게 꺼내쓸 수 있는데, 이 때 타입 추론을 위해 ?.toString()을 사용하면 편하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;function ReviewEditor() {
  async function createReviewAction(formData: FormData) {
    &quot;use server&quot;;

    const content = formData.get(&quot;content&quot;)?.toString();
    const author = formData.get(&quot;author&quot;)?.toString();

    console.log(content, author);
  }

  return (
    &amp;lt;section&amp;gt;
      &amp;lt;form action={createReviewAction}&amp;gt;
        &amp;lt;input name=&quot;content&quot; placeholder=&quot;리뷰 내용&quot; /&amp;gt;
        &amp;lt;input name=&quot;author&quot; placeholder=&quot;작성자&quot; /&amp;gt;
        &amp;lt;button type=&quot;submit&quot;&amp;gt;작성하기&amp;lt;/button&amp;gt;
      &amp;lt;/form&amp;gt;
    &amp;lt;/section&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;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 함수 하나만으로 API 역할을 할 수 있는 코드를 생성할 수 있으므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서버 측에서만 실행되기 때문에 보안상으로 안전&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;input에 &lt;b&gt;required&lt;/b&gt; 애트리뷰트를 추가하면 빈 칸 입력을 방지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;input에 &lt;b&gt;hidden&lt;/b&gt; 애트리뷰트를 추가하면 안보이게 할 수 있다 (bookId 같은 거 전달할 때 유용, 그리고 readOnly도 같이 추가해야 next에서 에러가 발생하지 않는다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지 재검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 액션이 성공적으로 종료되었을 때 서버 측에서 페이지(컴포넌트)를 재검증하기를 원한다면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;revalidatePath(현재 경로)&lt;/b&gt;를 활용하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Next 서버가 재검증 (방금 작성한 리뷰가 새로고침을 하지 않아도 보이게 됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 컴포넌트가 다시 렌더링되면서, 데이터 페칭도 다시 진행됨&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;주의할점1: 오직 서버에서만 호출할 수 있는 메서드 (클라이언트 컴포넌트x)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할점2: 페이지에 포함된 모든 캐시를 무효화시켜버림(force-cache라고 하더라도)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할점3: 페이지 자체를 캐싱하는 풀 라우트 캐시도 무효화됨(다음에 페이지에 접속할 때 dynamic page처럼 페이지가 다시 만들어짐)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;revalidatePath가 실행되서 캐시들이 무효화될 때, purge라는 표현을 씀(숙청하다, 제거하다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지 재검증의 다양한 방식&lt;/h3&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;    // 1. 특정 주소의 해당하는 페이지만 재검증
    revalidatePath(`/book/${bookId}`);

    // 2. 특정 경로의 모든 동적 페이지를 재검증
    revalidatePath(&quot;/book/[id]&quot;, &quot;page&quot;);

    // 3. 특정 레이아웃을 갖는 모든 페이지 재검증
    revalidatePath(&quot;/(with-searchbar)&quot;, &quot;layout&quot;);

    // 4. 모든 데이터 재검증
    revalidatePath(&quot;/&quot;, &quot;layout&quot;);

    // 5. 태그 기준, 데이터 캐시 재검증
    revalidateTag(`review-${bookId}`);&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트 컴포넌트에서의 서버 액션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 19의 기능인 &lt;b&gt;useActionState&lt;/b&gt;를 활용하자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export default function ReviewEditor({ bookId }: { bookId: string }) {
  const [state, formAction, isPending] = useActionState(
    createReviewAction,
    null
  );

&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;createReviewAction 함수의 타입도 수정해주자 &lt;b&gt;(첫 번째 매개변수로 컴포넌트의 스테이트까지 함께 받아오게 된다)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export async function createReviewAction(_: any, formData: FormData) {

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 경험을 좋게 하기 위해 클라이언트 컴포넌트로 폼 태그를 만드는 거 추천 (isPending 사용해야지)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Parallel route (병렬 라우트)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나의 화면 안에 여러 페이지(page.tsx)를 병렬로 렌더링 시켜주는 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 소셜미디어 서비스나 관리자의 대시보드 페이지처럼 엄청나게 복잡한 구조를 갖는 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;&lt;b&gt;Slot(슬롯)&lt;/b&gt;: 병렬로 렌더링될 페이지 컴포넌트를 보관하는 폴더 (앞에 @를 붙인다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;parallel 폴더 안에 layout.tsx와 @sidebar/page.tsx가 있다면, @sidebar/page.tsx는 layout.tsx에게 props로써 자동으로 전달이 된다. (이 때 props의 이름은, 슬롯의 이름인 sidebar, 타입은 ReactNode)&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;참고1: Slot(슬롯)은 URL 경로에는 아무런 영향도 미치지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고2: Slot은 개수 제한이 없다.&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;가끔 Parallel route를 사용하면 next.js에서 에러가 발생하는데, .next 폴더를 제거하고 npm run dev로 다시 가동을 해보자&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;(슬롯 안에 default.tsx를 만들면 에러 상황에 대처할 수 있다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Intercepting Route (인터셉팅 라우트)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 특정 경로로 접속해서 새로운 페이지를 요청할 때 이 요청을 가로채서 원래 렌더링되야되는 페이지가 아닌, 우리가 원하는 어떠한 페이지를 대신 렌더링하도록 설정하는 라우팅 패턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 사용자가 동일한 경로로 접속하게 되더라도 특정 조건을 만족하게 되면 다른 페이지를 렌더링&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 여기서 특정 조건: 초기 접속&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 링크를 클릭한다거나, router.push 같은 client side rendering 방식으로 페이지가 이동할 때에만 인터셉팅이 일어난다. (새로고침을 하면 발생x)&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;참고: Intercepting Route로 특정 페이지를 가로채 모달 슬롯에 띄우고, Parallel Route로 배경 페이지(기존 화면)를 함께 유지하여 자연스러운 UI를 만들 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 최적화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이미지 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹페이지에서 가장 많은 용량을 차지하는 요소는 이미지다.(58%)&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;webp, AVIF 등의 차세대 형식으로 변환하기&lt;/li&gt;
&lt;li&gt;디바이스 사이즈에 맞는 이미지 불러오기&lt;/li&gt;
&lt;li&gt;lazy loading 적용하기&lt;/li&gt;
&lt;li&gt;blur image 활용&lt;/li&gt;
&lt;li&gt;기타 등등&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Next.js에서는 &lt;b&gt;Image 컴포넌트를&lt;/b&gt; 활용하면 자동으로 적용이 된다!&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import Image from &quot;next/image&quot;;

&amp;lt;Image src={coverImgUrl} width={80} height={105} alt={`도서 ${title}의 표지 이미지`}
&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;외부 주소로 이루어진 이미지를 불러오고 싶다면 next.config.js(mjs)를 수정해야한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;images: {
	domains: [도메인 입력],
},
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색 엔진 최적화(SEO)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메타 데이터 설정하기 (페이지별로 동적으로)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page.tsx에 다음과 같은 코드를 넣자&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;export const metadata: Metadata = {
	title: &quot;제목&quot;,
	description: &quot;설명&quot;,
	openGraph: {
		title: &quot;제목&quot;,
		description: &quot;설정&quot;,
		images: [&quot;/thumbnail.png&quot;], // /로 시작하면 public 바로 아래를 가리킨다.
	},
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적인 값을 메타데이터를 설정하고 싶으면 &lt;b&gt;generateMetadata&lt;/b&gt; 함수를 생성하자&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>next.js</category>
      <category>next.js 최적화</category>
      <category>parallel route</category>
      <category>서버 액션</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/179</guid>
      <comments>https://quickchabun.tistory.com/179#entry179comment</comments>
      <pubDate>Sun, 8 Jun 2025 13:48:27 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js 강의 정리] App Router에 관하여</title>
      <link>https://quickchabun.tistory.com/178</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이정환님의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'한 입 크기로 잘라먹는 Next.js(v15)'&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;강의를 듣고 정리한 내용입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;강의 링크:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot;&gt;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1749199410913&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;한 입 크기로 잘라먹는 Next.js(v15) 강의 | 이정환 Winterlood - 인프런&quot; data-og-description=&quot;이정환 Winterlood | , [임베딩 영상]한 입 크기로 잘라먹는 Next.js | Official Trailler한입 크기로 잘라먹는 Next.js(15+)15시간의 분량으로 Page Router부터 App Router까지  Page Router란?Next.js&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot; data-og-url=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b22s2k/hyY1dz5mHQ/E46ZpmPlwXlZ8qQQntaGd1/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/bFfjK0/hyY4eYKWf8/Ki0j1QWlaF9DctL2nJGPP1/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/byCJOE/hyY46TB97V/AzWtyxthtRfaNQjSzrbqwK/img.jpg?width=6000&amp;amp;height=4000&amp;amp;face=2024_759_2624_1415&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b22s2k/hyY1dz5mHQ/E46ZpmPlwXlZ8qQQntaGd1/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/bFfjK0/hyY4eYKWf8/Ki0j1QWlaF9DctL2nJGPP1/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/byCJOE/hyY46TB97V/AzWtyxthtRfaNQjSzrbqwK/img.jpg?width=6000&amp;amp;height=4000&amp;amp;face=2024_759_2624_1415');&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;한 입 크기로 잘라먹는 Next.js(v15) 강의 | 이정환 Winterlood - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이정환 Winterlood | , [임베딩 영상]한 입 크기로 잘라먹는 Next.js | Official Trailler한입 크기로 잘라먹는 Next.js(15+)15시간의 분량으로 Page Router부터 App Router까지  Page Router란?Next.js&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;App Router: &lt;b&gt;Next.js 13 버전에 새롭게 추가된 라우터&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Page Router와 비교해서 &lt;b&gt;네비게이팅, 프리페칭, 사전 렌더링&lt;/b&gt;은 크게 바뀌지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. App Router 버전의 페이지 라우팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;page&amp;rdquo;라는 이름의 파일이 페이지 역할을 하는 파일로 자동 설정&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;~/book을 만드려면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;book 폴더 안에 page.tsx를 만들면 된다.&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;동적 경로에 대응하려면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;book 폴더 안의 [id] 폴더를 생성하고 page.tsx를 만들면 된다.&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;쿼리 스트링, URL 파라미터와 같은 경로상에 포함되는 값들은 &lt;b&gt;props&lt;/b&gt;에 전달이 된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default async function Page({
  searchParams,
}: {
  searchParams: Promise&amp;lt;{ q: string }&amp;gt;;
}) {
  const { q } = await searchParams;
  return &amp;lt;div&amp;gt;Search 페이지 {q}&amp;lt;/div&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;URL 파라미터가 중첩으로 여러개 전달되는 경로에도 대응하려면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 폴더명을 &lt;b&gt;[&amp;hellip;id]&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;URL 파라미터가 없을 때에도 대응하고 싶다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 폴더명을 &lt;b&gt;[[&amp;hellip;id]]&lt;/b&gt;로 만들자 (옵셔널 캐치 올 세그먼트)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 레이아웃&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 페이지의 레이아웃을 설정하고 싶으면 그 폴더 안에 &lt;b&gt;layout.tsx&lt;/b&gt;를 생성하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/search/layout.tsx를 만들면 &amp;rarr; &amp;ldquo;/search&amp;rdquo; 경로로 시작하는 모든 페이지의 레이아웃으로 적용된다.&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;/search/layout.tsx와 /search/setting/layout.tsx가 있다면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; /search/layout.tsx이 적용되고, 그 안쪽으로 /search/setting/layout.tsx가 적용된다.&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;/src/app에 있는 layout.tsx은 루트 레이아웃&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 절대 없어지면 안됨 (이름을 바꾸면 자동 생성될 정도)&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;레이아웃을 직접 생성할 때에는 Children이라는 props를 통해서 페이지 컴포넌트를 전달 받아서 return 문에 배치를 시켜야한다.(타입은 ReactNode)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;app 밑에 소괄호로 감싸진 폴더를 생성 (RouteGroup)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 경로상에 아무런 영향을 주지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 각기 다른 경로를 갖는 페이지 파일들을 하나의 폴더 안에 묶어둘 수 있음&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. React Server Component&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React 18v부터 추가된 새로운 유형의 컴포넌트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;서버측에서만 실행되는 컴포넌트&lt;/b&gt; (브라우저에서 실행x)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서버측에서 사전 렌더링을 진행할 때 딱 한번만 실행됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(클라이언트 컴포넌트는 사진 렌더링 진행할 때 한번, hydration 진행할 때 한 번 = 총 2번 실행)&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;&amp;rarr; 클라이언트 컴포넌트는 꼭 필요한 경우에만 사용&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;&amp;lsquo;use client&amp;rsquo;&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;참고: 앱 라우터에서는 파일의 이름이 Page나 Layout이 아닌 일반적인 파일을 생성해도 된다. (Co-Location)&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. 서버 컴포넌트에서는 브라우저에서 실행될 코드가 포함되면 안된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; React Hooks, 이벤트 핸들러, 브라우저에서 실행되는 기능을 담고 있는 라이브러리..&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항2. 클라이언트 컴포넌트는 클라이언트에서만 실행되지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서버와 클라이언트에서 모두 실행된다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항3. 클라이언트 컴포넌트에서 서버 컴포넌트를 import 할 수 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이 경우 Next.js는 자동으로 서버 컴포넌트를 클라이언트 컴포넌트로 변경&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 되도록이면 클라이언트 컴포넌트의 자식으로 서버 컴포넌트를 배치하는건 피하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 써야된다면 import해서 쓰지 말고 children props를 받아서 전달하면 된다&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export default function Home() {
	return (
			&amp;lt;div className={styles.page}&amp;gt;
				&amp;lt;ClientComponent&amp;gt;
					&amp;lt;ServerComponent /&amp;gt;
				&amp;lt;/ClientComponent&amp;gt;
			&amp;lt;/div&amp;gt;
		);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주의사항4. 서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화되지 않는 props는 전달 불가하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬화(serialization): 객체, 배열, 클래스 등의 복잡한 구조의 데이터를 네트워크 상으로 전송하기 위해 아주 단순한 형태(문자열, Byte)로 변환하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 함수는 직렬화 불가&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;&amp;rarr; 이 때 RSC 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;RSC Payload&lt;/b&gt;: React Server Component의 순수한 데이터(결과물)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; React Server Component를 직렬화한 결과&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; RSC Payload에는 서버 컴포넌트의 모든 데이터가 포함 (서버 컴포넌트의 렌더링 결과, 연결된 클라이언트 컴포넌트의 위치, 클라이언트 컴포넌트에게 전달하는 Props 값 등)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 네비게이팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 이동은 Client Side Rendering 방식으로 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 브라우저한테 JS Bundle을 전달할 때 RSC Payload도 같이 전달하게 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;programmatic하게 이동하려면 useRouter를 import한다 ( next/navigation에서 가져와야한다)&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useRouter } from &quot;next/navigation&quot;;

export default function Searchbar() {
	const router = useRouter();
	const [search, setSearch] = useState(&quot;&quot;);
	
	const onChangeSearch = (e: React.ChangeEvent&amp;lt;HTMLInputElement&amp;gt;) =&amp;gt; {
		setSearch(e.target.value);
	};
	
	const onSubmit = () =&amp;gt; {
		router.push(`/search?q=${search}`);
	};
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적인 페이지는 일단 RSC 페이로드만 가져온다.&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;App Router 버전에서는 서버 컴포넌트는 RSC 페이로드로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 컴포넌트는 JS Bundle로 나뉘어서 동시에 전달로 된다.&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;App Router에서 쿼리스트링을 불러오려면 useSearchParams를 활용해야 한다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;const searchParams = useSearchParams();

const q = searchParams.get(&quot;q&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 데이터 페칭 in App router&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 컴포넌트에서는 Async 키워드를 사용할 수 없었음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 브라우저에서 동작 시 문제를 일으킬 수 있기 때문에 권장되지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; useMemo나 useCallback같은 memoization 차원에서 문제 발생 가능성&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;하지만 서버 컴포넌트는 브라우저에서 실행되지 않기 때문에 Async 키워드 사용이 가능하다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; await 키워드로 비동기적으로 컴포넌트 내에서 직접 데이터 페칭 가능 (Fetching data where it&amp;rsquo;s needed)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 더 이상 페이지 컴포넌트로부터 데이터를 props나 context API로 넘겨줄 필요가 없다!&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;&amp;lsquo;NEXT_PUBLIC&amp;rsquo;&lt;/b&gt;을 붙이지 않으면 서버 측에서만 접근이 가능하게 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Data Cache&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;fetch 메서드를 활용해 불러온 데이터를 Next 서버에서 보관하는 기능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영구적으로 데이터를 보관하거나, 특정 시간을 주기로 갱신시키는 것도 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불필요한 데이터 요청의 수를 줄여서 웹 서비스의 성능을 크게 개선할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;const response = await fetch(~/api, { cache : &amp;ldquo;force-cache&amp;rdquo; });&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;cache: &amp;ldquo;force-cache&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 요청의 결과를 무조건 캐싱&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 한번 호출된 이후에는 다시 호출되지 않음&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 메서드를 사용해야 한다(axios같은 라이브러리 안됨)&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;cache: &amp;ldquo;no-store&amp;rdquo;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 데이터 페칭의 결과를 저장하지 않는 옵션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 캐싱을 아예 하지 않도록 설정하는 옵션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 캐싱 옵션으로 아무것도 주지 않으면 기본 값으로써 캐싱이 되지 않는 요청으로 동작한다. (15버전부터)&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.config.mjs 안에 아래 코드를 설정하면 모든 데이터 페칭이 로그로써 콘솔에 잘 출력된다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const nextConfig = {
	logging: {
		fetches: {
			fullUrl: 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;&lt;b&gt;next: { revalidate: 3 }&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 특정 시간을 주기로 캐시를 업데이트 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 마치 Page Router의 ISR 방식과 유사함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 3초 이후에 데이터 페칭을 하게 되면 캐시된 데이터를 stale로 설정하고 캐시된 데이터를 반환, 그리고 서버에서 데이터 페칭을 진행해서 새로운 데이터 캐싱&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;next: { tags: [&amp;rsquo;a&amp;rsquo;] }&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; On-Demand Revalidate&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 요청이 들어왔을 때 데이터를 최신화 함&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Request Memoization&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;하나의 페이지를 렌더링하는 동안 중복된 API 요청을 캐싱하기 위해 존재&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 렌더링이 종료되면 모든 캐시가 소멸&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 자동으로 작동&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;&amp;rarr; 백엔드 서버로부터 불러온 데이터를 거의 영구적으로 보관하기 위해 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서버 가동 중에는 영구적으로 보관&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;&amp;rarr; 서버 컴포넌트의 등장으로 데이터가 필요한 곳에서 직접 데이터를 페칭할 수 있게됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서로 다른 컴포넌트에서 중복된 api 요청이 생기는 일이 많아짐&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Full Route Cache&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Next 서버 측에서 빌드 타임에 특정 페이지의 렌더링 결과를 캐싱하는 기능&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;Dynamic Page로 설정되는 기준&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 특정 페이지가 접속 요청을 받을 때마다 매번 변화가 생기거나, 데이터가 달라질 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(서버 컴포넌트만 해당, 클라이언트 컴포넌트는 페이지 유형에 영향을 미치지 않음)&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;캐시되지 않는 Data Fetching을 사용할 경우&lt;/li&gt;
&lt;li&gt;동적 함수(쿠키, 헤더, 쿼리스트링)을 사용하는 컴포넌트가 있을 때&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static Page로 설정되는 기준&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Dynamic Page가 아니면 모두 Static Page (Default)&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;Static Page에 Full Route Cache가 적용됨&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(그렇다고 Dynamic Page를 지양해야한다는 것은 아님)&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;Full Route Cache 또한 revalidate가 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(데이터 캐시가 stale되므로 full route cache도 stale됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 단 하나의 컴포넌트라도 revalidate되면 Full Route Cache도 revalidate가 된다)&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;(사전 렌더링 과정에서 배제가 되게 하고 싶다면?)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;Suspense로 감싸고 fallback을 주자&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;&amp;lt;div&amp;gt;
	&amp;lt;Suspense fallback={&amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;}&amp;gt;
		&amp;lt;Searchbar /&amp;gt;
	&amp;lt;/Suspense&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;generateStaticParams 함수를 통해 정적인 파라미터를 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;qml&quot;&gt;&lt;code&gt;export function generateStaticParams() {
  return [{ id: &quot;1&quot; }, { id: &quot;2&quot; }, { id: &quot;3&quot; }];
}

&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;주의점1: 문자열로만 명시해야한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;export const dynamicParams = false; 옵션을 추가하면 동적 경로가 적용이 되지 않는다. (위의 1,2,3 경로빼고는 적용x)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. Route Segment Option&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특정 페이지의 유형을 강제로 Static, Dynamic 페이지로 설정&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;export const dynamic = &amp;lsquo;&amp;rsquo;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;auto: 기본값, 아무것도 강제하지 않음&lt;/li&gt;
&lt;li&gt;force-dynamic: 페이지를 강제로 Dynamic 페이지로 설정&lt;/li&gt;
&lt;li&gt;force-static: 페이지를 강제로 Static 페이지로 설정 (부작용 조심)&lt;/li&gt;
&lt;li&gt;error: 페이지를 강제로 Static 페이지 설정 (설정하면 안되는 이유 &amp;rarr; 빌드 오류)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. Client Router Cache&lt;/h2&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;페이지 이동을 효율적으로 진행하기 위해 페이지의 일부 데이터를 보관함&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;여러 개의 페이지가 공통된 레이아웃을 사용한다면, 중복된 RSC 페이로드를 여러 차례 브라우저에서 요청하거나 전달받게 된다는 문제 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 서버로부터 전달받게 되는 RSC 페이로드 값들 중 레이아웃에 해당하는 부분의 데이터만 따로 추출 후 Client Route Cache에 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 중복되는 레이아웃들을 Client Route Cache에 저장&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 새로고침을 하거나 탭을 껐다 켰을 때는 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 스트리밍&lt;/h2&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;rarr; 사용자에게 긴 로딩없이 좋은 경험 제공 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js는 HTML 페이지를 스트리밍하는 기능을 자체적으로 제공&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;a. 페이지 스트리밍&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래 걸리는 컴포넌트의 렌더링을 사용자가 좀 더 좋은 환경에서 기다리게 할 수 있기 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 빨리 렌더링할 수 있는 컴포넌트 먼저 출력&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 스트리밍은 Dynamic Page에 자주 사용됨&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;적용방법: 페이지가 있는 동일한 경로에 loading.tsx를 만들어주면 된다&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;주의1: async 키워드가 붙어 비동기로 작동하도록 설정한 페이지 컴포넌트에만 스트리밍을 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의2: loading.tsx에는 페이지 컴포넌트에만 적용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의3: 브라우저에서 쿼리스트링이 변경될 때에는 스트리밍이 트리거되지 않는다&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;b. 컴포넌트 스트리밍&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트에도 스트리밍을 적용하고 싶다면, Suspense로 컴포넌트를 감싸면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리스트링 변경 중에도 스트리밍을 적용하고 싶다면, key 값을 주면 된다 (key가 바뀔 때마다 컴포넌트가 바뀌므로)&lt;/p&gt;
&lt;pre class=&quot;django&quot;&gt;&lt;code&gt;    &amp;lt;Suspense key={q || &quot;&quot;} fallback={&amp;lt;div&amp;gt;Loading ...&amp;lt;/div&amp;gt;}&amp;gt;
      &amp;lt;SearchResult q={q || &quot;&quot;} /&amp;gt;
    &amp;lt;/Suspense&amp;gt;
  );
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;c. 스켈레톤 UI&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 로딩 시간부터 컴포넌트가 어떻게 생겼는지 예측할 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;skeleton 컴포넌트를 만들고, 몇 개의 스켈레톤 리스트가 필요한지 전달받는 컴포넌트를 생성해서 사용하면 편하다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import BookItemSkeleton from &quot;./book-item-skeleton&quot;;

export default function BookListSkeleton({ count }: { count: number }) {
  return new Array(count)
    .fill(0)
    .map((_, idx) =&amp;gt; &amp;lt;BookItemSkeleton key={`book-item-skeleton-${idx}`} /&amp;gt;);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React Loading Skeleton library를 사용해도 편하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;d. 에러 핸들링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지와 같은 경로에 &lt;b&gt;error.tsx&lt;/b&gt;를 생성하면 에러 발생 시 대체 UI를 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(&amp;rsquo;use client&amp;rsquo; 지시자를 추가해서 클라이언트 컴포넌트로 만들어주자 &amp;rarr; 서버에서 발생하는 에러, 클라이언트에서 발생하는 에러 모두 대응하기 위해)&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;에러 컴포넌트의 props에 error를 활용하면 메시지를 출력할 수 있다. (type은 Error)&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;reset&lt;/b&gt;이라는 props를 활용해서 컴포넌트를 다시 렌더링을 시도할 수 있다. (type은 () &amp;rArr; void )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 하지만 화면을 다시 렌더링하기만 할뿐, 서버 컴포넌트를 다시 실행하지는 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; window.location.reload()를 사용해서 강제로 새로고침을 해도 좋다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 더 좋은 방법은 &lt;b&gt;router.refresh()&lt;/b&gt; 사용 (현재 페이지에 필요한 서버 컴포넌트들을 다시 불러옴)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그리고 그 뒤에 reset()을 사용해서 에러 상태를 초기화하고 컴포넌트들을 다시 렌더링해야한다.&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;그런데 router.refresh()는 비동기적으로 실행되므로 reset()이 먼저 실행되는 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;startTransition&lt;/b&gt;으로 감싸서 router.refresh()와 reset()이 일괄적으로 실행되도록 하자&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;&quot;use client&quot;;

import { useRouter } from &quot;next/navigation&quot;;
import { startTransition, useEffect } from &quot;react&quot;;

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () =&amp;gt; void;
}) {
  const router = useRouter();

  useEffect(() =&amp;gt; {
    console.error(error.message);
  }, [error]);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h3&amp;gt;오류가 발생했습니다&amp;lt;/h3&amp;gt;
      &amp;lt;button
        onClick={() =&amp;gt; {
          // 함수 하나를 인수로받아서, 해당 함수 내부의 코드를 동기적으로 실행
          startTransition(() =&amp;gt; {
            router.refresh(); // 현재 페이지에 필요한 서버컴포넌트들을 다시 불러옴
            reset(); // 에러 상태를 초기화, 컴포넌트들을 다시 렌더링
          });
        }}
      &amp;gt;
        다시 시도
      &amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>app router</category>
      <category>next.js</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/178</guid>
      <comments>https://quickchabun.tistory.com/178#entry178comment</comments>
      <pubDate>Fri, 6 Jun 2025 17:44:03 +0900</pubDate>
    </item>
    <item>
      <title>[Next.js 강의 정리] Next.js와 Page Router에 관하여</title>
      <link>https://quickchabun.tistory.com/177</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이정환님의 &lt;b&gt;'한 입 크기로 잘라먹는 Next.js(v15)'&lt;/b&gt; 강의를 듣고 정리한 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강의 링크: &lt;a href=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1748174201663&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;한 입 크기로 잘라먹는 Next.js(v15) 강의 | 이정환 Winterlood - 인프런&quot; data-og-description=&quot;이정환 Winterlood | , [임베딩 영상]한 입 크기로 잘라먹는 Next.js | Official Trailler한입 크기로 잘라먹는 Next.js(15+)15시간의 분량으로 Page Router부터 App Router까지  Page Router란?Next.js&quot; data-og-host=&quot;www.inflearn.com&quot; data-og-source-url=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot; data-og-url=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bO94jP/hyYYAuHPlR/iPdWltiaTmcDCGH80xYtBk/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/tX8Kx/hyY0w5yDsz/qlkrq9FLlKXdNurP5ZEr9K/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/bjDszr/hyYYA2uiKz/MH9eK22v72Vz5Id9Vl6r4k/img.jpg?width=6000&amp;amp;height=4000&amp;amp;face=2024_759_2624_1415&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%ED%81%AC%EA%B8%B0-nextjs?srsltid=AfmBOoryPUbZjBwZDQne9kDfuiHCFu7VdxmeCNFtMqj4F58vfjIaezUX&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bO94jP/hyYYAuHPlR/iPdWltiaTmcDCGH80xYtBk/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/tX8Kx/hyY0w5yDsz/qlkrq9FLlKXdNurP5ZEr9K/img.png?width=1200&amp;amp;height=781&amp;amp;face=860_189_1008_350,https://scrap.kakaocdn.net/dn/bjDszr/hyYYA2uiKz/MH9eK22v72Vz5Id9Vl6r4k/img.jpg?width=6000&amp;amp;height=4000&amp;amp;face=2024_759_2624_1415');&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;한 입 크기로 잘라먹는 Next.js(v15) 강의 | 이정환 Winterlood - 인프런&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이정환 Winterlood | , [임베딩 영상]한 입 크기로 잘라먹는 Next.js | Official Trailler한입 크기로 잘라먹는 Next.js(15+)15시간의 분량으로 Page Router부터 App Router까지  Page Router란?Next.js&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.inflearn.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Next.js가 무엇일까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js = React의 활용판&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가 인기가 많은 이유: &lt;b&gt;Framework&lt;/b&gt;이므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주도권이 개발자에게 있다: Library&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;React.js (Library)에서 페이지 라우팅 기능을 구현한다면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;React Router말고 Tanstack Router같은 다른거 써도 됨&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;주도권이 개발자에게 없다: Framework&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;Next.js에서 페이지 라우팅 기능을 구현해야한다면 Next.js에서 제공하는 App Router나 Page Router를 써야함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자유도가 낮다 &amp;rarr; 거의 모든 기능을 제공&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Next.js 사전 렌더링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전 렌더링: 브라우저의 요청에 사전에 렌더링이 완료된 HTML을 응답하는 렌더링 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Client Side Rendering의 단점을 효율적으로 해결하는 기술&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Client Side Rendering(CSR)이란?&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트(브라우저)에서 직접 화면을 렌더링 하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점: 초기 접속 이후의 페이지 이동이 빠름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점: FCP(초기 접속 속도)가 느림&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;유저가 서버에 접속 요청&lt;/li&gt;
&lt;li&gt;서버가 브라우저에 빈껍데기인 index.html을 줌&lt;/li&gt;
&lt;li&gt;브라우저는 유저에게 빈 화면을 렌더링함&lt;/li&gt;
&lt;li&gt;서버는 리액트 앱을 번들링해서 브라우저에 보내준다 (서비스에서 접근 가능한 모든 컴포넌트 코드가 존재 &amp;rarr; 나중에 페이지를 이동해도 서버에게 새로운 페이지를 요청할 필요가 없음 &amp;rarr; 초기 접속 이후의 페이지 이동이 빠른 이유)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 시작 후 화면이 렌더링되기까지 꽤 많은 시간이 소요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;FCP(First Contentful Paint, 요청 시작 시점으로부터 컨텐츠가 화면에 처음 나타나는데 걸리는 시간)&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사전 렌더링&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서버가 서버 측에서 직접 리액트 앱을 실행을 시켜서 HTML로 변환&lt;/li&gt;
&lt;li&gt;서버에서 렌더링이 완료된 HTML 파일을 그대로 브라우저에게 보내줌&lt;/li&gt;
&lt;li&gt;브라우저는 HTML 파일을 그대로 화면에 렌더링&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;렌더링 단어 뜻 차이&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 실행되는 렌더링: 자바스크립트 코드(React 컴포넌트)를 HTML로 변환하는 과정&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;화면에 렌더링: HTML 코드를 브라우저가 화면에 그려내는 작업&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 화면에 HTML 파일이 렌더링을 하였다고 해서 클릭, 페이지 이동 등 상호작용은 동작하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 상호작용은 HTML이 아니라 자바스크립트를 활용해야되기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt;브라우저는 서버로부터 받은 자바스크립트 코드를 실행해서 HTML 요소들과 상호작용을 연결 &amp;rarr; 수화(hydration)&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;TTI(Time To Interactive)&lt;/b&gt;: 요청 시작 후 상호작용까지 걸리는 시간&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로의 페이지 이동 등의 과정은 CSR처럼 작동함&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에서 사전 렌더링을 도입함으로써&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;React App의 단점 해소: 빠른 FCP 달성&lt;/li&gt;
&lt;li&gt;React App의 장점 승계: 빠른 페이지 이동&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Page Router에 관하여&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Page Router&lt;/b&gt;: pages 폴더 아래에 들어있는 폴더/파일들의 이름을 기준으로 페이지 라우팅을 제공&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;pages/index.js &amp;rarr; ~/&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pages/about.js &amp;rarr; ~/about&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pages/about/index.js &amp;rarr; ~/about&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;동적 경로(Dynamic Routes)&lt;/b&gt;: 가변적인 값을 포함하고 있는 경로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;pages/item/[id].js &amp;rarr; ~/item/1, ~/item/2, &amp;hellip;&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;npx: node package excutor&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; npmjs.com에 등록되어 있는 최신 버전의 노드 패키지를 다운로드 없이 바로 실행시키는 명령어&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;_app.tsx&lt;/b&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;/li&gt;
&lt;li&gt;모든 페이지 역할을 하는 컴포넌트들의 부모 컴포넌트&lt;/li&gt;
&lt;li&gt;Component: 현재 페이지 역할을 할 컴포넌트를 받음&lt;/li&gt;
&lt;li&gt;pageProps: Component에 전달될 페이지의 props들을 모두 객체로 보관한 것&lt;/li&gt;
&lt;li&gt;만약 모든 페이지에 공통적으로 적용해야할 내용이 있으면 _app.tsx에 넣으면 적용이 된다&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;b&gt;_document.tsx&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 페이지에 공통적으로 적용이 되어야하는 Next.js 앱의 html 코드를 설정하는 컴포넌트: index.html과 비슷한 역할&lt;/li&gt;
&lt;li&gt;meta tag, 폰트, 캐릭터 셋, 서드 파티 스크립트 설정 등 페이지 전체에 다 적용되는 HTML 태그를 관리하깅 ㅟ해 사용&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;b&gt;nextconfig.mjs&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;next app의 설정을 관리하는 파일&lt;/li&gt;
&lt;li&gt;reactStrictMode on/off 가능&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;page router에서 쿼리스트링을 활용할거면 &lt;b&gt;useRouter&lt;/b&gt;를 사용하자 (next/router에서 import해야 함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;const { q } = router.query로 쿼리스트링의 값을 가져올 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;url parameter를 활용할거면 파일명에 대괄호([])를 활용하자 (예시: book/[id].tsx)&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;book/&lt;b&gt;[&amp;hellip;id].tsx&lt;/b&gt;로 파일명을 바꾸면 book 경로 뒤에 여러 개의 id가 연달아 들어올 수 있다. (/book/123/413/45/123434&amp;hellip;) : &lt;b&gt;Catch All segment&lt;/b&gt;라고 부름&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[[&amp;hellip;id]].tsx&lt;/b&gt;로 파일명을 바꾸면 book으로 경로가 끝나도 오류가 발생하지 않는다: &lt;b&gt;Optional Catch All Segment&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;&lt;b&gt;Page/404.tsx&lt;/b&gt;를 만들면 존재하지 않는 경로로 접속했을 때 404.tsx에 있는 내용이 뜨게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Page Router에서 Navigating&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a 태그는 서버에게 새로운 페이지를 매번 다시 요청하므로 쓰지 않는게 좋다.&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 app에서는 &lt;b&gt;Link&lt;/b&gt;를 사용하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;Link href={&amp;rdquo;/&amp;rdquo;}&amp;gt;index&amp;lt;/Link&amp;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;Programmatic Navigation: 특정 버튼이 클릭이 되었거나, 특정 조건이 만족했을 경우에 어떠한 함수 내부에서 페이지를 이동시키는 방법&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { useRouter } from &quot;next/router&quot;;

(...)
const router = useRouter();

const onClickButton = () =&amp;gt; {
	router.push(&quot;/test&quot;);
};

return (
	&amp;lt;&amp;gt;
		&amp;lt;div&amp;gt;
			&amp;lt;button onClick={onClickButton}&amp;gt;/test 페이지로 이동&amp;gt;&amp;lt;/button&amp;gt;
	&amp;lt;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;replace: 뒤로가기를 방지하며 페이지 이동&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프리페칭(Prefetching)&lt;/h3&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;rarr; 빠른 페이지 이동을 위해 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 프리페칭은 개발 모드에서 실행이 안되기 때문에 &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;사전 렌더링 기능에서 HTML 페이지 응답 이후 후속으로 모든 자바스크립트 코드를 번들 파일 형태로 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 페이지 이동 요청이 있어도 서버에게 추가적인 리소스를 요청할 필요가 없다고 알고 있는데 프리페칭이 필요한 이유가 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-&amp;gt;Next.js에서는 모든 리액트 컴포넌트들을 페이지별로 분리해서 저장을 해두기 때문&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;즉, 사전 렌더링의 과정에서 번들 파일을 전달할 때 모든 페이지에 필요한 자바스크립트 코드가 다 전달되는게 아니라, 현재 페이지에 필요한 번들 파일만 전달된다.&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;만약 모든 페이지의 번들파일을 전달하면 용량이 커져서 hydration이 늦어지기 때문&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 프리페칭이 작동하는데, 현재 페이지의 연결된 모든 페이지의 JS 번들을 불러오게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Link component로 명시된 경로가 아니면 프리페칭이 작동되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 만약 programmatic하게 경로를 이동한다면, 프리페칭을 할 수 있게 코드를 넣어주면 된다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const router = useRouter();

const onClickButton = () =&amp;gt; {
	router.push(&quot;/test&quot;);
};

useEffect(() =&amp;gt; {
	router.prefetch(&quot;/test&quot;);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 특정 페이지를 프리페칭을 시키고 싶지 않다면 해당 링크 컴포넌트에 &lt;b&gt;prefetch={false}&lt;/b&gt;를 넣어주면 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API Routes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Next.js에서 API를 구축할 수 있게 해주는 기능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/pages/api 안의 파일이 API 응답을 정의하는 파일&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import type { NextApiRequest, NextApiResponse } from &quot;next&quot;;

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const date = new Date();
  res.json({ time: date.toLocaleString() });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;type은 Next.js에 탑재된 NextApiRequest, NextApiResponse를 활용하자&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;위처럼 생성 후 localhost:3000/api/time로 이동 하면 코드의 작성된 것처럼 time: date.toLocalString을 확인할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스타일링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js에서는 &lt;b&gt;App component가 아닌 파일에서&lt;/b&gt; import문을 통해 css 파일을 그대로 불러오는 것을 제한한다(페이지별로 css의 className이 겹치는 문제를 방지하기 위해)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Next.js에서는 &lt;b&gt;CSS Module&lt;/b&gt;을 사용하자 (*.module.css)&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;import style from &quot;./index.module.css&quot;;

(...)

&amp;lt;h1 className={style.h1}&amp;gt;Hello&amp;lt;/h1&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;페이지별 레이아웃 설정하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 페이지에만 레이아웃을 설정하고 싶다면 페이지 파일에서 &lt;b&gt;getLayout 메서드&lt;/b&gt;를 생성하고, App component에서 받아오면 된다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SearchableLayout을 적용한다고 했을 때&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;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import SearchableLayout from &quot;@/components/searchable-layout&quot;;
import { ReactNode } from &quot;react&quot;;

export default function Home() {
  return (
    &amp;lt;&amp;gt;
      &amp;lt;h1&amp;gt;인덱스&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;H2&amp;lt;/h2&amp;gt;
    &amp;lt;/&amp;gt;
  );
}

Home.getLayout = (page: ReactNode) =&amp;gt; {
  return &amp;lt;SearchableLayout&amp;gt;{page}&amp;lt;/SearchableLayout&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;b&gt;_app.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import GlobalLayout from &quot;@/components/global-layout&quot;;
import &quot;@/styles/globals.css&quot;;
import { NextPage } from &quot;next&quot;;
import type { AppProps } from &quot;next/app&quot;;
import { ReactNode } from &quot;react&quot;;

type NextPageWithLayout = NextPage &amp;amp; {
  getLayout?: (page: ReactNode) =&amp;gt; ReactNode;
};

export default function App({
  Component,
  pageProps,
}: AppProps &amp;amp; {
  Component: NextPageWithLayout;
}) {
  const getLayout = Component.getLayout ?? ((page: ReactNode) =&amp;gt; page);

  return &amp;lt;GlobalLayout&amp;gt;{getLayout(&amp;lt;Component {...pageProps} /&amp;gt;)}&amp;lt;/GlobalLayout&amp;gt;;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 페칭&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리액트에서는 데이터 페칭을 어떻게 했었지?&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;불러온 데이터를 보관할 State 생성&lt;/li&gt;
&lt;li&gt;데이터 페칭 함수 생성&lt;/li&gt;
&lt;li&gt;컴포넌트 마운트 시점에 fetchData 호출&lt;/li&gt;
&lt;li&gt;데이터 로딩중일 때의 예외처리&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점: 초기 접속 요청부터 데이터 로딩까지 오랜 시간이 걸림&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이유: &lt;b&gt;컴포넌트 마운트가 된 이후&lt;/b&gt;에야 fetchData를 호출하기 때문&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에서는 백엔드 서버에서 &lt;b&gt;미리 렌더링을 할 때 (사전 렌더링 중)&lt;/b&gt; 필요한 데이터를 미리 불러오도록 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; React보다 &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;그런데 이 때 서버로부터 데이터를 받아오는 시간이 오래 걸려버릴 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이를 위해 Next.js는 &lt;b&gt;다양한 사전 렌더링 방식을&lt;/b&gt; 제공한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSR(Server Side Rendering)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본적인 사전 렌더링 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청이 들어올때마다 사전렌더링을 진행함&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js에서 페이지 파일 안에 getServerSideProps같은 메서드를 만들어서 내보내면 해당 페이지는 SSR로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;getServerSideProps: 컴포넌트보다 먼저 실행되어서, 컴포넌트에 필요한 데이터를 불러오는 함수&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체를 반환해야하는데 그 객체 안에는 props 프로퍼티가 있어야한다.&lt;/li&gt;
&lt;li&gt;서버에서 실행되기 때문에 브라우저에서 실행되는 함수를 실행하면 안된다(ex. window.location;)&lt;/li&gt;
&lt;li&gt;사실, 페이지에서 실행되는 컴포넌트도 서버에서 한 번 실행되기 때문에 window.location;을 실행하면 에러가 발생한다.&lt;/li&gt;
&lt;li&gt;만약 위 코드를 실행하고 싶으면 useEffect를 활용하면 된다.&lt;/li&gt;
&lt;li&gt;props의 타입은 &lt;b&gt;InferGetServerSidePropsType&amp;lt;typeof getServerSideProps&amp;gt;&lt;/b&gt;를 활용하면 된다.&lt;/li&gt;
&lt;li&gt;쿼리스트링, url 파라미터를 가져오려면 파라미터에 &lt;b&gt;context: GetServerSidePropsContext&lt;/b&gt;를 넣어주자 (자동 추론)&lt;/li&gt;
&lt;li&gt;컴포넌트에 전달할 때에도 타입은 &lt;b&gt;InferGetServerSidePropsType&amp;lt;typeof getServerSideProps&amp;gt;&lt;/b&gt;로 자동추론 해주자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const getServerSideProps = async (
  context: GetServerSidePropsContext
) =&amp;gt; {
	//   const id = context.params!.id;
  const q = context.query.q;
  const books = await fetchBooks(q as string);

  return {
    props: {
      books,
    },
  };
};

export default function Page({
  books,
}: InferGetServerSidePropsType&amp;lt;typeof getServerSideProps&amp;gt;) {
  return (
    &amp;lt;div&amp;gt;
      {books.map((book) =&amp;gt; (
        &amp;lt;BookItem key={book.id} {...book} /&amp;gt;
      ))}
    &amp;lt;/div&amp;gt;
  );
}

Page.getLayout = (page: ReactNode) =&amp;gt; {
  return &amp;lt;SearchableLayout&amp;gt;{page}&amp;lt;/SearchableLayout&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;api (/src/lib/*.ts)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환이 서버에서 비동기적으로 진행되므로 Promise 타입을 넣어주자&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;export default async function fetchBooks(q?: string): Promise&amp;lt;BookData[]&amp;gt; {
  let url = `http://localhost:(숫자)/book`;

  if (q) {
    url += `/search?q=${q}`;
  }

  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error();
    }

    return await response.json();
  } catch (err) {
    console.error(err);
    return [];
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSG(정적 사이트 생성)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSR에서 페이지를 접속할 때, 페이지가 백엔드 서버로부터 특정 데이터를 필요로 한다면 브라우저에 접속 요청이 있을 때마다 사전 렌더링 과정에서 새로운 데이터를 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 페이지 내부의 데이터를 항상 최신 버전으로 유지 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 데이터의 응답 속도가 느려진다면 브라우저 로딩이 길어져서 사용자 경험이 안 좋아지는 단점&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;정적 사이트 생성(Static Site Generation)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SSR의 단점을 해결하는 사전 렌더링 방식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빌드 타임에 페이지를 미리 사전렌더링&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; 백엔드 서버와의 소통이 서버가 가동되기 이전인 빌드타임에만 발생&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 사전 렌더링에 많은 시간이 소요되는 페이지더라도 사용자의 요청에는 매우 빠른 속도로 응답 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 매번 똑같은 페이지만 응답하므로, &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;SSG를 사용하려면 &lt;b&gt;getStatisProps&lt;/b&gt;를 활용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입을 적용할 때 &lt;b&gt;InferGetStaticPropsType&lt;/b&gt;, 제네릭으로 &lt;b&gt;getStaticProps&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;주의: SSG가 잘 동작하는 지 확인하려면 &lt;b&gt;빌드해서 프로덕션 모드&lt;/b&gt;에서 확인해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에 나타나는 페이지별 빌드 결과에서 흰색 동그라미 기호가 있는 페이지가 SSG 방식으로 동작하는 페이지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(f는 주문형, SSR로 작동한다는 뜻. 빈 동그라미는 getStaticProps가 설정되지 않은 정적 페이지(기본값, 404나 테스트 페이지) )&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;getStaticProps에는 쿼리스트링이 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 빌드 타임에 페이지를 미리 사전렌더링하므로 쿼리스트링에 어떤 값이 들어올지 알 수 없으므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 클라이언트에서 진행해줘야한다(&lt;b&gt;리액트 앱에서 했던 방식&lt;/b&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;SSG 동적 경로에도 적용하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 경로를 가진 페이지는 빌드타임 때 이 페이지에 어떤 경로들이 존재할 수 있는지(어떤 URL 파라미터들이 존재할 수 있는지) 설정해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 미리 각 페이지들을 사전렌더링 해야하므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;getStaticPaths&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;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;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;&lt;b&gt;fallback: false&lt;/b&gt; &amp;rarr; 404 Not Found 반환&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;fallback: &amp;lsquo;blocking&amp;rsquo;&lt;/b&gt; &amp;rarr; 실시간으로 요청받은 페이지를 사전 렌더링해서 브라우저에게 반환 (like SSR)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번 만들어지면 next 서버에 저장된 것을 확인할 수 있다.&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;fallback: true&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &amp;rarr; Props 없는 페이지(getStaticProps로 부터 받은 데이터가 없는 페이지) 반환&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나중에 Props를 계산해서 Props만 따로 반환&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;fallback 상태: 페이지 컴포넌트가 아직 서버로부터 데이터를 전달받지 못한 상태&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fallback 상태일 때와 에러 상태일 때를 구분하고 싶을 때는 &lt;b&gt;router.isFallback&lt;/b&gt;을 활용하자&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;const router = useRouter();

if (router.isFallback) return &quot;로딩중입니다&quot;;
if (!book) return &quot;문제가 발생했습니다 다시 시도하세요&quot;;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;not-found를 출력하고 싶을 때에는 getStaticProps에서 &lt;b&gt;notFound:true&lt;/b&gt; 문구를 주자.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;export const getStaticProps = async(
	context: GetStaticPropsContext
	) =&amp;gt; {
	const book = await fetchOneBook(Number(id));
	
	if (!book) {
		return {
			NotFound: true,
		};
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ISR(Incremental Static Regeneration, 증분 정적 재생성)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단순히 그냥 SSG 방식으로 생성된 정적 페이지를 일정 시간을 주기로 다시 생성하는 기술&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;만약 유통기한이 60초라면, 60초 이후 요청이 들어오면 초기 버전의 페이지를 반환하고, 그 때 새로 페이지를 생성한다. 그리고 그 다음부터 새로 생성한 페이지를 반환&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;매우 빠른 속도로 응답 가능 (SSG 장점) + 최신 데이터 반영 가능 (SSR 장점)&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;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;getStaticProps의 return문 안의 props 바깥에 &lt;b&gt;revalidate&lt;/b&gt;라는 프로퍼티를 추가하고, 유통기한을 초 단위로 주면 된다.&lt;/p&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;export const getStaticProps = async () =&amp;gt; {
	(...)
	
	return {
		props: {
			(...)
		},
		revalidate: 3, // 유통기한 3초
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ISR의 주문형 재검증(On-Demand-ISR)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(시간이 아닌) 요청을 받을 때마다 페이지를 다시 생성하는 ISR&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/api/routes에 revalidate.ts 생성&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;import { NextApiRequest, NextApiResponse } from &quot;next&quot;;

export default async function handler(
  req: NextApiRequest,    
  res: NextApiResponse 
) {
  try {
    // &quot;/&quot; 경로(홈페이지)를 on-demand로 revalidate
    await res.revalidate(&quot;/&quot;);

    // revalidate가 성공하면 JSON 응답을 전송
    return res.json({ revalidate: true });
  } catch (err) {
    res.status(500).send(&quot;Revalidation Failed&quot;);
  }
}
&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;b&gt;대부분의 케이스를 커버할 수 있는 강력한 사전 렌더링 방식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 오늘날 거의 대부분의 Next.js 구축된 웹서비스에서 사용됨&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SEO 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 상단 탭 favicon 설정 &amp;rarr; public/favicon.ico 변경&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;next/head의 Head 사용하기&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;return 문 안에 Head 태그에서 메타 태그 작성&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;return (
	&amp;lt;&amp;gt;
		&amp;lt;Head&amp;gt;
			&amp;lt;title&amp;gt;페이지 제목&amp;lt;/title&amp;gt;
			
			// 썸네일 (/로 시작은 /public을 의미)
			&amp;lt;meta property=&quot;og:image&quot; content=&quot;/thumbnail.png /&amp;gt;
			
			// open graph title
			&amp;lt;meta property=&quot;og:title&quot; content=&quot;페이지 제목&quot; /&amp;gt;
			
			// open graph description
			&amp;lt;meta property=&quot;og:description&quot; content=&quot;페이지에 대한 설명&quot; /&amp;gt;
		&amp;lt;/Head&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;getStaticPaths의 static:true를 활용하면 SEO 설정이 안되어버리는 문제 발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt;&amp;nbsp; &lt;b&gt;router.isFallback&lt;/b&gt;일 때에도 기본적인 메타태그들을 설정&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;if (router.isFallback) {
	return (
		&amp;lt;&amp;gt;
			&amp;lt;Head&amp;gt;
				(...)
			&amp;lt;/Head&amp;gt;
		&amp;lt;/&amp;gt;
	)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배포하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&lt;b&gt; (sudo) npm install -g vercel&lt;/b&gt;로 vercel 패키지 설치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;&amp;lsquo;vercel login&amp;rsquo;&lt;/b&gt; 명령어를 입력하여 로그인 진행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;&amp;lsquo;vercel&amp;rsquo;&lt;/b&gt; 명령어를 입력하여 배포 진행(백엔드 서버도 배포 진행해야함)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. vercel 홈페이지 dashboard에서 백엔드 프로젝트로 이동해서 도메인 주소 복사&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. &lt;b&gt;src/lib/*.ts&lt;/b&gt;의 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;즉시 배포 명령어: &amp;lsquo;vercel &amp;mdash;prod&amp;rsquo;&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결론&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Page Router의 장점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;파일 시스템 기반의 간편한 페이지 라우팅 제공&lt;/li&gt;
&lt;li&gt;다양한 방식의 사전 렌더링 제공&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Page Router의 단점&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;페이지별 레이아웃 설정이 번거롭다&lt;/li&gt;
&lt;li&gt;데이터 페칭이 페이지 컴포넌트에 집중된다&lt;/li&gt;
&lt;li&gt;불필요한 컴포넌트들도 JS Bundle에 포함된다(상호작용이 필요없는 컴포넌트들도 hydration)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 챕터부터 Page Router의 단점들을 보완한 App Router에 대해서 배우게 된다. 꾸준히 학습해보자!&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>next.js</category>
      <category>next.js 공부</category>
      <category>page router</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/177</guid>
      <comments>https://quickchabun.tistory.com/177#entry177comment</comments>
      <pubDate>Sun, 25 May 2025 21:16:29 +0900</pubDate>
    </item>
    <item>
      <title>useRef를 활용하여 stale closure를 해결해보자</title>
      <link>https://quickchabun.tistory.com/176</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주의: 정확하지 않은 서술이 있을 수 있습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 A에서 버튼을 클릭하면 recoil state가 변경되고, Hook B의 useEffect의 의존성 배열에 해당 recoil state가 있어서 Hook B에서 동작이 수행되게 설계를 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Hook B에서 recoil state의 변경을 잘 감지하지 못하는 경우가 발생했고, 따라서 의도치 않게 작동이 되는 상황이 벌어졌다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 왜 recoil state의 변경을 감지하지 못했을까?&lt;/h2&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;  // Component A
  const [something, setSomething] = useRecoilState(somethingState);

  const handleItemClick = (e: React.MouseEvent) =&amp;gt; {
        setSomething({A : newValue});
  }


  (...)

  &amp;lt;Button onClick={handleItemClick /&amp;gt;


  // Hook B
  const something = useRecoilValue(somethingState);

  useEffect(() =&amp;gt; {
      // do something with something
  }, [something])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 구성된 상황에서, 가끔씩 Hook B는 Component A의 변경된 상태가 아닌 이전 상태를 인식했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜냐하면 Recoil은 setSomething을 호출하면 상태 변경을 요청하는데, 이 상태 변경은 다음 렌더링 사이클에서 실제 반영한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 때, Hook B의 useEffect는 이전 렌더링 시점의 클로저를 캡처할 수 있기 때문에 상태 변경이 안되는 경우가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이 현상을 바로 &lt;b&gt;stale closure&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. useRef를 통해서 최신 상태를 참조하자​&lt;/h2&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;  const something = useRecoilValue(somethingState);
  const somethingRef = useRef(something);

  useEffect(() =&amp;gt; {
    somethingRef.current = something;
  }, [something]);

   useEffect(() =&amp;gt; {
      // do something with somethingRef.current
  }, [something])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 파일에 의존성 배열이 같은 useEffect가 있을 때 위에 있는 useEffect부터 실행된다(주의: 100% 보장은 아니라고한다).&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;더 위에 있는 useEffect를 통해 새로운 something을 somethingRef.current에 저장하고, 두 번째 useEffect(즉, 기존의 useEffect)에서 somethingRef.current를 통해서 갱신된 something 값을 사용하면 오류없이 작동을 보장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;주의: useEffect를 사용하지 않고 somethingRef.current = something;을 적용하는게 더 정확하다는 의견이 있다. 왜냐하면 직접 할당을 하면 렌더링 때마다 somethingRef.current에 something을 할당하기 때문에 더 안전하기 때문이라고 한다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>Intern</category>
      <category>stale closure</category>
      <category>useRef</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/176</guid>
      <comments>https://quickchabun.tistory.com/176#entry176comment</comments>
      <pubDate>Sat, 17 May 2025 22:46:38 +0900</pubDate>
    </item>
    <item>
      <title>배경이 따라오는 메모장을 만들기 위해 contentEditable을 사용해보자</title>
      <link>https://quickchabun.tistory.com/175</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 글을 입력할 수 있는 컴포넌트를 만들려면 보통 input이나 textarea를 활용한다.&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;글이 한 줄일 때는 뒤의 회색 배경이 글자와 딱 맞게 입력해야할 때마다 따라와야 하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 줄 이상일 때부터는 배경이 메모장의 최대 width로 유지되면서 줄 수 만큼 height가 늘어나야했다.&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;598&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJHMoA/btsNqq4UgVS/8Kf7v3PQl0Kpk0EvOEvp6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJHMoA/btsNqq4UgVS/8Kf7v3PQl0Kpk0EvOEvp6K/img.png&quot; data-alt=&quot;한 줄일 때는 뒤의 배경이 따라온다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJHMoA/btsNqq4UgVS/8Kf7v3PQl0Kpk0EvOEvp6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJHMoA%2FbtsNqq4UgVS%2F8Kf7v3PQl0Kpk0EvOEvp6K%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;400&quot; height=&quot;62&quot; data-origin-width=&quot;598&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;한 줄일 때는 뒤의 배경이 따라온다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/spV4L/btsNsS69kKJ/Cso7qq7zyGUSDI9WxTvgrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/spV4L/btsNsS69kKJ/Cso7qq7zyGUSDI9WxTvgrK/img.png&quot; data-alt=&quot;여러 줄일 때는 최대 width를 유지한채 height가 늘어난다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/spV4L/btsNsS69kKJ/Cso7qq7zyGUSDI9WxTvgrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FspV4L%2FbtsNsS69kKJ%2FCso7qq7zyGUSDI9WxTvgrK%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;400&quot; height=&quot;69&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;여러 줄일 때는 최대 width를 유지한채 height가 늘어난다&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;처음에는 &lt;b&gt;textarea&lt;/b&gt;를 활용해보았는데, 뒤의 회색 배경이 글자를 따라오게 만드는데 실패했다 (fit-content 속성을 주었음에도 불구하고)&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;input&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;그래서 div에 &lt;b&gt;contentEditable&lt;/b&gt; 속성을 주어 원하는 동작을 구현할 수 있었다. 그렇다면 어떻게 배경이 따라오는 메모장을 만들었는지 살펴보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;pre class=&quot;typescript&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;&amp;lt;MemoContainer className=&quot;memo-container&quot; onClick={handleMemoClick}&amp;gt;
    &amp;lt;img src={이미지 주소} alt=&quot;메모&quot; /&amp;gt;
    {isWriteMemo ? (
        &amp;lt;span className=&quot;memo-text editing&quot;&amp;gt;
            &amp;lt;EditableDiv
                className={!memo ? &quot;empty&quot; : &quot;&quot;}
                ref={editableDivRef}
                contentEditable
                onBlur={handleMemoBlur}
                onKeyDown={handleKeyDown}
                onInput={handleInput}
                suppressContentEditableWarning={true}
            &amp;gt;&amp;lt;/EditableDiv&amp;gt;
        &amp;lt;/span&amp;gt;
    ) : (
        &amp;lt;span className=&quot;memo-text&quot;&amp;gt;{memo || &quot;메모작성&quot;}&amp;lt;/span&amp;gt;
    )}
&amp;lt;/MemoContainer&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MemoContainer는 메모 컨테이너 전체의 레이아웃이고, EditableDiv는 실제 편집 가능한 텍스트 영역의 스타일이다.&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;EditableDiv의 속성 중에 &lt;b&gt;suppressContentEditableWarning = {true}&lt;/b&gt;가 있는데, 이 속성이 없으면 콘솔에 수정이 가능한 요소 내에 react에서 관리하는 children도 포함되어 있기에 조심해달라는 warning이 뜨게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(자세한 내용은 이 링크를 참고하자 &lt;a href=&quot;https://guiyomi.tistory.com/147&quot;&gt;https://guiyomi.tistory.com/147&lt;/a&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;pre class=&quot;scss&quot;&gt;&lt;code&gt;.memo-text {
    width: auto;
    max-width: 282px;
    white-space: pre-wrap;
    word-wrap: break-word;
    overflow-wrap: break-word;
    min-height: 21px;
    display: inline-block;

    &amp;amp;.editing {
      background: transparent;
      color: #202020;
      position: relative;
      z-index: 1;
      padding-left: 6px;

      // 배경 확장을 위한 가상 요소
      &amp;amp;::before {
        content: &quot;&quot;;
        position: absolute;
        top: -4px;
        left: 0;
        right: -6px;
        bottom: -4px;
        background: #f0f0f0;
        border-radius: 6px;
        z-index: -1;
      }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memo-text의 width는 auto이므로 글씨가 써질 때마다 늘어난다.&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;그 뒤의 회색 배경은 before로 만들었는데, editing의 position이 relative이므로 부모인 memo-text의 width를 따라간다. (가상 요소의 z-index는 -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;또한 편집 모드로 들어갈 때 &amp;lsquo;메모를 입력하세요&amp;rsquo; placeholder를 위치시키기 위해 가상 요소를 활용했다.&lt;/p&gt;
&lt;pre class=&quot;scss&quot;&gt;&lt;code&gt;&amp;amp;:empty:before,
&amp;amp;.empty:empty:before {
    content: &quot;메모를 작성하세요&quot;;
    color: #c4c4c4;
    (...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;amp;:empty:before&amp;nbsp;- div가 완전히 비어있을 때&lt;/li&gt;
&lt;li&gt;&amp;amp;.empty:empty:before&amp;nbsp;- div가 비어있고&amp;nbsp;empty&amp;nbsp;클래스가 있을 때&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 두가지 방식을 처리했다.&lt;/p&gt;</description>
      <category>Intern</category>
      <category>contenteditable</category>
      <category>input</category>
      <category>textarea</category>
      <category>웹 프론트엔드</category>
      <category>인턴</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/175</guid>
      <comments>https://quickchabun.tistory.com/175#entry175comment</comments>
      <pubDate>Sat, 19 Apr 2025 16:00:22 +0900</pubDate>
    </item>
    <item>
      <title>모노레포에서 다른 프로젝트로 파일을 옮길 때 조심해야 할 점</title>
      <link>https://quickchabun.tistory.com/174</link>
      <description>&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;common 프로젝트에 &lt;b&gt;컴포넌트 A&lt;/b&gt;가 있었다. 컴포넌트 A가 &lt;b&gt;common 프로젝트&lt;/b&gt;에 있긴 했지만 현재 내가 작업하고 있는 &lt;b&gt;프로젝트 B&lt;/b&gt;에서 밖에 사용이 안되었고, 프로젝트 B에서 만든 api hook을 활용해 컴포넌트 A에서 데이터를 주고받아야 했기 때문에,&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;고민 끝에 컴포넌트 A를 &lt;b&gt;common 프로젝트에서 프로젝트 B&lt;/b&gt;로 옮기기로 하였다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 그냥 drag and drop을 하면 될 것이라 생각했지만&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포넌트 A를 옮길 때 고려해야 될 내용이 있었나 싶었지만, vscode가 알아서 업데이트해주겠지 싶어서 일단은 그냥 drag and drop으로 파일을 옮겼다.&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;옮기고 나서 vscode가 자동으로 import를 업데이트해주어서 에러가 발생하지 않는 &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;계속 프로젝트 B의 작업을 하다가 common 프로젝트의 빌드의 watch를 끄고 (common project에서 &amp;lsquo;&lt;b&gt;pnpm run build &amp;mdash;watch&lt;/b&gt;&amp;rsquo; 명령어를 입력해야 common 프로젝트에서의 변경 내용이 바로 반영이 된다),&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;잠시 후에 다시 watch를 키니,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로젝트 B 에서 common 프로젝트의 컴포넌트를 인식하지 못하고 수많은 에러가 발생했다.&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 에러가 왜 발생했을까(tsconfig.json을 살펴보자)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 common 프로젝트 경로를 인식하지 못했을까? 처음에는 그냥 에러가 발생했나 싶어서 다시 common을 빌드하고, 이래도 에러가 계속 발생했길래 모든 프로젝트를 다시 빌드하기도 하였다.&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;&lt;b&gt;파일을 drag and drop으로 옮기면서 바뀐 내용 중 일부가 오류를 발생시켰음을 깨닫게 되었다.&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;p data-ke-size=&quot;size16&quot;&gt;무슨 파일이 바뀌었나 살펴보니 &lt;b&gt;common/tsconfig.json&lt;/b&gt;의 내용이 바뀌어있었다. common 프로젝트에 있는 파일을 다른 프로젝트로 옮기는 순간, include에 해당 파일이 옮긴 프로젝트의 경로로 삽입되어 있었다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;  // 이전
  &quot;include&quot;: [&quot;src&quot;]
  
  // 이후
  &quot;include&quot;: [&quot;src&quot;, &quot;../../../packages/projectB/src/ComponentForBlog.tsx&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 이후에 빌드가 되면, common/dist가 기존 common 프로젝트의 파일들 뿐만이 아니라 프로젝트 B의 파일들 또한 빌드가 되어 있었다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;터미널에서 common/dist에서 ls -la를 해본 결과

// 이전
drwxr-xr-x@ 13 myname  staff   416  4 19 14:35 .
drwxr-xr-x@  9 myname  staff   288  3 15 16:08 ..
drwxr-xr-x@ 11 myname  staff   352  4 19 14:35 api
drwxr-xr-x@ 30 myname  staff   960  4 19 14:35 components
drwxr-xr-x@  4 myname  staff   128  4 19 14:35 globalStyle
drwxr-xr-x@ 19 myname  staff   608  4 19 14:35 handler
drwxr-xr-x@ 34 myname  staff  1088  4 19 14:35 hooks
-rw-r--r--@  1 myname  staff  2335  4 19 14:35 index.d.ts
-rw-r--r--@  1 myname  staff  5915  4 19 14:35 index.js
drwxr-xr-x@ 55 myname  staff  1760  4 19 14:35 modules
drwxr-xr-x@  4 myname  staff   128  4 19 14:35 recoil
drwxr-xr-x@ 32 myname  staff  1024  4 19 14:35 resource
drwxr-xr-x@ 46 myname  staff  1472  4 19 14:35 utils

// 이후 (common/tsconfig.json에 project B의 경로가 포함되어 있을 때)
common projectB&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 파일을 다른 프로젝트로 이동시킬 때 자동으로 바뀌는 common/tsconfig.json의 include를 삭제하고 빌드를 해줘야 다른 프로젝트에서 common/dist에 있는 컴포넌트들의 경로를 잘 찾을 수 있다.&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;왜 파일을 drag and drop을 할 때 tsconfig.json의 include가 업데이트되었는지는 조금 더 탐구가 필요할 것 같다. 다른 컴퓨터에서 drag and drop을 할 때에는 이런 일이 발생하지 않았었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 아무튼 결론은, &lt;b&gt;어떤 프로젝트의 파일을 다른 프로젝트로 옮길 때 tsconfig.json을 잘 살펴보자!&lt;/b&gt;&lt;/p&gt;</description>
      <category>Intern</category>
      <category>tsconfig.json</category>
      <category>모노레포</category>
      <category>모노레포 tsconfig.json</category>
      <category>모노레포 빌드</category>
      <category>모노레포 에러</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/174</guid>
      <comments>https://quickchabun.tistory.com/174#entry174comment</comments>
      <pubDate>Sat, 19 Apr 2025 14:54:26 +0900</pubDate>
    </item>
    <item>
      <title>[Network] 프로토콜(OSI, TCP/IP), 네트워크 장치에 관하여</title>
      <link>https://quickchabun.tistory.com/173</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;책&lt;b&gt; '그림으로 배우는 네트워크 프로토콜 활용'&lt;/b&gt;을 읽고 정리한 내용입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 들어가며&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;호스트: 네트워크 상의 IP 주소를 가진 컴퓨터&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;/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;TSS(time sharing system: 시분할 시스템): 한 대의 컴퓨터를 여러 사람이 사용, 로컬 단말기에서 원격으로 액세스 가능&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;ARPANET: 세계 최초의 패킷 교환 방식의 네트워크&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;패킷 교환 방식(Packet Switching)&lt;/b&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;문제점: 하나의 회선을 여러 컴퓨터가 동시에 사용할 수 없다&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프로토콜(OSI, TCP/IP)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터가 서로 통신하는 절차나 표준을 규정한 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) HTTP(데이터형식), IP(논리적인 수단), 케이블(물리적인 수단)&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OSI 참조 모델 7계층&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1~4계층은 하위 계층, 5~7계층은 상위 게층&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제7층: 응용 계층 - 각 애플리케이션이 어떻게 통신하는지 구체적으로 규정 (HTTP, SMTP, SSH)&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;제5층: 세션 계층 - 애플리케이션 간 통신의 시작, 유지, 종료 등을 규정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제4층: 전송 계층 - 노드 간의 통신 신뢰성을 확보하기 위한 기능을 규정, 상위 계층에 데이터를 전달하기 위한 포트 번호를 정의하고, 신뢰성을 확보하기 위한 메커니즘 정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제3층: 네트워크 계층 - 다수의 네트워크 간 엔드 투 엔드 통신을 실현하는 기능을 규정, 통신을 전달하기 위해 필요한 주소 체계나 라우팅 등을 정의&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;제1층: 물리 계층 - 통신 데이터를 전기 신호나 광 신호로 변환하는 등 물리적인 수단을 규정, 데이터 링크 계층에서 받은 데이터를 전기 신호로 변환해 네트워크로 전송&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TCP/IP 모델 4계층&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제4층: 응용 계층 - 각 애플리케이션이 어떻게 통신하는지 구체적으로 규정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제3층: 전송 계층 - 노드 간 통신의 신뢰성에 관한 기능 규정&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;제1층: 링크 계층(네트워크 인터페이스 계층) - 직접 연결된 노드 사이의 통신을 실현하는 기능 및 통신 데이터를 전기 신호나 광신호로 변환하는 등 물리적인 수단을 규정&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OSI 참조 모델과 TCP/IP 모델의 대응&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI 참조 모델의 제7층(응용 계층), 제6층(표현 계층), 제5층(세션 계층) = TCP/IP 모델의 제4층(응용 계층)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI 참조 모델의 제4층(전송 계층) = TCP/IP 모델의 제3층(전송 계층)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI 참조 모델의 제3층(네트워크 계층) = TCP/IP 모델의 제2층(인터넷 계층)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSI 참조 모델의 제2층(데이터 링크 계층), 제1층(물리 계층) = TCP/IP 모델의 제1층(링크 계층, 네트워크 인터페이스 계층)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;계층별 통신의 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응용 계층 프로토콜에서 전달된 데이터에 각 계층에서 통신에 필요한 정보를 덧붙여 위에서 아래로 차례차례 전달&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;응용 계층: 응용 계층에서 데이터를 생성해 전송&lt;/li&gt;
&lt;li&gt;전송 계층: 목적지 포트 번호 / 출발지 포트 번호 추가&lt;/li&gt;
&lt;li&gt;인터넷 계층: 목적지 IP 주소 / 출발지 IP 주소 추가&lt;/li&gt;
&lt;li&gt;링크 계층: 목적지 MAC 주소 / 출발지 MAC 주소 추가, 전기 신호로 변환&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캡슐화와 비캡슐화&lt;/h3&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;&lt;b&gt;헤더&lt;/b&gt;: 캡슐화할 때 각 계층에서 덧붙이는 데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;페이로드&lt;/b&gt;: 상위 계층에서 전달받은 상태의 데이터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;PDU(Protocol Data Unit)&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;&lt;b&gt;비캡슐화&lt;/b&gt;: 각 계층마다 필요한 정보가 헤더로 추가되어 있으므로, 추가된 정보를 확인하고 헤더를 제거하는 과정, 확인한 내용에 따라 데이터를 상위 계층 프로토콜로 전달하고, 최종적으로 응용 계층 프로토콜에 도달&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 네트워크 장치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리피터&lt;/b&gt;: OSI 참조 모델의 물리 계층에서 동작하는 장치, 신호의 전송 거리를 늘리는 역할&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;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;L2(레이어 2) 스위치 및 브리지&lt;/b&gt;: 전달된 전기 신호를 데이터 링크 계층의 프레임으로 변환하고, 프레임 헤더에 포함된 MAC 주소 정보를 목적지로 판단해 전송&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수신한 프레임에 포함한 출발지 MAC 주소를 이용해 MAC 주소 테이블을 작성하고 목적지 정보를 관리&lt;/li&gt;
&lt;li&gt;OSI 참조 모델의 데이터 링크 계층에 해당&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;b&gt;라우터&lt;/b&gt;: 여러 네트워크를 연결해서 다른 네트워크에 있는 단말기와 통신할 수 있게 해줌&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수신한 패킷의 헤더를 확인하고, 목적지 IP 주소를 자신의 라우팅 테이블에서 검색한 후 적절한 목적지로 전송&lt;/li&gt;
&lt;li&gt;라우팅: IP 주소를 기반으로 패킷을 전송하는 것&lt;/li&gt;
&lt;li&gt;OSI 참조 모델에서 네트워크 계층의 기능을 가진 장치&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;L3 스위치&lt;/b&gt;: 라우터처럼 서로 다른 네트워크를 연결하는 라우팅 기능 수행&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;L2 스위치와 마찬가지로 인터페이스 수가 많음&lt;/li&gt;
&lt;li&gt;WAN 기능 없음&lt;/li&gt;
&lt;li&gt;OSI 참조 모델에서 네트워크 계층의 기능을 가진 장치&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;b&gt;방화벽&lt;/b&gt;: 네트워크의 경계에 설치해 외부에서 침입해 오는 통신을 차단하거나 내부에서 외부로 향하는 허가되지 않은 통신을 차단하는 보안 장치&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패킷의 출발지 및 목적지 IP 주소와 포트 번호를 보고 통신을 허용하거나 차단&lt;/li&gt;
&lt;li&gt;스테이트풀 인스펙션: 내부에서 외부로 나간 통신에 대해 동적으로 응답을 허가하는 기능&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Study/Network</category>
      <category>OSI</category>
      <category>TCP/IP</category>
      <category>네트워크</category>
      <category>장치</category>
      <category>프로토콜</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/173</guid>
      <comments>https://quickchabun.tistory.com/173#entry173comment</comments>
      <pubDate>Sat, 19 Apr 2025 02:19:39 +0900</pubDate>
    </item>
    <item>
      <title>의미는 곧 원동력이다 - &amp;lsquo;마음이 움직이는 순간들&amp;rsquo;을 읽고</title>
      <link>https://quickchabun.tistory.com/172</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;행동경제학자 댄 에리얼리의 &lt;b&gt;&amp;lsquo;마음이 움직이는 순간들&amp;rsquo;&lt;/b&gt;을 읽었다. 인턴 근무를 하게 된지 어느덧 한 달이 지났다. 스스로 일을 해 돈을 번다는 사실에 기뻤고, 내가 관심을 가지던 분야의 지식과 경험을 활용할 수 있다는 것에 감사했다. 이 사실들만으로도 일을 할 이유는 충분했지만, &lt;b&gt;&amp;lsquo;사람들은 왜 일을 하는가?&amp;rsquo;&lt;/b&gt;라는 질문은 머릿속에서 계속 떠올랐다. 취미가 업이 되면 괴롭다는 말에서 알 수 있듯이, 아무리 좋아하는 일이라도 그 일로 먹고 살기란 쉽지 않다. 이런 생각들을 하다보니 도서관에서 이 책이 눈에 띄었나보다. 인간은 무슨 상황에서 동기부여가 되는지 궁금했다. 이 질문에 대한 답을 알 수 있다면, 앞으로 나의 선택에 큰 도움이 되지 않을까.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 의외로, 돈이 오히려 동기부여의 방해 요소가 될 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 왜 사람들이 주에 5일이나 아침 일찍 일어나 회사에 가서 해가 질 때까지 일을 하는지에 대한 답변을 하는 것은 어렵지 않다. 먹고 살기 위해서. 즉, 돈을 벌기 위해서이다. 만약 월급을 안 준다면 지금 하던 일을 계속 한다는 사람이 과연 있을까 싶다. 하지만 돈을 많이 준다고해서, 동기부여가 더 잘 되는 것은 아니다.&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;인센티브로 금전적 보상, 피자 쿠폰, 칭찬 중 하나를 제공한다고 했을 때, 무엇이 가장 큰 성과 상승 효과를 발생시킬까? 가장 적은 상승 효과를 보인 것은 금전적 보상이다. 그리고 인센티브를 받은 다음 날, 현금 보상을 받은 집단의 성과가 아무것도 받지 않은 대조군보다 13.2%나 저조하다는 결과가 나왔다. 책에서는 &amp;lsquo;고액 상여금 효과&amp;rsquo;라고 하는데, 상여금이 많을 수록, 성과 하락의 폭도 커지는 의아한 현상을 나타낸다. 오히려 피자 쿠폰이나 칭찬같은 선의가 직원들의 동기 부여에 더 큰 도움이 되었다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 창조물에 대한 자기중심적인 편향, 그리고 의미&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람들은 대상에 &lt;b&gt;시간과 노력을 더 많이 투자할수록&lt;/b&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;중요한 사실은, 종이접기는 실험 참가자들의 정체성이나 직업과는 아무런 관련없는 활동이었다는 것이다. 그럼에도 불구하고 실험 참가자들은 그들의 &lt;b&gt;인정 욕구, 성취감, 창작&lt;/b&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;&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;생산성에 대해 애덤 스미스와 카를 마르크스는 서로 다른 견해를 가진다. 애덤 스미스는 &lt;b&gt;관리를 통해 동기를 저해하지 않고도 직장 구조를 바꾸고 효율성을 높일 수 있다&lt;/b&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;저자는 &lt;b&gt;지식 경제가 점점 심화되는 오늘날 마르크스의 관점에서 조직을 설계할 필요가 높아진다&lt;/b&gt;는 생각을 피력한다. 노동이 가지는 더 큰 의미를 파악하지 못하면, 직원들은 일에 의미있게 몰입하지 못한다. 그리고 지식 경제에서 직장은 &lt;b&gt;직원들의 신뢰와 선의, 몰입도&lt;/b&gt;에 크게 의존할 수 밖에 없다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 신뢰와 선의에 대한 강조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조직은 구체적이고 정의하기 쉬운, 측정 가능한 요소인 &lt;b&gt;가산 측면&lt;/b&gt;을 과대평가하고, 정의와 측정이 모두 어려운 요소인 &lt;b&gt;불가산 측면&lt;/b&gt;을 쉽게 셀 수 있는 것처럼 다루는 실수를 한다. 이 때문에 회사는 금전적 보상으로 직원들을 컨트롤하려는 실수를 하게된다. 저자는 &lt;b&gt;일의 의미와 연대를 강화할 수 있는 요소&lt;/b&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;&lt;b&gt;신뢰와 선의의 교환&lt;/b&gt;은 인간 동기의 가장 중요하고 내재적인 부분이다. 신뢰와 선의는 우리로 하여금 정해진 직무 범위를 벗어나 초과 성과를 달성하고 혁신하게 만든다. 사람들은 기대와 신뢰를 받고 있을 때나 선의를 베풀고자 할 때 의욕이 솟는다. 그리고 성과에 대한 &lt;b&gt;직접적인 현금 보상&lt;/b&gt;을 제공할 때, 신뢰와 선의는 무너지게 된다. (라고 저자가 말하긴 하지만, 보상을 덜 제공하기 위하여 신뢰와 선의를 이용하는 일은 없어야된다는 생각이 떠올랐다.)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 유산을 남기고자 하는 강력한 동기&lt;/h2&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;의식하든 하지 못하든, 우리는 유한한 육체의 삶을 넘어 나의 자녀나 업적을 통해 죽음 이후에도 기억되기를 원한다. 부자들은 자선 재단을 설립하고, 작가들이 글을 쓰고, 운동선수들이 세계 신기록을 세우기 위해 노력하고, 기네스 기록을 세우기 위해 사람들이 기이한 일을 벌이는 이유이다. 우리는 죽은 사람들의 유산을 보며 그 사람을 떠올린다. 프레디 머큐리는 이미 죽었지만, 1985년에 Live Aid에서 흰 나시 티를 입은 채 보헤미안 랩소디를 열창하는 그의 모습은 유튜브 영상에서, 몇 년 전에 개봉한 영화에서, 사람들의 머릿 속에서 계속 살아있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 무언가를 만들고, 보여주고 싶다는 생각&lt;/h2&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;웹 개발을 하며 내가 만든 결과물들을 많이 아낀다. AI한테 수십 번 질문을 하며, 머릿 속에 그리던 풀 페이지 애니메이션 효과를 결국 구현해 냈을 때 많이 뿌듯해했다. 지금&amp;nbsp;인턴 근무를 하면서 꽤 많은 기능들을 구현했는데, 나의 의도대로 기능이 동작하는 이 도메인을 더 완성도 있게 발전시켜 머지 않아 사람들에게 공개하고 싶다. 배포가 되면 친구들한테 링크를 주고 접속하는 모습을 지켜보며 흐뭇해할 것 같다. 글을 쓰면서, 무언가를 만들고 남들에게 공개하는 것을 좋아한다는 사실을 다시금 깨닫게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발과 글쓰기를 반복할 것이다. 그리고 계속 무언가를 만들겠지. 창조물에 대한 자기중심적인 편향과 객관적인 평가 사이의 괴리에 괴로워할 때도 있겠지만 그 발걸음을 멈추지 않기를 바란다. 그렇게 나는 무언가를 계속 남기려 노력할 것이다. 많은 시간이 지나고 문득 다시 되돌아 봤을 때, 내가 만든 그 무언가를 곁에 있는 사람들과 같이 지켜보며 뿌듯해하는 순간이 있기를 바라는 소망을 품게 되었다.&lt;/p&gt;</description>
      <category>생각</category>
      <category>독후감</category>
      <category>마음이 움직이는 순간들</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/172</guid>
      <comments>https://quickchabun.tistory.com/172#entry172comment</comments>
      <pubDate>Sun, 13 Apr 2025 23:52:47 +0900</pubDate>
    </item>
    <item>
      <title>5년 동안 쓰던 폰을 바꿨다</title>
      <link>https://quickchabun.tistory.com/171</link>
      <description>&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dh99B/btsM99CZBSX/NhDoSpcN2hXgwSvsq0prRk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dh99B/btsM99CZBSX/NhDoSpcN2hXgwSvsq0prRk/img.jpg&quot; data-alt=&quot;그동안 고생 많았다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dh99B/btsM99CZBSX/NhDoSpcN2hXgwSvsq0prRk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDh99B%2FbtsM99CZBSX%2FNhDoSpcN2hXgwSvsq0prRk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;400&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;그동안 고생 많았다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스마트폰 교체의 적절한 주기는 얼마나 될까. 사람마다 다양한 답이 나오겠지만 그 답변들 중 틀린 답변은 없을 것이다. (컴퓨터공학부에 다니지만) 전자기기에 무지해서 답변을 하기 쉽진 않지만, 기기 뒷면에서 유리조각이 떨어지면 폰을 바꿀 때가 된 것 같다는 확신이 들었다. 운이 좋게도 인턴을 하면서 첫 월급이 나왔다. 어느 정도 고민을 하다 3개월 할부로 새로운 아이폰을 구매했다. 너무 큰 소비를 한 것인가 하는 불안감과 새로운 폰을 사용하게 된다는 기대감이 머릿속에서 뒤섞였다. 그런 와중에 한 가지 사실이 떠올랐다. 오랜 시간 같이 지내왔던 스마트폰과 헤어질 순간이 얼마 남지 않았다는 사실. 아쉽다거나 그런건 아닌데, &lt;b&gt;시간이 많이 흘렀다는 것&lt;/b&gt;이 체감이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 기대와는 달랐던 20대 초반, 나는 폰을 들여다봤다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재수가 끝난 2019년 12월, 부모님과 함께 휴대폰 대리점에 찾아가 &lt;b&gt;아이폰11&lt;/b&gt;을 구매했던 순간으로부터 어느덧 5년이 흘렀다. 나의 첫 스마트폰이자 20대의 초반을 함께한 친구, 이 친구와의 첫만남 이후 얼마 지나지 않아 코로나가 찾아왔고, 상경의 꿈을 이루지 못하고 사람을 피한 채 좌절하던 그 우울했던 순간들을 아이폰을 쓰며 견딜 수 있었다. 자취방까지 구했지만 서울로 올라갈 기회는 찾아오지 않았다. 친구를 만나고도 싶었지만 전염병을 맞닥뜨리기 싫어 약속을 잡지도 않았다. 닭장같은 학원에 갇혀 캠퍼스 라이프를 꿈꾸며 하루하루 버틴 결과가 &lt;b&gt;사회적 거리두기&lt;/b&gt;라는 점. 이 괴리감이 도저히 익숙해지지 않아 답답했다. 그래서 KF94 마스크를 끼고, 카카오바이크로 천변을 가로질렀다. 사운드클라우드에 들어가 지브리 스튜디오 음악 로파이 버전을 반복 재생했다. 페달을 밟으며 바람을 맞다보면 먹구름이 낀 것 같던 감정에서 잠시 해방될 수 있었다.&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;아이폰이 있어 심심한 적은 없었다. 넷플릭스와 유튜브를 보고, 롤토체스를 하면서 시간을 보냈다. 사회적 거리두기도 어느덧 익숙해지고 폰으로 즐길 컨텐츠는 언제나 넘쳐났다. 새벽 4시까지 폰을 하다 점심 즈음에 깨어나 늦은 하루를 시작했다. 그렇게 시간을 보내다보니 2020년은 지나갔고, 2학년이 되어도 서울로 올라가지 못하고 비대면 수업을 듣다 나는 입대를 했다. 캠퍼스보다 훈련소를 먼저 갈 줄은 전혀 몰랐었는데. 조금 허탈한 마음이 들기는 했다.&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;10일&lt;/b&gt; 동안 야외훈련도 못하고 앉아있어야 했으니 시간은 정말로 느리게 흘렀다. 핸드폰을 키고 유튜브를 하고 싶었다. 훈련소 밖에 사람들은 폰으로 도쿄올림픽을 보고 있었을텐데. 인편으로 올림픽 소식을 확인할 수 있긴 하였지만, 나는 휴대폰을 하고 싶었다. 훈련소 어딘가에서 전원이 꺼진채 잠들고 있던 아이폰11이 그리웠다.&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;논산훈련소에서 3주가 지나고 경찰학교로 가게 되었다. 경찰학교에서는 하루 일과를 시작하기 전, 그리고 일과가 끝난 후 휴대폰을 사용할 수 있었다. &amp;lsquo;여기 복지 미쳤는데?&amp;rsquo;라는 생각이 저절로 들었다. 폰을 받고 나서는 롤토체스를 하고, 와일드 리프트를 하고, 넷플릭스를 보고, 유튜브를 보느라 너무나도 바빴다. 논산훈련소와는 다르게 여기서는 다들 휴식시간에 폰을 하느라 바쁘다보니 살짝 덜 친해진 느낌은 있었다. 그래도 뭐 어때. 휴대폰도 하고, 훈련도 열심히 받고. 3주는 훌쩍 흘러 나는 서울에 있는 기동대로 가게 되었다. 자대에 배치되고 난 뒤부터는 게임을 덜하게 되었다. 그 대신 드라마와 영화를 보고, 가끔은 밀리의 서재로 책을 읽었다. 18개월의 군 생활동안 아이폰 덕분에 덜 심심했다. 휴대폰 사용이 허락되지 않던 옛날 군 생활은 많이 끔찍했을 거라는 의견에 동의한다. 놀 거리가 없어 심심한 선임들이 얼마나 후임들을 괴롭혔을지, 그 일을 겪지 않아도 되서 감사할 따름이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 쇼츠, 릴스, brainrot&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중학생 시절, 유튜브 알고리즘에 우연히 대도서관의 마인크래프트 플레이 영상을 보고 그에게 푹 빠진 적이 있었다. 영상 하나에 30분 정도로 꽤 길었는데, (지금이라면 절대 안 그러겠지만) 배속도 하지 않은 채 집중해서 영상을 감상했었다. 그렇게 긴 영상들을 재밌게 보고는 했었는데, 언젠가부터 짧은 영상들을 보다보니 그 포맷에 너무 익숙해져 버렸다. 5초 정도보고 재미가 없겠다 싶으면 바로 넘겨버리면 되니 너무나도 간편했다. 숏폼에서는 나의 참을성을 지불하지 않아도 된다. 길어도 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;영상 길이는 짧아졌지만, 폰을 붙잡고 있는 시간은 더욱 길어졌다. 시간이 뜰 때마다 폰을 키고 멍하니 짧은 영상들을 보았다. 그 순간 동안 나는 휴식을 취하고 있었던 것일까. 내 뇌는 도파민에 절여지게 되었고, 내가 좋아하던 정적인 순간들이 어색하게 여겨지기 시작했다. 잠을 잘려고 침대에 누웠지만 계속 스크롤을 하느라 자는 타이밍을 놓치게 되었다. 외국에서 &amp;lsquo;&lt;b&gt;brainrot&lt;/b&gt;&amp;rsquo;라는 신조어가 유행한 적이 있었다. 말 그대로 뇌가 썩는다는 뜻인데, 짧은 영상들을 계속 보다보니 참을성이 없어지고 단순한 자극만을 찾게되는 현상을 풍자한 단어이다. 그리고 나는 그 단어에 딱 부합하는 사람이 되어가고 있었다. 멍청한 사람이 되어가고 있다는 사실을 깨달은 순간부터, 나는 폰을 멀리해야겠다는 다짐을 하게 되었다. 그래서 요즘은 쇼츠나 릴스를 안보냐고 묻는다면 그건 아니다. 하지만 시간이 아깝다는 자각을 전보다는 많이 하게 되었다. 결국 무엇이든 중독이 되면 그 결과는 안 좋으니까. 지금까지 내가 폰으로 무엇을 많이 했는지 되돌아봤을 때 떠오르는게 릴스를 보는 거였다는 사실이 살짝 부끄럽다. 글을 쓰면서 되돌아보니 더더욱 반성을 하게 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 폰을 바꾸었다는 것이 새로운 계기가 될 수 있을까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;다짐하는 것&lt;/b&gt;을 좋아한다. 그래서 나는 일상 속에서 이전과 다른 점이 있는지 살펴보고, 만약 찾게되면 그걸 핑계로 나와의 약속을 새롭게 만든다. 새로운 사람을 알게 되었으니 더 많이 다정해져야겠다, 오늘 단 걸 많이 먹었으니 앞으로 운동을 많이 해야겠다, 옷을 새로 샀으니 더 깔끔하게 입고 다녀야겠다 등등&amp;hellip; 그리고 나는 &lt;b&gt;5년 동안 같이했던 폰&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;대학생으로서 마지막 학기를 보낸다는 사실에 대한 아쉬움, 3월부터 시작한 인턴 생활에 대한 적응, 조금씩이나마 해야되는 취업 준비, 새로운 인간관계에 대한 반가움과 예상치 못한 사건들, 이 모든 것들이 뒤섞여 혼란스러운 감정을 꽤 느끼고 있었다. 그런데 지금 계속 글을 적고 있으니 마음이 안정이 되었다. 옛날에는 메모장을 키고 무언가를 계속 적곤했었는데. 귀찮아서 그냥 생각만 하고 말다보니 감정을 정리하지 못하고 나도 조금씩 충격을 떠안고 있었나보다. 마치&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;새롭게 폰을 바꾸면, 예전처럼 무언가를 계속 적어보는건 어떨까. 적게 되는 내용이 쓸모가 있는지 없는지는 판단하지말고. 생산적이기 위한 글쓰기는 물론 필요하지만 요즘따라 그거에 너무 얽매였던 거 같아서. 그냥 가볍게, 이 정도의 다짐만 해볼까한다. 부담스러운 다짐은 어차피 못 이뤄낸다는 사실을 너무나도 잘 아니까.&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;&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;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I3VkI/btsNax4n0zA/scQmC6IbKjEeT0DCeREKR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I3VkI/btsNax4n0zA/scQmC6IbKjEeT0DCeREKR0/img.png&quot; data-alt=&quot;앞으로 잘 부탁해&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I3VkI/btsNax4n0zA/scQmC6IbKjEeT0DCeREKR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI3VkI%2FbtsNax4n0zA%2FscQmC6IbKjEeT0DCeREKR0%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;400&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;앞으로 잘 부탁해&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>생각</category>
      <category>글</category>
      <category>생각</category>
      <category>아이폰11</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/171</guid>
      <comments>https://quickchabun.tistory.com/171#entry171comment</comments>
      <pubDate>Sun, 6 Apr 2025 20:14:15 +0900</pubDate>
    </item>
    <item>
      <title>네이버 지도 api 사용 시 마커가 많을 때 최적화 후기</title>
      <link>https://quickchabun.tistory.com/170</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 지도가 너무 렉이 걸려!&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 작업하고 있는 프로젝트는 네이버 지도를 활용해서 사용자가 저장한 식당 위치를 지도에 띄워주는 기능이 있다. 커스텀 마커를 생성하고 마커에 대한 정보들을 html로 변환 후, 마커의 content에 추가해주었다. 네이버 지도에 마커가 하나씩 생성될 때마다 DOM 요소가 하나씩 생성된다. 즉, 마커가 생성되는 개수가 늘어날수록 렌더링 성능에 영향을 끼치게 된다. 실제로 마커가 500개 넘게 네이버 지도에 추가되었을 경우, 줌과 드래그를 할 때 너무 렉이 걸려서 사용자 경험에 악영향을 끼치게 되었다. 그래서 일주일에 걸쳐서 네이버 지도를 최적화하기 위해서 여러 가지 시도를 하며 삽질을 해보았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;1196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oQOc3/btsMNe32naD/EouII7RDJpkxzyFt0C0y91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oQOc3/btsMNe32naD/EouII7RDJpkxzyFt0C0y91/img.png&quot; data-alt=&quot;500개 넘는 마커가 존재한다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oQOc3/btsMNe32naD/EouII7RDJpkxzyFt0C0y91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoQOc3%2FbtsMNe32naD%2FEouII7RDJpkxzyFt0C0y91%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;400&quot; height=&quot;540&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;1196&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;500개 넘는 마커가 존재한다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. HTML 마커 방식 대신 캔버스를 사용해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명했듯이, 마커가 하나씩 추가될 때마다 DOM 요소 또한 하나씩 추가되어서 렌더링 속도가 느려졌다. 그렇다면, 네이버 지도에 직접 마커를 추가하지 말고 네이버 지도 위에 캔버스를 추가해서 그 위에 마커를 렌더링하면 어떨까.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(네이버 지도를 캔버스로 어떻게 구현해야 할 지 아래 블로그를 통해서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작성자분께서 상세히 작성해주셔서 많은 도움이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크: &lt;a href=&quot;https://velog.io/@happyhyep/%EC%A7%80%EB%8F%84%EC%99%80-%ED%95%A8%EA%BB%98-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%EC%BA%94%EB%B2%84%EC%8A%A4-%EA%B5%AC%ED%98%84-%EC%8A%A4%ED%86%A0%EB%A6%AC&quot;&gt;https://velog.io/@happyhyep/지도와-함께-움직이는-캔버스-구현-스토리&lt;/a&gt; )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 구조를 설명하자면, 네이버 지도 위에 마커 렌더링용 캔버스를 추가하고, 그 위에 마우스 이벤트용 투명 레이어를 덧씌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스 방식에서 중요하게 생각해야할 점이 있다면,&lt;/p&gt;
&lt;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;1. 마커는 지도에 박혀있는 상황이 아니라, 지도 위의 캔버스에 좌표로 존재한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-&amp;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;&lt;b&gt;2. 마우스레이어 밑에 캔버스 밑에 네이버 지도가 있다. 드래그나 줌을 할 때 발생하는 이벤트가 캔버스에도, 네이버 지도에도 전달이 되어야한다. (마커에 hover를 했을 때에는 InfoWindow가 띄어져야 되서, 이 부분도 고려해야한다)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-&amp;gt; 이벤트 버블링과 캡처링을 잘 활용해야한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 드래그를 하면 마커가 자꾸 이상한 곳으로 이동한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번에서 많이 애를 먹었었다. 줌을 하고나면 자동으로 지도가 자동으로 재렌더링이 되면서 마커들이 원래 위치에 있었는데, 드래그를 할 때에는 마커들이 자꾸 따로 이상한 위치로 이동했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1726&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLNJdy/btsMMbNRv5F/UY7rYSB2lxuEvlik4nzoak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLNJdy/btsMMbNRv5F/UY7rYSB2lxuEvlik4nzoak/img.png&quot; data-alt=&quot;드래그 시 마커들만 따로 이동하는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLNJdy/btsMMbNRv5F/UY7rYSB2lxuEvlik4nzoak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLNJdy%2FbtsMMbNRv5F%2FUY7rYSB2lxuEvlik4nzoak%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;400&quot; height=&quot;221&quot; data-origin-width=&quot;1726&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;드래그 시 마커들만 따로 이동하는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로직 상으로는 드래그를 하면 캔버스가 재렌더링되면서 마커가 올바르게 이동해야되는데 왜 이런 일이 발생하는지 파악하지 못해서 답답했는데, 다행히 사수분께서 해결법을 알려주셔서 수정할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;    const projection = this.map.getProjection();
    if (!projection) return;

    // 각 마커의 위치를 현재 지도 투영에 맞게 업데이트
    this.markers = this.markers.map((marker) =&amp;gt; {
      // 지리적 좌표는 유지하되 화면 좌표를 재계산
      const position = projection.fromCoordToOffset(marker.position);

      // 마커 정보 유지하면서 업데이트된 화면 좌표 정보 추가
      return {
        ...marker,
        screenPosition: position, // 선택적으로 화면 좌표 캐싱
      };
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 나는 지도에 상호작용이 발생했을 때 map에서 가져온 projection을 fromCoordToOffset 메서드를 통해서 마커 위치를 재렌더링했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 하지만 알고보니 드래그 시에 projection 객체는 변하지 않았고, 직접 마커의 위도와 경도를 계산해서 적용하니 드래그를 해도 마커가 잘 따라올 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;  // screenPosition을 interface에 선언  
  screenPosition?: { x: number; y: number };
}

(...)

this.markers = this.markers.map((marker) =&amp;gt; {
        try {
          const lat = marker.position.lat();
          const lng = marker.position.lng();

          // 2. 직접 좌표 변환 로직 구현
          // 마커의 경/위도를 지도 경계 내 상대적 위치(0~1)로 변환
          const relativeX = (lng - minLng) / boundsWidth;
          const relativeY = (maxLat - lat) / boundsHeight; // 위도는 위에서 아래로 감소하므로 반전

          // 상대적 위치를 픽셀 좌표로 변환
          const x = relativeX * mapSize.width;
          const y = relativeY * mapSize.height;

          // 계산된 화면 좌표
          const position = { x, y };

          // 마커 정보 유지하면서 업데이트된 화면 좌표 정보 추가
          return {
            ...marker,
            screenPosition: position,
          };
        } catch (error) {
          // 오류 발생 시 기존 마커 정보 반환
          localConsole?.error(`마커 ${marker.id} 좌표 변환 오류:`, error);
          return marker;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;​&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 상호작용 중 마커를 안 보이게 하면 성능이 개선된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캔버스 방식으로 전환했지만 드래그나 줌을 할 때 렉이 개선되지 않았다. 아무래도 상호작용 중에 마커의 위치를 맞추기 위해 캔버스를 재렌더링하는 비용이 크기 때문인 것 같았다. &lt;b&gt;결국, 마커가 많을 때에는 html 마커 방식이나 캔버스 방식이나 렉이 걸렸다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런 와중 줌이나 드래그를 하는 중에 마커를 안보이게 하면 렌더링이 빨라지지 않을까 생각해서 한 번 구현해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;놀랍게도, 상호작용 중에 마커를 안 보이게 하면 속도가 훨씬 개선되었다! 아무래도 상호작용 중에 마커 렌더링을 하지 않아도 되니 계산량이 줄어들어 성능이 개선된 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;html 마커 방식에서 똑같이 적용하면 어떨까, 구현해보니 html 마커 방식에서도 속도가 눈에 띄게 개선되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 그래서 네이버 지도는 다시 &lt;b&gt;html 마커&lt;/b&gt; 방식으로 구현하기로 했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 마커 클러스터링과 마커 필터링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지도 위에 마커가 많을 때 어떻게 성능을 개선할 수 있을까 검색을 해보았을 때, 마커 클러스터링을 적용해 보라는 답변이 많이 나왔었다. 그래서 마커 클러스터링을 한 번 적용해보았는데 렉이 거의 발생하지 않았다. 하지만 마커 클러스터링이 사용자 경험에 별로 좋지 않다는 의견이 있었고, 마커 클러스터링을 적용하는 대신에 지도가 작을 때는 중요도가 낮은 마커만 표시하고 확대할 수록 식당들을 더 표기하는 마커 필터링을 적용하게 되었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;1130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y5JvC/btsMNjEciPG/qRNzTkKBlI3WPLu2uh6K01/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y5JvC/btsMNjEciPG/qRNzTkKBlI3WPLu2uh6K01/img.png&quot; data-alt=&quot;마커 클러스터링 구현 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y5JvC/btsMNjEciPG/qRNzTkKBlI3WPLu2uh6K01/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy5JvC%2FbtsMNjEciPG%2FqRNzTkKBlI3WPLu2uh6K01%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;400&quot; height=&quot;368&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;1130&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;마커 클러스터링 구현 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마커 필터링은 두 가지 방식으로 구현할 수 있었는데,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;지도를 그리드로 쪼갠 다음 그리드 안에 정해진 개수의 마커 개수 이상으로 표시되지 않게 하는 방법&lt;/li&gt;
&lt;li&gt;마커 간 거리 기준을 정해놓고(예시: 10px) 마커가 기준 이상으로 가까워지면 필터링하는 방법&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마커 겹침을 막기 위해서는 &lt;b&gt;2번 방법이&lt;/b&gt; 더 유효한 거 같아서 나는 2번으로 구현하였다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;      interface MarkerWithPosition {
        marker: naver.maps.Marker;
        screenPosition: naver.maps.Point;
        기준: number;
      }

      const markersToShow: naver.maps.Marker[] = [];
      const minDistance = 10; // 마커 간 최소 픽셀 거리
      const occupiedAreas: { x: number; y: number }[] = [];

      markersWithPositions.forEach(({ marker, screenPosition }) =&amp;gt; {
        // 다른 마커와 너무 가까운지 확인
        const hasOverlap = occupiedAreas.some((pos) =&amp;gt; {
          const dx = pos.x - screenPosition.x;
          const dy = pos.y - screenPosition.y;
          return dx * dx + dy * dy &amp;lt; minDistance * minDistance;
        });

        if (!hasOverlap) {
          markersToShow.push(marker);
          occupiedAreas.push(screenPosition);
        }
      });

      // 선택된 마커만 표시
      markersToShow.forEach((marker) =&amp;gt; marker.setVisible(true));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 마커를 표시할지는 기준을 정해서 정렬을 수행하였는데, 걱정과는 다르게 크게 렌더링 성능이 영향을 끼치지는 않은 것 같아서 다행이었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 네이버 지도 옵션 변경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 지도 옵션 중 렌더링에 영향을 주는 옵션들을 꺼버릴 수는 없을까. 최대한 성능을 최적화하기 위해서 다음과 같은 옵션을 껐다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;      if (!naverMap.current) {
        try {
          const mapOptions = {
            ...mapOptionSelector(domain),
            tileTransition: false, // 타일 전환 애니메이션 비활성화
            baseTileOpacity: 1, // 베이스 타일 최대 불투명도
            maxTileSize: 256, // 타일 크기 최적화
            baseTileFadeInZoom: 0, // 페이드인 효과 제거
            disableKineticPan: true, // 관성 이동 효과 비활성화
            scaleControl: false, // 줌 컨트롤러 비활성화
          };
          naverMap.current = new naver.maps.Map(&quot;map&quot;, mapOptions);
&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-80a6-b27f-cd9ad5dd435d&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능 개선에 제일 도움을 준건&lt;b&gt; &lt;span data-token-index=&quot;1&quot;&gt;tileTransition : false&lt;/span&gt;&lt;/b&gt;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 이 옵션을 해제하면 줌을 할 때 애니메이션이 적용되지 않아서 살짝 부자연스럽지만 (약간 깨져보이기도 한다) 속도가 빨라졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 회의 결과 이 애니메이션이 적용되는 것이 사용자 경험이 좋아보인다는 결론이 나와서 tileTransition은 유지하기로 했다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8082-a914-f27c67c6e672&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 줌 애니메이션 개선 시도&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8024-9874-e12be32a8f40&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;tileTransition 옵션을 true로 하기로 하면서 줌 애니메이션이 적용이 되었다. 마커가 많아질 때 줌 애니메이션이 버벅거리고 잔상이 남아서, 줌 애니메이션을 개선을 해보려고 했다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-804b-8a92-f0bddf98a4e9&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;먼저 지도 타일 로딩이 다 끝난 다음에 시간차를 두고 마커를 보이게 하면 어떨까. 지도 타일 로딩이 다 끝난 다음의 시점을 잡을 수 있나 찾아보니, &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; data-token-index=&quot;1&quot;&gt;&amp;lsquo;tilesloaded&amp;rsquo; eventlistener&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;가&lt;/b&gt; 있었다!&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;const tilesloadedListener = naver.maps.Event.addListener(
        naverMap.current,
        &quot;tilesloaded&quot;,
        () =&amp;gt; {
          setTimeout(() =&amp;gt; {
            if (!isDragging.current) {
              markersVisible = true;
              updateVisibleMarkers();
            }
          }, 200);
        }
      );
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-80c8-b62f-fb5ea8107998&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-806f-9747-df58d8411356&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타일 로딩이 다 끝나고 200ms 이후에 마커가 표시되도록 구현해보았지만 줌 애니메이션의 버벅임은 개선되지 않았다. 다른 것도 여러 번 시도 후에 나온 결론은,&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-80f3-903e-f10a39e106f6&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;tileTransition(줌 할때 실행되는 애니메이션)은 마커의 visibility와 상관없이 마커의 개수에 따라서 애니메이션의 속도가 결정된다는 것이었다.&lt;/span&gt;&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-80aa-8c80-f33617efc737&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;-&amp;gt; 그리고 이 뜻은, 마커의 개수를 바꾸지 않는한 내가 성능을 개선할 수 있는 여지가 많다는 뜻이기도 했다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8059-be76-cdd2a2f1f5f2&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;-&amp;gt; 그래서 최선은 마커 필터링을 통해서 마커의 개수를 제한하는 것!&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8078-9417-d9b73c80fbd2&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-807b-96f7-c4b83202b6a0&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;참고로 이벤트에 대해서 더 말하자면,&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-803e-9d25-f60069fc0975&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 dragend와 zoom_changed를 활용했는데, 지도에서 상호작용이 끝난 순간인 &amp;lsquo;idle&amp;rsquo; eventlistener를 대신 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 드래그가 끝났을 때와 줌이 끝났을 때의 구현을 달리할거면 따로 이벤트를 적용하는 것이 좋다. (setTimeout 시간을 따로 준다든지)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-809e-86b9-f2cc528d519b&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8066-845f-c7d01668f940&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 쿼드트리 알고리즘 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 뷰포트 바깥에 있는 마커들만 표시하는 최적화도 진행했는데, 이 때 쿼드트리 알고리즘을 적용했었다. (마커 필터링을 진행할 때 마커들 간의 거리를 계산할 때에도)&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8076-ad61-c41ae894ef4e&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;쿼드트리 알고리즘을 이번 기회에 처음 알게 되었는데, 이 알고리즘을 활용하면 2차원 공간을 재귀적으로 4개의 영역으로 분할하여 마커를 저장하고 검색할 수 있게 되어서 시간 복잡도가 O(log n)으로 줄어든다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8062-b050-d66119ffc065&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;(쿼드트리에 대한 자세한 설명은 하단 링크를 참고하자!&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8051-bb4a-ee13e598ae39&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;링크: &lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://velog.io/@youngjun_10/쿼드-트리Quad-Tree&quot;&gt;https://velog.io/@youngjun_10/쿼드-트리Quad-Tree&lt;/a&gt; )&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8023-8c16-d2f94054efab&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8078-ab2d-cce40f0603c6&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;나는 utils/quadtree.ts 파일을 따로 생성해서 적용했다.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-804c-8e71-c90b1db62909&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;  (...)
  
  /**
   * 현재 노드를 4개의 하위 노드로 분할
   */
  subdivide(): void {
    const x = this.bounds.x;
    const y = this.bounds.y;
    const w = this.bounds.width / 2;
    const h = this.bounds.height / 2;
    const nextDepth = this.depth + 1;

    // 북서쪽
    this.children[0] = new QuadNode(
      { x, y, width: w, height: h },
      this.maxPoints,
      this.maxDepth,
      nextDepth
    );

    // 북동쪽
    this.children[1] = new QuadNode(
      { x: x + w, y, width: w, height: h },
      this.maxPoints,
      this.maxDepth,
      nextDepth
    );

    // 남서쪽
    this.children[2] = new QuadNode(
      { x, y: y + h, width: w, height: h },
      this.maxPoints,
      this.maxDepth,
      nextDepth
    );

    // 남동쪽
    this.children[3] = new QuadNode(
      { x: x + w, y: y + h, width: w, height: h },
      this.maxPoints,
      this.maxDepth,
      nextDepth
    );

    // 기존 포인트들을 자식 노드로 이동
    this.points.forEach((point) =&amp;gt; {
      for (const child of this.children) {
        if (this.pointInBounds(point, child.bounds)) {
          child.insert(point);
          break;
        }
      }
    });

    this.points = [];
    this.divided = true;
  }
  
  (...)
​&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;​&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px; color: #000000;&quot;&gt;위와 같은 시행착오 끝에 결국 어떤 최적화를 적용했냐면,&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8033-9ab4-f8666fe4e9f2&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div style=&quot;text-align: start;&quot; contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;1. 마커 간의 거리를 기준으로 마커 필터링&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;2. 지도와 상호작용 시에는 마커를 안보이게 만듦&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;3. 뷰포트 바깥에 있는 마커는 표시하지 않음&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;4. 쿼드트리 알고리즘 사용&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;5. 네이버 지도 옵션에서 불필요한 옵션 끄기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-80bf-929e-ceb922a70b47&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-8064-a1c4-e806568de3c3&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div contenteditable=&quot;true&quot; data-content-editable-leaf=&quot;true&quot;&gt;버벅이던 지도를 쾌적하게 만들기까지 꽤 많은 시간이 투자되었다. 동아리에서 진행하던 프로젝트와는 다르게 실제로 많은 사람들이 사용하는 프로젝트에서 개발을 진행하다보니 고려해야 될 부분도 많았고, 기준 또한 엄격했다. 구현이 끝이 아니라 사용자들의 경험 측면을 생각하며 개발을 진행해야 했다(실제로 이 부분에서 피드백을 많이 받았고).&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #ffffff; color: #37352f; text-align: start;&quot; data-block-id=&quot;1b7e1f4d-1a3d-80c8-b90e-d4d1e7b325cc&quot;&gt;
&lt;div style=&quot;color: #000000;&quot;&gt;
&lt;div&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;상호작용이 부드럽고 자연스러워서 자꾸 쓰게되는 앱들이 있다. 프론트엔드 개발자로서 사용자들이 계속 사용하고싶게 만드는 결과물을 만들기 위해 앞으로 더 노력해야겠다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Intern</category>
      <category>네이버 지도 api</category>
      <category>네이버 지도 react</category>
      <category>네이버 지도 개선</category>
      <category>네이버 지도 마커</category>
      <category>인턴</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/170</guid>
      <comments>https://quickchabun.tistory.com/170#entry170comment</comments>
      <pubDate>Sat, 15 Mar 2025 18:21:50 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 자바스크립트] &amp;lsquo;디스크 컨트롤러&amp;rsquo; 풀기</title>
      <link>https://quickchabun.tistory.com/169</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크: &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42627&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/42627&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1741506959160&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42627&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bqBgqD/hyYm0A8vfy/c4Q2H7HIaymX5i5WOqrqH1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bSIOtR/hyYnbilClV/cdvuR9mJ4bjE0P8RljkkK1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42627&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42627&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bqBgqD/hyYm0A8vfy/c4Q2H7HIaymX5i5WOqrqH1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bSIOtR/hyYnbilClV/cdvuR9mJ4bjE0P8RljkkK1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 생각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위 큐(즉, Heap)을 사용해서 풀어야 하는 문제다. Javascript는 우선순위 큐를 직접 구현해야하는데, 아직 다 외우지 못해서 아래 블로그 글을 참고해서 우선순위 큐를 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(블로그 링크: &lt;a href=&quot;https://velog.io/@qltkd5959/JS&quot;&gt;https://velog.io/@qltkd5959/JS&lt;/a&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;작업의 소요시간이 짧은 것,&lt;/li&gt;
&lt;li&gt;작업의 요청 시각이 빠른 것&lt;/li&gt;
&lt;li&gt;작업의 번호가 작은 것&lt;/li&gt;
&lt;/ol&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;이 문제에서 주어지는 jobs는 [작업이 요청되는 시점, 작업의 소요시간] 배열이다. 작업 번호가 부여되지 않았으니 새로 jobsQueue를 만&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;그리고 작업 요청 시간순으로 한 번 sort를 하였고, 그리고 아까 구현한 MaxHeap(waitingQueue)을 사용하여 우선순위들을 나열해야한다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코드&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;function solution(jobs) {    
    // jobs = [작업이 요청되는 시점, 작업의 소요시간]

    // 1. 작업의 소요시간이 짧은 순으로 나열
    // 2. 작업의 요청 시간이 빠른 순으로 나열
    // 3. 작업 번호가 낮은 순으로 정렬


    // 새로운 jobsQueue를 만들고 작업 번호를 부여
    const jobsQueue = [];
    for(let i = 0; i &amp;lt; jobs.length; i++) {
        jobsQueue.push({
            jobNum: i,
            requestTime: jobs[i][0],
            durationTime: jobs[i][1],

        })
    }

    // 작업 요청 시간 순으로 나열
    jobsQueue.sort((a,b) =&amp;gt; a.requestTime - b.requestTime);

    let currentTime = 0;
    let totalTurnAroundTime = 0;
    let completedJobs = 0;

    const waitingQueue = new MaxHeap();

    while(completedJobs &amp;lt; jobs.length) {

        // 현재 시점까지 요청된 작업을 대기 큐에 추가
        while(jobsQueue.length &amp;gt; 0 &amp;amp;&amp;amp; jobsQueue[0].requestTime &amp;lt;= currentTime) {
            waitingQueue.heap_push(jobsQueue.shift());
        }

        // 대기 큐가 비어있으면 다음 작업 요청 시점으로 이동
        if(waitingQueue.isEmpty()) {
            if(jobsQueue.length &amp;gt; 0) {
                currentTime = jobsQueue[0].requestTime;
                continue;
            }
            else {
                break;
            }
        }

        // 우선순위가 가장 높은 작업 선택
        const job = waitingQueue.heap_pop();

        // 작업 완료 시점 계산
        const completionTime = currentTime + job.durationTime;

        // 반환 시간 계산
        const turnAroundTime = completionTime - job.requestTime;

        totalTurnAroundTime += turnAroundTime;
        currentTime = completionTime;
        completedJobs++;
    }

    return Math.floor(totalTurnAroundTime / jobs.length);
}

// 최대 힙
class MaxHeap {
    constructor() {
        this.heap = [null];
    }

    isEmpty() {
        return this.heap.length &amp;lt;= 1;
    }

    compare(a, b) {
        if(a.durationTime !== b.durationTime) {
            return a.durationTime &amp;gt; b.durationTime;
        }

        if(a.requestTime !== b.requestTime) {
            return a.requestTime &amp;gt; b.requestTime;
        }

        return a.jobNum &amp;gt; b.jobNum;
    }

    heap_push(value) {
        this.heap.push(value);
        let currentIndex = this.heap.length - 1;
        let parentIndex = Math.floor(currentIndex / 2);

        while (parentIndex !== 0 &amp;amp;&amp;amp; this.compare(this.heap[parentIndex], value)) {
            const temp = this.heap[parentIndex];
            this.heap[parentIndex] = this.heap[currentIndex];
            this.heap[currentIndex] = temp;
            currentIndex = parentIndex;
            parentIndex = Math.floor(currentIndex / 2);
        }
    }

    heap_pop() {
        if(this.heap.length === 2) return this.heap.pop();

        let returnValue = this.heap[1];
        this.heap[1] = this.heap.pop();
        let currentIndex = 1;
        let leftIndex = 2;
        let rightIndex = 3;
        while(
            (leftIndex &amp;lt; this.heap.length &amp;amp;&amp;amp; this.compare(this.heap[currentIndex], this.heap[leftIndex])) ||
            (rightIndex &amp;lt; this.heap.length &amp;amp;&amp;amp; this.compare(this.heap[currentIndex], this.heap[rightIndex]))
        ) {
            const temp = this.heap[currentIndex];

            if(rightIndex &amp;gt;= this.heap.length || 
               (leftIndex &amp;lt; this.heap.length &amp;amp;&amp;amp; this.compare(this.heap[rightIndex], this.heap[leftIndex]))) {
                this.heap[currentIndex] = this.heap[leftIndex];
                this.heap[leftIndex] = temp;
                currentIndex = leftIndex;
            } else {
                this.heap[currentIndex] = this.heap[rightIndex];
                this.heap[rightIndex] = temp;
                currentIndex = rightIndex;
            }

            leftIndex = currentIndex * 2;
            rightIndex = leftIndex + 1;
    }
        return returnValue;
}
    heap_return() {
        return this.heap;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 내 힘으로 못 풀고 AI의 도움을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선순위 큐 문제만 풀려고하면 머릿속이 복잡해지는데, 계속 문제와 맞닥뜨리면서 익숙해져야겠다.&lt;/p&gt;</description>
      <category>Algorithm/Programmers(JavaScript)</category>
      <category>디스크 컨트롤러</category>
      <category>디스크 컨트롤러 자바스크립트</category>
      <category>프로그래머스</category>
      <category>프로그래머스 자바스크립트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/169</guid>
      <comments>https://quickchabun.tistory.com/169#entry169comment</comments>
      <pubDate>Sun, 9 Mar 2025 16:56:25 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 자바스크립트] &amp;lsquo;다리를 지나는 트럭&amp;rsquo; 풀기</title>
      <link>https://quickchabun.tistory.com/168</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크: &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42583&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/42583&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740985252688&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42583&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bLi8mx/hyYmWdbIxJ/MlK9An20g5pMCKe1x1Ugj1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/m0Bq2/hyYmN1Bbc2/Rjc8cuAFZpAqjc5kp74oMK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42583&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42583&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bLi8mx/hyYmWdbIxJ/MlK9An20g5pMCKe1x1Ugj1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/m0Bq2/hyYmN1Bbc2/Rjc8cuAFZpAqjc5kp74oMK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 생각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제일 헷갈렸던 부분은, 트럭 한대가 다리를 건너는 데 bridge_length초 걸린다는 사실이다. 문제에서 이 부분에 대한 설명 미흡해서 이해가 어려웠다.&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;맨 처음, truck_weights에 다리를 지난 시간을 나타내는 time 속성을 각 객체에 map을 통하여 추가해주었다.&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;그리고 truck_weights의 첫 번째 트럭을 새로 생성한 bridge 배열에 넣어주고 time 속성을 +1 해준다음, 우리가 구해줘야 하는 총 시간인 usedTime도 +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;이제 truck_weights의 트럭들을 bridge에 옮길 차례이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;bridge 제일 앞에 있는 트럭의 time이 bridge_length초 이상이면 bridge 배열에서 제거한다.&lt;/li&gt;
&lt;li&gt;다리를 건너는 트럭들의 무게의 합 + truck_weights의 제일 앞에서 기다리고 있는 트럭 무게 &amp;le; weight 이며 다리에 트럭이 최대 bridge_length대 올라갈 수 있는 조건을 만족할 때 bridge 배열에 트럭을 추가시킨다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 작업들을 bridge 배열과 truck_weights 배열이 빌 때까지 반복한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코드&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function solution(bridge_length, weight, truck_weights) {
    // 다리에는 트럭이 최대 bridge_length대 올라갈 수 있음
    // bridge_length: 다리의 길이, 최대 몇대 올라갈 수 있는지
    // 다리는 weight 이하까지의 무게를 견딜 수 있음

    // 예시에는 bridge_length : 2, weight: 10, truck_weights: [7,4,5,6]

    // 소요된 시간(초)
    let usedTime = 0;

    // 다리 배열
    let bridge = [];

    truck_weights = truck_weights.map((element) =&amp;gt; {
        return {
            weight: element,
            time: 0,
        }
    })

    let firstTruck = truck_weights.shift();
    bridge.push(firstTruck);
    firstTruck.time++;
    usedTime++;

    while(bridge.length &amp;gt; 0 || truck_weights.length &amp;gt; 0) {

        if (bridge.length &amp;gt; 0 &amp;amp;&amp;amp; bridge[0].time &amp;gt;= bridge_length) {
            bridge.shift();
        }

        if(truck_weights.length &amp;gt; 0) {

            // 가장 앞에서 기다리는 트럭
            let waitingTruck = truck_weights[0]; 

            // 트럭 무게의 총합
            let truckWeightSum = 0;

            // 다리를 건너는 트럭들의 무게의 합 구하기
            if(bridge.length !== 0) {
                bridge.forEach((element) =&amp;gt; {
                    truckWeightSum += element.weight;
                })
            }

            // 다리를 건너는 트럭들의 무게의 합 + waitingTruck.weight &amp;lt;= weight일 때
            // 그리고 다리에는 트럭이 최대 bridge_length대 올라갈 수 있는 조건에 부합하는지 체크
            if(truckWeightSum + waitingTruck.weight &amp;lt;= weight &amp;amp;&amp;amp; bridge.length &amp;lt; bridge_length) {
                bridge.push(truck_weights.shift());
            }
        }

        bridge.forEach((element) =&amp;gt; {
            element.time++;
        })

        usedTime++;
    }

    return usedTime;

}&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고려해야 될 조건이 많아서 풀기 어려웠던 문제였던 것 같다.&lt;/p&gt;</description>
      <category>Algorithm/Programmers(JavaScript)</category>
      <category>다리를 지나는 트럭</category>
      <category>다리를 지나는 트럭 자바스크립트</category>
      <category>프로그래머스</category>
      <category>프로그래머스 자바스크립트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/168</guid>
      <comments>https://quickchabun.tistory.com/168#entry168comment</comments>
      <pubDate>Mon, 3 Mar 2025 16:00:55 +0900</pubDate>
    </item>
    <item>
      <title>[KUIT 4기] 개발 동아리 회장 후기</title>
      <link>https://quickchabun.tistory.com/167</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 제안과 고민&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동아리 회장을 맡아달라는 제안을 들었다. 처음에는 부담스러웠다. &lt;b&gt;&lt;u&gt;&amp;lsquo;회장&amp;rsquo;이라는 단어는 내게 너무 이질적으로 다가왔다.&lt;/u&gt;&lt;/b&gt; 초등학교 때 몇 번 반장을 맡았던 적은 있었지만 그건 그 때 뿐이었고, 내가 나서는 것을 좋아하지 않는다는 사실을 알게 된 이후로 무언가를 대표하는 직책을 맡지 않았었다. 웹 파트장도 내가 웹 프론트엔드를 좋아해서 맡은 거였다고. 회장은 자신감이 넘쳐야하고, 남들을 설득할 줄 알아야하고, 책임을 질 줄 알아야한다. 나에게는 &lt;u&gt;&lt;b&gt;없는&lt;/b&gt;&lt;/u&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;u&gt;&lt;b&gt;아마 다시는 오지 않을 기회.&lt;/b&gt;&lt;/u&gt; 회장을 맡으면 더 성숙한 내가 될 수 있지 않을까. 자리가 사람을 바꾼다는 뻔한 말을 한 번 믿어볼까. &amp;lsquo;내가 모든 걸 망쳐버리면 어떡하지?&amp;rsquo;라는 생각이 머릿속을 휘저었다. 하지만 &amp;lsquo;망쳐봤자 얼마나 망치겠나?&amp;rsquo; 싶어서, &amp;lsquo;안 해보고 후회하기보다는 &lt;u&gt;&lt;b&gt;까짓거 해 보자&lt;/b&gt;&lt;/u&gt;&amp;rsquo;라는 생각이 들어서, 그래서 한다고 했다. 나름 재밌겠다는 생각도 들었고, 실제로 꽤 재미를 느끼기도 했었던 것 같다. 모든 일이 순탄하게 흘러가지는 않았지만, 우려했던 최악의 상황이 현실이 된 적은 없어서 정말 다행이라고 생각한다. 고민과 고충은 늘 존재했지만, 어떻게든 해결할 방법을 마련할 수 있었음에 감사할 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4기 프리뷰 사진.jpg&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;2425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PpECu/btsMvwLhkcq/6LhrE5yAP6sdTqOuw9mlpK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PpECu/btsMvwLhkcq/6LhrE5yAP6sdTqOuw9mlpK/img.jpg&quot; data-alt=&quot;3기 데모데이 중 진행된 4기 프리뷰 발표&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PpECu/btsMvwLhkcq/6LhrE5yAP6sdTqOuw9mlpK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPpECu%2FbtsMvwLhkcq%2F6LhrE5yAP6sdTqOuw9mlpK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;533&quot; data-filename=&quot;4기 프리뷰 사진.jpg&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;2425&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;3기 데모데이 중 진행된 4기 프리뷰 발표&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 여기저기 왔다갔다&lt;/h2&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;공과대학 포스터.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QCGgW/btsMvgIB8nu/ScpWnhr5r7AlMNhhP7pH41/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QCGgW/btsMvgIB8nu/ScpWnhr5r7AlMNhhP7pH41/img.jpg&quot; data-alt=&quot;공학관에 부착한 부원 모집 포스터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QCGgW/btsMvgIB8nu/ScpWnhr5r7AlMNhhP7pH41/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQCGgW%2FbtsMvgIB8nu%2FScpWnhr5r7AlMNhhP7pH41%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;400&quot; data-filename=&quot;공과대학 포스터.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;공학관에 부착한 부원 모집 포스터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얼마 지나지 않아 디자이너 모집을 시작할 때에는 돈을 더 들여 더 좋은 재질의 포스터를 만들었다. 공대생이라 가볼 일이 없었던 예디대 건물에 들어가 지하1층, 1층, 7층, 8층에 포스터를 부착했다. 공대생은 학교 반대편에 위치한 예디대 건물을 거의 가지 않는다. 건물에 들어서니 마주한 &lt;u&gt;&lt;b&gt;낯선 풍경들은&lt;/b&gt;&lt;/u&gt; 나를 이리저리 헤메게 만들었다. 이른 아침에 계단을 오르내리며 포스터를 붙였지만, 안타깝게도 이번에는 효과가 크지 않았다. 에브리타임에도 홍보글을 여러번 올렸었지만, 지원자는 많지 않았고 어떻게 디자이너를 모집해야되나 한동안 고민을 하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;예디대 포스터.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccYVKz/btsMwrh90HB/OePkyjJw0NSQv9ZijtJWsK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccYVKz/btsMwrh90HB/OePkyjJw0NSQv9ZijtJWsK/img.jpg&quot; data-alt=&quot;예디대 건물에 부착한 디자이너 모집 포스터&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccYVKz/btsMwrh90HB/OePkyjJw0NSQv9ZijtJWsK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccYVKz%2FbtsMwrh90HB%2FOePkyjJw0NSQv9ZijtJWsK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;400&quot; data-filename=&quot;예디대 포스터.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예디대 건물에 부착한 디자이너 모집 포스터&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포스터를 부착하러 학교 여기저기를 돌아다니는 것은 나름 재밌었다. &lt;u&gt;&lt;b&gt;어떤 목적을 가지고&lt;/b&gt;&lt;/u&gt; 여러 곳을 돌아다닌 적은 거의 없었으니까. 사람들이 포스터를 많이 봐주고 관심을 가졌을지는 잘 모르겠다. 이후에 나는 거리에 포스터가 붙어있을 때마다 주의깊게 들여다보곤 했다. 정성들여 만들어진 종이 안에는 &lt;u&gt;&lt;b&gt;어떤 이야기&lt;/b&gt;&lt;/u&gt;가 담겨있는지 궁금했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 알림은 사람을 불안하게 만든다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회장을 하면서, 언제 가장 힘들었냐는 질문을 들으면 머릿속에 바로 떠오르는 &lt;u&gt;&lt;b&gt;특정한 순간&lt;/b&gt;&lt;/u&gt;이 있다. 2024년 9월 5일, 학기가 시작하고 얼마 지나지 않았을 때이다. 파트장들이 지원자들의 서류를 읽고 서류합격자 명단을 나에게 전달했다. 그리고 면접은 3일 간 진행되는데, 2시간 동안 이 분들이 언제 면접을 보면 좋을지 노션에 차근차근 정리했다. 서류합격자분들께 문자를 발송할 때 이 노션 링크를 같이 전달하며 한 마디를 덧붙였다. 만약 노션에 적힌 시간에 면접을 볼 수 없다면, 인스타 DM이나 카카오톡채널을 통해 &lt;u&gt;&lt;b&gt;변경 희망 시간을&lt;/b&gt;&lt;/u&gt; 적어달라고 했다. 당연히 필요한 절차다. 시간이 맞지 않아 면접을 보지 못해 떨어지는 일은 없어야하니까. &lt;i&gt;&lt;b&gt;하지만 그 문구는 나를 힘들게 했다&lt;/b&gt;.&lt;/i&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;6시 즈음에 문자를 일괄적으로 발송했다. 나는 경영대 K-hub에서 혼자 면접 시간 변경 요청이 오기를 기다리기로 했다. 핸드폰에서 자꾸 진동이 울렸다. 인스타 DM, 카카오톡채널 알림이 핸드폰 상단에 계속 갱신되었다. 정확한 숫자는 기억이 안나지만 &lt;u&gt;&lt;b&gt;25명 넘게&lt;/b&gt;&lt;/u&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;경우의 수가 나오지 않았다. 어떤 사람의 시간을 변경하려고 했지만 그 시간에는 다른 면접자가 이미 면접을 보기로 되있었거나, 다수의 사람들이 같은 시간에 면접을 보기 희망했다. 차근차근 한명씩 면접시간을 바꿔보려고 했다. 하지만 결국 어느 순간에는 답이 나오지 않고 막혀버렸다. 25명이 넘는 사람들이 나의 답변을 기다리고 있었고, 내 옆에는 이 상황을 도와줄 사람이 있지 않았다. 갑작스러운 상황에 조급함과 불안함이 나를 덮쳤고, 머릿속은 어느새 새하얘져 어떤 행동을 해야할지 &lt;u&gt;&lt;b&gt;감이 잡히지 않았다.&lt;/b&gt;&lt;/u&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;저 멀리 도망가던 정신줄을 간신히 붙잡고, 서류합격자분들께 면접 시간 변경에 시간이 조금 걸린다는 답변을 일괄적으로 발송했다. 일단 시간을 번 다음, 한명씩 시간을 확정짓고 천천히 답변을 드렸다. 혼자서는 이 일을 못한다는 확신을 가지고 다른 운영진들한테 도움을 요청했고, 다른 운영진들이 K-hub에 와줘서 시간 변경을 도와줬다. 도와준 덕분에 &lt;u&gt;&lt;b&gt;무사히&lt;/b&gt;&lt;/u&gt; 면접 시간 변경을 끝마칠 수 있었다. 잘 마무리되어서 다행이었지만, 지금도 그 때를 떠올리면 정신이 아득했던 경험이 떠올라 괜히 닭살이 돋곤 했다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;면접 일정 조정.jpg&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dy0oIx/btsMvO5VPaz/qCnoelyrwuZXk31z7JAdPk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dy0oIx/btsMvO5VPaz/qCnoelyrwuZXk31z7JAdPk/img.jpg&quot; data-alt=&quot;다급했던 카톡의 흔적&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dy0oIx/btsMvO5VPaz/qCnoelyrwuZXk31z7JAdPk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdy0oIx%2FbtsMvO5VPaz%2FqCnoelyrwuZXk31z7JAdPk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;182&quot; data-filename=&quot;면접 일정 조정.jpg&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;501&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다급했던 카톡의 흔적&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 변수는 사람을 불안하게 만든다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 무슨 행사를 준비했나 되돌아보았다. OT, 깃 세션, MT, 기획설명회, 팀 매칭, 해커톤, 데모데이&amp;hellip; 이 중 기획설명회와 팀 매칭은 내 예측과는 전혀 다르게 상황이 흘러갔다.&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;개발 부원분들이 스터디를 진행하는 동안 PM과 디자이너는 방학 동안 진행할 프로젝트를 기획하는데, 10주간의 스터디가 끝나가는 학기 말에 비대면으로 PM들이 개발 부원들한테 지금까지의 기획을 발표하는 기획설명회가 진행된다. 저번 기수는 ms teams로 진행했었는데, 이번에는 &lt;u&gt;&lt;b&gt;유튜브 라이브&lt;/b&gt;&lt;/u&gt;로 진행하기로 했다. &lt;i&gt;&lt;b&gt;그러지 말았어야했는데&lt;/b&gt;&lt;/i&gt;. 맨 처음 간 스터디카페에서 라이브가 너무 렉이 걸려서 다른 스터디카페로 급하게 위치를 옮겼다. 하지만 렉은 줄어들지 않았고, 라이브를 중단하고 PM분들의 &lt;u&gt;&lt;b&gt;발표영상 링크를 뿌리는 수밖에 없었다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;기획설명회 후기.jpg&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;1400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uTqMy/btsMt8dqvgS/9nb4R0F76uDlX5GmfaWJ30/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uTqMy/btsMt8dqvgS/9nb4R0F76uDlX5GmfaWJ30/img.jpg&quot; data-alt=&quot;기획설명회 망치고 회식가서 공지 작성하기&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uTqMy/btsMt8dqvgS/9nb4R0F76uDlX5GmfaWJ30/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuTqMy%2FbtsMt8dqvgS%2F9nb4R0F76uDlX5GmfaWJ30%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;400&quot; data-filename=&quot;기획설명회 후기.jpg&quot; data-origin-width=&quot;1050&quot; data-origin-height=&quot;1400&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;기획설명회 망치고 회식가서 공지 작성하기&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그 다음 날 진행된 팀 매칭. 팀 간 밸런스를 어떻게 조정해야할까 고민하다 파트별로 잘하는 사람들을 헤드 부원으로 지정해 한 팀당 헤드 부원 한 명씩만 들어갈 수 있게 했는데, 생각보다 많은 부원분들이 이 제도가 &lt;u&gt;&lt;b&gt;좋지 않다는 의견을&lt;/b&gt;&lt;/u&gt; 피력했다. 급하게 설문조사를 진행해 여론을 확인하고, 헤드 부원을 없애고 다시 팀 매칭을 진행했다. 그리고 팀 매칭이 무사히 진행되었으면 좋았겠지만, 갑작스러운 부원분들의 탈주 통보와 팀 내에서의 갈등의 표출은 나를 당황스럽게 만들었다. &lt;u&gt;&lt;b&gt;나가려는 사람을 붙잡는 시도는 성공할 수 없다.&lt;/b&gt;&lt;/u&gt; 이 부분에서 나는 무기력할 수 밖에 없었다. 돌이켜보면 이 때 &lt;u&gt;&lt;b&gt;내가 확신할 수 있었던 것은 모든 것이 불확실하다는 것이었다.&lt;/b&gt;&lt;/u&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 회장은 J가 맡는게 맞는 것 같습니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오프라인으로 진행되는 행사는 준비할 게 많았다. 해야할 일들과 준비할 물품들의 리스트를 적어두고 체크를 하지 않으면 까먹어서 나중에 고생을 하게 된다. &lt;u&gt;&lt;b&gt;무언가를 적어두고 계획하는 행위가 사실 내게는 조금 어색했다.&lt;/b&gt;&lt;/u&gt; 그렇게 계획적인 사람은 아니었던지라(mbti P). 가장 열심히 준비했던 행사들은 방학에 진행되었다. 해커톤과 데모데이. 해커톤은 학교로부터 지원을 받았는데, 직원분께 받은 카드를 가지고 식당 여기저기를 돌아다니면서 결제를 하고 영수증을 폰으로 촬영했던 기억이 난다. PPT를 미리 제작하고, 해커톤이 진행될 장소를 미리 갔다왔다. 운영진 친구가 디자인해준 판넬과 현수막을 주문하고 픽업했다. 고려해야할 점이 많았지만 다른 운영진분들이 도와준 덕분에 무사히 해커톤을 끝마칠 수 있었다. 해커톤은 밤을 새야해서 잠이 많은 내가 잘 진행할 수 있을지 조금 불안했는데, &lt;i&gt;그래도 아직은 젊으니까.&lt;/i&gt; 그럭저럭 잘 버틸 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;해커톤 이후 맞이한 아침.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eqpqn7/btsMtsKiwI9/ewmsmlkC670ywm1tDSXg5k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eqpqn7/btsMtsKiwI9/ewmsmlkC670ywm1tDSXg5k/img.jpg&quot; data-alt=&quot;해커톤을 진행하다가 바라본 창 밖. 해가 뜨고 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eqpqn7/btsMtsKiwI9/ewmsmlkC670ywm1tDSXg5k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feqpqn7%2FbtsMtsKiwI9%2FewmsmlkC670ywm1tDSXg5k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;400&quot; data-filename=&quot;해커톤 이후 맞이한 아침.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;해커톤을 진행하다가 바라본 창 밖. 해가 뜨고 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데모데이는 &lt;u&gt;&lt;b&gt;주문해야할 것들이 많았다.&lt;/b&gt;&lt;/u&gt; 스티커, 포스트잇, X배너, 팜플렛.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스티커는 외주를 맡겼는데, 사실 지금까지 외주를 맡겨본 경험이 없어 조금 긴장을 했지만 다행히도 좋은 디자이너분과 매칭이 되어서 좋은 퀄리티의 디자인을 얻을 수 있었다. (사실 맨 처음 컨택한 디자이너분은 연락이 안되어서 고객센터에 연락해 환불을 받긴했다). X배너는 저번 기수 때 썼던 지지대를 활용했고, 팜플렛은 데모데이 전까지 배송이 올까 불안했는데 무사히 그 전에 와서 다행이었다. 준비를 하면서 가장 걱정이 많이 되었던 행사였지만, &lt;u&gt;&lt;b&gt;무사히 끝낼 수 있어서 속이 후련했다.&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 익숙해 질 때쯤 찾아온 끝&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌이켜보면 크게 난이도를 요구하는 일은 없었던 것 같다. 사람들 앞에 서서 사회를 보는 일은 처음에는 무척 떨렸지만 하다보니 익숙해졌고, 행사를 계획하고, 관계자분들과 연락을하고, 부원들에게 공지를 하는것은 넉넉하게 시간을 투자하면 해결되는 일이었다. &lt;u&gt;&lt;b&gt;시간, 회장을 맡으며 가장 많이 투자한 자원은 바로 시간이었다.&lt;/b&gt;&lt;/u&gt; 업무가 익숙해지만 일을 처리하는 데 더 적은 시간이 들었지만, 가끔씩 내가 해야되는 다른 일에 집중을 하지 못하고 동아리 업무를 해야할 때에는 시간이 아깝다라는 생각이 들 때도 있었다. 예기치 못한 일이 생겨 원래 하던 일을 못할 때에는 더 더욱. 사실 내게 돌아올 무언가를 기대하고 회장을 맡으면 후회할 가능성이 크다. &lt;u&gt;&lt;b&gt;회장이라는 경험을 해본다는 것이 내게 돌아온 가장 큰 성과였다.&lt;/b&gt;&lt;/u&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;u&gt;&lt;b&gt;활발한 갓생러&lt;/b&gt;&lt;/u&gt;들이었다. 그 친구들을 보면서 많은 동기부여가 되었고, 나도 자극을 받아 주어진 일에 더 열심히 몰입하게 되었다. 회장을 맡기 전의 나와 지금의 나를 비교해보았을 때, 가장 크게 바뀐 것은 마음가짐인 것 같다. 생각보다 많은 일을 겪어서 이제 예상치 못한 일이 생기더라도 &lt;u&gt;&lt;b&gt;당황을 훨씬 덜 하게 되었다.&lt;/b&gt;&lt;/u&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;u&gt;&lt;b&gt;최선을 다한 거 같아 후회는 없다.&lt;/b&gt;&lt;/u&gt; 사실 이런 경험을 언제 해보겠나 싶어서 재밌게 업무에 임했던 것 같다. 이제 내가 회장을 맡았던 KUIT 4기가 끝나고, 곧 KUIT 5기 부원 모집이 시작된다. 새로 회장을 맡은 친구가 나보다 일을 똑부러지게 잘해서 더 탄탄한, 완성도 있는 동아리가 될 것 같다. 이렇게 글을 쓸 수 있어서 다행이다. &lt;u&gt;&lt;b&gt;사실 다음 기수까지 동아리를 이어가지 못하고 망해버리면 어쩌나 걱정을 좀 했었으니까.&lt;/b&gt;&lt;/u&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;KUIT 4기를 같이해준 운영진들, 튜터, 그리고 부원분들에게 같이 활동해서 재밌었고, 또 &lt;u&gt;&lt;b&gt;고맙다고 말하고 싶다.&lt;/b&gt;&lt;/u&gt; 회장이 조금 띨띨해서 걱정 많이들 했을텐데 무사히 끝나서 다들 안도의 한숨을 쉬고 있지 않을까싶다. 계속 이메일과 카톡을 주고받으며 동아리를 지원해주신 학교 직원분께도 감사하다고 말씀드리고 싶다. 위에서 많이 떠들어댔지만 사실 나 혼자서 할 수 있었던 것은 &lt;u&gt;&lt;b&gt;아무것도 없었다.&lt;/b&gt; &lt;/u&gt;다들 열정적으로 도와주셔서 여기까지 올 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;4기 데모데이.jpg&quot; data-origin-width=&quot;3958&quot; data-origin-height=&quot;2706&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ozf7Q/btsMwqQ6cnY/zXvEAwciMErDse6LDoJQC1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ozf7Q/btsMwqQ6cnY/zXvEAwciMErDse6LDoJQC1/img.jpg&quot; data-alt=&quot;데모데이를 끝으로, 회장으로서 업무는 끝이 났다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ozf7Q/btsMwqQ6cnY/zXvEAwciMErDse6LDoJQC1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fozf7Q%2FbtsMwqQ6cnY%2FzXvEAwciMErDse6LDoJQC1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;273&quot; data-filename=&quot;4기 데모데이.jpg&quot; data-origin-width=&quot;3958&quot; data-origin-height=&quot;2706&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데모데이를 끝으로, 회장으로서 업무는 끝이 났다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;完. Season 25의 완결, Season 26의 시작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종이 울리고 새해를 맞았을 때, 새로운 시작을 한다는 느낌이 들지 않았었다. 아직 회장이 다 끝나지 않았었으니까. 2월 21일 데모데이가 끝나고 나는 &lt;u&gt;&lt;b&gt;비로소 25살을 끝마칠 수 있었다.&lt;/b&gt;&lt;/u&gt; 동아리 회장의 끝, 4학년 1학기의 끝, 25살의 끝. 정신없이 바빴던 25시즌이 끝나고 새로운 26시즌이 이제 시작된다.&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;u&gt;&lt;b&gt;어떤 상황이 오든 최선을 다해야겠다고 다짐했다.&lt;/b&gt; &lt;/u&gt;다음에 작성하게 될 후기에도 쓰고 싶은 말들이 많았으면 좋겠다. 힘들었지만 많은 점을 배운, 기억에 남는 경험들이 쌓였으면 좋겠다.&lt;/p&gt;</description>
      <category>KUIT</category>
      <category>KUIT</category>
      <category>개발 동아리</category>
      <category>개발 동아리 회장</category>
      <category>동아리 회장</category>
      <category>동아리 회장 후기</category>
      <category>회장</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/167</guid>
      <comments>https://quickchabun.tistory.com/167#entry167comment</comments>
      <pubDate>Mon, 24 Feb 2025 22:36:51 +0900</pubDate>
    </item>
    <item>
      <title>CORS 에러는 프론트가 해결해야하는가? (어떤 트윗을 보고)</title>
      <link>https://quickchabun.tistory.com/166</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. CORS 에러는 프론트에서 해결이 가능하다고?&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;a href=&quot;https://x.com/robinyoondev/status/1891720787863687634&quot;&gt;https://x.com/robinyoondev/status/1891720787863687634&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1196&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DXPQK/btsMo4ahPda/t7hQ6Bi2IHG7MnXhCaoJJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DXPQK/btsMo4ahPda/t7hQ6Bi2IHG7MnXhCaoJJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DXPQK/btsMo4ahPda/t7hQ6Bi2IHG7MnXhCaoJJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDXPQK%2FbtsMo4ahPda%2Ft7hQ6Bi2IHG7MnXhCaoJJ1%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;545&quot; height=&quot;360&quot; data-origin-width=&quot;1196&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;CORS 에러는 서버 측에서 &lt;b&gt;Access-Control-Allow-Origin&lt;/b&gt; 헤더를 설정하면, 특정 출처에서의 접근을 허용함으로써 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(CORS에 대해 알고 싶다면 아래 링크에서 잘 설명되어 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크: &lt;a href=&quot;https://www.maeil-mail.kr/question/78&quot;&gt;https://www.maeil-mail.kr/question/78&lt;/a&gt; )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 위 트윗에서는, 백엔드 개발자분께서 CORS 에러를 프론트엔드에서 해결할 수 있다고 말했다고 한다. 프론트엔드에서 어떻게 CORS 에러를 해결할 수 있을까?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;1254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1maCO/btsMnK5exVt/HkK3Vuml1Jk68CecuujMVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1maCO/btsMnK5exVt/HkK3Vuml1Jk68CecuujMVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1maCO/btsMnK5exVt/HkK3Vuml1Jk68CecuujMVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1maCO%2FbtsMnK5exVt%2FHkK3Vuml1Jk68CecuujMVk%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;568&quot; height=&quot;595&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;1254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 트윗들이 달려있었는데, 이해가 가지 않는 단어들이 있어서 검색을 해보았다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. BFF란 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFF(Backend For Frontend): 프론트엔드를 위한 백엔드 서버 &amp;rarr; 프론트엔드를 요구사항에 맞게 구현하기 위한 도움을 주는 보조 서버&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(자세한 설명은 링크를 참고하자. 링크: &lt;a href=&quot;https://velog.io/@seeh_h/BFF%EB%9E%80&quot;&gt;https://velog.io/@seeh_h/BFF란&lt;/a&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;결국 따로 서버를 파서 CORS를 피하라는건데 해당 개발자는 React + Vite여서 서버 기능이 없어 BFF를 구현하지 못했다고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. vite 프록시 설정 변경으로 프로덕션에서 CORS를 회피할 수 있는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; CORS 회피를 위한 Vite의 프록시 설정은 &lt;b&gt;개발 환경에서만&lt;/b&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;Vite의 프록시 설정은 개발 서버(vite dev)에서만 작동&lt;/li&gt;
&lt;li&gt;프로덕션 빌드(vite build) 후에는 정적 파일들만 생성되므로 프록시 기능이 없어짐&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Nginx 같은 웹 서버에서 /api를 API 서버로 가게끔 proxy pass를 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드와 백엔드가 서로 다른 도메인이라 CORS가 발생했던거라, 둘 사이에 Nginx proxy pass 설정을 해서 CORS를 회피할 수 있다고 한다.&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;만약 요청이 &lt;a href=&quot;https://your-domain.com/api/users%EB%9D%BC%EB%A9%B4&quot;&gt;https://your-domain.com/api/users라면&lt;/a&gt;,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 웹사이트에서 /api/users를 요청&lt;/li&gt;
&lt;li&gt;Nginx 서버가 이 요청을 받음&lt;/li&gt;
&lt;li&gt;Nginx는 /api로 시작하는 요청이라는 것을 확인&lt;/li&gt;
&lt;li&gt;Nginx는 /api를 제거하고 백엔드 서버(&lt;a href=&quot;http://backend-server:8080/users)%EB%A1%9C&quot;&gt;http://backend-server:8080/users)로&lt;/a&gt; 요청을 전달&lt;/li&gt;
&lt;li&gt;백엔드 서버가 처리 후 결과를 보냄&lt;/li&gt;
&lt;li&gt;Nginx는 받은 결과를 클라이언트에게 전달&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx를 사용해서 CORS를 우회할 수는 있겠지만, 그냥 백엔드에서 설정하는 것이 서로에게 훨씬 편하지 않을까라는 생각이 들었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 작성자가 새로운 트윗을 올렸다는 것을 확인했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1070&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwb838/btsMombE6cx/ukdy0SxDKMI9JKGx6pUTnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwb838/btsMombE6cx/ukdy0SxDKMI9JKGx6pUTnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwb838/btsMombE6cx/ukdy0SxDKMI9JKGx6pUTnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcwb838%2FbtsMombE6cx%2Fukdy0SxDKMI9JKGx6pUTnK%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;657&quot; height=&quot;586&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1070&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. AWS 람다로도 CORS를 회피할 수 있다고?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx의 proxy pass와 비슷한 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(AWS 람다가 무엇인지 궁금하다면 링크를 참고하자. 링크: &lt;a href=&quot;https://velog.io/@xgro/AWS-Lambda&quot;&gt;https://velog.io/@xgro/AWS-Lambda&lt;/a&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;다음과 같은 방식으로 CORS를 회피할 수 있다고 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프론트엔드에서 AWS Lambda 함수의 API Gateway 엔드포인트로 요청&lt;/li&gt;
&lt;li&gt;Lambda 함수가 실제 백엔드 API를 호출&lt;/li&gt;
&lt;li&gt;Lambda가 응답을 받아서 클라이언트에게 전달&lt;/li&gt;
&lt;/ol&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 람다를 사용한다는 것은 들어본 적이 없는데, 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;트윗을 자세히 보니 다음과 같은 문답이 있었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Q. AWS 람다는 프론트엔드 영역이 아니지 않나?&lt;br /&gt;A. 자신도 백엔드만 하고 있는 게 아니다.&lt;/p&gt;
&lt;/blockquote&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;아하! 프론트엔드에서 AWS 람다를 활용하는 것은 아니었다. 내가 만약 저 답변을 들었다면 머리가 새하얘졌을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;994&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEXzwx/btsMnL4a0CY/6brHTZLKm9TaFvdSR7Sx80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEXzwx/btsMnL4a0CY/6brHTZLKm9TaFvdSR7Sx80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEXzwx/btsMnL4a0CY/6brHTZLKm9TaFvdSR7Sx80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEXzwx%2FbtsMnL4a0CY%2F6brHTZLKm9TaFvdSR7Sx80%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;589&quot; height=&quot;480&quot; data-origin-width=&quot;1220&quot; data-origin-height=&quot;994&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;Nginx는 웹 &amp;lsquo;서버&amp;rsquo;이므로 프론트엔드가 아니라 백엔드의 영역이라고 생각한다. (물론 알아놓으면 많이 도움이 될 것 같다.) 작성된 트윗을 더 읽어보니 해피엔딩으로 끝난 것은 아닌 것 같아서 조금 찝찝했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 예전에 프로젝트 개발을 진행할 때 CORS 문제가 발생해서 Proxy로 CORS를 우회했던 경험이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Proxy는 결국 개발 단계에서만 적용 가능하므로 프로덕션 단계에서 CORS를 해결하려면 백엔드가 해결해줘야 한다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국, 위 트윗에서 CORS 에러는 백엔드 개발자분께서 해결해주시는 게 더 낫지 않았을까 하는 생각이 든다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>AWS 람다</category>
      <category>BFF</category>
      <category>CORS</category>
      <category>cors 프론트엔드</category>
      <category>nginx</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/166</guid>
      <comments>https://quickchabun.tistory.com/166#entry166comment</comments>
      <pubDate>Wed, 19 Feb 2025 21:41:00 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 자바스크립트] &amp;lsquo;베스트앨범&amp;rsquo; 풀어보기</title>
      <link>https://quickchabun.tistory.com/165</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크: &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42579&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/42579&lt;/a&gt; &lt;/p&gt;
&lt;figure id=&quot;og_1738479268361&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42579&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bLwCXJ/hyYch2dVJS/kXF0fP8wxJxvZRoELoaAV0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/j3jmA/hyYcjMvkKc/4Tb2KvfFj3dM8OUuOUDCG1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42579&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42579&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bLwCXJ/hyYch2dVJS/kXF0fP8wxJxvZRoELoaAV0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/j3jmA/hyYcjMvkKc/4Tb2KvfFj3dM8OUuOUDCG1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 생각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬 기준이 무려 세가지다!&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;속한 노래가 많이 재생된 장르를 먼저 수록&lt;/li&gt;
&lt;li&gt;장르 내에서 많이 재생된 노래를 먼저 수록&lt;/li&gt;
&lt;li&gt;장르 내에서 재생 횟수가 같은 노래 중에서는 고유 번호가 낮은 노래를 먼저 수록&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일단 Map을 생성한 후,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속한 노래가 많이 재생된 장르를 먼저 수록하기 위해&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;genres와 plays를 순회를 돌며 genres 별로 plays들을 더해줘서 plays가 큰 순서대로 Map을 내림차순 정렬을 해주었다. (이 때, Map을 정렬해주기 위해서 Map을 배열로 바꾸고 정렬을 해주었다)&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;그러면 이제 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;그리고 배열을 다시 Map으로 바꾸어주고, Map의 key를 순회를 돈다. 새로운 Map을 만들어주고, key 값이 인덱스(고유번호), value가 plays(재생 횟수)인 요소들을 Map에 넣어준다.&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번 기준과 3번 기준을 바탕으로 정렬을 진행해주는데, 고유번호가 다른 요소들끼리는 재생횟수가 더 큰 요소가 앞에 오도록, 재생횟수가 같은 요소들 끼리는 고유번호가 낮은 요소가 더 앞으로 오도록 정렬을 해주었다.&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;그리고 answer 배열에 위 배열의 첫 번째 인덱스의 값과 두 번째 인덱스 값을 push해주는 작업을 반복했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코드&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function solution(genres, plays) {
    const answer = [];   
    const map = new Map();
    
    // 각 장르별 plays의 합을 value에 더해줌
    for(let i = 0; i &amp;lt; genres.length; i++) {
        map.set(genres[i], map.get(genres[i]) + plays[i] || plays[i])
    }
    
    // 정렬을 위해 map을 배열로 바꿈
    const array = [...map];
    
    // plays의 총합을 기준으로 장르를 내림차순 정렬
    const sortedMap = new Map(array.sort((a, b) =&amp;gt; b[1] - a[1]));
        
    // 장르별로 순회를 돌며 고유번호, plays로 Map 생성
    sortedMap.forEach((value, key) =&amp;gt; {
        const map2 = new Map();
        
        for(let i = 0; i &amp;lt; genres.length; i++) {
            
            if(genres[i] === key) {
                map2.set(i, plays[i]);
            }
        }
        
        const array2 = [...map2];
        
        array2.sort((a, b) =&amp;gt; {
            
            // 만약 plays가 다르면 plays가 더 큰 요소가 앞으로 오게 정렬
            if(a[0] !== b[0]) {
                return b[1] - a[1];
            }
            
            // 만약 plays가 같으면 고유번호가 낮은 요소가 먼저 앞으로 오게 정렬
            return a[0] - b[0];
        });
        
        let count = 0;
        
        for(let i = 0; i &amp;lt; array2.length; i++) {
            
            if(count === 2){
                break;
            }
    
            answer.push(array2[i][0]);
            count++;
        }
    })
    
    return answer;
}
&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;Map을 정렬하기 위하여 배열로 변환시켜주고, 정렬 후 다시 Map으로 바꾸는 작업에 익숙해질 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, sort를 할 때 두 가지 기준으로 정렬을 할 수 있다는 사실 또한 알 수 있었던 문제였다.&lt;/p&gt;</description>
      <category>Algorithm/Programmers(JavaScript)</category>
      <category>베스트앨범</category>
      <category>베스트앨범 자바스크립트</category>
      <category>프로그래머스</category>
      <category>프로그래머스 자바스크립트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/165</guid>
      <comments>https://quickchabun.tistory.com/165#entry165comment</comments>
      <pubDate>Sun, 2 Feb 2025 15:55:33 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 자바스크립트] &amp;lsquo;네트워크&amp;rsquo; 풀어보기</title>
      <link>https://quickchabun.tistory.com/164</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크: &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43162&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/43162&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737966820045&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43162&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bj8q1M/hyX7YB35MM/cliyCIRRoMXepHZWe6lGV1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/ng8Nv/hyX7Xb6id7/v6kMxn4jIpQuBmmJ3lRwBk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43162&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43162&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bj8q1M/hyX7YB35MM/cliyCIRRoMXepHZWe6lGV1/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/ng8Nv/hyX7Xb6id7/v6kMxn4jIpQuBmmJ3lRwBk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 생각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DFS를 사용해서 풀어야 하는 문제다. 자바스크립트에서 DFS를 어떻게 풀어야되는지 아래의 블로그 글을 참고했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크: &lt;a href=&quot;https://chamdom.blog/dfs-using-js/&quot;&gt;https://chamdom.blog/dfs-using-js/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737966822080&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;[알고리즘] JavaScript로 구현하는 DFS&quot; data-og-description=&quot;dfs에 대해 알아보기 전에 우선 그래프에 대한 이해가 필요하다. 그래프에 대한 설명은 여기 에 자세히 정리해두었다. DFS란? DFS(Depth-First-Search) 는 &amp;hellip;&quot; data-og-host=&quot;chamdom.blog&quot; data-og-source-url=&quot;https://chamdom.blog/dfs-using-js/&quot; data-og-url=&quot;https://chamdom.blog/dfs-using-js/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Vz2AT/hyX7PE7Xfb/LfDJvCszXX9vB3ut7C13sk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/b9Awy7/hyX4p9byPJ/C8WDPHEuCxWkCz4sTkFCL1/img.png?width=480&amp;amp;height=255&amp;amp;face=0_0_480_255,https://scrap.kakaocdn.net/dn/uDd2C/hyX70fAtCA/Tw8YnNcdwOCRWcW1FXjPH0/img.png?width=480&amp;amp;height=255&amp;amp;face=0_0_480_255&quot;&gt;&lt;a href=&quot;https://chamdom.blog/dfs-using-js/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://chamdom.blog/dfs-using-js/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Vz2AT/hyX7PE7Xfb/LfDJvCszXX9vB3ut7C13sk/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/b9Awy7/hyX4p9byPJ/C8WDPHEuCxWkCz4sTkFCL1/img.png?width=480&amp;amp;height=255&amp;amp;face=0_0_480_255,https://scrap.kakaocdn.net/dn/uDd2C/hyX70fAtCA/Tw8YnNcdwOCRWcW1FXjPH0/img.png?width=480&amp;amp;height=255&amp;amp;face=0_0_480_255');&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;[알고리즘] JavaScript로 구현하는 DFS&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;dfs에 대해 알아보기 전에 우선 그래프에 대한 이해가 필요하다. 그래프에 대한 설명은 여기 에 자세히 정리해두었다. DFS란? DFS(Depth-First-Search) 는 &amp;hellip;&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;chamdom.blog&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;문제에서 computers 배열을 제공하는데, 이 배열은 어떤 한 노드가 다른 노드와 연결되어 있으면 연결되있는 노드의 index에 1을, 연결되있지 않으면 그 노드의 index에 0을 작성해놓은 배열들로 구성된 배열이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) [[1, 1, 0], [1, 1, 1], [0, 1, 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;하지만 내가 필요한 배열은 어떤 한 노드가 어떤 노드와 연결되어있는지, 그 index가 담겨져있는 배열들의 모음이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) [[1],[0,2],[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;따라서 computers 배열을 순회하며 DFS에 필요한 배열을 생성하는 makeGraph 메서드를 만들었다.&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;그리고 DFS에 필요한 visited 배열을 만들고, 0번 노드부터 n-1번 노드까지 순회하며 DFS를 실행했다. visited[n]이 false면 n번 노드에 dfs가 실행되지 않았다는 뜻이므로(즉, 다른 네트워크라는 뜻이므로) answer에 1을 더 해주었다. 그리고 순회가 끝나면 더해진 answer를 return했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코드&lt;/h2&gt;
&lt;pre class=&quot;matlab&quot;&gt;&lt;code&gt;function solution(n, computers) {
    let answer = 0;
    const graph = makeGraph(computers);
    const visited = Array(n).fill(false);
    
    
    for(let i = 0; i &amp;lt; n; i++) {
        if(visited[i] === false) {
            answer++;
            dfs(graph, i, visited);
        }
    }
    
    return answer;
}

function makeGraph(graph) {
    const graphArray = [];
    
    for(let i = 0; i &amp;lt; graph.length; i++) {
        const array = [];
        for(let j = 0; j &amp;lt; graph[i].length; j++) {
            
            if(i !== j) {
                if(graph[i][j] === 1) {      
                    array.push(j);
                }
            }
        }
        
        graphArray.push(array);
    }
    
    return graphArray;
}

function dfs(graph, v, visited, answerArray) {
    visited[v] = true;
    
    
    for (let node of graph[v]) {
        if(!visited[node]) {
            dfs(graph, node, visited);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/Programmers(JavaScript)</category>
      <category>DFS</category>
      <category>네트워크</category>
      <category>프로그래머스</category>
      <category>프로그래머스 자바스크립트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/164</guid>
      <comments>https://quickchabun.tistory.com/164#entry164comment</comments>
      <pubDate>Mon, 27 Jan 2025 17:34:15 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 자바스크립트] &amp;lsquo;정수 삼각형&amp;rsquo; 풀어보기</title>
      <link>https://quickchabun.tistory.com/163</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크: &lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43105&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/43105&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737698934351&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43105&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/JOmuV/hyX4l6p0F7/FpY8mkrkKKzll454eAkQBk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/c8levV/hyX4oPBaUN/BMG1Y9NEKZod8xzHWngTnk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43105&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43105&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/JOmuV/hyX4l6p0F7/FpY8mkrkKKzll454eAkQBk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/c8levV/hyX4oPBaUN/BMG1Y9NEKZod8xzHWngTnk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 처음 든 생각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dynamic Programming을 활용하여 푸는 문제라는 사실을 알고 있었기 때문에 고민하는데 드는 시간이 오래 걸리지는 않았다. 삼각형 칸 하나 당 배열을 생성하고, 위 칸의 배열에 존재하는 합들을 아래 칸에 더해서, 아래 칸의 배열에 넣어주는 행위를 반복하고, 맨 밑의 칸들을 순회하면서 가장 큰 값을 찾으면 될 것 같다는 결론을 내렸다. (참고로 맨 왼쪽에 있는 칸과 맨 오른쪽에 있는 칸은 윗 줄에서 한 가지 칸의 누적합들만 받아오면 되고, 다른 칸들은 윗 줄에서 두 가지 칸의 누적합들을 더 해줘야 된다).&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;그리고 차례대로 구현을 했고, 테스트 1개를 통과해서 자신있게 코드를 제출했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 메모리 부족 코드&lt;/h2&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;function solution(triangle) {
    
    const height = triangle.length;
        
    // 삼각형에 맞게 빈 배열 생성
    const dp = Array.from({length: height}, (_, i) =&amp;gt; Array.from({length: i + 1}, () =&amp;gt; []));
    
    // 맨 위의 값 넣기
    dp[0][0].push(triangle[0][0]);
    
    for(let i = 1; i &amp;lt; height; i++) {
    
        for (let j = 0; j &amp;lt; triangle[i].length; j++) {
            if(j === 0) {
                dp[i-1][0].forEach((element) =&amp;gt; {
                    dp[i][0].push(element + triangle[i][0]);
                })
            }
            else if (j === triangle[i].length - 1) {
                dp[i-1][dp[i-1].length - 1].forEach((element) =&amp;gt; {
                    dp[i][dp[i].length - 1].push(element + triangle[i][triangle[i].length - 1]);
                })
            }
            else {
                dp[i-1][j-1].forEach((element) =&amp;gt; {
                    dp[i][j].push(element + triangle[i][j]);
                })
                
                dp[i-1][j].forEach((element) =&amp;gt; {
                    dp[i][j].push(element + triangle[i][j]);
                })
                
                
            }
        }
    }
    
    const answerArray = [];
    
    const bottomLength = triangle[height - 1].length;
    
    for(let i = 0; i &amp;lt; bottomLength; i++) {
        dp[height-1][i].forEach((element) =&amp;gt; {
            answerArray.push(element);
        })
    }
    
    return Math.max(...answerArray);
}

&lt;/code&gt;&lt;/pre&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;1006&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVt0J2/btsLZ2Lb7Wa/XPRgLP8bea7NPivWrdyTBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVt0J2/btsLZ2Lb7Wa/XPRgLP8bea7NPivWrdyTBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVt0J2/btsLZ2Lb7Wa/XPRgLP8bea7NPivWrdyTBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVt0J2%2FbtsLZ2Lb7Wa%2FXPRgLP8bea7NPivWrdyTBk%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;626&quot; height=&quot;284&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;456&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;하지만 테스트를 통과하지 못하였다. 실패 옆에 뜬 signal: aborted (core dumped)는 무슨 뜻일까 검색해보니 메모리가 부족하다는 뜻이었다. 생각해보니 삼각형의 한 칸마다 빈 배열을 만들고, 위에서부터 누적된 합들을 전부 저장하는 것은 효율적이지 않은 방식이었다. 어떻게 하면 메모리를 덜 잡아먹는 효율적인 코드를 만들 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 이번에는 시간 초과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 모든 누적된 합들을 저장할 필요는 없었다. 누적된 합들을 비교해서 가장 큰 값만 받아와서 더해주면, 각 칸들에는 최댓값 한 개만 저장이 되므로 메모리가 초과되지 않을 것 같았다. 그래서 Math.max를 활용해서 최댓값만 저장했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;function solution(triangle) {
    
    const height = triangle.length;
        
    // 삼각형에 맞게 빈 배열 생성
    const dp = Array.from({length: height}, (_, i) =&amp;gt; Array.from({length: i + 1}, () =&amp;gt; []));
    
    // 맨 위의 값 넣기
    dp[0][0].push(triangle[0][0]);
    
    for(let i = 1; i &amp;lt; height; i++) {
    
        for (let j = 0; j &amp;lt; triangle[i].length; j++) {
            if(j === 0) {
                const tempArray_1 = [];

                dp[i-1][0].forEach((element) =&amp;gt; {
                    tempArray_1.push(element + triangle[i][0]);
                })

                const max_1 = Math.max(...tempArray_1);
                dp[i][0].push(max_1);
                
            }
            else if (j === triangle[i].length - 1) {
                const tempArray_2 = [];
                
                dp[i-1][dp[i-1].length - 1].forEach((element) =&amp;gt; {
                    tempArray_2.push(element + triangle[i][triangle[i].length - 1]);
                });
                
                const max_2 = Math.max(...tempArray_2);
                dp[i][dp[i].length - 1].push(max_2);
            
                
            }
            else {
                const tempArray_compare = [];
                
                const tempArray_3 = [];
    
                dp[i-1][j-1].forEach((element) =&amp;gt; {
                    tempArray_3.push(element + triangle[i][j]);
                })
                
                const max_3 = Math.max(...tempArray_3);
                
                tempArray_compare.push(max_3);
                
                const tempArray_4 = [];
                
                dp[i-1][j].forEach((element) =&amp;gt; {
                    tempArray_4.push(element + triangle[i][j]);
                })
                
                const max_4 = Math.max(...tempArray_4);
                
                tempArray_compare.push(max_4);
                
                const max_compare = Math.max(...tempArray_compare);
                
                dp[i][j].push(max_compare);
            }
        }
    }
    
    const answerArray = [];
    
    const bottomLength = triangle[height - 1].length;
    
    for(let i = 0; i &amp;lt; bottomLength; i++) {
        dp[height-1][i].forEach((element) =&amp;gt; {
            answerArray.push(element);
        })
    }
    
    return Math.max(...answerArray);
}
&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3QggN/btsLZvtAcjH/4tB7F5FWtI0HqDyS7ia5C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3QggN/btsLZvtAcjH/4tB7F5FWtI0HqDyS7ia5C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3QggN/btsLZvtAcjH/4tB7F5FWtI0HqDyS7ia5C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3QggN%2FbtsLZvtAcjH%2F4tB7F5FWtI0HqDyS7ia5C1%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;560&quot; height=&quot;273&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;452&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 드디어 정답&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 삼각형의 칸마다 새로 빈 배열을 만들지말고 값 하나만 넣으면 되는 일이었다. 그래서 빈 배열이 아니라 전부 0으로 초기화를 시켜주었고, 맨 왼쪽 칸과 맨 오른쪽 칸이 아닌 경우에는 위 칸의 두 가지 값만 비교해서 큰 값을 더해주었다. 이렇게 구조를 바꾸어주니 드디어 문제를 통과할 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;function solution(triangle) {
    
    const height = triangle.length;
        
    const dp = Array.from({length: height}, (_, i) =&amp;gt; Array.from({length: i + 1}, () =&amp;gt; 0));
        
    // 맨 위의 값 넣기
    dp[0][0] = triangle[0][0];
    
    for(let i = 1; i &amp;lt; height; i++) {
    
        for (let j = 0; j &amp;lt; triangle[i].length; j++) {
            if(j === 0) {
                dp[i][j] = dp[i-1][j] + triangle[i][j];
            }
            else if (j === triangle[i].length - 1) {
                dp[i][dp[i].length - 1] = dp[i-1][dp[i-1].length - 1] + triangle[i][triangle[i].length - 1];
            }
            
            else {                
                dp[i][j] = Math.max(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j];
            }
        
        
        }
    }
    
    return Math.max(...dp[height - 1]);
}

&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 문제를 풀면서, 모든 경우의 수를 저장할 필요가 없이 최적의 값만 저장하는 것이 시간 측면에서나 메모리 측면에서 훨씬 효율적이라는 사실을 깨달았다. 또한, 불필요한 배열을 생성하는 것을 자제해야 한다는 사실도 알 수 있었다. 메모리 부족으로 통과를 못한 적이 이번이 거의 처음이었기 때문에 메모리 관리의 중요성도 알 수 있었던 기회였다.&lt;/p&gt;</description>
      <category>Algorithm/Programmers(JavaScript)</category>
      <category>DP</category>
      <category>dynamic programming</category>
      <category>정수 삼각형</category>
      <category>프로그래머스</category>
      <category>프로그래머스 자바스크립트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/163</guid>
      <comments>https://quickchabun.tistory.com/163#entry163comment</comments>
      <pubDate>Fri, 24 Jan 2025 15:13:57 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 자바스크립트] &amp;lsquo;가장 큰 수&amp;rsquo; 풀어보기</title>
      <link>https://quickchabun.tistory.com/162</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크: &lt;span&gt;&lt;/span&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42746&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/42746&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737374062062&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42746&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/AR6TU/hyX4mQR9vm/Rq1u5jmZba4ocap4xz48vk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/j1Xb8/hyX4Ahiu8i/kwe1rdH3BWGc9GtvK97K30/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42746&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42746&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/AR6TU/hyX4mQR9vm/Rq1u5jmZba4ocap4xz48vk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/j1Xb8/hyX4Ahiu8i/kwe1rdH3BWGc9GtvK97K30/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&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;h2 data-ke-size=&quot;size26&quot;&gt;1. 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;무슨 기준으로 정렬을 해야할까?&amp;rsquo;를 고민했다. 앞자리에 있는 수가 클수록 먼저 배치해야하는 것은 쉽게 알 수 있었다. 생각해봐야할 케이스는 34와 341 같은 경우이다. 둘 중에 무엇을 먼저 앞에 놓아야할까? 34134와 34341을 비교하면 34를 먼저 배치했을 때가 더 숫자가 컸다. 즉, 마지막 자리의 숫자가 더 큰 것이 먼저 배치가 되면 된다.&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;그래서 두 숫자를 비교할 때 각 숫자의 index를 설정하고, 맨 앞자리부터 숫자의 크기를 비교했다. 그리고 어떤 수의 index가 일의 자리까지 도착하면 그 수의 index는 더 이상 증가시키지 않았다.&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;&lt;b&gt;시간 초과 코드&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function sortNumbers(a, b) {
    
    a = String(a);
    b = String(b);
    
    let a_index = 0;
    let b_index = 0;
    
    const a_length = a.length;
    const b_length = b.length;
    
    while(a_index &amp;lt; a_length) {
        
        if(b_index &amp;gt;= b_length) {
            break;
        }
        
        
        if(a[a_index] &amp;gt; b[b_index]) {
            return 1;
        }
        else if(a[a_index] &amp;lt; b[b_index]) {
            return -1;
        }
        else {
            
            if(a_index &amp;lt; a_length - 1) {
                a_index++;
            }
            
            if(b_index &amp;lt; b_length - 1) {
                b_index++;
            }
        }
    }
}

function solution(numbers) {
    
    let answerArray = numbers.sort((a,b) =&amp;gt; sortNumbers(a,b));
    answerArray = answerArray.reverse();
        
    let answer = '';
    
    answerArray.forEach((e) =&amp;gt; {
        answer += e;
    })
    
    return answer;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제출을 해봤는데 시간이 초과되서 테스트를 통과하지 못했다. 숫자 두 개를 비교할 때마다 인덱스를 두고 하나씩 증가시키는 방식은 시간이 오래 걸리는 것 같다. 어떻게 하면 시간 초과를 방지할 수 있을까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코드&lt;/h2&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function sortNumbers(a, b) {
    
    a = String(a);
    b = String(b);
    
    const first_a = a + b;
    const first_b = b + a;
    
    if (first_a &amp;gt; first_b) {
        return 1;
    }
    else {
        return -1;
    }
}

function solution(numbers) {
    
    let answerArray = numbers.sort((a,b) =&amp;gt; sortNumbers(a,b));
    answerArray = answerArray.reverse();
        
    let answer = '';
    
    answerArray.forEach((e) =&amp;gt; {
        answer += e;
    })
    
    if(Number(answer) === 0) {
        return '0';
    }
    
    return answer;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 그냥 두 수를 차례대로 붙여보고, 거꾸로 붙여봐서 둘 중에 무엇이 더 큰지 비교하면 되는거였다. 참고로 sort를 쓸 때는 1 아니면 -1을 return해야 제대로 작동이 된다. 그 사실을 잘 모르고 문자열을 return했더니 sort가 잘 동작하지 않았다.&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;그리고 테스트 케이스 하나가 끝끝내 통과되지 않아서 답답했는데, 찾아보니 [0, 0 ,0]인 케이스에서 자꾸 막혔던 것이었다. 내가 짠 코드에서는 &amp;lsquo;000&amp;rsquo;으로 출력되었어서 통과를 못했던 것이었고, answer를 숫자로 바꿀 때 0이면 문자 &amp;lsquo;0&amp;rsquo;을 출력되게 만들면 해당 테스트를 통과할 수 있었다.&lt;/p&gt;</description>
      <category>Algorithm/Programmers(JavaScript)</category>
      <category>가장 큰 수</category>
      <category>프로그래머스</category>
      <category>프로그래머스 자바스크립트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/162</guid>
      <comments>https://quickchabun.tistory.com/162#entry162comment</comments>
      <pubDate>Mon, 20 Jan 2025 20:55:16 +0900</pubDate>
    </item>
    <item>
      <title>Vite 프로젝트에 PWA와 FCM 푸시 알림 적용하기</title>
      <link>https://quickchabun.tistory.com/161</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. PWA와 FCM을 쓰려는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;친구들과 공모전을 나가기로 했었다. 공모전에 프로젝트를 제출하려면 몇 가지 조건을 충족해야했는데, iOS와 Android에서 모두 작동해야하며, 푸시 알림이 구현되어야 한다는 조건이었다. React와 문법이 비슷한 React Native를 빠르게 학습한 후 이 언어를 활용하여 개발을 진행하는 방법이 먼저 떠올랐다. 그 다음에 떠오른 것이 바로 PWA(Progressive Web App)였다.&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;PWA 기술을 활용하면 웹사이트지만 네이티브 앱처럼 설치하여 사용할 수 있고, 푸시 알림, 오프라인 작동 등 앱과 유사한 기능들도 구현할 수 있다. 푸시 알림은 FCM(Firebase Cloud Messaging)을 통하여 구현하기로 했다. Firebase을 독학하여 CRUD를 구현해본 적은 있지만 푸시 알림은 구현해본 적이 없었기 때문에 이번 기회에 FCM을 사용해보고 싶었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. &amp;lsquo;vite-pwa&amp;rsquo;를 통하여 PWA를 구현하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PWA를 어떻게 구현할 수 있을지 찾아보니, 어떤 스택을 활용하여 프로젝트를 빌드하느냐에 따라 PWA 구현 방법도 달라졌다. Next.js를 활용하면 &amp;lsquo;next-pwa&amp;rsquo; 라이브러리를, Vite를 활용하면 &amp;lsquo;vite-pwa&amp;rsquo; 라이브러리를 활용하면 되었다.&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;lsquo;지금 내가 구현하려는 프로젝트가 SSR(Server Side Rendering)이 꼭 필요한가?&amp;rsquo; 하는 의문이 들었다. 사람들이 Next.js를 사용하는 이유가 SSR을 구현하기 위함이 전부가 아님을 알고 있다. 하지만 내가 구현하려는 프로젝트는 SEO가 중요하지 않고, 또 백엔드 팀원들이 존재한다. 그래서 Next.js를 사용하지 않고 가벼운 vite를 활용하기로 했다.&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;FCM을 구현하기 전 먼저 PWA부터 구현했었는데, vite-pwa는 service worker를 자동으로 생성해주기 때문에 별도의 service worker 파일을 만들어줄 필요가 없다고해서 따로 생성하지 않았다. (물론 뒤에 FCM을 구현할 때에는 service worker 파일을 생성했다.) 그 대신 vite.config.ts에서 VitePWA 설정을 추가해주었다.&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;&lt;b&gt;vite.config.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;plugins: [
    react({
      jsxImportSource: '@emotion/react',
    }),
    VitePWA({
      registerType: 'autoUpdate',
      devOptions: {
        enabled: true,
        type: 'module'
      },
      manifest: {
        name: '앱 이름',
        short_name: '앱 이름',
        description: '앱 설정',
        theme_color: '#ffffff',
        icons: [
          {
            src: '/favicon/favicon-16x16.png',
            sizes: '16x16',
            type: 'image/png'
          },
          {
            src: '/favicon/favicon-32x32.png',
            sizes: '32x32',
            type: 'image/png'
          },
          
          (...)         
          
          
        ]
      }
    })
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 결과 sw.js를 따로 사용하지 않아도 PWA가 구현이 되었다! 어떤 환경에서는 앱 설치가 되지 않는 문제가 발생했지만, 계속 새로고침을 반복하다보니 다른 환경에서도 앱 설치가 가능했다. 이렇게 일단 PWA를 구현하는데는 성공했고, 이제 FCM을 구현해보기로 하였다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. FCM을 구현해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글링을 해보면 어떻게 FCM을 구현할 수 있는지 상세히 작성되어 있는 글들을 확인할 수 있다. 또한 공식 문서에도 설명이 자세히 되어 있으니 이 글을 참고해서 FCM 설정을 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서 링크: &lt;a href=&quot;https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko&amp;amp;source=post_page-----368d90974b7--------------------------------&quot;&gt;https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko&amp;amp;source=post_page-----368d90974b7--------------------------------&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1737043187017&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;자바스크립트 Firebase 클라우드 메시징 클라이언트 앱 설정 &amp;nbsp;|&amp;nbsp; Firebase Cloud Messaging&quot; data-og-description=&quot;2024년 데모 데이에서, Firebase를 사용하여 AI 기반 앱을 빌드하고 실행하는 방법에 관한 데모를 시청하세요. 지금 시청하기 의견 보내기 자바스크립트 Firebase 클라우드 메시징 클라이언트 앱 설정 &quot; data-og-host=&quot;firebase.google.com&quot; data-og-source-url=&quot;https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko&amp;amp;source=post_page-----368d90974b7--------------------------------&quot; data-og-url=&quot;https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko&amp;amp;source=post_page-----368d90974b7--------------------------------&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://firebase.google.com/docs/cloud-messaging/js/client?hl=ko&amp;amp;source=post_page-----368d90974b7--------------------------------&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;자바스크립트 Firebase 클라우드 메시징 클라이언트 앱 설정 &amp;nbsp;|&amp;nbsp; Firebase Cloud Messaging&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;2024년 데모 데이에서, Firebase를 사용하여 AI 기반 앱을 빌드하고 실행하는 방법에 관한 데모를 시청하세요. 지금 시청하기 의견 보내기 자바스크립트 Firebase 클라우드 메시징 클라이언트 앱 설정&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;firebase.google.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;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Service Worker 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firebase-messaging-sw.js를 생성해서 아래 코드와 같이 service worker를 설정했다.&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;&lt;b&gt;firebase-messaging-sw.js&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// Service Worker에서 사용할 Workbox 매니페스트
self.__WB_MANIFEST

importScripts('&amp;lt;https://www.gstatic.com/firebasejs/9.0.0/firebase-app-compat.js&amp;gt;');
importScripts('&amp;lt;https://www.gstatic.com/firebasejs/9.0.0/firebase-messaging-compat.js&amp;gt;');

// Firebase 초기화
firebase.initializeApp({
  apiKey: &quot;&quot;,
  authDomain: &quot;&quot;,
  projectId: &quot;&quot;,
  storageBucket: &quot;&quot;,
  messagingSenderId: &quot;&quot;,
  appId: &quot;&quot;,
  measurementId: &quot;&quot;
});

const messaging = firebase.messaging();

// Background 메시지 처리
messaging.onBackgroundMessage((payload) =&amp;gt; {
  console.log('Background 메시지 수신:', payload);

  const notificationTitle = payload.notification.title;
  const notificationOptions = {
    body: payload.notification.body,
    icon: '/logo.png'
  };

  self.registration.showNotification(notificationTitle, notificationOptions);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;service worker 파일을 public 디렉토리에 추가한 이유&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 service worker 파일은 public 디렉토리에 추가하라고 해서 public 디렉토리에 추가했다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;루트 경로 접근성&lt;/b&gt;: 서비스 워커는 보안상의 이유로 반드시 웹사이트의 루트 경로에서 접근 가능해야 합니다.&amp;nbsp;&lt;b&gt;public&lt;/b&gt;&amp;nbsp;폴더의 내용은 빌드 시 프로젝트의 루트 디렉토리로 복사되어 서비스됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스코프 제한&lt;/b&gt;: 서비스 워커는 자신이 위치한 디렉토리와 그 하위 디렉토리에만 영향을 미칠 수 있습니다. 루트에 위치함으로써 전체 애플리케이션에 대한 푸시 알림을 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;직접 서빙&lt;/b&gt;:&amp;nbsp;&lt;b&gt;public&lt;/b&gt;&amp;nbsp;폴더의 파일들은 번들링이나 트랜스파일링 없이 있는 그대로 서빙됩니다. 서비스 워커는 클라이언트 사이드 JavaScript와는 별도로 동작해야 하므로, 번들러의 처리 없이 그대로 서빙되어야 합니다.&lt;/li&gt;
&lt;/ol&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;public 폴더에 있으면 문제점이 하나 생기는데, 바로 &lt;b&gt;.env 파일의 환경변수를 사용하지 못한다는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 스크립트를 작성해서 환경변수를 주입하는 방법도 사용할 수 있겠으나, 검색해보니 firebase의 키 값들은 노출되도 별 문제가 없다는 답변이 많아 위 코드와 같이 초기화를 진행했다.&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;Workbox 매니페스트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Workbox 매니페스트를 선언했다. Workbox 매니페스트는 생소한 개념이라 어떤 용도로 사용되는지 잘 몰랐다. 찾아본 결과, 웹사이트를 빌드(build)할 때,&amp;nbsp;self.__WB_MANIFEST는 실제 파일 목록으로 바뀐다고 한다.이 매니페스트가 캐시할 파일 목록들을 저장해놓기 때문에, 인터넷이 끊겨도 웹사이트가 작동할 수 있게 해주고, 파일들을 미리 저장해두어 웹사이트 로딩 속도를 빠르게 해준다고 한다.&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;그리고 밑에는 백그라운드 메시지 처리 코드들이 있다. FCM 알림은 백그라운드(Background) 알림과 포그라운드(Foreground) 알림으로 나뉘는데, 포그라운드 알림은 앱이 켜져 있을 때 오는 알림이고, 백그라운드 알림은 앱이 꺼져 있을 때 오는 알림이다. service worker는 앱이 꺼져 있을 때에도 실행될 수 있는 유일한 컨텍스트이므로, 백그라운드 알림 관련 코드는 service worker 파일에 추가하는 것이 맞아보인다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. firebase.ts 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firebase.ts를 생성해줘서 firebase 초기화 및 설정을 진행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잠깐만, 위의 service worker 파일에서 firebase 초기화를 이미 진행해준거 아닌가, 왜 또 진행하지 하는 의문을 가질 수 있는데 찾아본 결과,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Service Worker는 완전히 독립된 JavaScript 실행 환경&lt;/li&gt;
&lt;li&gt;메인 앱의 JavaScript 컨텍스트와 변수나 상태를 공유할 수 없음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 firebase 초기화를 각각 해줘야 한다고 한다.&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;&lt;b&gt;firebase.ts&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
  measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
};

// Firebase 초기화
const app = initializeApp(firebaseConfig);
console.log('Firebase initialized:', app);

const messaging = getMessaging(app);
console.log('Messaging initialized:', messaging);

// FCM 토큰 가져오기
export const requestForToken = async () =&amp;gt; {
  try {
    const currentToken = await getToken(messaging, {
      vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
    });
    
    if (currentToken) {
      console.log('FCM 토큰 가져오기 성공!')

      // 테스트 시 사용
      // console.log('FCM 토큰 값: ', currentToken);

      // 나중에 토큰을 백엔드 서버로 전송하는 로직 추가
      return currentToken;
    } else {
      console.log('토큰을 가져올 수 없습니다.');
    }
  } catch (err) {
    console.log('토큰 발급 중 에러 발생:', err);
  }
};

// foreground 메시지 수신
export const onMessageListener = () =&amp;gt;
  new Promise((resolve) =&amp;gt; {
    onMessage(messaging, (payload) =&amp;gt; {
      console.log('Foreground message received:', payload);
      resolve(payload);
    });
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FCM 토큰을 가져오는 requestForToken 메서드와 포그라운드 메시지를 수신하는 메서드도 firebase.ts에서 생성해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주목할 점은 FCM 토큰을 가져올 때는 vapidKey를 담아서 getToken으로 보내줘야 한다는 것이다.&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;또한 firebase.ts는 src 파일에 있으므로 환경변수들을 활용해서 키들을 공개하지 않으려 했다. (물론 service worker에 다 공개되 있으므로 크게 의미는 없지만..)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3. App.tsx에서 코드 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최상단 파일인 App.tsx에서 useEffect를 활용하여 앱이 실행될 때마다 알림 권한을 요청하고, FCM 토큰을 요청해서 FCM 토큰을 받아오게 했다. 코드에서 확인할 수 있듯이 permission이 &amp;lsquo;granted&amp;rsquo;여야 FCM 토큰을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 firebase.ts의 onMessageListener 메서드를 활용하여 포그라운드 메시지가 작동하게 코드를 작성하였다.&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;App.tsx&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { useEffect } from 'react';
import Onboarding from './pages/Onboarding';
import MyPage from './pages/MyPage';
import Register from './pages/Register';
import { requestForToken, onMessageListener } from './firebase/firebase';

function App() {
  useEffect(() =&amp;gt; {
    // 알림 권한 요청
    if ('Notification' in window) {
      console.log('Current notification permission:', Notification.permission);
      Notification.requestPermission().then((permission) =&amp;gt; {
        console.log('알림 허용이 되어있나요 :', permission);
        if (permission === 'granted') {
          
          // FCM 토큰 요청
          requestForToken().then((token) =&amp;gt; {
            if (token) {
              console.log('requestForToken 성공!');
            }
          });
        }
        if (permission === 'denied') {
          console.log('알림이 거부되었어요');
        }
      });
    } else {
      console.log('알림이 되지 않아요!');
    }

    // Foreground 메시지 수신 처리
    onMessageListener()
      .then((payload : any) =&amp;gt; {
        // Foreground에서 알림 표시
        if (Notification.permission === 'granted') {
          new Notification(payload.notification.title, {
            body: payload.notification.body,
            icon: '/logo.png'
          });
        }
        if (Notification.permission === 'denied') {
          console.log('알림이 거부되었어요!'); 
                }
      })
      .catch((err) =&amp;gt; console.log('메시지 수신 에러:', err));
  }, []);

  return (
    &amp;lt;BrowserRouter&amp;gt;
      &amp;lt;Routes&amp;gt;
        &amp;lt;Route path='/onboarding' element={&amp;lt;Onboarding /&amp;gt;} /&amp;gt;
        &amp;lt;Route path='/myPage' element={&amp;lt;MyPage /&amp;gt;} /&amp;gt;
        &amp;lt;Route path='/register' element={&amp;lt;Register /&amp;gt;} /&amp;gt;
        &amp;lt;Route path='/' element={&amp;lt;Navigate to='/onboarding' replace /&amp;gt;} /&amp;gt;
      &amp;lt;/Routes&amp;gt;
    &amp;lt;/BrowserRouter&amp;gt;
  );
}

export default App;

&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. FCM이 잘 작동하는지 테스트해보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;firebase 홈페이지에서 알림을 보내서 FCM이 잘 작동하는지 테스트할 수 있다.&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;&lt;a href=&quot;https://firebase.google.com/docs/cloud-messaging/js/first-message?hl=ko&quot;&gt;https://firebase.google.com/docs/cloud-messaging/js/first-message?hl=ko&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 링크로 접속해서 테스트 알림 메시지를 보내는 방법이 나오는데, 아래와 같이 따라하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;FCM 등록 토큰을 넣어야되는데, firebase.ts에서 토큰을 가져오는 메서드를 만들 때 임시로 토큰을 알려줄 수 있도록 코드를 수정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// FCM 토큰 가져오기
export const requestForToken = async () =&amp;gt; {
  try {
    const currentToken = await getToken(messaging, {
      vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
    });
    
    if (currentToken) {
      console.log('FCM 토큰 가져오기 성공!')

      // 테스트 시 사용
      // console.log('FCM 토큰 값: ', currentToken);

      // 나중에 토큰을 백엔드 서버로 전송하는 로직 추가
      return currentToken;
    } else {
      console.log('토큰을 가져올 수 없습니다.');
    }
  } catch (err) {
    console.log('토큰 발급 중 에러 발생:', err);
  }
};

&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;주석 친 &amp;lsquo;테스트 시 사용&amp;rsquo; 밑의 코드를 활성화 시키고. 나온 토큰 값을 복사해서 웹 페이지 작성란에 넣자.&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;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oTOSH/btsLQh9rBvX/vZfwcL57DvMMiCfXLiD9VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oTOSH/btsLQh9rBvX/vZfwcL57DvMMiCfXLiD9VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oTOSH/btsLQh9rBvX/vZfwcL57DvMMiCfXLiD9VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoTOSH%2FbtsLQh9rBvX%2FvZfwcL57DvMMiCfXLiD9VK%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;769&quot; height=&quot;244&quot; data-origin-width=&quot;1032&quot; data-origin-height=&quot;328&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 FCM과 관련해서 백엔드와 연동을 진행하지는 않았기에 그 때에도 FCM이 잘 작동할지는 확신할 수 없지만, 계속 디벨롭해가면서 푸시 알림이 잘 작동되는 PWA를 완성해보려 한다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>FCM</category>
      <category>Firebase Cloud Messaging</category>
      <category>progressive web app</category>
      <category>pwa</category>
      <category>pwa fcm</category>
      <category>vite fcm</category>
      <category>vite-pwa</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/161</guid>
      <comments>https://quickchabun.tistory.com/161#entry161comment</comments>
      <pubDate>Fri, 17 Jan 2025 01:03:57 +0900</pubDate>
    </item>
    <item>
      <title>[SQL] SELECT, FROM, WHERE, 비교 연산자, 문자열 검색에 관하여</title>
      <link>https://quickchabun.tistory.com/160</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;책 &lt;b&gt;'그림으로 배우는 SQL 입문'&lt;/b&gt;을 읽은 후 정리한 글입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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;Relational Database&lt;/b&gt;(관계 데이터베이스: RDB 형식)&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;RDB는 행(record)과 열(column)으로 구성된 표(table)로 데이터를 다룸&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 안의 1개의 요소를 &lt;b&gt;&amp;lsquo;필드&amp;rsquo;&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;RDB를 관리하기 위한 DBMS를 관계 데이터베이스 관리 시스템, &lt;b&gt;RDBMS&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;&lt;b&gt;SQL: 데이터베이스 조작이나 정의를 시행하기 위한 언어&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. SELECT, FROM&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에서 데이터를 가져오기 위해서는 SQL에서 &lt;b&gt;SELECT&lt;/b&gt;라는 구문을 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(SELECT 구문을 사용한 SQL을 &lt;b&gt;SELECT 문&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;예시) product_id와 product_name을 product 테이블에서 가져온다&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT
	product_id,
	product_name

FROM
	product;
&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;SELECT 다음에 &lt;b&gt;&amp;lsquo;무엇&amp;rsquo;&lt;/b&gt;을 가져올지에 해당하는 column명을 작성 (여러개인 경우 콤마로 구분)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FROM 다음에 &lt;b&gt;&amp;lsquo;어디&amp;rsquo;&lt;/b&gt;에서 가져오는지에 해당하는 table명을 작성&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;1개의 SQL문 마지막에는 반드시 &lt;b&gt;&amp;lsquo;;&amp;rsquo;&lt;/b&gt;을 붙임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*을 사용해서, &lt;b&gt;테이블의 모든 데이터&lt;/b&gt;를 가져올 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) *을 사용해서, product 테이블의 모든 데이터를 가져온다&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT
	*

FROM
	product;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SELECT 구에서 같은 컬럼을 여러 개 지정하거나 *와 column명을 같이 사용할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) product_id를 2회, product_name을 product 테이블에서 가져옴&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT
	product_id, product_name, product_id

FROM
	product;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) 모든 column과 product_id를 product 테이블에서 가져옴&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT
	*, product_id
	
FROM
	product;
&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;SQL을 적을 때, 각각의 단어 사이는 &lt;b&gt;1개 이상의 공백&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;&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;p data-ke-size=&quot;size16&quot;&gt;SELECT 구에서 데이터를 가져올 때 &lt;b&gt;AS 구를 활용하여 별명을 지정할 수 있음&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(별명을 에일리어스, alias라고 부름)&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;예시) product_id와 product_name을 별명으로 가져온다&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
	product_id AS 상품ID,
	product_name AS 상품명

FROM
	product;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. WHERE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WHERE&lt;/b&gt; 뒤에 조건을 줌으로써 특정 조건에 해당하는 행(record)만 가져올 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) membertype_id가 2인 레코드의 customer_name 컬럼을 customer 테이블에서 가져온다&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT
	customer_name

FROM
	customer

WHERE
	membertype_id = 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 비교 연산자, 데이터형, NULL, IS와 IS NOT&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비교 연산자 목록&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산자 = : a와 b는 같다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산자 &amp;lt;=&amp;gt; : a와 b는 같다(NULL 대응)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산자 != : a와 b는 다르다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산자 &amp;lt;&amp;gt; : a와 b는 다르다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산자 &amp;lt; : a는 b보다 작다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산자 &amp;gt; : a는 b보다 크다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산자 &amp;lt;= : a는 b 이하&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산자 &amp;gt;= : a는 b 이상&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환 결과는 1(TRUE), 0(FALSE), NULL&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: 어떤 테이블에서도 데이터를 가져오지 않을 때는 FROM 구를 적지 않는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터형&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 : CHAR(&amp;rsquo;A&amp;rsquo;), VARCHAR(&amp;rdquo;abc123&amp;rdquo;), TEXT(&amp;rsquo;가나다라&amp;rsquo;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정수: INT, TINYINT&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실수(소수): DOUBLE, FLOAT, DECIMAL&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;날짜시각: DATE, DATETIME (&amp;rsquo;2020-01-01&amp;rsquo;, &amp;lsquo;2020/01/01&amp;rsquo;, &amp;lsquo;2020-01-01 01:23:34&amp;rsquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부울형: BOOLEAN( 1(TRUE), 0(FALSE) )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위도경도: GEOMETRY(&amp;rsquo;POINT(139.721251 35.689607)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열, 날짜 시각은 &amp;lsquo;이나 &amp;ldquo;를 감싸서 적는데, &amp;lsquo;이 일반적&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NULL&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL이란 어떤 데이터도 가지지 않는 상태&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(길이가 0인 문자열 &amp;ne; NULL)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL인 경우를 조건으로 하려면 &lt;b&gt;IS NULL&lt;/b&gt;을 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) customer_name이 NULL인 customer_id를 customer 테이블에서 가져온다&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT
	customer_id

FROM
	customer
	
WHERE
	customer_name IS NULL;
	
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NULL이 아니다를 조건으로 하는 경우 &lt;b&gt;IS NOT NULL&lt;/b&gt;을 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;=&amp;gt; 이외의 연산자로&lt;/b&gt; NULL을 비교 대상으로 하면 NULL 자신을 포함하는 어떤 값과 비교해도 &lt;b&gt;결과는 모두 NULL&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 &amp;lt;=&amp;gt; NULL &amp;rarr; 0&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 = NULL &amp;rarr; NULL&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 != NULL &amp;rarr; NULL&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 &amp;lt;&amp;gt; NULL &amp;rarr; NULL&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IS와 IS NOT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 값이 BOOLEAN 형의 TRUE 또는 FALSE인지를 판정할 때에는 &lt;b&gt;IS&lt;/b&gt;와 &lt;b&gt;IS NOT&lt;/b&gt;을 사용&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 문자열 검색&lt;/h2&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;예시) val이 &amp;lsquo;A&amp;rsquo;인 레코드를 search 테이블에서 가져온다&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
	*
	
FROM
	search
	
WHERE
	val = 'A';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예문은 val = &amp;lsquo;a&amp;rsquo;인 레코드도 가져옴&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;BINARY&lt;/b&gt;를 사용하면 &lt;b&gt;대소문자 문제와 끝의 공백이 무시되는 문제 해결 가능&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) val이 &amp;lsquo;A&amp;rsquo;인 레코드를 search 테이블에서 가져온다&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
	*

FROM
	search

WHERE
	val = BINARY 'A';
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열이 일치하는지 여부를 판정하는데에 &lt;b&gt;LIKE&lt;/b&gt;와 &lt;b&gt;NOT LIKE&lt;/b&gt; 사용 가능&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
	*

FROM
	search
	
WHERE
	val LIKE 'A';

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대소문자도 구분하고 싶으면 WHERE에 &lt;b&gt;val LIKE BINARY &amp;lsquo;A&amp;rsquo;;&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;p data-ke-size=&quot;size16&quot;&gt;LIKE는 문자열의 일부만 일치하는지 판정할 수 있음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;%: 임의의 0개 이상의 문자&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시) product_name의 맨 앞에 &amp;lsquo;약용&amp;rsquo;이 들어가 있는 레코드르 product 테이블에서 가져온다&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
	*

FROM
	product

WHERE
	product_name LIKE '약용%';
&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;&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;예시) product_name에 &amp;lsquo;100%&amp;rsquo;가 들어가 있는 레코드를 product 테이블에서 가져온다&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
	*

FROM
	product
	
WHERE
	product_name LIKE '%100\\%%';
&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;&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;p data-ke-size=&quot;size16&quot;&gt;예시) birthday가 &amp;lsquo;1900-01-01&amp;rsquo;보다 이전인 레코드를 customer 테이블에서 가져온다&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT
	*

FROM
	customer

WHERE
	birthday &amp;lt; '1990-01-01';
&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;예시) val이 &amp;lsquo;A&amp;rsquo;보다 큰 레코드를 search 테이블에서 가져온다&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT
	*

FROM
	search

WHERE
	val &amp;gt; 'A';
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &amp;lsquo;4&amp;rsquo; &amp;lt;&amp;rsquo; 10&amp;rsquo;은 false이다.&lt;/p&gt;</description>
      <category>Study/SQL</category>
      <category>SQL</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/160</guid>
      <comments>https://quickchabun.tistory.com/160#entry160comment</comments>
      <pubDate>Sat, 11 Jan 2025 19:59:10 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 자바스크립트] &amp;lsquo;모의고사&amp;rsquo; 풀어보기</title>
      <link>https://quickchabun.tistory.com/159</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크: &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42840&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/42840&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1736587681506&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42840&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dDeZb1/hyX0wsll1x/UzABdOzStA3R1fsNFMlns0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bGQajc/hyXWAb6eqO/ZtTRJSz1t2qG7nYm4knpgk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42840&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42840&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dDeZb1/hyX0wsll1x/UzABdOzStA3R1fsNFMlns0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/bGQajc/hyXWAb6eqO/ZtTRJSz1t2qG7nYm4knpgk/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 풀이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 수포자는 1,2,3,4,5번을 계속 반복하면서 찍고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 수포자는 2,1,2,3,2,4,2,5번을 반복하면서 찍고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번 수포자는 3,3,1,1,2,2,4,4,5,5번을 반복하면서 찍는다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;answers 배열을 순회하면서 해당 배열이 가리키는 index를 각각 5, 8, 10으로 나눈 값을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 수포자가 찍은 번호 배열의 index를 가리키게 한 다음 같은지 비교를 했다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순회가 끝나면 people 객체 배열를 선언해줬다. 프로퍼티는 name과 count로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;name에는 1,2,3이, count에는 아까 세어준 count들을 넣어줬다.&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;people 객체를 count 프로퍼티의 오름차순으로 정렬해주고, people 객체 배열의 맨 마지막 객체의 count를 max로 지정해주었다.&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;그리고 마지막으로 people을 순회하며 count와 max가 같은 객체의 name을 answer 배열에 push하고 return 해주었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 코드&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;function solution(answers) {
    const one_array = [1,2,3,4,5];
    const two_array = [2,1,2,3,2,4,2,5];
    const three_array = [3,3,1,1,2,2,4,4,5,5];
    
    let one_count = 0;
    let two_count = 0;
    let three_count = 0;
    
    answers.forEach((e, i) =&amp;gt; {
        if (e === one_array[i % 5]) {
            one_count++;
        }
        if(e === two_array[i % 8]) {
            two_count++;
        }
        if(e === three_array[i % 10]) {
            three_count++;
        }
    })
    
    const people = [
        {name: 1, count: one_count},
        {name: 2, count: two_count},
        {name: 3, count: three_count},
    ]
    
    const sortedPeople = people.sort((a, b) =&amp;gt; a.count - b.count);
    const max = people[people.length -1].count;
        
    let answer = [];

    
    people.forEach((e) =&amp;gt; {
        if(e.count === max) {
            answer.push(e.name);
        }
    })
        
    return answer;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/Programmers(JavaScript)</category>
      <category>모의고사</category>
      <category>자바스크립트</category>
      <category>프로그래머스</category>
      <category>프로그래머스 자바스크립트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/159</guid>
      <comments>https://quickchabun.tistory.com/159#entry159comment</comments>
      <pubDate>Sat, 11 Jan 2025 18:28:23 +0900</pubDate>
    </item>
    <item>
      <title>[코딩테스트] Javascript로 코딩테스트 보기 전 봐야 할 핵심로직 정리</title>
      <link>https://quickchabun.tistory.com/158</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;큰돌의터전&amp;rsquo;님의 &amp;lsquo;자바스크립트로 코딩테스트 보기전 봐야할 핵심로직 12가지&amp;rsquo; 유튜브 영상을 보고 정리한 글입니다. 자세한 내용은 해당 유튜브 영상에 나와있으니 참고 부탁드립니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유튜브 링크: &lt;a href=&quot;https://www.youtube.com/watch?v=MlvZ2IufTFI&quot;&gt;https://www.youtube.com/watch?v=MlvZ2IufTFI&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=MlvZ2IufTFI&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/cvtar1/hyX0pzXVAo/1jbv50CZ6XuSufMddobG01/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720,https://scrap.kakaocdn.net/dn/BhZCb/hyX0uH3L1U/5R34JYkTMxMXYljE5Z2Q80/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=0_0_1280_720&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;자바스크립트로 코딩테스트 보기전 봐야할 핵심로직 12가지&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/MlvZ2IufTFI&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 배열 순회&lt;/h2&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;a = [1, 3, 45, 2, 10]

a.forEach((e, i) =&amp;gt; {
	console.log(e,i)
})

// 결과
// element와 index가 출력된다.
// 1 0
// 3 1
// 45 2
// 2 3
// 10 4
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 문자열 분할&lt;/h2&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;const str = &quot;Hello World&quot;;
const ret = str.split(&quot; &quot;)
console.log(ret)

// 결과 (공백을 기준으로 분할)
// [ 'Hello', 'World' ]

const a = ret.join('')
console.log(a)
// 결과 (배열을 문자열로 만들어줌)
// HelloWorld

// 배열을 합칠 때 사이에 무언가를 넣고싶다면
// join에 입력하면 그 사이에 들어간다
// 저 위에는 ''를 넣었으므로 아무것도 안들어갔다.

&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 정렬&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;let numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 4, 5]
numbers = numbers.sort((a, b) =&amp;gt; a - b)
console.log(numbers)

// 내림차순을 하고 싶으면 아래와 같이 바꾸자
// numbers = numbers.sort((a, b) =&amp;gt; (a - b) * -1)

&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 배열에서 특정 조건을 만족하는 요소만 꺼내고 싶으면&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const numbers = [1,2,3,4,5,6];
const ret = numbers.filter(e =&amp;gt; e % 2 == 0 )
console.log(ret)

// 결과 : 짝수만 필터링 배열 반환
// [2, 4, 6]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 배열의 요소들에 2를 곱해서 반환하고 싶다면&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const numbers = [1, 2, 3, 4, 5];
const ret = numbers.map(e =&amp;gt; e * 2)
console.log(ret)

// 결과
// [2, 4, 6, 8, 10[
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 배열의 요소들의 합을 알고 싶다면&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const numbers = [1, 2, 3, 4, 5];
const ret = numebrs.reduce((total, e) =&amp;gt; total + e, 0)
console.log(ret)

// 결과: 15
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. DFS를 구현하고 싶다면&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const graph = {
	1: [2, 3],
	2: [4],
	3: [4, 5],
	4: []
	5: []
};

const dfs = (here, visited = new Set()) =&amp;gt; {
	if(visited.has(here)) return
	visited.add(here)
	console.log(here)
	graph[here].forEach(e =&amp;gt; dfs(e, visited))
}

dfs(1);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 이분탐색을 하고 싶다면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬된 배열에서 O(log N)만에 탐색 가능&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const a = [1, 2, 3, 4, 5, 6, 7, 8]

const bs = () =&amp;gt; {
	let lo = 0;
	let hi = a.length - 1;
	const target = 3;
	
	while(lo &amp;lt;= hi) {
		let mid = Math.floor((lo + hi) / 2)
		
		if(a[mid] === target) {
			return &quot;찾았다!!&quot;
		} else if (a[mid] &amp;gt; target) {
				hi = mid - 1
		} else {
				lo = mid + 1
		}
	}
	
	return -1;
}

const ret = bs()
console.log(ret)

// 결과: 찾았다!!
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 자바스크립트로 배열을 생성할 때&lt;/h2&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;// 50개짜리 0이 있는 배열을 생성하고 싶을 때
let a = Array(50).fill(0)
console.log(a)

// 결과
// [
//		0, 0, 0, 0, 0, 0, .....
//		....
//		....         , 0
//  ]

// 1이 있는 5x5 2차원 배열을 만들고 싶을 때
let b = Array(5).fill().map(e =&amp;gt; Array(5).fill(1))
console.log(b)

// 결과
// [
//   [ 1, 1, 1, 1, 1 ],
//   [ 1, 1, 1, 1, 1 ],
//   [ 1, 1, 1, 1, 1 ],
//   [ 1, 1, 1, 1, 1 ],
//   [ 1, 1, 1, 1, 1 ]
// ]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 피보나치 배열을 DP를 사용해서 구현해보자면&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const fibo = (idx, memo = {} ) =&amp;gt; {
	if (idx &amp;lt;= 2) return 1
	if (idx in memo) return memo[idx]
	memo[idx] = fibo(idx - 1, memo) + fibo(idx - 2, memo)
	return memo[idx]
}
const ret = fibo(10)
console.log(ret)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;memo는 계산한 결과를 저장해두는 객체 (처음에는 비어있음)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 배열의 요소들을 swap하고 싶다면&lt;/h2&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const arr = [1, 2, 3, 4, 5];
[arr[1], arr[3]] = [arr[3], arr[1]]
console.log(arr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조 분해 할당을 활용하여 간단하게 배열 내 요소들을 swap할 수 있다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>자바스크립트</category>
      <category>자바스크립트 코딩테스트</category>
      <category>코딩테스트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/158</guid>
      <comments>https://quickchabun.tistory.com/158#entry158comment</comments>
      <pubDate>Sat, 11 Jan 2025 17:20:18 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 자바스크립트] &amp;lsquo;올바른 괄호&amp;rsquo; 풀어보기</title>
      <link>https://quickchabun.tistory.com/157</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;문제 링크: &lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/12909&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/12909&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 문제를 보고 든 생각&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;(&amp;rsquo;이 먼저 나오고, 그리고 나중에 &amp;lsquo;)&amp;rsquo;으로 이 괄호가 닫혀야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 알 수 있는 사실은&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&amp;lsquo;)&amp;rsquo;이 먼저 나오면 안된다.&lt;/li&gt;
&lt;li&gt;&amp;lsquo;(&amp;rsquo;의 개수와 &amp;lsquo;)&amp;rsquo;의 개수는 같아야한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 0으로 시작하는 변수를 두고, &amp;lsquo;(&amp;rsquo;이 나오면 1을 더해주고, &amp;lsquo;)&amp;rsquo;이 나오면 1을 빼주면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;)&amp;rsquo;이 먼저 나오면 안된다고 했다. &amp;rarr; 이 경우는 해당 변수가 음수가 되는 경우이므로 음수가 되면 바로 false를 return 해주면 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 풀이&lt;/h2&gt;
&lt;pre class=&quot;axapta&quot;&gt;&lt;code&gt;function solution(s){
    
    
    let count = 0;
    
    for(let index = 0; index &amp;lt; s.length; index++) {
        if(s[index] === '(') {
            count++;
        }
        else if (s[index] === ')') {
            count--;
        }
        
        if(count &amp;lt; 0) {
            return false;
        }
        
    }
    
    if(count === 0) {
        return true;
    }
    else {
        return false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 든 생각대로 코드를 작성했더니 정답이었다!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;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;나는 count라는 변수를 활용해서 문제를 풀었지만, 스택을 활용해서 문제를 푸는게 정석인 것 같다.&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;pre class=&quot;cpp&quot;&gt;&lt;code&gt;function solution(s) {
    const stack = [];
    
    for(let char of s) {
        if(char === '(') {
            stack.push(char);
        } else if(char === ')') {
            if(stack.length === 0) return false;
            stack.pop();
        }
    }
    
    return stack.length === 0;
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/Programmers(JavaScript)</category>
      <category>올바른 괄호</category>
      <category>프로그래머스</category>
      <category>프로그래머스 자바스크립트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/157</guid>
      <comments>https://quickchabun.tistory.com/157#entry157comment</comments>
      <pubDate>Tue, 7 Jan 2025 23:02:55 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 자바스크립트] &amp;lsquo;입국심사&amp;rsquo; 풀어보기</title>
      <link>https://quickchabun.tistory.com/156</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래머스의 코딩테스트 문제 &amp;rarr; 알고리즘 고득점 Kit &amp;rarr; 이분탐색 카테고리에서 풀었다.&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;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/43238&lt;/a&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;lsquo;&lt;b&gt;모든 사람이 심사를 받는데 걸리는 시간의 최솟값&lt;/b&gt;&amp;rsquo;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n은 입국심사를 기다리는 사람 수이며, 배열 times에는 각 심사관이 한 명을 심사하는데 걸리는 시간이 담겨있다.&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;lsquo;모든 사람이 심사를 받는데 걸리는 시간의 최솟값&amp;rsquo;을 안다면(answer라고 하자),&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심사관 A가 한 명을 심사하는데 걸리는 시간을 answer로 나눈 값 = &lt;b&gt;A가 심사한 사람의 수이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, times를 순회하며 심사관들이 심사한 사람들의 수의 합이 n과 같은지 찾아보면 된다.&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;일단 times 배열을 오름차순으로 정렬한다. (const sortedTimes = times.sort((a, b) =&amp;gt; a - b); 을 사용한다)&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;left = 1, right = sortedTimes의 가장 오른쪽 값(가장 큰 값)으로 놓는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, right는 가장 느리게 심사받을 때의 시간이다.&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;그리고 이분탐색을 진행한다. (left가 right보다 작거나 같을 동안 무한반복)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mid = Math.floor((left + right) / 2)로 구하고, sortedTimes를 순회하면서 각 요소들을 mid로 나누어 사람들의 수를 구한다. 그리고 그&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;사람들의 수를 변수 people에 더한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;people이 n보다 크거나 같다면(n보다 많거나 같은 수의 사람들을 심사했다면) &amp;rarr; right = mid - 1로 설정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;people이 n보다 작다면(n보다 더 적은 사람들을 심사했다면) &amp;rarr; left = mid + 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;이렇게 범위를 좁혀나가다가, right보다 left보다 작게 되어 while문을 벗어나면 n명을 심사할 때의 최소 시간인 left를 return한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(부연설명을 하자면, while문이 끝나는 시점에서 left는 &quot;n명을 심사할 수 있는 가장 작은 시간&quot;을 가리킴)&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;n=6, times=[7,10]인 경우를 가정한다면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;left=1, right=60, mid=30&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;심사=6명 (30/7=4, 30/10=3)&lt;/li&gt;
&lt;li&gt;right=29&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;left=1, right=29, mid=15&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;심사=3명 (15/7=2, 15/10=1)&lt;/li&gt;
&lt;li&gt;left=16&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;left=16, right=29, mid=22&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;심사=6명 (22/7=3, 22/10=2)&lt;/li&gt;
&lt;li&gt;right=21&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;left=16, right=21, mid=18&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;심사=4명 (18/7=2, 18/10=1)&lt;/li&gt;
&lt;li&gt;left=19&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;left=19, right=21, mid=20&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;심사=5명 (20/7=2, 20/10=2)&lt;/li&gt;
&lt;li&gt;left=21&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;left=21, right=21, mid=21&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;심사=5명 (21/7=3, 21/10=2)&lt;/li&gt;
&lt;li&gt;left=22&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;left=22, right=21&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;while문 종료&lt;/li&gt;
&lt;li&gt;left=22 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;22분이 6명을 심사할 수 있는 최소시간이다.&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;(여기서 n이 people과 같은 값일 때에도 계속 루프가 진행되어야할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 최소값을 찾기 위해서. 현재 mid로 n명을 심사할 수 있다고 해도, 더 작은 시간으로도 n명을 심사할 수 있을 가능성이 있기 때문에 right를 줄여가며 계속 탐색해야 한다.)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;코드&lt;/h2&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;function solution(n, times) {
    const timesLength = times.length;
        
    const sortedTimes = times.sort((a, b) =&amp;gt; a - b);

    // 이분탐색을 통해 모든 사람이 심사를 받는데 걸리는 최소한의 시간을 찾자
    // left, right 초기값 설정
    // 여기서 right는 가장 오래걸리는 시간
    
    let left = 1;
    let right = n * sortedTimes[timesLength - 1];
    let mid = Math.floor((left + right) / 2);

    
    // left가 right보다 작거나 같을 동안에는 무한반복
    while(left &amp;lt;= right) {
        mid = Math.floor((left + right) / 2);

        
        // 사람의 수 people 선언
        let people = 0;
        
        // sortedTimes을 순회하면서 mid에 sortedTimes의 요소를 나눈 값을 people에 더해준다
        // mid에 sortedTimes의 요소를 나눈 값 -&amp;gt; mid 동안 해당 time이 심사한 사람들의 수
        for(let time = 0; time &amp;lt; timesLength; time++) {
            people += Math.floor(mid / sortedTimes[time]); 
        }
        
        // 만약 같거나 많은 사람들을 심사했다면
        if(people &amp;gt;= n) {
            right = mid - 1;
            
        }
        // 만약 더 적은 사람들을 심사했다면
        else {
            left = mid + 1;
        }
    }
    
    return left;
    

}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/Programmers(JavaScript)</category>
      <category>이분탐색</category>
      <category>입국심사</category>
      <category>프로그래머스</category>
      <category>프로그래머스 자바스크립트</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/156</guid>
      <comments>https://quickchabun.tistory.com/156#entry156comment</comments>
      <pubDate>Tue, 31 Dec 2024 02:00:16 +0900</pubDate>
    </item>
    <item>
      <title>[프롬프트 엔지니어링] 함수 호출, 프롬프트 평가, LLM 보안, Autonomous Agent에 대하여</title>
      <link>https://quickchabun.tistory.com/155</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;김진중(골빈해커)님의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'최고의 프롬프트 엔지니어링 강의'&lt;/b&gt;를 읽고 정리한 글입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;함수 호출&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;호출할 수 있는 함수(기능)를 미리 설정해두면 사용자의 필요에 따라 해당 함수 이름을 호출하여 요청에 응답하는 기능&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 프로그램 내의 함수를 직접 호출하는 것이 아니라 어떤 함수와 파라미터가 필요한지를 JSON 형식으로 응답해줌&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;{
	&quot;name&quot;: &quot;get_current_weather&quot;,
	&quot;description&quot;: &quot;주어진 위치의 현재 날씨를 가져옵니다.&quot;,
	
	&quot;parameters&quot;: {
		&quot;type&quot;: &quot;object&quot;,
		&quot;properties&quot;: {
			&quot;location&quot;: {
				&quot;type&quot;: &quot;string&quot;,
				&quot;description&quot;: &quot;도시 또는 지역, 예) 서울&quot;,
			},
			&quot;unit&quot;: {&quot;type&quot;: &quot;string&quot;, &quot;enum&quot;: [&quot;celsius&quot;, &quot;fahrenheit&quot;]},
		},
		&quot;required&quot;: [&quot;location&quot;],
	},
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프롬프트: 날씨 알려줘&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;1c&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;{
	'role': 'assistant',
	'content': '어느 도시나 지역의 날씨를 알고 싶으세요?'
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프롬프트: 서울 날씨 알려줘.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;결과&lt;/p&gt;
&lt;pre class=&quot;rust&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;{
	'role': 'assistant',
	'content': None,
	'function_call': {
		'name': 'get_current_weather',
		'arguments':
			'{\\n &quot;location&quot;: &quot;Seoul, Korea&quot;,\\n
			&quot;format&quot;: &quot;celsius&quot;\\n}'
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;함수 호출 사용 시 주의사항&lt;/h3&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프롬프트 인젝션에 취약할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 사용자의 아이디나 비밀번호 등 민감한 정보는 파라미터 설정에 넣지 않는 것이 좋음&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;호출 요류가 생길 수도 있다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 반환되는 값을 항상 확인 후 실행해야&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;프롬프트 평가와 시스템&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;답변을 위해 필요한 적절한 컨텍스트 정의 &amp;larr; 컨텍스트 데이터&lt;/li&gt;
&lt;li&gt;원하는 결과 추출을 위한 프롬프트 작성 &amp;larr; 인스트럭션 or 사용자 입력 데이터&lt;/li&gt;
&lt;li&gt;결과물의 형식을 지정 &amp;larr; 출력 데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;대부분의 LLM 애플리케이션에는 다컨텍스트, 사용자의 요청, 컨텍스트와 사용자의 요청에 따라 만들어지는 출력 샘플의 세 가지 데이터를 먼저 정리해 놓으면 프롬프트 명세가 완성&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;머신러닝 데이터 분류&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;훈련/학습 데이터 60% (학습용 데이터)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;검증 데이터 20% (어떤 모델이 가장 좋은가?)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;평가 데이터 20% (이 모델이 실제로 얼마나 좋은가?)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;프롬프트 애플리케이션은 이미 학습이 끝난 LLM 모델 위에서 돌아가는 것이기 때문에 성능 평가를 위한 데이터만 필요&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;프롬프트 버전 관리&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;/li&gt;
&lt;li&gt;LLM 모델이 변경되어 프롬프트 재탐색이 필요한 경우&lt;/li&gt;
&lt;li&gt;변경 사항 추적이 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;/li&gt;
&lt;li&gt;출력 내용이나 구성이 많이 변경되는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; 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;/li&gt;
&lt;li&gt;생성 옵션이 변경되는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;생성 결과 평가하기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;질문한 대로 정확하게 값을 출력하는지 여부&lt;/li&gt;
&lt;li&gt;예시 데이터와 생성 결과의 임베딩 유사도 평가&lt;/li&gt;
&lt;li&gt;인간 평가&lt;/li&gt;
&lt;li&gt;생성 모델로 평가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;LLM 보안&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ChatGPT에 입력한 데이터를 OpenAI에서 학습 데이터로 사용하지 않는다. (약관에 명시)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;ChatGPT 서비스에서도 자신의 데이터를 학습 데이터로 사용하지 않도록 옵트 아웃 시킬 수 있다. (사용자가 거부 가능)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;마스킹&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;개인 정보 등의 민감한 정보를 비식별 단어로 치환하여 보안성을 높이는 방법&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자 입력과 DB에서 가져온 정보에 개인 정보가 있는지를 내부적으로 판별&lt;/li&gt;
&lt;li&gt;개인 정보가 있다면 해당 정보를 제거하고 그 자리에 다른 텍스트를 끼워 넣는 방식&lt;/li&gt;
&lt;li&gt;마스킹한 정보를 기반으로 프롬프트를 합성한 후 GPT API를 호출해 프롬프트를 생성&lt;/li&gt;
&lt;li&gt;생성한 프롬프트로 생성 결과를 받아오면 여기에 개인 정보를 주입&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;LLM의 취약점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프롬프트 인젝션: 정교하게 제작된 프롬프트를 사용하여 필터를 우회하거나 LLM을 임의로 조작해 이전 지시 사항을 무시하거나 의도하지 않은 행동을 수행하게 만드는 경우&lt;/li&gt;
&lt;li&gt;데이터 유출: LLM의 응답을 통해 민감한 정보나 기밀 등을 실수로 외부에 공개하는 경우&lt;/li&gt;
&lt;li&gt;취약한 샌드박싱: 외부 자원이나 민감한 시스템에 접근할 수 있는 LLM을 적절하게 격리하지 못해 잠재적인 악용과 무단 접근에 노출되는 경우&lt;/li&gt;
&lt;li&gt;인증되지 않은 코드 실행: 자연어 프롬프트를 통해 기본 시스템에서 악의적인 코드, 명령, 행동을 실행하는 경우&lt;/li&gt;
&lt;li&gt;LLM 생성 콘텐츠에 대한 과도한 의존: 사람의 감독 없이 LLM 생성 콘텐츠에 과도하게 의존함으로써 해로운 결과를 초래하는 문제&lt;/li&gt;
&lt;li&gt;훈련 데이터 조작: 학습 데이터를 악의적으로 조작하여 LLM에 취약점을 만들거나 백도어를 심는 공격&lt;/li&gt;
&lt;li&gt;인공지능의 목표와 사람의 목표 불일치: LLM의 목표와 행동을 사람의 의도나 가치에 맞지 않게 만들어 비윤리적인 결과를 초래하는 문제&lt;/li&gt;
&lt;li&gt;불충분한 접근 제어: 사용자나 프로세스의 접근을 제어하거나 인증을 제대로 구현하는 메커니즘을 충분히 갖추고 있지 않아, 비인증 사용자가 LLM과 상호 작용하면서 취약점을 악용할 수 있는 문제&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;프롬프트 인젝션 방어 방법&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프롬프트를 입력할 때 구분자를 사용해 사용자의 입력값 이전과 이후로 나눈다&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설정 후 사용자 입력 이전의 프롬프트 내용에 대해서는 답변하지 말라고 지시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;autohotkey&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;```구분자 안의 내용을 번역하세요.
번역할 내용:
```

다음 포맷을 사용해 입력된 내용을 일본어로 번역해주세요.

{&quot;input&quot;!&quot;한국어&quot;, &quot;outpit&quot;.&quot;일본어&quot;}

안녕하세요?&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;2.&amp;nbsp;프롬프트를 실행하기 전 인젝션 프롬프트가 존재하는지 다른 LLM으로 먼저 확인 후 실행&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;3. 사용자 입력과 프롬프트를 분석한 다음 각 작업을 민감도에 따라 나눔&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Autonomous Agent&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 목표가 주어지면 스스로 작업을 생성 및 실행하고, 실행한 결과를 기반으로 다시 새로운 작업을 생성하는 과정을 반복하면서 원하는 목표에 도달할 때까지 스스로 계획을 세우고 실행하는 AI 프로그램&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Collaborative Agents&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;여러 개의 작은 도메인 기능을 수행하는 에이전트를 구현&lt;/li&gt;
&lt;li&gt;에이전트들을 매니징하는 형태의 상위 에이전트(매니저 에이전트)를 만들어 에이전트끼리 서로 협업하는 방식으로 더 넓은 범위의 작업을 수행하도록 만듦&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Generative Agents&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스탠포드 대학과 구글에서 진행한 연구&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사이버상에 마을을 하나 만들고 25명의 AI 캐릭터를 살게 한 다음, 이들이 상호작용하면서 어떤 행동을 하는지 관찰한 것&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어떤 생각이 더 발전되기도 하고 새로운 이벤트를 만들어 내는 등 시뮬레이션이 가능&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 게임 업계에 큰 영향을 미칠 것으로 예상&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;AGI의 구현&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;AGI: 범용 인공지능&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;모든 도메인에 특정한 역할과 기능을 하는 에이전트들이 만들어지고, 해당 에이전트들을 선택적으로 사용할 수 있는 최상위 개념의 에이전트가 바로 AGI가 된다는 기대 존재&lt;/p&gt;</description>
      <category>Study/프롬프트 엔지니어링</category>
      <category>autonomous agent</category>
      <category>llm 보안</category>
      <category>프롬프트 엔지니어링</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/155</guid>
      <comments>https://quickchabun.tistory.com/155#entry155comment</comments>
      <pubDate>Sun, 29 Dec 2024 16:34:33 +0900</pubDate>
    </item>
    <item>
      <title>[프롬프트 엔지니어링] 프롬프트 작성 도움, 환각 줄이기, 외부 지식 주입에 대하여</title>
      <link>https://quickchabun.tistory.com/154</link>
      <description>&lt;p style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;김진중(골빈해커)님의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'최고의 프롬프트 엔지니어링 강의'&lt;/b&gt;를 읽고 정리한 글입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모델을 선택할 때 고려해야 할 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성능이 높은 모델일수록 속도는 떨어진다. 즉, 성능이 낮은 모델일수록 속도는 빨라짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;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;&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;h3 data-ke-size=&quot;size23&quot;&gt;성능(추론 능력)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 기관에서 제공하는 LLM 모델의 성능 순위 리더 보드를 참조해 성능 비교 가능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;벤치마크 점수가 높아도 실제 사례에서는 원하는 만큼 성능이 안나올 수 있으니 깊게 신뢰하지는 않는게 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;경향성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude 모델은 결과를 구어체로 생성하는 경향이 있고, GPT-4 모델은 격식을 갖춘 어느 정도 구조화된 리포트 형태로 생성하는 경향이 있다.&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;생성 결과의 안전성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM API를 사용한다면 API를 제공하는 회사가 폭력적이거나 비윤리적인 답변을 못하게 막는 장치를 제공하는지 확인할 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; OpenAI나 MS Azure는 Moderations API를 제공&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;API 서버 안전성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 모델이 업데이트되는 초반에는 MS나 구글도 안전성이 급격히 떨어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM API를 사용하면 기본적으로 안정성이 낮을 것을 염두에 두고 문제가 생겼을 때는 여러 번 다시 시도하거나 에러 상황 발생에 잘 대비하는 것이 좋다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프롬프트 작성 도움받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 도움을 받으면 더 좋은 프롬프트를 만들 수 있다.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 예시&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;유튜브나 블로그의 제목을 다듬는 프롬프트를 만들려고 해. 제목은 다음과 같이 개선을 하는 것이 목적이야.
- --
원문: 스벨트가... 리액트보다 좋다는 놈들이 있는데
개선: 스벨트가 리액트보다 좋다는 사람이 있는데

원문: 웹 포트폴리오에 간지나게 3D 모델을 추가해보자
개선: 웹 포트폴리오에 멋지게 3D 모델을 추가해보자

원문: 제대로 배우는 프롬프트 엔지니어링
개선: 제대로 배우는 프롬프트 엔지니어링
- --
원문을 개선한 제목으로 바꾸는 프롬프트를 만들어줘. 예시의 개선을 모두 할 수 있는 하나의 프롬프트를 만들어줘.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프롬프트 평가 및 개선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 프롬프트가 태스크를 잘 수행할 수 있는지 LLM이 잘 이해하고 명령을 수행할 수 있는지를 평가하고 개선해달라고 이야기하자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 작성한 프롬프트 초안을 그대로 사용하고, 원하는 목적에 맞는 프롬프트인지를 확인하고 개선해 달라는 요청을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;- --
이 작업을 위해 다음의 프롬프트를 사용하려고 해. 다음의 프롬프트가 목적에 잘 맞는지, 의미는 명확한지,
GPT가 잘 이해하고 명령을 수행할 수 있을지 평가하고 개선해 줘.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프롬프트 다듬기&lt;/h3&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;rarr; 개선된 프롬프트 의 내용과 개선된 프롬프트로 실행한 결과를 확인한 후 개선 규칙을 계속 추가하거나 수정하면서 의도에 맞는 충분한 품질의 결과가 나올 때까지 반복&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPT가 보다 정확한 답변을 제공할 수 있도록 주제나 지시를 명확하게 세분화하고 구체적으로 작성하세요.&lt;/li&gt;
&lt;li&gt;GPT가 이해하기 쉬운 단어와 문장 구조를 사용해 주세요. 복잡한 어휘나 전문 용어의 사용은 가능한 한 줄여주세요.&lt;/li&gt;
&lt;li&gt;GPT가 당신의 의도를 정확하게 파악할 수 있도록 간단하고 명확한 언어를 사용하여 작성하세요.&lt;/li&gt;
&lt;li&gt;객관적이고 균형 잡힌 답변을 얻기 위해 지시를 중립적이고 구체적인 방식으로 제시하세요.&lt;/li&gt;
&lt;li&gt;충분한 문맥과 명확성을 제공할 만큼의 길이로 프롬프트를 작성하세요. 이전 정보가 다음 질문의 맥락에 중요한 경우 이를 명확하게 전달하세요.&lt;/li&gt;
&lt;li&gt;혼란이나 집중력의 손실을 방지하기 위해 간결하게 작성하세요.&lt;/li&gt;
&lt;li&gt;감정적인 측면보다 결과에 초점을 맞추어 사실과 결과에 기반한 지시를 하세요.&lt;/li&gt;
&lt;li&gt;GPT가 보다 정확하고 유용한 답변을 제공하기 위해 주관적인 질문을 피하고 보편적인 질문으로 재구성하십시오.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프롬프트 번역하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어에 비해 한국어 성능이 비교적 떨어지고 토큰 수도 상대적으로 많기 때문에 가능하면 영어로 작성하는 편이 이득&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;다음은 GPT에게 지시할 프롬프트입니다. 프롬프트를 영어로 번역해서 작성해주세요.
프롬프트

&quot;
내용
&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트를 영어로 작성하되, 고유명사나 한국어로 명시하는 것이 좋은 부분만 한국어로 작성하면 영어 혹은 한국어만으로 작성하는 것보다 좋은 결과를 얻을 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;환각 줄이기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에서 환각은 모델이 생성한 텍스트가 사실과 다른 내용이나 주어진 정보에 없는 내용, 즉 실제로는 존재하지 않는 정보를 생성하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; LLM은 어떻게든 적절한 답변을 생성하려고 노력하기 때문에 잠꼬대처럼 상상한 답변도 내놓을 수 있다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제공한 정보를 바탕으로 답변하도록 명시적으로 지시&lt;/li&gt;
&lt;li&gt;주어진 문서에서 질문의 답과 관련한 내용을 인용하도록 지시. 이때 문서 요약과 함께 제공하면 조금 더 정확한 결과를 얻을 수 있음&lt;/li&gt;
&lt;li&gt;모른다는 답을 허용하도록 명시&lt;/li&gt;
&lt;li&gt;프롬프트를 출력할 때 사고와 답변을 분리하여 스스로 생각하게 만듦&lt;/li&gt;
&lt;li&gt;각각 다른 방법으로 여러 개의 출력을 생성한 후 각각의 답변이 일관성이 있는지 답변하도록 지시 (Self-Consistency)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;외부 지식 주입하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그라운딩과 RAG&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그라운딩: LLM에게 검색 등을 통해 문맥에 더 적합한 정보를 제공하거나 계산기 등을 사용해 정확한 계산 결과를 제공하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 환각 현상을 줄이고, 모델 학습에 사용한 데이터 이후에 나오는 최신 정보를 이용한 결과도 생성 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG(Retrieval-Augmented Generation, 검색 증강 생성): 정보를 검색한 결과를 기반으로 텍스트를 생성하는 방법&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;임베딩 모델 선택하기&lt;/h3&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 어떤 모델을 선택할지 고민할 때는 RAG를 고려하여 Retrieval 성능을 확인하는 것이 좋음&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;Retrieval: 모델이 필요한 정보를 검색 엔진이나 데이터베이스로부터 효율적으로 찾아내는 과정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 텍스트 조각들을 벡터화한 대량의 자료 뭉치에서 답변에 필요한 정보를 정확히 잘 찾아낼 수 있는지에 대한 성능&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;&amp;rarr; 실제 사용할 데이터로 사전 테스트를 거친 후 가장 적합한 모델을 선택해야&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;벡터 DB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 서치를 본격적으로 사용하려면 벡터 DB를 사용하는 것이 일반적&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;벡터 서치를 위해 사용하는 기본적인 라이브러리들은 벡터 DB에서 찾아온 벡터 값만 보여줄 뿐 그 벡터가 어떤 텍스트와 관련된 것인지는 알려주지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 벡터 DB는 벡터 서치를 통해 가장 가까운 벡터를 찾은 다음 그것이 어떤 문서의 벡터인지를 알 수 있도록 문서 제목이나 내용 등의 메타 정보를 함께 반환&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;벡터 DB가 제공하는 편의 기능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메타 데이터와 함께 결과 반환&lt;/li&gt;
&lt;li&gt;키워드 필터링 등을 이용한 하이브리드 검색&lt;/li&gt;
&lt;li&gt;실시간 인덱싱을 통한 대규모 벡터 정보 검색&lt;/li&gt;
&lt;li&gt;다양한 인덱싱 방법 및 검색 알고리즘 제공&lt;/li&gt;
&lt;li&gt;높은 확장성 및 개발자 편의 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 DB로 유명한 서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파인콘을 제외하고는 전부 오픈 소스이지만 파인콘이 가장 기능이 다양하고 개발자 경험이 좋아 많이 사용 중&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(크로마도 인기가 높아지고 있음)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pinecone, Chroma, Milvus, Redis, Weaviate, Elasticsearch, Qdrant, PostgreSQL&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;벡터 DB가 LLM 애플리케이션의 핵심 컴포넌트로 떠오르면서 거의 모든 데이터베이스가 벡터 서치를 지원하기 시작해 점점 더 선택권이 넓어질 것으로 예상&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;간단한 서비스라면 Chroma를, 본격적인 서비스라면 레퍼런스가 풍부한 Pinecone을, 온프라미스로 직접 구축하고 싶다면 Qdrant(쿼드란트라고 읽음)를 추천&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;벡터 서치가 계속해서 좋아지고 있지만, 성능이 잘 따라오지 못하는 경우가 종종 있음 &amp;rarr; 하이브리드 서치 사용&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하이브리드 서치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하이브리드 서치: 검색 정확도를 높이기 위해 키워드 필터링이나 Dense 벡터, Sparse 벡터 등을 조합해 검색하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dense 벡터: 대부분의 요소가 0이 아닌 값을 가지는 베터 (&amp;rsquo;밀집&amp;rsquo;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sparse 벡터: 대부분의 요소가 0이고 소수의 비-0 값만을 가지는 벡터 (&amp;rsquo;희소&amp;rsquo;)&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리랭크&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차로 벡터 서치나 키워드 서치 등에서 가져온 결과에서 한 번 더 필터링을 거치는 방법&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;rarr; 다른 임베딩 모델을 사용해 순서를 재정렬하거나, 검색해 온 문서들을 프롬프트에 넣은 다음 이 중에서 사용자가 요청한 내용과 가장 유사한 것을 일부 선택해 재정렬하라고 하는 것&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;rarr; 비용을 크게 늘리지 않으면서 답변의 정확성을 높일 수 있음&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;청킹&lt;/h3&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;rarr; 텍스트를 여러 개의 작은 부분으로 나누고 각 부분을 독립적으로 임베딩할 수 있게 하면 텍스트의 특징을 더욱 잘 표현하면서 더욱 정교한 검색이나 분석이 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오버랩: 텍스트를 나눌 때 각 청크가 일부 공통된 데이터를 포함하도록 하는 기법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬라이딩: 텍스트를 일정한 길이의 작은 단어 조각으로 순차적으로 이동하며 데이터의 청크를 캡처하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬라이딩 윈도우: 슬라이드할 토큰의 길이&lt;/p&gt;</description>
      <category>Study/프롬프트 엔지니어링</category>
      <category>외부 지식</category>
      <category>프롬프트 엔지니어링</category>
      <category>환각</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/154</guid>
      <comments>https://quickchabun.tistory.com/154#entry154comment</comments>
      <pubDate>Sun, 29 Dec 2024 16:29:47 +0900</pubDate>
    </item>
    <item>
      <title>전혀 예상치 못했던, 우아한테크코스 7기 최종 코딩테스트 후기</title>
      <link>https://quickchabun.tistory.com/153</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 월요일 오후 3시, 메일 한 통이 도착했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1시 반에 시작했던 수업이 끝나고, 친구가 아직 밥을 안 먹었다고 해서 같이 학생회관에 있는 식당으로 갔다. 친구가 밥을 먹는 사이에 나는 무심코 핸드폰을 들여다보고 있었다. 그러다가 오후 3시, 정확히는 2시 59분 즈음에 메일이 도착했다는 알림이 도착했다. &amp;lsquo;[우아한테크코스] 1차 심사 결과 안내&amp;rsquo;라는 제목이었다. 이제 결과가 도착했구나. 4주차 테스트를 다 통과하지 못했었기 때문에 합격을 할 것이라는 기대는 별로 갖지 않았다. 아쉽긴 했지만, 한 달 동안 열심히 몰입하고 성장해으니 그걸로 만족해야지 생각하고, 앞으로 무슨 활동을 해야할지 고민하고 있었다. 그래도 메일은 도착했으니 열어봐야지. 알림을 클릭해 메일로 들어가니 상세 내용이 보였다. 맨 첫 줄에 다음과 같은 글이 쓰여있었다.&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;i&gt;&lt;b&gt;&amp;lsquo;*이 메일을 받는 분들은 최종 코딩 테스트 대상자입니다.&amp;rsquo;&lt;/b&gt;&lt;/i&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;lsquo;내가 왜 합격이지?&amp;rsquo;라는 물음이 계속 내 머리 속을 맴돌았다. 이 질문에 제대로 된 답변을 할 수 없다는 사실을 깨닫고 마음을 진정시켰다. 그리고 이제 무엇을 해야할지 생각했다. 원래라면, 다음 주 월요일부터 시작하는 기말고사를 대비하기 위해 시험 공부를 계속 해야했다. 그리고 최종 코딩 테스트는 이번 주 토요일이다. 프리코스 때 그랬듯이, 시간을 잘 분배해서 내가 해야할 공부를 성실히 해나가면 된다. 잘 될지는 모르겠지만, 일단 해보는 수밖에는 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;우테코 메일 캡처.jpeg&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Oynu9/btsLoqE098T/KXNAxhWJnMMMlDfvfqzKXK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Oynu9/btsLoqE098T/KXNAxhWJnMMMlDfvfqzKXK/img.jpg&quot; data-alt=&quot;우아한테크코스 메일 캡처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Oynu9/btsLoqE098T/KXNAxhWJnMMMlDfvfqzKXK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOynu9%2FbtsLoqE098T%2FKXNAxhWJnMMMlDfvfqzKXK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;407&quot; height=&quot;334&quot; data-filename=&quot;우테코 메일 캡처.jpeg&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;우아한테크코스 메일 캡처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 얼마 남지 않은 시간과 잘 작동되지 않는 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급했듯이, 내가 프리코스를 통과할 것이라는 생각을 전혀 하지 않았었기에, 고개를 내려 발등을 보니 환하게 불이 타오르고 있었다. 저 불을 도대체 어떻게 꺼야할까. 일단 화요일에는 디스코드 커뮤니티에 접속해 다른 분이 작성한 프리코스 코드를 보면서 그대로 따라 작성했다. 나는 그냥 Model, View, Controller만 썼었는데 이분은 Service를 도입했었다. 나는 지금까지 Model에 메서드들을 적용했었는데, Service로 메서드들을 독립시키는 것이 훨씬 가독성이 좋아보였다. 이 외에도, 깔끔하게 작성된 코드들을 보며 앞으로 어떻게 코드를 작성하면 될 지 감을 익히는데 노력했다. 다른 분들의 코드에 비해 내 코드가 많이 부족했음을 깨달을 수 있었던 시간이었다.&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;그리고 그 이후로는 기출문제들을 풀었다. 먼저 6기 최종 코딩테스트 &amp;lsquo;oncall&amp;rsquo;을 풀어보았는데, 생성형 AI를 사용하지 않다보니 막히는 부분이 너무 많았다. 결국 목표했던 5시간 안에 프로젝트는 완성하지 못했고, 에러가 발생할 때마다 무엇이 문제였고 어떻게 고치면 되는지 그 방법을 찾느라 시간이 훅훅 지나갔다. 어떤 상황에서 어떻게 하면 될지 머리 속으로 생각이 떠오르기는 했지만 실제로 코드로 옮기는 작업이 원활하게 진행되지 못했다. AI를 쓰지 못하게 되면 급속도로 무능력해지는 상황이 별로 마음에 들지 않았다. 계속 생각하고, 코드를 작성하며 익숙해지는 수 밖에는 없는 노릇이었다. 그래도 계속 코드를 작성하다보니 감각이 손에 익는다는 느낌이 들었다. 어쩌면 나는 발전하고 있는 것일까. 기말고사 공부는 잠시 뒤로 한 채 계속 기출문제를 풀었고, 어느 새 최종 코딩테스트 당일이 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 멋진 캠퍼스에서 진행된 생애 첫 오프라인 코딩 테스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아침에 집에 있는 과일들로 배를 채우고, 지하철을 타고 선릉역으로 향했다. 선릉역에서 나와 조금 걷다보니 커다란 건물이 나왔고, 그 건물에서 엘리베이터를 탑승해 우아한테크코스 선릉 캠퍼스에 도착했다. 캠퍼스에 도착하니 코치분들(혹은 직원분들이라고 불러야하는지 애매하지만)께서 지원자들을 환영해주었다. 신분증 검사를 하고, 웹 프론트엔드 시험을 보는 강의실로 갔다. 꽤 일찍 도착했다고 생각했지만 강의실은 사람들로 가득 차 있었다. 이 분들이 다 프리코스를 통과하신 분들이구나. 대단한 분들이겠지 이 중 몇 명이 합격을 하는 것일까. 나는 그저 이 테스트에 참여할 수 있었다는 것에 감사할 뿐이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;우테코 코테 편집 사진.jpg&quot; data-origin-width=&quot;2493&quot; data-origin-height=&quot;3000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MYOOH/btsLmNhucrv/QdzhJ53KPwhznfHBh5CDn1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MYOOH/btsLmNhucrv/QdzhJ53KPwhznfHBh5CDn1/img.jpg&quot; data-alt=&quot;시험장에 도착해서 촬영한 사진&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MYOOH/btsLmNhucrv/QdzhJ53KPwhznfHBh5CDn1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMYOOH%2FbtsLmNhucrv%2FQdzhJ53KPwhznfHBh5CDn1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;505&quot; data-filename=&quot;우테코 코테 편집 사진.jpg&quot; data-origin-width=&quot;2493&quot; data-origin-height=&quot;3000&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;시험장에 도착해서 촬영한 사진&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 오프라인 코딩 테스트는 살면서 처음이라 조금 떨렸다. 시험 방식은 지금까지 진행된 프리코스와 별로 다르지 않을 것이고, 막상 시작하면 정신없이 문제를 푸는 데에만 집중할테니 긴장 할 필요가 없다고 스스로를 안심시켰다. 그리고 한 시가 되었다. 웹 페이지에 접속해 문제를 확인했다. 출석부 프로그램을 만들어야 하는구나. public에 있는 csv 파일에 있는 정보들을 활용해야한다고? csv 파싱은 해본 적이 없는데. 현재 날짜와 시간을 가져오려면 Datetimes의 now()를 활용하라고? 프리코스에는 저런 라이브러리를 본 적이 없었는데. 생소한 정보들의 연속이었다. 뭐 그래도 다른 사람들도 전부 생소할테니까. 문제를 천천히 읽으며 어떻게 프로젝트를 구성하면 될 지 설계를 했다. 그리고 개발 시작. 과연 5시간 안에 개발을 다 할 수 있을까.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 시험을 치는게 아니라 공부를 하는 기분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 csv 파일을 읽는 방법은 구글링을 하다보니 코드를 발견해서 어찌저찌 해결했다. 솔직히 여기서 한 시간 정도를 쓸 줄 알았는데 다행이었다. csv 파일을 읽어서 배열(attendanceArrayFromCSV)로 만들어주었다.&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;빈 배열을 하나 생성한다.&lt;/li&gt;
&lt;li&gt;attendanceArrayFromCSV를 순회하며 닉네임들을 빈 배열에 push했다.&lt;/li&gt;
&lt;li&gt;닉네임이 채워진 배열을 Set로 만들어 중복을 제거하고, 다시 배열(앞으로 newArray라고 부른다)로 만들어준다.&lt;/li&gt;
&lt;li&gt;newArray를 map 함수로 nickname property와 attendanceRecord(배열) property가 요소마다 존재하도록 매핑한다. &amp;rarr; namePropertyArray라고 부른다.&lt;/li&gt;
&lt;li&gt;namePropertyArray를 순회하면서, 또 그 안에서 attendanceArrayFromCSV를 순회하며 namePropertyArray의 닉네임과 attendanceArrayFromCSV의 닉네임이 같은지 체크한다.&lt;/li&gt;
&lt;li&gt;만약 닉네임이 같다면, attendanceArrayFromCSV의 datetime을 namePropertyArray의 attendanceRecord에 push하는데, 이 때 push되는 요소의 구조는&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;{
	datetime: arrayElement.datetime,
	status: &quot;출석&quot;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아까 언급했듯이 이렇게 만들면 되겠다는 생각을 떠올리는 데에는 시간이 오래걸리지는 않았지만 실제로 구현하는데 시간이 너무 많이 걸렸던 것 같다. 배열과 객체를 올바르게 접근하는 부분에서 조금 헤맸었다. 그리고 정작 models/Person.js를 만들었는데 해당 파일을 사용하지도 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3시가 되자 감독관분들이 모두 노트북을 덮고 10분 간 휴식을 취하자고 하셨다. 시험장 밖을 나와 캠퍼스를 둘러보았다. 책장에는 개발 관련 책들이 꽂혀있었고, 창문 바깥으로는 건물들이 즐비했다. 신발을 벗고 휴식을 하는 공간도 있었는데, 그 공간은 보자마자 왠지 모르게 편안한 기분이 들었다. 이런데서 공부를 한다면 얼마나 좋을까하는 생각이 들었다. 시험장에 도착하기 전 사온 초코바를 먹으며 허기를 달랬고, 10분이 지나 다시 책상 앞에 앉아 개발을 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;우테코 소파.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L9tBB/btsLm17zJfZ/TfhCKiB3Y8kGbzY9oGfF3K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L9tBB/btsLm17zJfZ/TfhCKiB3Y8kGbzY9oGfF3K/img.jpg&quot; data-alt=&quot;우테코 선릉 캠퍼스의 휴식 공간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L9tBB/btsLm17zJfZ/TfhCKiB3Y8kGbzY9oGfF3K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL9tBB%2FbtsLm17zJfZ%2FTfhCKiB3Y8kGbzY9oGfF3K%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;314&quot; height=&quot;419&quot; data-filename=&quot;우테코 소파.JPG&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;우테코 선릉 캠퍼스의 휴식 공간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;그리고 그 뒤로는, 계속 시행착오를 반복하며 프로젝트를 완성하기 위해 노력하는 과정이었다. 개발을 다 구현하는데에도 실패했고, 이상하게도 &amp;lsquo;npm test&amp;rsquo;는 잘 작동되지 않고 오류가 발생했다. 시간이 6시에 가까워지며 마음 속으로는 구현을 다 못하겠다는 생각이 점점 커져갔다. 옛날이라면 많이 아쉬워했을 것 같은데, 솔직히 너무 정신이 없어서 그런 감정을 느낄 시간도 많지 않았다.&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;어느덧 6시가 되었다. 테스트는 잘 동작하지 않았고, 창문 밖을 바라보니 어느 덧 어두컴컴해져있었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 좋은 경험이었던 순간들&lt;/h2&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;군대를 전역하고, 학교를 다니다가 우연히 친한 형이 우아한테크코스에 있었다는 사실을 알게 되었고, 그 때부터 우아한테크코스에 관심을 갖게 되었다. 언젠가는 꼭 우아한테크코스에 지원을 해봐야겠다고 다짐을 했었다. 그리고 시작한 프리코스. 이전까지는 React에서 뷰를 만들고 라이브러리를 사용하는 방법을 익히는데에만 열중했었다. 그러나 이번 프리코스를 통해 순수한 자바스크립트로 코딩을 하며, 무작정 프로그램이 작동하는 것에만 집중하지 않고 어떻게 하면 가독성있는, 깔끔한 코드를 생성할 수 있는지 익힐 수 있게 되어 좋았다.&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;특히 최종 코딩테스트를 준비하는 과정, 또 코딩테스트를 보는 과정에서 AI 없이 코딩을 하는 연습을 하게 되어 실력이 꽤 향상한 것 같다. 이 기세를 타서 이제 종강도 했으니 꾸준히 자바스크립트로 알고리즘 문제를 풀며 계속 코딩 실력을 쌓아 나가려 한다. AI는 계속 발전하겠지만 기초적인 코딩 실력은 앞으로도 계속 개발자에게 필요한 조건이리라 믿기 때문이다.&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;&amp;nbsp;&lt;/p&gt;</description>
      <category>우아한테크코스(프리코스)</category>
      <category>우아한테크코스</category>
      <category>우아한테크코스 7기</category>
      <category>우아한테크코스 7기 최종 코딩 테스트</category>
      <category>우테코</category>
      <category>우테코 7기</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/153</guid>
      <comments>https://quickchabun.tistory.com/153#entry153comment</comments>
      <pubDate>Wed, 18 Dec 2024 18:46:36 +0900</pubDate>
    </item>
    <item>
      <title>&amp;lsquo;엔비디아 RTX AI PC 캠퍼스 세미나&amp;rsquo;를 다녀오고 개념 정리하기</title>
      <link>https://quickchabun.tistory.com/152</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;아직 12월은 아니었지만 오늘 첫눈이 내렸다. 어제까지만 해도 초록색을 내뿜던 나무들은 흰 눈에 덮여 그 무게를 견디고 있었다. 그 모습을 사진으로 남기고, 발걸음을 옮겨 학교로 향했다. 오늘 학교로 온 이유는 바로 대공연장에서 &amp;lsquo;엔비디아 RTX AI PC 캠퍼스 세미나&amp;rsquo;가 열렸기 때문이다.&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;800&quot; data-origin-height=&quot;1393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cRKTgx/btsKY9YEmCH/Xs4j9gJyCKKKooSk7LYhZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cRKTgx/btsKY9YEmCH/Xs4j9gJyCKKKooSk7LYhZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cRKTgx/btsKY9YEmCH/Xs4j9gJyCKKKooSk7LYhZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcRKTgx%2FbtsKY9YEmCH%2FXs4j9gJyCKKKooSk7LYhZk%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;350&quot; height=&quot;609&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;1393&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;미국 주식에 관심이 있어 엔비디아에 관심이 있었던 적이 있었지만, 정작 엔비디아가 어떤 기업이고, 무슨 서비스를 내세우고 있는지에 크게 관심을 두지는 않았었다. AI 대격변의 시대가 열리고 AI를 개발하는 수많은 회사들이 엔비디아의 제품을 필요로 했기에 엔비디아의 주가가 상승했다고 짐작을 할 뿐이었다. 오늘 세미나에 참석해 엔비디아는 무슨 회사인지, 그리고 제목에 써져있는 RTX는 무슨 뜻인지 확인하고 싶었다.&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;&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;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yvQ15/btsKZPk4co2/IKSwkMXChJYkYGJyoxLJ9k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yvQ15/btsKZPk4co2/IKSwkMXChJYkYGJyoxLJ9k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yvQ15/btsKZPk4co2/IKSwkMXChJYkYGJyoxLJ9k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyvQ15%2FbtsKZPk4co2%2FIKSwkMXChJYkYGJyoxLJ9k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;313&quot; height=&quot;417&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&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;p data-ke-size=&quot;size16&quot;&gt;(1,2,3,4,5번은 세미나 발표에 포함되지 않은, 개인적으로 정리한 내용이다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CPU와 GPU의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔비디아는 GPU를 판매하는 회사이다. CPU와 GPU의 차이는 무엇일까? 컴퓨터공학부 4학년이 되었음에도 불구하고 가끔 헷갈리는 것이 부끄럽다. 이 참에 둘의 차이를 짚고 넘어가자. Claude에게 둘의 차이를 물어보았다.&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;CPU(Central Processing Unit): 중앙 처리 장치&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;GPU(Grapical Processing Unit): 그래픽 처리 장치&lt;/b&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;처리 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU: 복잡한 연산을 순차적으로 처리하는 데 특화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소수의 강력한 코어로 구성되어 있어 다양한 종류의 작업을 빠르게 처리 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU: 단순하지만 대량의 연산을 병렬로 처리하는 데 특화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수천 개의 작은 코어로 구성되어 동일한 연산을 동시에 수행 가능&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 용도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU: 운영체제 실행, 프로그램 관리, 일반적인 컴퓨터 작업 등 대부분의 컴퓨터 작업을 담당&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU: 주로 그래픽 처리, 게임 렌더링, 인공지능 학습, 암호화폐 채굴 등 병렬 처리가 필요한 작업에 사용&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU: 비교적 적은 용량의 캐시 메모리를 가지고 있지만, 시스템의 RAM에 빠르게 접근 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU: 전용 비디오 메모리(VRAM)를 갖추고 있어 그래픽 데이터를 빠르게 처리할 수 있음&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. AI 붐이 찾아오고, GPU의 중요성이 증가했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT와 같은 LLM이 출시되며 AI 붐이 시작되었다. 이에 따라 수많은 기업들이 AI 모델 학습에 뛰어들었는데, AI 모델 학습은 수많은 행렬 연산을 동시에 처리해야 한다. 그리고 GPU는 수천 개의 코어로 병렬 처리가 가능해 동시에 많은 연산을 처리할 수 있다(그에 반해 CPU는 순차적으로 처리해야되기 때문에 몇십 배에서 몇백 배까지 시간이 더 걸린다고 한다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 수많은 회사들이 &lt;b&gt;AI 모델 학습을 위해 GPU 구매에 열을 올리고 있다!&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;그리고 엔비디아는 GPU를 잘 만든다. 엔비디아는 GPU 시장의 선두주자이며, 특히 AI 학습용 GPU 시장의 80% 이상을 차지하고 있다. 엔비디아의 H100, A100 등 기존 GPU 아키텍처에 AI 연산에 최적화된 기능을 추가한 제품이 시장을 지배하고 있으며, 이 제품들은 대규모 언어 모델(LLM) 학습에서 뛰어난 성능을 보여주고 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. CUDA가 무엇일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔비디아에 대해 찾다보면 자연스럽게 &amp;lsquo;CUDA&amp;rsquo;라는 단어가 계속 언급된다. CUDA란 무엇일까?&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;CUDA는 엔비디아가 개발한 병렬 컴퓨팅 플랫폼 및 프로그래밍 모델로, GPU에서 범용 연산을 수행할 수 있게 해준다. C, C++, Python 등 익숙한 프로그래밍 언어로 GPU 프로그래밍을 할 수 있다. 개발자들이 사용하는 딥 러닝 프레임워크들(TensorFlow, PyTorch 등)이 CUDA를 기반으로 최적화되어 있다.&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;CUDA를 활용하면 개발자들이 GPU의 병렬 처리 능력을 쉽게 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그리고 CUDA는 &lt;b&gt;엔비디아의 GPU&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;CUDA를 사용하려면 엔비디아의 GPU를 사용할 수 밖에 없으니 엔비디아의 지위는 더욱 공고해졌다. CUDA가 AI 개발의 사실상의 표준 플랫폼이 되었다고 할 수 있다는 말도 나오는 것을 보면, CUDA는 엔비디아에게 없어서는 안될 존재라고 해도 될 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 그래픽 카드가 뭐지?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 제목에 있는 RTX는 GPU인가? 아니다. 찾아보니 RTX는 &lt;b&gt;엔비디아의 그래픽 카드 제품 라인&lt;/b&gt;을 나타내는 브랜드명이라고 한다. 많이 들어본 단어이다. 그렇다면 정확히 그래픽 카드는 무슨 뜻일까? Claude에게 물어봤다.&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;그래픽 카드(Graphics Card): CPU의 명령하에 이루어지는 그래픽 작업을 전문적으로 빠르게 처리하고 디지털 신호를 영상 신호로 바꿔 모니터로 전송하는 장치&lt;/b&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래픽 카드의 구성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GPU: 그래픽 처리를 담당하는 핵심 프로세서&lt;/li&gt;
&lt;li&gt;VRAM(Video RAM): 그래픽 작업을 위한 전용 메모리&lt;/li&gt;
&lt;li&gt;냉각 시스템: 팬이나 방열판으로 구성&lt;/li&gt;
&lt;li&gt;전원 관리 시스템&lt;/li&gt;
&lt;li&gt;디스플레이 출력 포트(HDMI, DisplayPort 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래픽 카드의 주요 기능&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게임의 3D 그래픽 처리&lt;/li&gt;
&lt;li&gt;영상 편집 및 렌더링&lt;/li&gt;
&lt;li&gt;AI 학습 및 추론&lt;/li&gt;
&lt;li&gt;암호화폐 채굴&lt;/li&gt;
&lt;li&gt;전문 설계/디자인 작업&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시장 구도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;게이밍용: 엔비디아 GeForce, AMD Radeon 시리즈&lt;/li&gt;
&lt;li&gt;전문가용: 엔비디아 RTX/Quadro, AMD Radeon Pro&lt;/li&gt;
&lt;li&gt;AI/데이터센터용: 엔비디아 Tesla, AMD Instinct&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 쉽게 말해서, &lt;b&gt;그래픽 카드는 GPU를 실제로 사용할 수 있게 해주는 완성된 하드웨어 제품!&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(CPU가 메인보드에 장착되듯이, 그래픽 카드는 메인보드의 PCIe 슬롯에 장착되어 사용)&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 레이 트레이싱이 뭐지?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세미나에서 RTX에 대한 설명을 들을 때, 레이 트레이싱(Ray Tracing)이라는 단어가 많이 언급되었었다. 레이 트레이싱이라는 단어는 아예 처음 들어 봤었기에, 레이 트레이싱이 과연 무엇인지 찾아보기로 했다.&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;레이 트레이싱(Ray Tracing): 빛의 물리적 특성을 실시간으로 시뮬레이션하는 렌더링 기술&lt;/b&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;/li&gt;
&lt;li&gt;빛의 반사, 그림자, 투명도 등을 현실감 있게 표현 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 방식은 미리 계산된 조명과 그림자를 사용하지만, 레이 트레이싱은 실시간으로 빛의 경로를 계산해서 훨씬 더 사실적인 그래픽을 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 하지만 많은 연산 능력이 필요하다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그리고 엔비디아의 RTX 시리즈는 레이 트레이싱 전용 하드웨어를 탑재해 효율적인 처리가 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHLiEc/btsKZsqjoqG/TXyBQaVDfvYDp96eo7GDv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHLiEc/btsKZsqjoqG/TXyBQaVDfvYDp96eo7GDv0/img.png&quot; data-alt=&quot;RTX와 레이 트레이싱 (엔비디아 공식 홈페이지)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHLiEc/btsKZsqjoqG/TXyBQaVDfvYDp96eo7GDv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHLiEc%2FbtsKZsqjoqG%2FTXyBQaVDfvYDp96eo7GDv0%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;544&quot; height=&quot;335&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;RTX와 레이 트레이싱 (엔비디아 공식 홈페이지)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Stable Diffusion, ComfyUI, 그리고 가속 컴퓨팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세미나 도중 ComfyUI에서 RTX 가속에 대한 성능 비교에 대한 설명이 있었다. ComfyUI가 뭐지? ComfyUI가 무엇인지 알려면 일단 Stable Diffusion에 대해서 알아야한다.&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;Stable Diffusion은 Stability AI에서 공개한 오픈소스 text-to-image 생성 모델로, 텍스트 설명을 기반으로 고품질의 이미지를 생성할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; 그리고 ComfyUI는 Stable Diffusion 기반의 이미지 생성 모델을 위한 그래픽 사용자 인터페이스(GUI) 도구&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Stable Diffusion과 ComfyUI에 대하여 더 자세히 알아보고 싶으면 밑의 링크에서 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;링크: &lt;a href=&quot;https://marcus-story.tistory.com/55&quot;&gt;https://marcus-story.tistory.com/55&lt;/a&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;그리고 세미나에서는 ComfyUI에서 이미지를 생성하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apple Macbook M3 Pro에는 &lt;b&gt;154초&lt;/b&gt;,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apple Macbook M3 Max에서는 &lt;b&gt;66초&lt;/b&gt;,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GeForce RTX 4070 Laptop에서는 &lt;b&gt;23초&lt;/b&gt;,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GeForce RTX 4090 Laptop에서는 &lt;b&gt;9초&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;위 발표에 의하면 RTX의 성능이 매우 뛰어난 것 같다. 여담이지만, 나는 지난주에 맥북 에어 M3를 구매했다..ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 가속 컴퓨팅은 특정 연산을 가속하기 위해 특수 하드웨어를 사용하는 기술로, RTX 가속을 통해 ComfyUI의 성능을 크게 향상시킬 수 있다고 한다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 온디바이스 AI와 ChatRTX&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 세미나에서는 &amp;lsquo;온디바이스 AI&amp;rsquo;에 대하여 꽤 많은 시간이 할애되었다. 온디바이스 AI가 뭘까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온디바이스 AI란 클라우드나 외부 서버가 아닌 사용자의 기기 내에서 직접 AI 모델을 실행하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(삼성 갤럭시에서의 빅스비나 아이폰에서 Siri가 온디바이스 AI에 해당한다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;온디바이스 AI의 장점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프라이버시 보호: 데이터가 기기를 떠나지 않기 때문에 개인정보 보호에 유리&lt;/li&gt;
&lt;li&gt;실시간 처리: 네트워크 지연 없이 즉각적인 응답 가능&lt;/li&gt;
&lt;li&gt;오프라인 작동: 인터넷 연결 없이도 AI 기능 사용 가능&lt;/li&gt;
&lt;li&gt;비용 효율성: 클라우드 서버 비용이 들지 않음&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단점 또한 존재하는데,&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;온디바이스 AI의 단점&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;기기의 처리 능력과 메모리 한계로 인해 복잡한 AI 모델 실행이 어려울 수도&lt;/li&gt;
&lt;li&gt;모델 업데이트가 필요할 때마다 앱 업데이트 필요&lt;/li&gt;
&lt;li&gt;기기의 배터리 소모 증가&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 단점에도 불구하고, 프라이버시가 보호되고 응답이 빠르며, 오프라인에서도 작동이 된다는 장점 때문에 온디바이스 AI가 각광받는 것 같다.&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;그리고 엔비디아에서는 윈도우 컴퓨터에서 사용할 수 있는 온디바이스 AI인 &amp;lsquo;NVIDA ChatRTX&amp;rsquo;를 출시했는데, PC의 로컬 파일들을 인식해서 개인에게 맞춤화된 질의응답을 제공한다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9ZUUd/btsKYYJP0gr/sxmdGp4Sv7iZYPyXmvBjAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9ZUUd/btsKYYJP0gr/sxmdGp4Sv7iZYPyXmvBjAK/img.png&quot; data-alt=&quot;ChatRTX 설명 (엔비디아 공식 홈페이지)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9ZUUd/btsKYYJP0gr/sxmdGp4Sv7iZYPyXmvBjAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9ZUUd%2FbtsKYYJP0gr%2FsxmdGp4Sv7iZYPyXmvBjAK%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;609&quot; height=&quot;385&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1296&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ChatRTX 설명 (엔비디아 공식 홈페이지)&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;엔비디아 RTX가 탑재된 윈도우11 PC를 사용하고 있는 사람은 ChatRTX를 사용할 수 있다. 자세한 시스템 요구사양은 밑 이미지를 참고하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G9h03/btsKXHWBumh/qA3JGo6Z4k8MuzhsxwcBx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G9h03/btsKXHWBumh/qA3JGo6Z4k8MuzhsxwcBx0/img.png&quot; data-alt=&quot;ChatRTX 시스템 요구사양&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G9h03/btsKXHWBumh/qA3JGo6Z4k8MuzhsxwcBx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG9h03%2FbtsKXHWBumh%2FqA3JGo6Z4k8MuzhsxwcBx0%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;617&quot; height=&quot;264&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ChatRTX 시스템 요구사양&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 디지털 트윈과 NVIDIA Omniverse&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 주제들에 비해 크게 언급되지는 않았지만, 디지털 트윈과 NVIDIA Omniverse에 대한 소개 또한 진행되었다. 먼저 디지털 트윈이 무엇인지 살펴보자.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디지털 트윈의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실시간 데이터 동기화: 센서와 IoT 기기를 통해 실제 객체의 상태를 지속적으로 반영&lt;/li&gt;
&lt;li&gt;예측적 분석: 발생 가능한 문제를 미리 파악하고 대응 방안 수립&lt;/li&gt;
&lt;li&gt;최적화: 성능 향상과 효율성 개선을 위한 시뮬레이션 수행&lt;/li&gt;
&lt;li&gt;의사결정 지원: 데이터 기반의 인사이트 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 NVIDIA Omniverse는 디지털 트윈 구현을 위한 플랫폼으로, RTX 기능을 활용하여 사실적인 렌더링이 가능하다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8hY14/btsK0m3J42N/K1Z9k6V0tdYb7TFvVs9eN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8hY14/btsK0m3J42N/K1Z9k6V0tdYb7TFvVs9eN1/img.png&quot; data-alt=&quot;NVIDIA Omniverse에 대한 설명 (엔비디아 공식 홈페이지)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8hY14/btsK0m3J42N/K1Z9k6V0tdYb7TFvVs9eN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8hY14%2FbtsK0m3J42N%2FK1Z9k6V0tdYb7TFvVs9eN1%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;603&quot; height=&quot;347&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;1178&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;NVIDIA Omniverse에 대한 설명 (엔비디아 공식 홈페이지)&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;디지털 트윈은 팬데믹 이후 각광받은 메타버스에 필수적인 기술인 것 같다. 지금은 잠시 주춤한 것 같지만, 메타버스 붐이 다시 한 번 찾아온다면 이 디지털 트윈이라는 기술도 크게 주목받지 않을까.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 세미나를 통해 왜 엔비디아가 어떤 회사이고, 또 신제품에 대한 소개를 들으며 엔비디아가 어떤 미래를 준비하는지 확인할 수 있었다. 생각보다 엔비디아는 시장에서 압도적인 위치를 차지하고 있었고, AI의 중요성이 더욱 상승하는 상황에서 더욱 발전할 가능성이 큰 회사였다. 그리고 사람들이 왜이리 NVIDIA 그래픽카드를 찾으려고 눈에 불을 키고 다니는지 그 이유를 알 수 있었다(게임을 하거나, 암호화폐 채굴을 하거나..)&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;LLM의 성능은 더욱 향상하고 있고, AI는 이제 사람들의 일자리를 위협할 수준에 근접하고 있다. 엔비디아는 AI의 발전에 필수적인 존재이고, 그 가능성이 확인되었을 때부터 주가는 폭발적으로 상승하고 있다. 이번 세미나를 통해 세상이 얼마나 빠르게 발전하고 있는지 알 수 있었다. 앞으로 어떤 기술이 우리를 놀라게 할까 궁금해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Event</category>
      <category>chatrtx</category>
      <category>CPU</category>
      <category>GPU</category>
      <category>nvidia</category>
      <category>RTX</category>
      <category>그래픽카드</category>
      <category>디지털 트윈</category>
      <category>레이 트레이싱</category>
      <category>엔비디아</category>
      <category>엔비디아 rtx ai pc 캠퍼스 세미나</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/152</guid>
      <comments>https://quickchabun.tistory.com/152#entry152comment</comments>
      <pubDate>Wed, 27 Nov 2024 23:34:43 +0900</pubDate>
    </item>
    <item>
      <title>[JS Deep Dive] 27장 정리 - 배열에 관하여</title>
      <link>https://quickchabun.tistory.com/151</link>
      <description>&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;요소(element): 배열이 가지고 있는 값&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자바스크립트의 모든 값은 배열의 요소가 될 수 있음&lt;/li&gt;
&lt;li&gt;배열의 요소는 배열에서 자신의 위치를 나타내는 0 이상의 정수인 인덱스를 갖는다(0부터 시작)&lt;/li&gt;
&lt;li&gt;배열은 요소의 개수, 배열의 길이를 나타내는 length 프로퍼티를 가짐&lt;/li&gt;
&lt;li&gt;배열이라는 타입은 존재하지 않으며, 배열은 객체 타입이다.&lt;/li&gt;
&lt;li&gt;배열은 배열 리터럴, Array 생성자 함수, Array.of, Array.from 메서드로 생성 가능&lt;/li&gt;
&lt;li&gt;배열의 생성자 함수는 Array이며, 배열의 프로토타입 객체는 Array.prototype이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 객체와 배열을 구분하는 가장 명확한 차이는 &amp;ldquo;값의 순서&amp;rdquo;와 &amp;ldquo;length 프로퍼티&amp;rdquo;&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;&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;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;인덱스를 나타내는 문자열을 프로퍼티 키로 가지며, length 프로퍼티를 갖는 특수한 객체&lt;/li&gt;
&lt;li&gt;자바스크립트 배열의 요소는 사실 프로퍼티 값&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바스크립트 배열은 해시 테이블로 구현된 객체이므로 인덱스로 요소에 접근하는 경우 일반적인 배열보다 성능적인 면에서 느릴 수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 요소를 삽입 또는 삭제하는 경우에는 일반적인 배열보다 빠른 성능을 기대할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 모던 자바스크립트 엔진은 배열을 일반 객체와 구별하여 좀 더 배열처럼 동작하도록최적화하여 구현&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;length 프로퍼티와 희소 배열&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;length 프로퍼티의 값은 빈 배열일 경우 0이며, 빈 배열이 아닐 경우 가장 큰 인덱스에 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;배열은 요소를 최대 2^32-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;length 프로퍼티 값은 요소의 개수, 즉 배열의 길이를 바탕으로 결정되지만 임의의 숫자 값을 명시적으로 할당 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 length 프로퍼티 값보다 작은 숫자 값을 할당하면 배열의 길이가 줄어듬&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const arr = [1,2,3,4,5];

arr.length = 3;

console.log(arr); // [1,2,3]
&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;하지만 length 프로퍼티 값보다 큰 숫자 값을 할당하는 경우 length 프로퍼티 값은 변경되지만 실제로 배열의 길이가 늘어나지는 않음&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;/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;희소 배열은 length와 배열 요소의 개수가 일치하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;희소 배열의 length는 희소 배열의 실제 요소 개수보다 언제나 크다&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;&amp;rarr; 배열에는 같은 타입의 요소를 연속적으로 위치시키는 것이 최선&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배열 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배열 리터럴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 일반적이고 간편한 배열 생성 방식은 배열 리터럴을 사용하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 배열 리터럴은 0개 이상의 요소를 쉼표로 구분하여 대괄호로 묶음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(프로퍼티 키가 없고 값만 존재)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const arr = [1,2,3];
console.log(arr.length); // 3
&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array 생성자 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object 생성자 함수를 통해 객체를 생성할 수 있듯이 Array 생성자 함수를 통해 배열을 생성할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const arr = new Array(10);

console.log(arr); // [empty * 10]
console.log(arr.length); // 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Array 생성자 함수는 new 연사자와 함께 호출하지 않더라도, 즉 일반 함수로서 호출해도 생성하는 생성자 함수로 동작&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.of&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달된 인수를 요소로 갖는 배열을 생성&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Array.of(1); // -&amp;gt; [1]
Array.of(1,2,3); // -&amp;gt; [1,2,3]
Array.of('string'); // -&amp;gt; ['string']
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.from&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6에서 도입된 Array.from 메서드는 유사 배열 객체 또는 이터러블 객체를 인수로 전달받아 배열로 변환하여 반환&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;Array.from({ length: 2, 0: 'a', 1: 'b' }); // -&amp;gt; ['a', 'b']

// 문자열은 이터러블
Array.from('Hello'); // =&amp;gt; ['H', 'e', 'l', 'l', 'o']

Array.from({ length: 3 }); // -&amp;gt; [undefined, undefined, undefined]

// Array.from은 두 번째 인수로 전달한 콜백 함수의 반환값으로 구성된 배열을 반환
Array.from({ length: 3}, (_, i) =&amp;gt; i); // -&amp;gt; [0,1,2]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배열 요소의 참조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열의 요소를 참조할 때에는 대괄호([]) 표기법을 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대괄호 안에는 인덱스가 와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열도 존재하지 않는 요소를 참조하면 undefined를 반환&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배열 요소의 추가와 갱신&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체에 프로퍼티를 동적으로 추가할 수 있는 것처럼 배열에도 요소를 동적으로 추가 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;존재하지 않는 인덱스를 사용해 값을 할당하면 새로운 요소가 추가된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배열 요소의 삭제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열의 특정 요소를 삭제하기 위해 delete 연산자를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;희소 배열을 만드는 delete 연산자는 사용하지 않는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 희소 배열을 만들지 않으면서 배열의 특정 요소를 완전히 삭제하려면 Array.prototype.splice 메서드를 사용&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배열 메서드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배열을 다룰 때 유용한 다양한 빌트인 메서드를 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 배열을 직접 변경하는 메서드, 원본 배열을 직접 변경하지 않고 새로운 배열을 생성하여 반환하는 메서드가 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.isArray&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달된 인수가 배열이 배열이면 true, 배열이 아니면 false를 반환&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.indexOf&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;indexOf 메서드는 원본 배열에서 인수로 전달된 요소를 검색하여 인덱스를 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;indexOf 메서드는 배열에 특정 요소가 존재하는지 확인할 때 유용하다&lt;/p&gt;
&lt;pre class=&quot;erlang&quot;&gt;&lt;code&gt;if (foods.indexOf('orange') === -1 ) {
	(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(indexOf 메서드 대신 ES7에서 도입된 Array.prototype.includes 메서드를 사용하면 가독성이 더 좋다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.push&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수로 전달받은 모든 값을 원본 배열의 마지막 요소로 추가하고 변경된 length 프로퍼티 값을 반환 (원본 배열을 직접 변경)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 요소로 추가할 요소가 하나 뿐이라면 push 메서드를 사용하지 않고 length 프로퍼티를 사용하여 배열의 마지막에 요소를 직접 추가할 수 있음 (push 메서드보다 빠름)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.pop&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 배열에서 마지막 요소를 제거하고 제거한 요소를 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 배열이 빈 배열이면 undefined를 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(원본 배열을 직접 변경)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.unshift&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수로 전달받은 모든 값을 원본 배열의 선두에 요소로 추가하고 변경된 length 프로퍼티 값을 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(원본 배열을 직접 변경)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.shift&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 배열에서 첫 번째 요소를 제거하고 제거한 요소를 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 배열이 빈 배열이면 undefined를 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(원본 배열을 직접 변경)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;shift 메서드와 push 메서드를 사용하면 큐를 쉽게 구현 가능&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.concat&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;concat 메서드는 인수로 전달된 값들(배열 또는 원시값)을 원본 배열의 마지막 요소로 추가한 새로운 배열을 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수로 전달한 값이 배열인 경우 배열을 해체하여 새로운 배열의 요소로 추가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(원본 배열은 변경되지 않음)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const arr1 = [1,2];
let result = arr1.concat(3);
console.log(result); // [1,2,3];

const arr2= [3,4];
result = arr1.concat(arr2);
console.log(result); // [1,2,3,4];
&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;push와 unshift 메서드는 concat 메서드로 대체 가능 (push와 unshift 메서드는 원본 배열을 변경)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;concat 메서드는 ES6의 스프레드 문법으로 대체 가능&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;let result = [1,2].concat([3,4]);
console.log(result); // [1,2,3,4]

result = [...[1,2], ...[3,4]];
console.log(result); // [1,2,3,4]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 push/unshift 메서드와 concat 메서드를 사용하는 것보다는, ES6의 스프레드 문법을 일관성있게 사용하는 것을 권장&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.splice&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 배열의 중간에 요소를 추가하거나 중간에 있는 요소를 추가하거나 중간에 있는 요소를 제거하는 경우 splice 메서드르 사용&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;start: 원본 배열의 요소를 제거하기 시작할 인덱스, start가 음수인 경우 배열의 끝에서의 인덱스를 나타냄, -1이면 마지막 요소를 가리킴)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deleteCount: 원본 배열의 요소를 제거하기 시작할 인덱스인 start부터 제거할 요소의 개수, deleteCount가 0인 경우 아무것도 제거 안됨(옵션)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;items: 제거한 위치에 삽입할 요소들의 목록. 생략할 경우 원본 배열에서 요소들을 제거하기만 함(옵션)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const arr = [1,2,3,4];

// 원본 배열의 인덱스 1부터 2개의 요소를 제거하고 그 자리에 새로운 요소 20,30을 삽입
const result = arr.splice(1, 2, 20, 30);

console.log(result); // [2,3]
console.log(arr); // [1, 20, 30, 4]
&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;배열에서 특정 요소를 제거하려면 indexOf 메서드를 통해 특정 요소의 인덱스를 취득한 다음 splice 메서드를 사용&lt;/p&gt;
&lt;pre class=&quot;xquery&quot;&gt;&lt;code&gt;const arr = [1,2,3,1,2];

// 배열 array에서 item 요소를 제거, item 요소가 여러 개이면 첫 번째 요소만 제거
function remove(array, item) {

	const index = array.indexOf(item);
	
	if (index !== -1) array.splice(index, 1);
	
	return array;
}

console.log(remove(arr, 2)); // [1, 3, 1,, 2]
&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;filter 메서드를 사용하여 특정 요소를 제거할 수도 있다(특정 요소가 중복된 경우 모두 제거)&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const arr = [1, 2, 3, 1, 2;

function removeAll(array, item) {
	return array.filter(v =&amp;gt; v !== item);
}

console.log(removeAll(arr, 2)); // [1, 3, 1]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.slice&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;slice 메서드는 인수로 전달된 범위의 요소들을 복사하여 배열로 반환&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;slice 메서드는 두 개의 매개변수를 갖는다&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;start: 복사를 시작할 인덱스. 음수인 경우 배열의 끝에서의 인덱스를 나타낸다. 예를 들어, slice(-2)는 배열의 마지막 두 개의 요소를 복사하여 배열로 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;end: 복사를 종료할 인덱스. 이 인덱스에 해당하는 요소는 복사되지 않는다. end는 생략 가능하며 생략 시 기본값은 length 프로퍼티 값&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const arr = [1,2,3];

// arr[0]로부터 arr[1] 이전 (arr[1] 미포함)
arr.slice(0,1); // -&amp;gt; [1]

// arr[1]부터 arr[2] 이전(arr[2] 미포함)까지 복사하여 반환
arr.slice(1, 2); // -&amp;gt; [2]

// 원본은 변경되지 않는다.
&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;slice 메서드의 인수를 모두 생략하면 원본 배열의 복사본을 생성하여 반환&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;slice 메서드가 복사본을 생성하는 것을 이용하여 arguments, HTMLCollection, NodeList 같은 유사 배열 객체를 배열로 변환할 수 있&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;Array.from메서드를 사용하면 더욱 간단하게 유사 배열 객체를 배열로 변환 가능. Array.from 메서드는 유사 배열 객체 또는 이터러블 객체를 배열로 변환&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function sum() {
	const arr = Array.from(arguments);
	console.log(arr); // [1,2,3]
	
	return arr.reduce((pre, cur) =&amp;gt; pre + cur, 0);
}

console.log(sum(1,2,3)); // 6
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.join&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 배열의 모든 요소를 문자열로 변환한 후, 인수로 전달받은 문자열, 즉 구분자로 연결한 문자열을 반환. 구분자는 생략 가능하며 기본 구분자는 콤마(&amp;rsquo;,&amp;rsquo;)&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;const arr = [1,2,3,4];

// 기본 구분자는 콤마
// 원본 배열 arr의 모든 요소를 문자열로 변환한 후 기본 구분자로 연결한 문자열을 반환
arr.join(); // -&amp;gt; '1,2,3,4';

// 원본 배열 arr의 모든 요소를 문자열로 변환한 후, 빈 문자열로 연결한 문자열을 반환
arr.join(''); // -&amp;gt; '1234'

// 원본 배열 arr의 모든 요소를 문자열로 변환한 후, 구분자 ':'로 연결한 문자열을 반환
arr.join(':'); // -&amp;gt; '1:2:3:4'
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.reverse&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reverse 메서드는 원본 배열의 순서를 반대로 뒤집는다. (원본 배열이 변경됨)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반환값은 변경된 배열&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.fill&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6에서 도입된 fill 메서드는 인수로 전달받은 값을 배열의 처음부터 끝까지 요소로 채움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(원본 배열이 변경됨)&lt;/p&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;const arr = [1,2,3];

// 인수로 전달받은 값 0을 배열의 처음부터 끝까지 요소로 채움
arr.fill(0);

// fill 메서드는 원본 배열을 직접 변경
console.log(arr); // [0,0,0]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 인수로 요소 채우기를 시작할 인덱스를 전달 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째 인수로 요소 채우기를 멈출 인덱스로 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fill 메서드로 요소를 채울 경우 모든 요소를 하나의 값만으로 채울 수 밖에 없다.&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;Array.from 메서드를 사용하면 두 번째 인수로 전달한 콜백 함수로 전달한 콜백 함수를 통해 요소값을 만들면서 배열을 채울 수 있다.&lt;/p&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;// 인수로 전달받은 점수만큼 요소를 생성하고 0부터 1씩 증가하면서 요소를 채운다.
const sequences = (length = 0) =&amp;gt; Array.from( { length }, (_, i) =&amp;gt; i);
// const sequences = (length = 0) =&amp;gt; Array.from(new Array(length), (_,i) =&amp;gt; i);

console.log(sequences(3)); // [0, 1, 2]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.includes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES7에서 도입된 includes 메서드는 배열 내에 특정 요소가 포함되어 있는지 확인하여 true 또는 false를 반환&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const arr = [1,2,3];

// 배열에 요소 2가 포함되어 있는지 확인
arr.includes(2); // -&amp;gt; true
&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;두 번째 인수를 생략할 경우 기본값 0이 설정됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 두 번째 인수에 음수를 전달하면 length 프로퍼티 값과 음수 인덱스를 합산하여(length + index) 검색 시작 인덱스를 설정&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const arr = [1,2,3];

// 배열에 요소 3이 포함되어 있는지 인덱스 2(arr.length - 1)부터 확인
arr.includes(3, -1); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;indexOf 메서드를 사용하여도 배열 내에 특정 요소가 포함되어 있는지 확인 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(indexOf 메서드를 사용하면 반환값이 -1인지 확인해 보아야하고 배열에 NaN이 포함되어 있는지 확인할 수 없다는 문제가 있음)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.flat&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인수로 전달한 깊이만큼 재귀적으로 배열을 평탄화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[1, [2,3,4,5]].flat(); // &amp;rarr; [1, 2, 3, 4, 5]&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;인수로 Infinity를 전달하면 중첩 배열 모두를 평탄화&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배열 고차 함수&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고차 함수: 함수를 인수로 전달받거나 함수를 반환하는 함수&lt;/p&gt;
&lt;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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 순수 함수를 통해 부수 효과를 최대한 억제&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.sort&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 배열을 직접 변경하며 정렬된 배열을 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;sort 메서드는 기본적으로 오름차순으로 요소를 정렬&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;문자열 배열 [&amp;rsquo;2&amp;rsquo;, &amp;lsquo;10&amp;rsquo;]을 sort 메서드로 정렬하면 문자열 &amp;lsquo;10&amp;rsquo;의 유니코드 코드 포인트가 문자열 &amp;lsquo;2&amp;rsquo;의 유니코드 코드 포인트보다 앞서므로 [&amp;rsquo;10&amp;rsquo;, &amp;lsquo;2&amp;rsquo;]로 정렬된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 숫자 요소를 정렬할 때는 sort 메서드에 정렬 순서를 정의하는 비교 함수를 인수로 전달해야&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 비교 함수는 양수나 음수 or 0을 반환해야&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;비교 함수의 반환값이 0보다 작으면 비교 함수의 첫 번째 인수를 우선하여 정렬하고, 0이면 정렬하지 않으며, 0보다 크면 두 번째 인수를 우선하여 정렬&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;const points = [40, 100, 1, 5, 2, 25, 10];

// 숫자 배열의 오름차순 정렬. 비교 함수의 반환값이 0보다 작으면 a를 우선하여 정렬
points.sort((a, b) =&amp;gt; a - b);
console.log(points); // [1, 2, 5, 10, 25, 40, 100
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.forEach&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건문이나 반복문은 로직의 흐름을 이해하기 어렵게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; for문은 반복을 위한 변수를 선언해야 하며, 조건식과 증감식으로 이루어져 있어 함수형 프로그래밍이 추구하는 바와 맞지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach 메서드는 for 문을 대체할 수 있는 고차 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 자신의 내부에서 반복문을 실행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 반복문을 추상화한 고차 함수로서 내부에서 반복문을 통해 자신을 호출한 배열을 순회하면서 수행해야 할 처리를 콜백 함수로 전달받아 반복 호출&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;forEach 메서드는 콜백 함수를 호출할 대 3개의 인수, forEach 메서드를 호출한 배열의 요소값과 인덱스, 호출한 배열(this)를 순차적으로 전달.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// forEach 메서드는 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달
[1,2,3].forEach((item, index, arr) =&amp;gt; {
	console.log('요소값: ${item}, 인덱스: ${index}, this: ${JSON.stringify(arr)}`);
});

/*
요소값: 1, 인덱스: 0, this: [1,2,3]
요소값: 2, 인덱스: 1, this: [1,2,3]
요소값: 3, 인덱스: 2, this: [1,2,3]
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;forEach 메서드도 내부에서는 반복문(for 문)을 통해 배열을 순회&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 반복문을 메서드 내부로 은닉하여 로직의 흐름을 이해하기 쉽게 만듬&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;forEach 메서드는 break, continue문 사용불가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 배열의 모든 요소를 빠짐 없이 순회하며 순회 중단 불가&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.map&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신이 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 콜백 함수의 반환값들로 구성된 새로운 배열을 반환함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(원본 배열 변경 x)&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const numbers = [1,4,9];

const roots = numbers.map(item =&amp;gt; Math.sqrt(item));

console.log(roots); // [1, 2, 3]
console.log(numbers); // [1, 4, 9]
&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;map 메서드가 생성하여 반환하는 새로운 배열의 length 프로퍼티 값은 map 메서드를 호출한 배열의 length 프로퍼티 값과 반드시 일치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; map 메서드를 호출한 배열과 map 메서드가 생성하여 반환한 배열은 1: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;map 메서드의 콜백 함수는 map 메서드를 호출한 배열의 요소값과 인덱스, map 메서드를 호출한 배열 자체, 즉 this를 순차적으로 전달받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// map 메서드는 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달
[1,2,3].map((item, index, arr) =&amp;gt; {
	console.log(`요소값: ${item}, 인덱스: ${index}, this: ${JSON.stringfy(arr)}`);
	return item;
});

/*
요소값: 1, 인덱스: 0, this: [1,2,3]
요소값: 2, 인덱스: 1, this: [1,2,3]
요소값: 3, 인덱스: 2, this: [1,2,3]
*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 나은 방법은 ES6의 화살표 함수를 사용하는 것&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.filter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter 메서드는 자신이 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(콜백 함수의 반환값이 true인 요소로만 구성된 새로운 배열을 반환)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const numbers = [1,2,3,4,5];

const odds = numbers.filter(item =&amp;gt; item % 2);
console.log(odds); // [1,3,5]
&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;filter 메서드가 생성하여 반환한 새로운 배열의 length 프로퍼티 값은 filter 메서드를 호출한 배열의 length 프로퍼티 값과 같거나 작다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter 메서드 또한 콜백 함수를 호출하면서 3개(요소값, 인덱스, this)의 인수를 전달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;filter 메서드를 사용해 특정 요소를 제거할 경우 특정 요소가 중복되어 있다면 중복된 요소가 모두 제거됨&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.reduce&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신을 호출한 배열의 모든 요소를 순회하며 인수로 전달받은 콜백 함수를 반복 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 콜백 함수의 반환값을 다음 순회 시에 콜백 함수의 첫 번째 인수로 전달하면서 콜백 함수를 호출하여 하나의 결과값을 만들어 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 인수로 콜백 함수, 두 번째 인수로 초기값을 전달받음&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;// 1부터 4까지 누적을 구한다
const sum = [1,2,3,4].reduce((accumulator, currentValue, index, array)
=&amp;gt; accumulator + currentValue, 0 );

console.log(sum); // 10
&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;reduce 메서드는 하나의 결과값을 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 구하기, 최대값 구하기, 요소의 중복 횟수 구하기, 중첩 배열 평탄화, 중복 요소 제거, 등등..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중복되지 않는 유일한 값들의 집합인 Set을 사용할 수 있다. (추천)&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;const values = [1, 2, 1, 3, 5, 4, 5, 3, 4, 4];

const result = [...new Set(values)];
console.log(result); // [1,2,3,5,4]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reduce 메서드를 호출할 때는 언제나 초기값을 전달하는 것이 안전&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.some&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;some 메서드는 자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수의 반환값이 단 한 번이라도 참이면 true, 모두 거짓이면 false 반환&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;// 배열의 요소 중 10보다 큰 요소가 1개 이상 존재하는지 확인
[5, 10, 15].some(item =&amp;gt; item &amp;gt; 10); // -&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.every&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콜백 함수의 반환값이 모두 참이면 true, 단 한 번이라도 거짓이면 false를 반환&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[5, 10, 15].every(item =&amp;gt; item &amp;gt; 3); // -&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.find&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6에서 도입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출하여 반환값이 true인 첫 번째 요소를 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 만약 없으면 undefined 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(find 메서드는 배열이 아니라 요소를 반환한다)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.findIndex&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6에서 도입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신을 호출한 배열의 요소를 순회하면서 인수로 전달된 콜백 함수를 호출하여 반환값이 true인 첫 번째 요소의 인덱스를 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; true인 요소가 없으면 -1을 반환&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Array.prototype.flatMap&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES10에서 도입된 flatMap 메서드는 map 메서드를 통해 생성된 새로운 배열을 평탄화한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;const arr = ['hello', 'world'];

// map과 flat을 순차적으로 실행
arr.map(x =&amp;gt; x.split('')).flat();
// -&amp;gt; ['h', 'e', 'l', 'l', 'o', 'w','o', 'r', 'l', 'd'[

.. flatMap은 map을 통해 생성된 새로운 배열을 평탄화
arr.flatMap(x =&amp;gt; x.split(''));
// 위와 같은 결과&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Study/Modern JS Deep Dive</category>
      <category>javascript</category>
      <category>js deep dive</category>
      <category>배열</category>
      <category>오블완</category>
      <category>자바스크립트</category>
      <category>티스토리챌린지</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/151</guid>
      <comments>https://quickchabun.tistory.com/151#entry151comment</comments>
      <pubDate>Wed, 27 Nov 2024 00:11:54 +0900</pubDate>
    </item>
    <item>
      <title>[프롬프트 엔지니어링] 프롬프트 엔지니어링 기법들에 대하여</title>
      <link>https://quickchabun.tistory.com/150</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;김진중(골빈해커)님의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;'최고의 프롬프트 엔지니어링 강의'&lt;/b&gt;를 읽고 정리한 글입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트 엔지니어링이란 AI로 원하는 결과를 생성하기 위해 컴퓨터와 대화하는 방법&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;1. 프롬프트 엔지니어링의 가장 대표적인 다섯 가지 방법&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제로샷 프롬프팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 아무런 데이터나 예시를 주지 않고 바로 특정 작업을 수행하도록 지시하는 것&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원샷 러닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 명령을 내릴 때 실행 방법에 대한 예시 한 개를 동시에 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 영어를 한국어로 번역해줘. This is an apple을 한국어로 번역하면 &amp;lsquo;이것은 사과입니다&amp;rsquo;야.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;퓨샷 러닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 명령을 내릴 때 2~3개부터 수십 개 정도의 예시를 함께 제공하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 부족하거나 특정 작업에 대한 사례가 많지 않을 때 유용&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CoT(Chain of Thought)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 문제 해결 과정에서 따라야 할 생각의 단계나 논리적 순서를 제시하면 LLM은 이 사고 과정을 따라 문제를 분석하고 각 단계를 거치면서 최종 단계에 도달&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; AI가 문제 해결 과정에 필요한 논리적 사고를 모방하도록 하는 것&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제로샷 CoT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoT와 비슷하지만 문제 해결 과정에서 따라야 할 생각의 단계나 논리적 순서 등의 가이드를 제공하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그 대신 해결할 문제를 주고 천천히 생각해보라고 지시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기본적인 프롬프트 구성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시중에 돌아다니는 프롬프트 템플릿을 받아 사용하는 것은 블라인드 프롬프팅 or 프롬프트 라이팅이라고 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 프롬프트의 구성은,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;답변을 위해 필요한 적절한 컨텍스트 제공 -&amp;gt; 컨텍스트는 보통 정제하지 않은 긴 텍스트로 제공&lt;/li&gt;
&lt;li&gt;원하는 결과 추출을 위한 프롬프트 작성&lt;/li&gt;
&lt;li&gt;결과물의 형식을 지정&lt;/li&gt;
&lt;/ol&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;프롬프트 = 컨텍스트 + 인스트럭션&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 프롬프트 = 프로그램(앱)&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;rarr; 인컨텍스트 러닝&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 임베딩과 벡터 서치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사전적 의미: 무언가를 다른 공간이나 형태에 집어 넣는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에서 임베딩이란 단어나 문장 같은 언어의 조각들을 숫자로 바꾸는 과정&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;벡터화: 각 단어들을 숫자의 집합(실수 형태의 집합)으로 바꾸는 과정&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;우리가 LLM을 사용할 때는 일반 텍스트로 프롬프트를 입력하지만 실제로 LLM 모델이 받아들이는 것은 벡터화된 데이터&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Apple과 Banana는 둘 다 과일이라는 공통점이 있기에 가까운 곳에 위치&lt;/li&gt;
&lt;li&gt;House와 Car는 먼 곳에 위치(의식주와 관련이 있기 때문)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베딩 공간: 단어를 n차원에 배치한 것&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;Apple과 Banana 간 위치를 거리 또는 유사도라고 함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이 값이 가까운 것을 찾는 방법이 바로 벡터 서치&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 의미 기반으로 검색하므로 &amp;lsquo;시멘틱 서치&amp;rsquo;라고 함&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 벡터 서치의 명암&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 서치는 전통적인 검색 엔진처럼 복잡한 시스템을 필요로 하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 누구나 쉽고 빠르게 고성능의 검색 엔진 구축 가능&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. 속도 문제&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;문제2. 성능 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANN(Approximate Nearest Neighbor) 알고리즘은 정도는 약간 떨어지는 대신 빠르게 유사한 벡터를 찾을 수 있는 기술&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANN은 고차원 데이터 공간을 더 작은 공간으로 &amp;lsquo;근사화&amp;rsquo;&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;lsquo;차원 축소&amp;rsquo; 기법은, 데이터의 차원을 줄여 계산 복잡도를 낮추는 동싱 원본 데이터의 중요한 특성을 유지하려고 시도 &amp;rarr; 정확도가 떨어짐&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 보완하기 위해 &amp;lsquo;하이브리드 서치&amp;rsquo;라는 방식을 사용하기도&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;키워드 검색을 통해 DB에서 후보 데이터를 일부 검색&lt;/li&gt;
&lt;li&gt;사용자가 요청한 내용과 유사한 데이터를 벡터 서치로 다시 필터링하는 방식&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 벡터 서치를 위한 벡터 DB를 만드는 회사들이 관심을 받고 있음&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 프롬프트 엔지니어링 과정의 세분화(5단계)&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;1. 프롬프트 결과 설정&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;2. 프롬프트 결과 설계&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;3. 그라운딩 설계 및 평가&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그라운딩은 AI가 답변을 생성할 때 신뢰할 수 있는 정보나 데이터에 기반하여 정확성과 연관성을 확보하는 과정&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;4. 프롬프트 디자인&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;5. 모니터링 및 개선&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트가 실제 환경에서 사용되는 동안에는 지속적으로 성능을 모니터링하고 개선하는 것이 중요&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 프롬프트 디자인 프레임 워크&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI의 페르소나 또는 역할을 정의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 당신은 법률 전문가입니다. 법률 관련 질문에 답하세요&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 누구에게 정보를 제공하고 있는지, 즉 응답의 대상이 되는 사용자나 그룹을 명시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 초등학생에게 태양계의 행성에 대해 설명해주세요&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문과 관련해서 참고할만한 지식과 정보를 DB나 검색 엔진 등에서 가져와 삽입&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 위키피디아의 내용에 따라 나폴레옹에 대해 설명해주세요&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수행해야 하는 특정 작업이나 목표를 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 500단어로 자기 소개서를 작성해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자기 소개서를 작성해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서론, 본론, 결론으로 나누어 작성해주세요.&lt;/p&gt;
&lt;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;h3 data-ke-size=&quot;size23&quot;&gt;정책 및 규칙, 스타일 가이드, 제약 사항 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답을 만들 때 따라야 하는 특정 정책이나 규칙, 스타일 가이드, 제약 사항을 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 성격의 긍정적인 측면만을 강조하며 자기 소개서를 작성해주세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;140자 이내로 트윗을 작성해주세요.&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답이 따라야 하는 특정 형식이나 구조를 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) JSON 형식으로 결과를 출력해주세요&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 응답 형식이나 내용을 구체적으로 보여 주는 예시를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 다음의 예시를 참고하여 응답하세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User: I love you.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Assistant: 나는 당신을 사랑합니다.&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;참고) STICC&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;상황(Situation)&lt;/li&gt;
&lt;li&gt;과제/작업(Task)&lt;/li&gt;
&lt;li&gt;의도(Intent)&lt;/li&gt;
&lt;li&gt;우려/고려사항(Concerns)&lt;/li&gt;
&lt;li&gt;조정(Calibrate)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 프롬프트 테크닉 TOP8&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 결과를 더 정확하게 잘 이끌어내는 것을 목표로 프롬프트를 디자인하는 것을 프롬프팅이라고 부름&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 제공&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제로샷: 예제를 제공하지 않음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원샷: 한 개의 예제를 제공&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퓨샷: 두 개 이상의 예제를 제공&lt;/p&gt;
&lt;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;고양이: 동물&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오렌지: 과일&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토마토: 식물&lt;/p&gt;
&lt;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;퓨샷은 파라미터(모델의 크기) 크기가 큰 LLM에서 훨씬 성능이 좋게 나옴&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;h3 data-ke-size=&quot;size23&quot;&gt;생각 사슬(Chain of Thought, CoT)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 문제의 이유 혹은 추론 과정에 대해 직접 설명하도록 만들어 답변을 더 정확하게 생성하는 기술&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoT 방식의 핵심: 생각의 과정을 먼저 제시&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;제로샷 CoT는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let&amp;rsquo;s think step by step.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계별로 생각해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차근차근 생각해 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 문구를 마지막에 추가함으로써 마치 CoT 예제를 제공하는 것과 같은 효과를 내어 성능을 향상시키는 방법&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CoT를 한 단계 더 발전시켜 동일한 문제에 대해 여러 가지 다른 추론 경로를 고려한 후 가장 일관되게 나온 답을 선택&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;ex) 다양한 계산 방식의 예제를 먼저 주고나서 질문을 하면 LLM이 그 중 하나의 방식을 선택해서 답변 &amp;rarr; 그리고 그 답변을 여러번 반복하게 함&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;&amp;rarr; 즉, 하나의 문제에 대해 여러 가지 방식으로 접근하고 해결해 보는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; AI가 복잡한 문제를 보다 신뢰할 수 있는 방법으로 해결하도록 돕는다.&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;&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;rarr; 아주 정확한 결과가 필요한 경우에만 사용하는 것을 권장&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;샘플링 투표(Sampling-and-Voting)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;복잡한 CoT 프롬프팅 대신 기본적인 지시를 사용하고 출력을 가공 없이 활용&lt;/li&gt;
&lt;li&gt;단순히 출력 간 유사도를 계산하여 다수결로 답안을 선정&lt;/li&gt;
&lt;li&gt;추론 작업뿐만 아니라 코드 생성 등 다양한 작업에 대해 일반화된 접근법&lt;/li&gt;
&lt;li&gt;자기 일관성과 결합하여 상호 보완적으로 활용 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 아이디어: 여러 개의 LLM 에이전트를 활용하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 입력으로 LLM을 반복적으로 실행하여 여러 개의 출력 샘플을 생성&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;샘플링 단계에서는 작업에 대한 질의 혹은 지시를 LLM에게 N번 반복하여 출력 샘플을 다양하게 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 다양한 출력을 얻기 위해 별도의 페르소나를 설정하거나 서로 다른 LLM 모델을 사용 or 서로 다른 LLM 모델 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 그냥 같은 LLM 여러번 실행해도 됨&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;&amp;rarr; 유사도는 임베딩 벡터로 계산하거나 BLEU 점수(기계 번역 성능을 평가하기 위해 사용되는 지표) 등을 사용&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;앙상블 규모(샘플 개수)를 충분히 늘리면 작은 LLM이 더 큰 모델의 단일 출력 성능을 능가 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 비용적인 측면에서 큰 이득은 없음&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;선택 추론(Selection-Inference)&lt;/h3&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 컨텍스트에서 답할 수 있는 정보를 먼저 선택한 다음 그 정보를 기반으로 답변&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 질문의 답에 필요한 내용을 Context에서 추출해서 나열하세요&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;&amp;rarr; 실제 케이스에 구현하려면 추론 단계를 생성하고 종료하는 프레임워크를 세심하게 구성해야&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;최소에서 최대로(Least-to-Most)&lt;/h3&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기에 추가로 &amp;lsquo;단계적으로 생각해 보세요&amp;rsquo;라는 제로샷 CoT를 넣으면 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연구 결과에 따르면 최소에서 최대로 기법은 다른 기법들에 비해 굉장히 높은 성능을 보임&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소에서 최대로 기법은 CoT와 선택 추론을 결합한 방법&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;리액트(ReAct)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행 계획을 유도하고 추적하여 작업별로 실행할 액션을 선택하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) Search, Lookup, Finish 유형을 LLM에 알려주고, 실행시킴&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자기 평가(Self Evaluation)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 생성한 결과를 LLM 스스로 평가하게 하여 오류를 잡거나 결과를 향상시키는 기법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 답변을 맞게 했는지 단계적으로 생각해보세요. 당신의 답변이 틀렸다면 틀린 이유를 설명하세요. 설명만 작성하세요. 그 외의 부가적인 말은 하지마세요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 프롬프트 엔지니어나 자율 실행 에이전트와 같이 AI가 스스로를 평가하고 향상시키는 기술에 많이 사용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 예시를 제공하고 생각하게 만들기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전문가 역할극 프롬프팅(Expert Prompting)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에게 전문가로서 응답하도록 요청하는 방법&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;위키피디아에 따르면(According to Wikipedia)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &amp;lsquo;위키피디아를 참조해서 답하세요&amp;rsquo;라고 하는 것만으로도 높은 성능을 얻을 수 있는 기법&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;h3 data-ke-size=&quot;size23&quot;&gt;지식 생성 프롬프팅(Generated Knowledge Prompting)&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;검색 증강 생성(Retrieval Augmented Generation, RAG)&lt;/h3&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;복잡한 수식을 계산하는 수학 묹 풀이 엔진을 통해 결과를 계산해서 가져오는 것도 RAG&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 전략을 짜고 스스로 평가하기&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생각 트리(Tree-of-Thought)&lt;/h3&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;/h4&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;자기 일관성 프롬프팅: 여러 추론 경로를 고려하여 가장 일관된 답변을 선택&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각 트리 프롬프팅: 여러 단계의 추론 경로를 다양하게 탐색하며 최적의 추론 경로를 찾아나감. 각 노드(상자)는 하나의 추론 단계를 대표&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각 트리는 매우 많은 생성 단계를 거치기에 일반적으로 사용하기는 어렵지만, 다른 기법과 혼합해 사용하면 극도로 높은 성능을 기대 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;계획-풀이 프롬프팅(Plan-and-Solve Prompting)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 작업을 더 작은 하위 작업으로 계획을 세우고 그 계획에 따라 하위 작업을 수행하거나 평가하면서 전체 문제를 해결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 미리 풀어야 할 하위 문제를 모두 생성해두고 문제를 품&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;자동 프롬프트 엔지니어(Automatic Prompt Engineer)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 프롬프트를 자동으로 생성하는 기법&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;h3 data-ke-size=&quot;size23&quot;&gt;지금까지 배운 프롬프트 기법의 핵심&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;예시를 제공&lt;/li&gt;
&lt;li&gt;생각을 많이 하게 함&lt;/li&gt;
&lt;li&gt;문제 풀이 전략을 세우게 함&lt;/li&gt;
&lt;li&gt;스스로 평가&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 포맷팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력하는 포맷을 지정하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) 9 + 10 x 3을 계산하세요. 다음 포맷으로 답변하세요. Answer: {number}&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특별한 순서 없이 앞에 대시나 점 등으로 표현하는 방법, 순서(1,2,3)로 표현하는 방법&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Key-Value Pair&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에는 Key 값, 뒤에는 Value 값&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표 형식으로 출력&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마크다운&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구조화된 문서를 작성할 때 HTML 태그 대신 간단한 텍스트 형식으로 구성해 이를 쉽게 HTML로 변환할 수 있도록 정한 규칙&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;YAML&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 쉽게 읽을 수 있는 데이터 직렬화 언어&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JSON&lt;/h3&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. 프롬프트 체이닝&lt;/h2&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;h3 data-ke-size=&quot;size23&quot;&gt;단순 프롬프트 연결하기&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;이전 결과에 따른 분기 처리하기&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;결과 내 유사 체이닝 효과 주기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 좋은 프롬프트 만들기&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;지시문을 명확하게 만들기&lt;/li&gt;
&lt;li&gt;적절한 예시를 제공하기&lt;/li&gt;
&lt;li&gt;모델에게 생각할 시간을 줌&lt;/li&gt;
&lt;li&gt;작업을 하위 작업으로 분해&lt;/li&gt;
&lt;li&gt;적절한 컨텍스트를 제공&lt;/li&gt;
&lt;li&gt;프롬프트 엔지니어링 기법이 작동하지 않는 상황도 고려&lt;/li&gt;
&lt;li&gt;프롬프트를 구조화하여 작성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체적인 프롬프트를 다음과 같이 구성하자&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;작업의 목적 및 배경 설명&lt;/li&gt;
&lt;li&gt;컨텍스트&lt;/li&gt;
&lt;li&gt;구체적인 질문이나 지시 및 출력 형식이나 가이드&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원하는 결과가 잘 나오지 않는다면?&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;지시문을 반복해서 사용&lt;/li&gt;
&lt;li&gt;지시문이나 컨텍스트의 위치를 바꿈&lt;/li&gt;
&lt;li&gt;지시문의 단어를 다른 단어로 바꿈&lt;/li&gt;
&lt;/ol&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;</description>
      <category>Study/프롬프트 엔지니어링</category>
      <category>ai</category>
      <category>LLM</category>
      <category>오블완</category>
      <category>최고의 프롬프트 엔지니어링 강의</category>
      <category>티스토리챌린지</category>
      <category>프롬프트 엔지니어링</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/150</guid>
      <comments>https://quickchabun.tistory.com/150#entry150comment</comments>
      <pubDate>Mon, 25 Nov 2024 01:40:41 +0900</pubDate>
    </item>
    <item>
      <title>[프롬프트 엔지니어링] AI와 LLM에 대하여</title>
      <link>https://quickchabun.tistory.com/149</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;김진중(골빈해커)님의 &lt;b&gt;'최고의 프롬프트 엔지니어링 강의'&lt;/b&gt;를 읽고 정리한 글입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;0.&amp;nbsp; 소프트웨어의 발전&lt;/h2&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;소프트웨어 1.0: 데이터를 알고리즘으로 조작해서 결과를 도출하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 2.0: 데이터를 학습시킨 머신러닝 모델을 만들고, 이 모델을 통해 결과를 도출하는 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 3.0: 머신러닝 모델을 프롬프트로 제어하는 것&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;소프트웨어 1.0은 결정론적인 방법 &amp;rarr; 입력값에 대해 항상 동일한 출력값 반환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소프트웨어 2.0은 머신러닝 모델이 상황에 맞게 스스로 논리 구조를 생성하는 비결정론적 방법&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;프롬프트 엔지니어링 = 자연어로 컴퓨터와 상호작용하는 방법&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.&amp;nbsp; AI의 구분&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI는 규칙 기반 AI와 머신러닝으로 구분&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;규칙 기반 AI&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;머신 러닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 바나나 사진을 보여주고 이게 바나나라고 알려주는 것. 그리고 AI는 바나나의 특징을 직접 추출하고 기억&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레이블링: 사람이 &amp;ldquo;이게 바나나야&amp;rdquo;라고 알려 주는 것&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;지도 학습(Supervised Learning)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 레이블링한 데이터를 학습하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 사람이 AI에게 명확한 지시와 가이드라인을 제공하여 학습시키는 방법&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;비지도 학습(Unsupervised Learning)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 바나나라고 알려주지 않고, 사진을 먼저 보여주고, AI가 먼저 특징을 파악하는 방식&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인공 신경망을 학습시키는 방법으로, 사람의 뇌 작동 방식을 모방해서 만든 기술&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력값(X)을 가져오면 가중치(W)를 곱한 값을 출력(Y)해 다양한 방법으로 계속 연결&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lsquo;신선한 바나나&amp;rsquo;의 척도가 되는 조건에는 높은 가중치, 그렇지 않은 조건에는 낮은 가중치를 적용하는 방식&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;lsquo;피처 엔지니어링&amp;rsquo; 과정이 전통적인 머신 러닝에 비해 적게 필요하다.&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;/h3&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;rarr; 피처 엔지니어링 &amp;rarr; 피처 &amp;rarr; 전통적인 머신 러닝 모델 &amp;rarr; 출력&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;rarr; 뉴럴넷 기반 학습(딥러닝) &amp;rarr; 출력&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;딥러닝은 극도로 많은 데이터를 기반으로 AI 모델을 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 데이터의 특징을 표현하는 뉴런과 층을 수없이 많이 쌓는다.&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;대규모 언어 모델(LLM)은 수십억에서 수조 개 이상의 많은 뉴런을 가진 신경망에 방대한 자연어 데이터를 학습시키는 모델&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. LLM(Large Language Model)&lt;/h2&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;/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;2018년에 개발된 BERT가 LLM의 시초격인 모델&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 구글에서 개발한 인공 신경망 구조인 트랜스포머가 BERT와 GPT를 만든 기반 기술&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;ex) &amp;ldquo;This movie was not bad at all&amp;rdquo;에서 &amp;lsquo;not&amp;rsquo;과 &amp;lsquo;bad&amp;rsquo; 사이의 관계를 이해하여 이 두 단어가 긍정적인 의미임을 파악&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ChatGPT의 출시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2022년 초 OpenAI에서 &amp;lsquo;InstructGPT&amp;rsquo;라는 GPT-3의 개량판 출시&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Instruct Data와 RLHF(Reinforcement Learning from Human Feedback) 사용&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;Instruct Data&lt;/h3&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;h3 data-ke-size=&quot;size23&quot;&gt;RLHF(Reinforcement Learning from Human Feedback)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-3가 만들어 낸 결과를 사람이 평가하게 한 다음 그 평가 점수와 수정된 내용을 기반으로 스스로가 다시 학습하도록 한 방식&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 기술들을 활용한 ChatGPT가 출시되자, 세상이 바뀌기 시작했다.&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;자기회귀 모델(Autoregressive model)&lt;/h3&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Attention Mechanism(어텐션 메커니즘)&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;(최근에는 모델의 크기를 키우는 것보다 양질의 데이터를 최대한 더 많이 넣는 것이 추세)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. LLM 성능을 향상시킨 주요 기술&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 데이터 학습&lt;/h3&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;LLM은 비정형 데이터를 정형 데이터로, 정형 데이터를 비정형 데이터로 변환할 수 있는 능력이 중요&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;h3 data-ke-size=&quot;size23&quot;&gt;인스트럭션 튜닝(Instruction Tuning)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명령이나 지시 형태로 표현된 텍스트를 이해한 뒤 해당 작업을 수행하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 대량의 텍스트를 학습한 기본 모델을 만든 후 지시와 결과물의 쌍으로 된 인스트럭트 데이터를 학습시켜서 튜닝&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;h3 data-ke-size=&quot;size23&quot;&gt;RLHF&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에 언급되었다시피, GPT가 생성한 결과를 사람이 평가해서 점수를 주거나 결과를 수정하고, 이후사람이 평가한 내용을 바탕으로 스스로 다시 학습하도록 만드는 기술&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 모델 자가 학습의 기반&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 윤리적 가이드라인을 더 잘 준수: 얼라인먼트(alignment)&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;RLHF의 과정&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;프롬프트 데이터셋에서 여러 프롬프트 샘플을 추출&lt;/li&gt;
&lt;li&gt;이 샘플은 초기 언어 모델에 입력되어 프롬프트와 관련된 텍스트를 생성&lt;/li&gt;
&lt;li&gt;생성된 텍스트는 사람에 의해 평가되며, 각각의 텍스트는 품질에 따라 점수를 받음&lt;/li&gt;
&lt;li&gt;이 점수를 통해 보상(선호도) 모델을 훈련&lt;/li&gt;
&lt;li&gt;보상 모델을 사용하여 언어 모델을 계속 훈련시켜 사람들이 높은 점수를 준 텍스트와 유사한 품질의 답변을 더 잘 생성하도록 함&lt;/li&gt;
&lt;/ol&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미지와 소리, 텍스트 등과 같이 서로 다른 형태의 데이터를 동시에 학습시킴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; LLM이 더 풍부한 이해 능력을 가지게 됨&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;NAS(Neural Architecture Search): 인공 신경망 모델을 만드는 AI&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; NAS에도 LLM을 적용 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 인경 신경망 후보를 제시하고 평가하는 과정을 반복해 인공 신경망 구조를 개선하고 성능 향상 가능&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. LLM, 특이점의 시작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT는 사람의 말을 이해한 것을 바탕으로 코드를 생성하는 기능인 &amp;lsquo;코드 인터프리터&amp;rsquo;라는 도구를 사용&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;rarr; 데이터를 따로 학습시키거나 전처리하는 과정 없이도 기존 데이터를 참고해 요구 사항에 따른 적절한 액션까지 수행하는 것이 가능&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;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머신 러닝 프로그램을 사람들이 실제로 사용하려면 기나긴 연구 개발 과정을 거쳤어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 지금 LLM을 통해 만드는 프롬프트 기반 모델의 경우 프롬프트를 개발하고 배포하기까지 매우 짧은 시간이 소요&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;레이블링 필요 X&lt;/li&gt;
&lt;li&gt;데이터를 분석할 필요 X&lt;/li&gt;
&lt;li&gt;최신 데이터 바로 반영 가능&lt;/li&gt;
&lt;/ol&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;h3 data-ke-size=&quot;size23&quot;&gt;전통적인 머신러닝 개발&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터를 모아 훈련 데이터와 평가 데이터로 나눔&lt;/li&gt;
&lt;li&gt;모델링과 훈련 과정을 거침&lt;/li&gt;
&lt;li&gt;훈련을 다 마치면 평가 진행&lt;/li&gt;
&lt;li&gt;모델을 배포할 수 있도록 패키징&lt;/li&gt;
&lt;li&gt;배포 진행&lt;/li&gt;
&lt;/ol&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;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;평가 데이터 수집&lt;/li&gt;
&lt;li&gt;프롬프트 제작&lt;/li&gt;
&lt;li&gt;평가 후 바로 반영&lt;/li&gt;
&lt;/ol&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;&amp;rarr; 프롬프트 엔지니어링만으로도 기존의 머신러닝이 하던 영역을 상당 부분 대체 가능&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;그럼에도 불구하고, 모델링이나 파인튜닝이 필요한 경우&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;숫자를 예측하는 선형 회귀 문제&lt;/li&gt;
&lt;li&gt;대량의 로그성 데이터의 실시간 처리&lt;/li&gt;
&lt;li&gt;특수 목적의 매우 높은 정밀도를 요구하는 문제&lt;/li&gt;
&lt;li&gt;데이터의 최신성이 중요하지 않은 경우&lt;/li&gt;
&lt;li&gt;데이터 보안이 매우 중요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생성 AI 모델의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답변이 정밀하지 못하고 올바른지 확인이 어려움&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 비결정론적인 방법을 사용하기 때문에 발생하는 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; LLM에게 특정 목적에 따른 툴을 제공하거나 최신 정보를 주입하면 어느정도 해결 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷이 등장하면서 엄청나게 많은 규모의 산업이 생겼지만, AI로 인해서 그 열 배에 달하는 규모의 산업이 새로 나타날 것이라는 전망&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(물론 AI하드웨어나 GPT 등의 고성능의 기본 모델을 만드는 파운데이션 모델 회사가 많이 늘어날 것)&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;AI is not going to take your job, The person who uses AI will take your job.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 사고방식을 완전히 LLM으로 트랜스포메이션해야&lt;/p&gt;</description>
      <category>Study/프롬프트 엔지니어링</category>
      <category>ai</category>
      <category>LLM</category>
      <category>오블완</category>
      <category>최고의 프롬프트 엔지니어링 강의</category>
      <category>티스토리챌린지</category>
      <category>프롬프트 엔지니어링</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/149</guid>
      <comments>https://quickchabun.tistory.com/149#entry149comment</comments>
      <pubDate>Sun, 24 Nov 2024 01:06:55 +0900</pubDate>
    </item>
    <item>
      <title>[JS Deep Dive] 24, 25, 26장 정리 - 클로저, 클래스, ES6 함수의 추가 기능에 관하여</title>
      <link>https://quickchabun.tistory.com/148</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;클로저&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합&lt;/p&gt;
&lt;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;&amp;rarr; &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;ldquo;외부 렉시컬 환경에 대한 참조&amp;rdquo;에 저장할 참조값,&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 상위 스코프의 참조를 저장&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;함수 객체의 내부 슬롯 [[Environment]]에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 상위 스코프이며, 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 &amp;ldquo;외부 렉시컬 환경에 대한 참조&amp;rdquo;에 저장될 참조값&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;함수 객체는 내부 슬롯 [[Environment]]에 저장한 렉시컬 환경의 참조, 즉 상위 스코프를 자신이 존재하는 한 기억&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;함수 실행 컨텍스트 생성&lt;/li&gt;
&lt;li&gt;함수 렉시컬 환경 생성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- this 바인딩&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 외부 렉시컬 환경에 대한 참조 결정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 함수 환경 레코드 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 렉시컬 환경의 구성 요소인 외부 렉시컬 환경에 대한 참조에는 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조가 할당&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클로저와 렉시컬 환경&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const x = 1;

// 1
function outer() {
	const x = 10;
	const inner = function () { console.log(x); }; // 2
	return inner;
}

// outer 함수를 호출하면 중첩 함수 inner 반환
// outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거
const innerFunc = outer(); // 3
innerFunc(); // 4 | 결과: 10
&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;outer 함수를 호출하면 outer 함수는 중첩 함수 inner를 반환하고 생명주기 마감&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; outer 함수의 지역 변수 x 또한 생명 주기를 마감&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 지역 변수 x는 더 이상 유효하지 않음&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;하지만 실행 결과(4)는 outer 함수의 지역 변수 x의 값인 10&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;외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수 참조 가능 &amp;rarr; 이러한 중첩 함수를 클로저라 부름&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;클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고, 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(상위 스코프의 어떤 식별자도 참조하지 않는 경우 대부분의 모던 브라우저는 최적화를 통해 상위 스코프를 기억하지 않음)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클로저의 활용&lt;/h3&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;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 카운트 상태 변수
let num = 0;

// 카운트 상태 변경 함수
const increase = function () {
	// 카운트 상태를 1만큼 증가
	return ++num;
};

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;카운트 상태(num 변수의 값)는 increase 함수가 호출되기 전까지 변경되지 않고 유지되어야&lt;/li&gt;
&lt;li&gt;카운트 상태(num 변수의 값)는 increase 함수만이 변경할 수 있어야&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 카운트 상태 변경 함수
const increase = (function () {
	// 카운트 상태 변수
	let num = 0;
	
	// 클로저
	return function () {
		return ++num;
	};
}());

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉시 실행 함수는 호출된 이후 소멸되지만 즉시 실행 함수가 반환한 클로저는 increase 변수에 할당되어 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이 때 즉시 실행 함수가 반환한 클로저는 자신이 정의된 위치에 의해 결정된 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 즉시 실행 함수가 반환한 클로저는 카운트 상태를 유지하기 위한 카운트 상태를 유지하기 위한 자유 변수 num을 언제 어디서 호출하든지 참조하고 변경 가능&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수형 프로그래밍에서 클로저를 활용하는 간단한 예제&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는
// 클로저를 반환

function makeCounter(aux) {
	// 카운트 상태를 유지하기 위한 자유 변수
	let counter = 0;
	
	// 클로저를 반환
	return function () {
		// 인수로 전달받은 보조 함수에 상태 변경을 위임
		counter = aux(counter);
		return counter;
	}
}
// 보조 함수
function increase(n) {
	return ++n;
}

// 보조 함수
function decrease(n) {
	return --n;
}

// 함수로 함수 생성
// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환
const counter = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2

// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에 카운터 상태 연동 x
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;makeCounter 함수는 보조 함수를 인자로 전달받고 함수를 반환하는 고차 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; makeCounter 함수가 반환하는 함수는 자신이 생성됐을 때의 렉시컬 환경인 makeCounter 함수의 스코프에 속한 counter 변수를 기억하는 클로저&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;makeCounter 함수를 호출해 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 가짐&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캡슐화와 정보 은닉&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캡슐화는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉이라고 함.&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;자바스크립트 객체의 모든 프로퍼티와 메서드는 기본적으로 외부에 공개되어 있다. (즉, 객체의 모든 프로퍼티와 메서드는 기본적으로 public)&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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클래스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로타입 기반 객체지향 언어는 클래스가 필요 없는 객체지향 프로그래밍 언어.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6의 클래스가 기존의 프로토타입 기반 객체지향 모델을 폐지하고 새롭게 클래스 기반 객체지향 모델을 제공하는 것은 아님&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 클래스는 함수이며 기존 프토토타입 기반 패턴을 클래스 기반 패턴을 클래스 기반 패턴처럼 사용할 수 있도록 하는 &amp;lsquo;문법적 설탕&amp;rsquo;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 생성자 함수와의 차이&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클래스는 new 연산자 없이 호출하면 에러 발생&lt;/li&gt;
&lt;li&gt;클래스는 상속을 지원하는 extends와 super 키워드를 제공&lt;/li&gt;
&lt;li&gt;클래스는 호이스팅이 발생하지 않는 것처럼 동작&lt;/li&gt;
&lt;li&gt;클래스 내의 모든 코드는 암묵적으로 strict mode가 지정되어 실행됨 &amp;rarr; 해제 불가&lt;/li&gt;
&lt;li&gt;클래스의 construcor, 프로토타입 메서드, 정적 메서드는 모두 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 false&lt;/li&gt;
&lt;/ol&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;클래스는 class 키워드를 사용하여 정의 (파스칼 케이스를 사용하는 것이 일반적)&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;/li&gt;
&lt;li&gt;변수나 자료구조에 저장 가능&lt;/li&gt;
&lt;li&gt;함수의 매개변수에게 전달 가능&lt;/li&gt;
&lt;li&gt;함수의 반환값으로 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 몸체에서 정의할 수 있는 메서드는 constructor(생성자), 프로토타입 메서드, 정적 메서드 세 가지가 있음&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// 클래스 선언문
class Person {
	// 생성자
	constructor(name) {
		// 인스턴스 생성 및 초기화
		this.name = name; // name 프로퍼티는 public하다.
	}
	
	// 프로토타입 메서드
	sayHi() {
		console.log(`Hi: My name is ${this.name}`);
	}
	
	// 정적 메서드
	static sayHello() {
		console.log('Hello!');
	}
}

// 인스턴스 생성
const me = new Person('Lee');

// 인스턴스의 프로퍼티 참조
console.log(me.name); // Lee

// 프로토타입 메서드 호출
me.sayHi(); // Hi! My name is Lee

// 정적 메서드 호출
Person.sayHello(); // Hello!
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스와 생성자 함수의 정의 방식은 형태적인 면에서 매우 유사&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클래스 호이스팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 선언문으로 정의한 클래스는 함수 선언문과 같이 소스코드 평가 과정, 즉 런타임 이전에 먼저 평가되어 함수 객체를 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 클래스가 평가되어 생성된 함수 객체는 생성자 함수로서 호출할 수 있는 함수, 즉 constructor&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 생성자 함수로서 호출할 수 있는 함수는 함수 정의가 평가되어 함수 객체를 생성하는 시점에 프로토타입도 더불어 생성&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;클래스 선언문도 호이스팅이 발생하지만, let, const 키워드로 선언한 변수처럼 호이스팅됨&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인스턴스 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 생성자 함수이며 new 연산자와 함께 호출되어 인스턴스를 생성&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메서드&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. constructor&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;constructor는 인스턴스를 생성하고 초기화하기 위한 특수한 메서드 (이름 변경 불가능)&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;rarr; new 연산자와 함께 클래스를 호출하면 클래스는 인스턴스를 생성&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;constructor는 메서드로 해석되는 것이 아니라 클래스가 평가되어 생성한 함수 객체 코드의 일부가 됨&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로, 클래스의 constructor 메서드와 프로토타입의 constructor 프로퍼티는 직접적인 관련이 없다.)&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;constructor는 생략이 가능하며, 클래스에 빈 constructor가 암묵적으로 정의됨&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;프로퍼티가 추가되어 초기화된 인스턴스를 생성하려면 construcor 내부에서 this에 인스턴스 프로퍼티를 추가한다.&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;constructor는 별도의 반환문을 가지지 않아야한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 프로토타입 메서드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 몸체에서 정의한 메서드는 클래스의 prototype 프로퍼티에 메서드를 추가하지 않아도 기본적으로 프로토타입 메서드가 된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;class Person {
	// 생성자
	constructor(name) {
		// 인스턴스 생성 및 초기화
		this.name = name;
	}
	
	// 프로토타입 메서드
	sayHi() {
		console.log(`Hi: My name ${this.name}`);
	}
}

const me = new Person('Lee');
me.sayHi(); // Hi! my name is Lee
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성자 함수와 마찬가지로 클래스가 생성한 인스턴스는 프로토타입 체인의 일원이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국, 클래스는 생성자 함수와 같이 인스턴스를 생성하는 생성자 함수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 정적 메서드&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 메서드는 인스턴스를 생성하지 않아도 호출할 수 있는 메서드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스에서는 메서드에 static 키워드를 붙이면 정적 메서드(클래스 메서드)가 된다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;class Person {
	// 생성자
	constructor(name) {
		// 인스턴스 생성 및 초기화
		this.name = name;
	}
	
	// 정적 메서드
	static sayHi() {
		console.log('Hi!');
	}
}
&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;p data-ke-size=&quot;size16&quot;&gt;정적 메서드는 프로토타입 메서드처럼 인스턴스로 호출하지 않고 클래스로 호출한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 정적 메서드는 클래스로 호출한다.
// 정적 메서드는 인스턴스 없이도 호출할 수 있다.
Person.sayHi(); // Hi!
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 정적 메서드와 프로토타입 메서드의 차이&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;정적 메서드와 프로토타입 메서드는 자신이 속해 있는 프로토타입 체인이 다르다.&lt;/li&gt;
&lt;li&gt;정적 메서드는 클래스로 호출하고 프로토타입 메서드는 인스턴스로 호출한다.&lt;/li&gt;
&lt;li&gt;정적 메서드는 인스턴스 프로퍼티를 참조할 수 없지만 프로토타입 메서드는 인스턴스 프로퍼티를 참조할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 내부에서 인스턴스 프로퍼티를 참조할 필요가 있다면 this를 사용해야 하며, 이러한 경우 프로토타입 메서드로 정의해야&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 메서드 내부에서 인스턴스 프로퍼티를 참조해야 할 필요가 없다면 this를 사용하지 않게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 클래스에서 정의한 메서드의 특징&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;function 키워드를 생략한 메서드 축약 표현을 사용한다.&lt;/li&gt;
&lt;li&gt;객체 리터럴과는 다르게 클래스에 메서드를 정의할 때는 콤마가 필요없다.&lt;/li&gt;
&lt;li&gt;암묵적으로 strict mode로 실행된다.&lt;/li&gt;
&lt;li&gt;for &amp;hellip; in 문이나 Object.keys 메서드 등으로 열거할 수 없다.&lt;/li&gt;
&lt;li&gt;내부 메서드 [[Construct]]를 갖지 않는 non-constructor &amp;rarr; new 연산자와 함께 호출 불가능&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클래스의 인스턴스 생성 과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;인스턴스 생성과 this 바인딩&lt;/li&gt;
&lt;li&gt;인스턴스 초기화&lt;/li&gt;
&lt;li&gt;인스턴스 반환&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로퍼티&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스 프로퍼티는 constructor 내부에서 정의해야한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;접근자 프로퍼티&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;접근자 프로퍼티는 자체적으로는 값을 갖지 않고 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 사용하는 접근자 함수로 구성된 프로퍼티&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; getter 함수와 setter 함수로 구성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getter는 호출하는 것이 아니라 프로퍼티처럼 참조하는 형식으로 사용, 참조 시에 내부적으로 getter가 호출&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;setter도 호출하는 것이 아니라 프로퍼티처럼 값을 할당하는 ㅎ여식으로 사용하며, 할당 시에 내부적으로 setter가 호출&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클래스 필드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 필드는 클래스 기반 객체지향 언어에서 클래스가 생성할 인스턴스의 프로퍼티를 가리키는 용어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 몸체에서 클래스 필드를 정의하는 경우 this에 클래스 필드를 바인딩해서는 안된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; this는 클래스의 constructor와 메서드 내에서만 유효&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;클래스 필드를 참조하는 경우 자바스크립트에서는 this를 반드시 사용해야 한다.&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;클래스 필드에 초기값을 할당하지 않으면 undefined를 갖는다.&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;인스턴스를 생성할 때 클래스 필드를 초기화를 해야한다면 constructor 내부에서 클래스 필드를 참조하여 초기값을 할당해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수는 일급 객체이므로 함수를 클래스 필드에 할당 가능 &amp;rarr; 클래스 필드를 통해 메서드 정의 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 클래스 필드에 함수를 할당하는 경우, 이 함수는 인스턴스 메서드가 된다. &amp;rarr; 권장 x&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상속에 의한 클래스 확장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상속에 의한 클래스 확장은 기존 클래스를 상속받아 새로운 클래스를 확장하여 정의하는 것&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스는 상속을 통해 다른 클래스를 확장할 수 있는 문법인 extends 키워드가 기본적으로 제공됨&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;// 수퍼클래스
class Base {}

// 서브클래스
class Derived extends Base {}

&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;위 예제의 클래스에는 다음과 같이 암묵적으로 constructor가 정의&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;// 수퍼클래스
class Base {
	constructor() {}
}

// 서브클래스
class Derived extends Base {
	constructor(...args) { super(...args); }
}

const derived = new Derived();
console.log(derived);
&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;super를 호출하면 수퍼클래스의 constructor(super-constructor)를 호출&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;// 수퍼클래스
class Base {
	constructor(a, b) {
		this.a = a;
		this.b = b;
	}
}

// 서브클래스
class Derived extends Base {
	constructor(a, b, c) {
		super(a,b);
		this.c = c;
	}
}

const derived = new Derived(1,2,3);
console.log(derived; // Derived {a: 1, b: 2, c: 3}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;서브클래스에서 constructor를 생략하지 않는 경우 서브클래스의 constructor에서는 반드시 super를 호출해야&lt;/li&gt;
&lt;li&gt;서브클래스의 constructor에서 super를 호출하기 전에는 this를 참조할 수 없다.&lt;/li&gt;
&lt;li&gt;super는 반드시 서브클래스의 constructor에서만 호출해야&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메서드 내에서 super를 참조하면 수퍼클래스의 메서드를 호출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;extends 키워드를 사용하여 표준 빌트인 생성자 함수를 확장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) class MyArray extends Array&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ES6 함수의 추가 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6 이전의 모든 함수는 이반 함수로서 호출할 수 있는 것은 물론 생성자 함수로서 호출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; callable이면서 constructor&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6 사양에서 메서드는 메서드 축약 표현으로 정의된 함수만을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6 사양에서 정의한 메서드(이하 ES6 메서드)는 인스턴스를 생성할 수 없는 non-constructor&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6 메서드는 자신을 바인딩한 객체를 가리키는 내부 슬롯 [[HomeObject]]를 가짐.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ES6 메서드가 아닌 함수는 super 키워드 사용 불가능&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;화살표 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표 함수는 function 키워드 대신 화살표를 사용하여 기존의 함수 정의 방식보다 간략하게 함수를 정의할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매개변수가 없는 경우 소괄호 ()를 생략할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) const arrow = () &amp;rArr; { &amp;hellip; };&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 리터럴을 반환하는 경우 객체 리터럴을 소괄호 ()로 감싸 주어야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) const create = (id, content) &amp;rArr; ({ id, content });&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표 함수도 일급 객체이므로 Arrray.prototype.map, Array.prototype.filter, Array.prototype.reduce같은 고차 함수에 인수로 전달할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;// ES5
[1,2,3].map(function (v) {
	return v * 2;
});

// ES6
[1,2,3].map(v =&amp;gt; v * 2); // -&amp;gt; { 2, 4, 6 }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;화살표 함수와 일반 함수의 차이&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;화살표 함수는 인스턴스를 생성할 수 없는 non-constructor&lt;/li&gt;
&lt;li&gt;중복된 매개변수 이름 선언 불가&lt;/li&gt;
&lt;li&gt;화살표 함수는 함수 자체의 this, arguments, super, &lt;a href=&quot;http://new.target&quot;&gt;new.target&lt;/a&gt; 바인딩을 갖지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;this&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표 함수의 this는 일반 함수의 this와 다르게 동작&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 정의할 때 this에 바인딩할 객체가 정적으로 결정되는 것이 아니고, 함수를 호출할 때 함수가 어떻게 호출되었는지에 따라 this에 바인딩할 객체가 동적으로 결정됨&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;화살표 함수는 함수 자체의 this 바인딩을 갖지 않음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 따라서 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; lexical this&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;&amp;rarr; 메서드를 정의할 때는 ES6 메서드 축약 표현으로 정의한 ES6 메서드를 사용하는 것이 좋다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// bad
const person = {
	name: 'Lee',
	sayHi: () =&amp;gt; console.log(`Hi ${this.name}`)
};

// good
const person = {
	name: 'Lee',
	sayHi() {
		console.log(`Hi ${this.name}`);
	}
}
	
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;화살표 함수는 함수 자체의 super 바인딩을 갖지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 화살표 함수 내부에서 super를 참조하면 this와 마찬가지로 상위 스코프의 super를 참조&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;화살표 함수는 함수 자체의 arguments 바인딩을 갖지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 화살표 내부에서 arguments를 참조하면 this와 마찬가지로 상위 스코프의 arguments를 참조&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rest 파라미터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rest 파라미터는 매개변수 이름 앞에 세개의 점 &amp;hellip;을 붙여서 정의한 매개변수를 의미&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; Rest 파라미터는 함수에 전달된 인수들의 목록을 배열로 전달받음&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;Rest 파라미터는 단 하나만 선언 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rest 파라미터는 함수 정의 시 선언한 매개변수 개수를 나타내는 함수 객체의 length 프로퍼티에 영향을 주지 않음&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function baz(x, y, ...rest) {}
console.log(baz.length); // 2
&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;ES6에서는 rest 파라미터를 사용하여 가변 인자 함수의 인수 목록을 배열로 직접 전달받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 이를 통해 유사 배열 객체인 arguments 객체를 배열로 변환되는 번거로움을 피할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;매개변수 기본값&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수를 호출할 때 매개변수의 개수만큼 인수를 전달하는 것이 바람직하지만 그렇지 않은 경우에도 에러가 발생x &amp;rarr; 자바스크립트 엔진이 매개변수의 개수와 인수의 개수를 체크하지 않기 때문&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;pre class=&quot;scilab&quot;&gt;&lt;code&gt;function sum(x = 0, y = 0) {
	return x + y;
}

console.log(sum(1,2)); // 3
console.log(sum(1)); // 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Study/Modern JS Deep Dive</category>
      <category>ES6</category>
      <category>오블완</category>
      <category>자바스크립트</category>
      <category>클래스</category>
      <category>클로저</category>
      <category>티스토리챌린지</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/148</guid>
      <comments>https://quickchabun.tistory.com/148#entry148comment</comments>
      <pubDate>Sun, 17 Nov 2024 23:35:14 +0900</pubDate>
    </item>
    <item>
      <title>우아한테크코스 7기 프리코스 최종 회고 (웹 프론트엔드)</title>
      <link>https://quickchabun.tistory.com/147</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. 언젠가 한 번 지원해야겠다 생각했던&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우아한테크코스에 대해서는 옛날부터 들어서 알고 있었다. 내가 아는 지인이 우테코에 다니기도 했었고. 대학에서 컴퓨터공학부 수업을 듣고 있기는 했지만, 웹 프론트엔드에 대한 수업은 많지 않았기에 우테코에 들어가서 1년간 제대로 배우면 좋겠다하는 생각을 가지고 있었다. (물론 학교에서의 수업이 중요하지 않다는 의미는 아니다). 또 합격하지는 못하더라도 4주 동안 진행되는 프리코스를 통해 얻어가는 것이 많다고 하기에 더욱 기대가 되었었다.&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;그리고 시간은 흘러 우아한테크코스 7기 입학설명회 영상이 업로드가 되었다. 이번 7기의 주제는 &amp;lsquo;&lt;b&gt;메타인지&lt;/b&gt;&amp;rsquo;였다. 메타인지? 어디선가 들어봤지만 정확히 무슨 뜻인지는 잘 몰랐다. 메타인지는 아는 것과 알지 못하는 것을 구분할 수 있는 능력으로, 자신의 인지 과정을 객관적으로 인지하는 능력으로 메타인지가 높으면 본인이 어느 위치에 있는지 잘 파악할 수 있기 때문에 목표를 세우고 이루는데 큰 도움이 된다. 내가 메타인지가 높았나 생각을 해보았다. 작업을 하다 때때로 길을 잃고 헤매는 경험이 많았기에 메타인지를 길러야한다는 사실을 알게 되었다. 그래서 자기소개서에서 목표를 작성하는 란에 &lt;b&gt;&amp;lsquo;메타인지에 대한 의식&amp;rsquo;&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;그리고 &lt;b&gt;&amp;lsquo;프리코스를 해치우지 않는다&amp;rsquo;&lt;/b&gt;라는 목표도 세웠다. 사실 학업을 병행하면서 프리코스를 진행하기에 여유롭게 프리코스를 진행할 수 있는 상황이 아니리라 예상이 되었다(프리코스 1주차가 중간고사와 겹치기도 했고). 하지만 프리코스 미션을 끝내는데에만 집중한다면 몰입을 하지 못하고 대충대충할 것이 뻔했기 때문에 마음이 조급해지더라도 차근차근 미션을 진행해야 겠다고 다짐했었다. 자기소개서를 작성하며 생각이 정리가 되었다. 프리코스가 시작하기 하루 전에 프리코스 커뮤니티(디스코드)가 열렸고, 디스코드에 자기소개서를 작성하며 느낀 바를 짧게 작성해서 올렸다. 과연 어떤 미션이 나올까 기대가 되었다. 그리고 하루가 지나, 프리코스 1주차가 시작되었다.&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;761&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boaZz5/btsKK1Al6hj/4LXOrS9oPAaY557Z6smjk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boaZz5/btsKK1Al6hj/4LXOrS9oPAaY557Z6smjk0/img.png&quot; data-alt=&quot;프리코스 커뮤니티(디스코드)에 작성했던 짧은 글의 일부&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boaZz5/btsKK1Al6hj/4LXOrS9oPAaY557Z6smjk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboaZz5%2FbtsKK1Al6hj%2F4LXOrS9oPAaY557Z6smjk0%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;354&quot; height=&quot;389&quot; data-origin-width=&quot;761&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;프리코스 커뮤니티(디스코드)에 작성했던 짧은 글의 일부&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 프리코스를 진행하며 배운 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나의 예상과는 다르게, html, css를 활용한 뷰 구현은 아예 없었다. React를 사용하지도 않았다. 오직 Javascript를 활용해서 특정한 내용을 입력하면 요구사항을 반영한 출력값이 도출되어야하는 프로그램을 만들어야했다. 대학교 과제같다는 느낌도 들었다. 특징이라면 어떤 내용을 예외처리를 해야하는지가 두루뭉술하게 서술되어 있어서 생각을 많이 해야했다는 점? 1주차, 2주차&amp;hellip; 한 주차 미션이 끝날 때마다 공통 피드백이 업로드되었고, 그 다음 주 미션을 진행할 때에는 피드백을 바탕으로 더 발전된 코드를 작성하려고 노력했다(요구사항이 발전된 코드를 요구하기도 했고). 배운 점들을 짧게 정리해보자면,&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;a. 커밋 메시지 컨벤션(AngularJS commit convention)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프리코스를 진행하기 전까지는 개발을 할 때 일관성 없는 커밋을 진행했었다. 이와 달리 프리코스 미션을 진행할 때에는 &lt;b&gt;AngularJS Commit convention&lt;/b&gt;을 배우고 해당 컨벤션을 활용하여 커밋을 작성하였다. 이 컨벤션을 활용한 후, 깃 로그를 통하여 지금까지 커밋별로 어떤 작업을 어디서 진행했었는지 쉽게 파악할 수 있었다. 또한 기능별로 커밋을 작성했는데, 언제 커밋을 해야되는지에 대한 기준이 있으니 더욱 일관적인 개발을 진행할 수 있었다. 이 컨벤션은 다른 프로젝트를 진행하면서도 활용하고 있고, 앞으로도 계속 활용할 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;b. 단일 책임 원칙(SRP)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 메서드가 하나의 역할을 해야한다는 &amp;lsquo;&lt;b&gt;단일 책임 원칙&lt;/b&gt;&amp;rsquo;은 객체지향적이고 깔끔한 코드에 필수적인 요소이다. 단일 책임 원칙을 적용한 후 특정 상황에서 어떤 메서드를 활용해야 되는지가 더욱 명확해졌으며, 문제가 생겼을 때 어디를 수정해야되는지를 찾는 일이 더욱 쉬워졌다. 프리코스가 끝날 때까지 이 원칙을 계속 지키려고 노력했다. 그리고 앞으로 다른 프로젝트에서 코딩을 할 때에도 이 원칙은 철저히 지키려고 노력할 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;c. MVC 패턴(Model-View-Controller)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 웹 프론트엔드 개발을 진행하면서 MVC 패턴을 활용할 일이 많지 않았다. 웹 프론트엔드는 실시간 UI 업데이트에 대한 고려를 많이 해야되고, MVC 패턴이 이에 적합하지는 않았기 때문이다. 하지만 프리코스에서는 실시간 UI 업데이트가 많이 필요하지 않고, 객체지향적인 코드를 작성해야되기 때문에 도입했고 이는 많은 도움이 되었다. 사실 프리코스 이전에는 디자인 패턴에 대한 고려를 많이 하지 않았었는데, 이번 프리코스를 통하여 프로젝트를 효율적으로 설계하는 방법을 배울 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;d. getter의 사용을 지양하고 객체에 메시지를 전달하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향은 객체가 스스로 일을 하도록 하는 프로그래밍이다. 모든 멤버변수에 getter를 생성해 놓고 상태값을 꺼내 그 값으로 객체 외부에서 로직을 수행한다면, 객체가 로직(행동)을 갖고 있는 형태가 아니고 메시지를 주고 받는 형태도 아니게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처: &lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2020-04-28-ask-instead-of-getter/&quot;&gt;https://tecoble.techcourse.co.kr/post/2020-04-28-ask-instead-of-getter/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(피드백에 있었던 링크였던걸로 기억한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;2주차 미션은 필드를 private로 선언하지 않았었고, 3주차 미션은 필드를 private으로 선언했지만 getter를 많이 활용했었는데, getter의 사용을 지양하고 객체에 메시지를 전달해야한다는 피드백을 확인하게 되었었다. 그리고 4주차 미션에는 getter의 사용을 줄이고 객체 스스로 로직을 수행하게 구현하려 노력했다. 이 점들을 지키려고 노력하다보니 코드가 조금 길어졌지만 위 법칙을 통하여 객체의 활용을 더 적극적으로 유도해낼 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;e. 객체지향적인 개발&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 프론트엔드가 사용하는 Javascript는 Java나 Kotlin에 비해서는 객체지향에 친화적인 언어가 아니라고 생각한다. class가 도입된 것도 비교적 최근으로 알고 있다. 그렇다면 객체지향에 대해서는 신경을 안써도 되는 것일까. 아니라고 생각한다. 객체지향은 개발에 필수와 같은 존재이며, Javascript를 통한 웹 프론트엔드 개발이라하더라도 예외는 아니라고 생각한다. 백엔드 개발에 비해서 중요도가 낮기는 하지만, 객체지향은 분명히 사용될 것이라고 생각한다. 이번 프리코스를 통해서 객체지향적인 개발이 무엇인지 배울 수 있게 된 것은 큰 수확이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;f. 상수 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일에 값을 하드코딩하지 않고, 상수들을 따로 분리해서 활용하면 휴먼 에러를 방지할 수 있고 가독성 또한 증가한다. constants 폴더를 만들고 역할에 따른 파일들을 생성한 다음, 그 파일들에 상수들을 작성하고, 상수를 활용해야하는 곳에 상수들을 import해서 적용했다. 상수 분리를 하면서 이 작업이 프로젝트 개발에 매우 유용한 방법이라고 생각을 하였고, 주차가 지날수록 상수 분리에 더 신경을 썼던 것 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;g. Jest 활용법(테스팅 라이브러리)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 지금까지 짠 코드에 대한 테스트를 진행하게 되면 미처 발견하지 못했던 에러를 발견할 수 있게 된다. 테스트 라이브러리 Jest에 대하여 알고는 있었지만 실제로 활용해보는 것은 이번이 처음이었으며, 또한 &amp;lsquo;단위 테스트&amp;rsquo;라는 개념에 대하여 미숙했었는데 프리코스를 통해 배우고 또 적용할 수 있었다. 확실한 것은 아니지만 에러에 민감한 실무에서는 코딩을 하고난 후, 바뀐 코드에 대한 테스트를 진행하지 않을까. 어떤 테스트를 진행할까 고민하면서, 또 Jest를 어떻게 활용해야되는지 시행착오를 거치며 테스팅에 대한 실력을 향상시킬 수 있었던 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 4주차 미션에 대한 아쉬움&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1,2,3주차 미션과는 다르게 편의점 프로그램을 구현해야하는 4주차 미션은 내게는 꽤 어렵게 다가왔다. 일단 문제 조건들이 많아서 이해하는데 애를 조금 먹었었다. 4주차 도중에 1박 2일로 MT를 다녀왔기에 시간이 부족한 탓도 있었겠지만, 프로모션 재고를 전부 구매한 다음 일반 재고를 구매하는 기능을 구현하는 부분에서 많은 시간 지체가 있었다(결국 끝까지 구현하는데에는 실패했다). 각 재고들에 대해 독립적으로 객체를 생성했지만 그러면 위와 같은 상황을 대처하는데 힘들다는 사실을 깨닫고 뒤늦게 Map을 활용한 ProductGroup 클래스를 도입했다.&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;10줄 이내로 메서드를 구현해야하다보니 메서드를 잘개 쪼갤 수 밖에 없었고, 이 때문에 한 파일에 코드 줄 길이가 너무 길어졌었다. 따라서 모델들을 다시 여러 모델로 쪼개는 리팩토링을 미션 도중에 진행했었다. 하지만 그럼에도 불구하고, 기능 하나를 구현하거나 수정해야 할 때마다 건드려야 하는 부분이 너무 많고 또 흩어져있었기에 개발 속도가 계속 느려졌다. 결국, 내가 지금 어떤 상태이고 무엇을구현해야되는지에 대한 자각을 잘 하지 못했었던 것이다. 다시 말해 메타인지가 잘 안되었다는 뜻이다.&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;시간은 계속 흘러 어느덧 제출 마감 데드라인이 다가오고 있었다. 구현이 마무리가 되지 않았기에, 검증도, 테스팅도 구현하지 못하였고, 12시에 다다르기 얼마 전에 부랴부랴 마무리하고 회고를 작성했었다. 예제 테스트 결과는 2/4. 너무나도 아쉬웠다. 설계가 미흡했었나하는 생각, 잠을 줄여서라도 시간 투자를 좀 더 했어야했나 하는 생각 등 온갖 생각이 뒤섞였다. 하지만 이미 4주차 미션은 마무리가 되었고, 이것은 프리코스가 끝났다는 뜻이기도 했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lv4dR/btsKKxT1slE/3K6sqJDmXoJhrwcga5M1Rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lv4dR/btsKKxT1slE/3K6sqJDmXoJhrwcga5M1Rk/img.png&quot; data-alt=&quot;4주차 프리코스 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lv4dR/btsKKxT1slE/3K6sqJDmXoJhrwcga5M1Rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLv4dR%2FbtsKKxT1slE%2F3K6sqJDmXoJhrwcga5M1Rk%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;206&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;4주차 프리코스 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 그럼에도 얻어가는 것이 많았던 프리코스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;돌이켜보면 프리코스에 꽤 많은 시간을 쏟았다. 나름대로 기능을 구현하는 것에 재미를 느끼고 시간 가는 줄도 모르고 코딩을 하기도 했다. 이번 프리코스를 통해 얻은 큰 수확은 바로 &amp;lsquo;&lt;b&gt;일관성있고 깔끔한, 가독성 있는 코드를 작성하는 법&amp;rsquo;&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;아무래도 내 예상으로는, 최종 코딩테스트에 진출하지 못할 것 같다. 아쉽기는 하지만, 최선을 다했기에 후회가 들지는 않는다. 단지 합격을 하기에는 실력이 부족하였음을 깨닫고 더 노력해야겠다는 사실을 받아들였다. 한 달 동안 많은 내용들을 배웠고, 한 층 더 성장한 것 같다. 진행했던 프로젝트, 지금 진행 중인 프로젝트, 그리고 진행 예정일 프로젝트들에 프리코스 때 배웠던 내용을 활용할 것이다(아마 대대적인 리팩토링을 진행해야 되지 않을까 고민중이다).&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;프리코스가 끝났다고 널널해지지는 않았다. 목표가 있기에 할 일은 끊임없이 생겨났다. 그래서 잠시 프리코스 최종 회고를 작성하는 것이 조금 늦어졌지만(5일 정도), 회고를 작성하기 전까지는 매듭이 지어진 것이 아니었기에 뒤늦게나마 회고를 작성한다. 좋은 기회를 준 우아한테크코스에 감사하며, 앞으로 계속 발걸음을 내딛으려 한다.&lt;/p&gt;</description>
      <category>우아한테크코스(프리코스)</category>
      <category>오블완</category>
      <category>우아한테크코스</category>
      <category>우아한테크코스 프리코스</category>
      <category>우아한테크코스 후기</category>
      <category>우테코</category>
      <category>우테코 프리코스</category>
      <category>우테코 후기</category>
      <category>티스토리챌린지</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/147</guid>
      <comments>https://quickchabun.tistory.com/147#entry147comment</comments>
      <pubDate>Sat, 16 Nov 2024 02:45:09 +0900</pubDate>
    </item>
    <item>
      <title>우아한테크코스 7기 프리코스 2주차 회고 (웹 프론트엔드)</title>
      <link>https://quickchabun.tistory.com/146</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;중간고사와 프리코스 1주차가 마무리되고, 10월 22일 오후 3시부터 프리코스 2주차가 시작되었다. 사실 화요일부터 바로 2주차 프리코스를 시작하지는 않았고, 프리코스 커뮤니티(디스코드)에서 다른분들의 PR을 보며 리뷰를 하고, 또 내가 1주차 미션 PR 링크를 올려서 리뷰를 받았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 1주차 피드백&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 감사하게도 네 분께서 코드 리뷰를 해주셨다. 리뷰 중 기억해두면 좋을 내용들을 요약해보자면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;변경될 일이 없는 상수값(기본 구분자)는 클래스의 정적 속성이나 상수 변수로 분리하고, 커스텀 구분자만 별도로 관리할 것이 명확&lt;/li&gt;
&lt;li&gt;추출된 숫자 배열은 메서드 내부의 let으로 둬서 더 명확하게 표현하는 것이 좋을 듯&lt;/li&gt;
&lt;li&gt;calculateSum 메서드는 sum만 구현하고 다른 로직은 코드 분리하기&lt;/li&gt;
&lt;li&gt;3번과 비슷한 맥락으로, validateNumber는 숫자 유효성만 확인하고, splitAndExtractNumbers는 단순히 분리된 숫자를 배열로 만들고, 다른 로직은 코드 분리하기&lt;/li&gt;
&lt;li&gt;조건을 변수로 네이밍을 해서 조건절에 변수를 사용하기&lt;/li&gt;
&lt;li&gt;출력 방법을 분리했으니 입력 방법도 분리하기&lt;/li&gt;
&lt;li&gt;str.trim().length === 0; &amp;rarr; !!!str.trim()으로도 표현 가능( !!는 Boolean()과 동일, !!!는 !!의 부정)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 책임 원칙에 따라 하나의 메서드가 하나의 기능만 동작하도록 구현하는 부분이 조금은 미흡하지 않았나 하는 생각이 든다. 또한 조건을 변수로 네이밍하는 것도 괜찮은 방법인 것 같다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 피드백들을 복기하며, 프리코스 2주차를 시작했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 기능 요구사항&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초간단 자동차 경주 게임을 구현한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.&lt;/li&gt;
&lt;li&gt;각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.&lt;/li&gt;
&lt;li&gt;자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.&lt;/li&gt;
&lt;li&gt;사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.&lt;/li&gt;
&lt;li&gt;자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.&lt;/li&gt;
&lt;li&gt;우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다.&lt;/li&gt;
&lt;li&gt;사용자가 잘못된 값을 입력할 경우 &quot;[ERROR]&quot;로 시작하는 메시지와 함께&amp;nbsp;Error를 발생시킨 후 애플리케이션은 종료되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1주차 미션 때에는 모든 로직을 App.js에 담았었다. (사실 파일들을 분리해도 되는건지에 대한 확신이 들지를 않아서 그랬는데, 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;이번에는 구조를 설계하고 폴더와 파일들을 그 구조에 맞게 분리를 해서 코드의 가독성을 늘릴 계획이다.&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;이번 미션에서는 어떤 구조를 적용할까 고민을 해봤는데, MVC 패턴을 적용하면 어떨까하는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 프론트엔드 프로젝트를 진행할 때에는 MVC 패턴이 아닌 MVVM 패턴으로 구현하는데, 이번 미션은 실시간 UI 업데이트가 필요하지 않으므로 ViewModel이 필요가 아닌 것 같아 MVC 패턴을 사용하는 것이 더 좋을 것 같았다.&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;&lt;a href=&quot;https://blog.naver.com/designerkhs/223522670304&quot;&gt;https://blog.naver.com/designerkhs/223522670304&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(MVC 패턴과 MVVM 패턴 관련해서 위 링크의 글을 참고했다)&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;MVC 패턴을 사용해서 폴더 구조를 설계해보았다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt; src
 ┣  controller
 ┃ ┗  gameController.js
 ┣  models
 ┃ ┣  car.js
 ┃ ┗  racingGame.js
 ┣  utils
 ┃ ┣  constants.js
 ┃ ┣  random.js
 ┃ ┗  validator.js
 ┣  views
 ┃ ┣  inputView.js
 ┃ ┗  outputView.js
 ┣  App.js
 ┗  index.js
&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;각 파일마다 무슨 역할을 하는지 README.md에 작성했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;  구현할 기능 목록

###   Model

car.js (자동차 데이터 관리)

- [x] 자동차 상태 객체 생성
- [x] 자동차 상태 업데이트

racingGame.js (게임 상태 관리)

- [x] 게임 상태 객체 생성
- [x] 게임 상태 업데이트
- [x] 우승자(들) 찾기
- [x] 게임 종료 상태 확인

###   View

inputView.js (사용자 입력 처리)

- [x] 자동차 이름 입력 받기
- [x] 입력받은 자동차 이름을 쉼표(,) 기준으로 구분
- [x] 시도 횟수 입력 받기

outputView.js (게임 상태 출력)

- [x] 현재 라운드 상태 출력
- [x] 최종 우승자 출력
- [x] 에러 메시지 출력

###   Controller

gameController.js (게임 로직 제어)

- [x] 자동차 이름 입력받기 (InputView 활용)
- [x] 시도 횟수 입력 받기 (InputView 활용)
- [x] 참가자 정보 검증하기 (Validator 활용)
- [x] 레이싱 게임 객체 생성하기 (RacingGame 활용)
- [x] 자동차들의 이동 처리하기 (RacingGame 활용)
- [x] 라운드 진행 상태 보여주기 (OutputView 활용)
- [x] 우승자 찾기 (RacingGame 활용)
- [x] 우승자 발표하기 (OutputView 활용)

###   Utils

validator.js (입력값 검증)

 ️ 자동차 이름 검증

- [x] 최소 1대 이상의 자동차가 있는지 확인
- [x] 이름이 빈 문자열이나 공백인지 확인
- [x] 이름이 5자 이하인지 확인
- [x] 중복된 이름이 있는지 확인

  시도 횟수 검증

- [x] 빈 값이나 공백이 입력되었는지 확인
- [x] 숫자가 아닌 값이 입력되었는지 확인
- [x] 음수가 입력되었는지 확인
- [x] 0이 입력되었는지 확인
- [x] 소수가 입력되었는지 확인
- [x] 입력된 값이 오버플로우를 발생시키는지 확인

random.js (난수 생성)

- [x] 난수 생성
  - MissionUtils.Random.pickNumberInRange 사용

constants.js (상수 정의)

- [x] 에러 메시지 상수
- [x] 입력 메시지 상수
- [x] 출력 메시지 상수
- [x] 자동차 이름 검증을 위한 테스트 케이스 상수
- [x] 시도 횟수 검증을 위한 테스트 케이스 상수

###   Application

App.js (애플리케이션 진입점)

- [x] 게임의 시작점으로서 GameController 초기화 및 실행
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. Model&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 model을 생성했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;class Car {
  name;

  position;

  constructor(name) {
    this.name = name;
    this.position = 0;
  }

  move(number) {
    if (number &amp;gt;= 4) {
      this.position += 1;
    }
  }

  getPosition() {
    return this.position;
  }

  getName() {
    return this.name;
  }
}

export default Car;

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 car.js에서 Car 클래스를 생성했는데, name, position 필드와 생성자를 만들었고, move 메서드와 getter 함수를 생성했다. 코드를 작성하면서 학교에서 자바 프로그래밍을 배웠을 때의 기억이 떠올랐다. 자바스크립트에서 이렇게 객체 지향적인 코드를 작성해도 될까? 하는 의문이 살짝 들었었지만, 실시간 UI 업데이트가 없이 MVC 모델로 구현하기로 한 만큼 이렇게 구현해도 괜찮다는 생각이 들어 계속 진행했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. View&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;view는 inputView.js와 outputView.js로 구성되어 있었는데, outputView.js에는 포매팅하는 메서드와 출력 메서드를 구현했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import { Console } from '@woowacourse/mission-utils';
import { OUTPUT_MESSAGES } from '../utils/constants.js';

const OutputView = {
  printGameStart() {
    Console.print(OUTPUT_MESSAGES.GAME_START);
  },

  formatCarStatus(car) {
    const position = '-'.repeat(car.getPosition());
    return `${car.getName()} : ${position}`;
  },

  printRoundStatus(cars) {
    cars.forEach((car) =&amp;gt; {
      Console.print(this.formatCarStatus(car));
    });
    Console.print('');
  },

(...)

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. Validator&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Validator에는 검증하는 메서드들이 있다. 밑의 코드에서 확인할 수 있다시피 특정한 기능에 대한 validate 메서드가 있고, 이 기능들을 한번에 실행시켜주는 통합 메서드(validateCarNames)를 만들었다. 이 메서드는 Controller에서 실행된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import OutputView from '../views/outputView.js';
import { ERROR_MESSAGES } from './constants.js';

const Validator = {
  validateCarCount(carNames) {
    if (!carNames || carNames.length === 0) {
      OutputView.throwError(ERROR_MESSAGES.NO_CARS);
    }
  },

  validateEmptyNames(carNames) {
    const hasEmptyName = carNames.some((name) =&amp;gt; name.trim().length === 0);

    if (hasEmptyName) {
      OutputView.throwError(ERROR_MESSAGES.EMPTY_NAME);
    }
  },

  validateNameLength(carNames) {
    const hasLongName = carNames.some((name) =&amp;gt; name.length &amp;gt; 5);

    if (hasLongName) {
      OutputView.throwError(ERROR_MESSAGES.NAME_TOO_LONG);
    }
  },

  validateDuplicateNames(carNames) {
    const uniqueNames = new Set(carNames);

    if (uniqueNames.size !== carNames.length) {
      OutputView.throwError(ERROR_MESSAGES.DUPLICATE_NAME);
    }
  },

  validateCarNames(carNames) {
    this.validateCarCount(carNames);
    this.validateEmptyNames(carNames);
    this.validateNameLength(carNames);
    this.validateDuplicateNames(carNames);
	},
	
	
(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. Controller&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller는 지금까지 View, Model, Validator에서 만든 메서드들을 가져와서 start 메서드에 적용을 해주었다. 이 start 메서드는 App.js의 run에서 실행된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import RacingGame from '../models/racingGame.js';
import Validator from '../utils/validator.js';
import InputView from '../views/inputView.js';
import OutputView from '../views/outputView.js';

class GameController {
  carNames;

  gameRounds;

  racingGame;

  async start() {
    await this.readCarNames();
    this.validateCarNames();
    await this.readGameRounds();
    this.validateGameRounds();
    this.createRacingGame();
    OutputView.printGameStart();

    while (!this.racingGame.isGameFinished()) {
      this.playOneRound();
    }

    this.announceWinners();
  }

  async readCarNames() {
    this.carNames = await InputView.readCarNames();
  }

  validateCarNames() {
    Validator.validateCarNames(this.carNames);
  }

  async readGameRounds() {
    this.gameRounds = await InputView.readGameRounds();
  }

  validateGameRounds() {
    Validator.validateGameRounds(this.gameRounds);
  }

  createRacingGame() {
    this.racingGame = new RacingGame(this.carNames, this.gameRounds);
  }

  playOneRound() {
    this.racingGame.playOneRound();
    this.printRoundStatus();
  }

  printRoundStatus() {
    const cars = this.racingGame.getCars();
    OutputView.printRoundStatus(cars);
  }

(...)

&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 테스트 진행&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ApplicationTest.js에서 코드를 수정해서 검증하고 싶은 부분에 대한 테스트를 진행했다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;##   테스트 목록

### 자동차 이름 검증 테스트

- [x] 자동차 이름이 빈 문자열인 경우 에러를 발생 ([&quot;pobi&quot;, &quot;&quot;])
- [x] 자동차 이름이 공백인 경우 에러를 발생 ([&quot;pobi&quot;, &quot; &quot;])
- [x] 자동차 이름이 5자를 초과하는 경우 에러 발생 ([&quot;pobi&quot;, &quot;abcdef&quot;])
- [x] 중복된 자동차 이름이 있는 경우 에러 발생 ([&quot;pobi&quot;, &quot;jason&quot;, &quot;pobi&quot;])

### 시도 횟수 검증 테스트

- [x] 시도 횟수가 빈 값이나 공백이 입력된 경우 에러 발생 (&quot; &quot;)
- [x] 시도 횟수가 숫자가 아닌 값이 입력된 경우 에러 발생 (&quot;abc&quot;)
- [x] 시도 횟수가 음수가 입력된 경우 에러 발생 (&quot;-1&quot;)
- [x] 시도 횟수가 0이 입력된 경우 에러 발생 (&quot;0&quot;)
- [x] 시도 횟수가 소수가 입력된 경우 에러 발생(&quot;1.5&quot;)
- [x] 시도 횟수가 오버플로우를 발생시키는 값이 입력된 경우 에러 발생 (&quot;Number.MAX_SAFE_INTEGER + 1&quot;)

&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;constants.js에서 테스트 케이스 상수들을 입력했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const CAR_NAME_TEST_CASES = {
  EMPTY_NAME: {
    input: ['pobi', ''],
    errorMessage: `${OUTPUT_MESSAGES.ERROR_PREFIX} ${ERROR_MESSAGES.EMPTY_NAME}`,
  },
  EMPTY_NAME_WITH_SPACE: {
    input: ['pobi', ' '],
    errorMessage: `${OUTPUT_MESSAGES.ERROR_PREFIX} ${ERROR_MESSAGES.EMPTY_NAME}`,
  },
  NAME_TOO_LONG: {
    input: ['pobi', 'abcdef'],
    errorMessage: `${OUTPUT_MESSAGES.ERROR_PREFIX} ${ERROR_MESSAGES.NAME_TOO_LONG}`,
  },
  DUPLICATE_NAME: {
    input: ['pobi', 'jason', 'pobi'],
    errorMessage: `${OUTPUT_MESSAGES.ERROR_PREFIX} ${ERROR_MESSAGES.DUPLICATE_NAME}`,
  },
  VALID_NAMES: {
    input: ['pobi', 'woni', 'jun'],
    expected: ['pobi', 'woni', 'jun'],
  },
};

(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 ApplicationTest.js에서 위 상수들과 test.each를 활용해 테스트 케이스 상수들의 루프를 돌면서 테스트를 진행하였고, 다행히도 테스트들은 무사히 통과되었다.&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;991&quot; data-origin-height=&quot;481&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RIEY6/btsKl1tPMbO/54Fn14on3wqF7GK6moCeK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RIEY6/btsKl1tPMbO/54Fn14on3wqF7GK6moCeK0/img.png&quot; data-alt=&quot;테스트 케이스 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RIEY6/btsKl1tPMbO/54Fn14on3wqF7GK6moCeK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRIEY6%2FbtsKl1tPMbO%2F54Fn14on3wqF7GK6moCeK0%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;736&quot; height=&quot;357&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;481&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;테스트 케이스 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 리팩토링&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 리팩토링이 진행되었다.&lt;/p&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;## ♻️ 리팩토링 목록

- outputView.js에서 printRoundStatus 메서드를 단일 책임 원칙에 따라 두 개의 메서드로 분리
- validator.js에서 기존에 하드코딩되었던 에러 메시지들을 constants.js의 ERROR_MESSAGES로 대체
- inputView.js에서 기존에 하드코딩되었던 입력 메시지들을 constants.js의 INPUT_MESSAGES로 대체
- outputView.js에서 기존에 하드코딩되었던 입력 메시지들을 constants.js의 OUTPUT_MESSAGES로 대체
- constants.js에서 테스트 케이스 상수 CAR_NAME_TEST_CASES, GAME_ROUNDS_TEXT_CASES의 이름들을 ERROR_MESSAGES 값들과 일치 시켜서 혼란을 방지
- constants.js에서 실제 구현 가능한 테스트 케이스만 남도록 NO_CARS 상수 삭제
- constants.js에서 불필요한 상수 제거 및 TEST_DESCRIPTIONS를 test.each 사용에 적합하게 사용
- ApplicationTest.js에서 테스트 설명을 상수들을 활용
- ApplicationTest.js에서자동차 이름 검증과 시도 횟수 검증을 test.each를 활용해서 코드 가독성 향상

&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;대부분이 constants.js에 작성되있는 상수들을 활용하여 하드코딩되었던 메시지들을 대체한 내용이다. 다른 분들의 1주차 코드를 확인했을 때상수를 특정 파일에 저장해놓고 불러오는 방식이 마음에 들었었고, 이번 2주차 때 활용하기로 했다. 이 때 상수명이 명확해야 가독성이 좋으므로 그 부분에 유의했다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const ERROR_MESSAGES = {
  EMPTY_NAME: '자동차 이름은 빈 문자열이나 공백일 수 없습니다.',
  NAME_TOO_LONG: '자동차 이름은 5자 이하만 가능합니다.',
  DUPLICATE_NAME: '자동차 이름은 중복될 수 없습니다.',

  EMPTY_ROUNDS: '시도 횟수는 빈 값이나 공백일 수 없습니다.',
  NOT_A_NUMBER: '시도 횟수는 숫자여야 합니다.',
  NEGATIVE_NUMBER: '시도 횟수는 음수일 수 없습니다.',
  ZERO_ROUNDS: '시도 횟수는 0일 수 없습니다.',
  NOT_INTEGER: '시도 횟수는 정수여야 합니다.',
  OVERFLOW: `시도 횟수는 ${Number.MAX_SAFE_INTEGER}보다 작거나 같아야 합니다.`,
};

export const INPUT_MESSAGES = {
  CAR_NAMES: '경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\\n',
  GAME_ROUNDS: '시도할 횟수는 몇 회인가요?\\n',
};

(...)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 배운 점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-1. some()의 활용&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;validateEmptyNames(carNames) {
    const hasEmptyName = carNames.some((name) =&amp;gt; name.trim().length === 0);

    if (hasEmptyName) {
      OutputView.throwError(ERROR_MESSAGES.EMPTY_NAME);
    }
  },

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증 메서드를 생성할 때 some()을 많이 사용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;some()은 배열의 요소 중 하나라도 주어진 조건을 만족하면 true를 반환하고, 모든 요소가 조건을 만족하지 않으면 false를 반환하는 배열 메서드이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-2. MVC 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 웹 프론트엔드 개발을 하면서 MVC 패턴을 활용할 기회가 없었다. MVC 패턴에 대해 들어본 적은 있지만 확실히 이해를 하지는 못했었는데, 이번 기회를 통해서 MVC 패턴에 대해서(Controller는 무슨 역할을 하는 지 등&amp;hellip;) 알 수 있었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-3. Jest 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차 프리코스 때 Jest를 사용해보았다. describe와 test를 사용하여 테스트를 구조화하고, expect와 toThrow 등의 matcher를 활용하여 다양한 테스트 케이스를 작성하는 방법을 배웠다. 나중에 개발을 진행할 때 Jest를 활용하면 미처 잡아내지 못한 에러에 대한 대응을 할 수 있기 때문에 이 유용한 도구의 사용법을 더 익혀놓아야겠다는 다짐을 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7-4. getter 메서드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프리코스를 진행하면서 getter 메서드를 사용했는데, Car 클래스에서 직접적으로 position이나 name 속성에 접근하지 않고, getPosition()과 getName() 메서드를 통해 접근하도록 구현했다. 이러한 캡슐화를 통해 객체의 내부 상태를 보호하고, 객체의 상태를 안전하게 관리할 수 있다는 점을 알게 되었고, 다음에 클래스 기반으로 또 구현을 할 일이 생기면 getter 메서드를 활용해야겠다는 생각을 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 후기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 질문에 대한 회고를 진행하라는 조건이 있었기에, 각 질문에 대해 깊게 생각해보고 답변을 작성하였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8-1. 지원서에 작성한 목표를 얼마나 달성하고 있다고 생각하는지에 대한 질문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지원서 목표에 작성 했던 목표를 크게 2가지로 나눈다면 바로 &amp;lsquo;메타인지에 대한 의식&amp;rsquo;과 &amp;lsquo;프리코스를 해치우지 말자&amp;rsquo;입니다. 프리코스 1,2주차를 진행하며 개발을 진행하기 전에 먼저 구현할 기능들이 무엇인지 생각하고 설계를 진행했습니다. README.md에 작성된 기능 옆에 체크박스를 놓고 해당 기능이 구현될 때마다 체크박스를 채웠습니다. 기능들을 하나씩 구현하면서 제가 목표했던 바에 몇 퍼센트 정도 다가갔는지 확인할 수 있었습니다. 또 학습적인 측면에서 Javascript에 대해 제가 얼마나 부족했었는지 알게 되는 계기가 되었습니다. class 내에서 메서드를 생성하려면 명시적으로 function을 작성하면 안된다는 기초적인 사실도 모르고 있었고, 알고 있었다고 생각했던 reduce나 some의 사용법 또한 헷갈려했습니다. 돌이켜보면 react나 새로운 라이브러리(프레임워크)를 사용해보는 것에 빠진 나머지 기초적인 Javascript 지식들에 대한 복기가 부족했던 것 같습니다. 프리코스를 통해 배운 점들을 글로 작성하고 있습니다. 프리코스가 끝나고 스스로를 되돌아봤을 때 정말 많이 배워갔다라는 소감을 남기고 싶습니다.&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;1주차 프리코스를 진행할 때에는 중간고사 기간과 겹쳐서 프리코스에 몰입하기 어려운 환경이었습니다. 저 스스로 시험 공부와 프리코스에 투자하는 시간을 1대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;중간고사가 끝난 2주차 프리코스부터는 프리코스에 시간 투자를 많이 할 수 있었습니다. 먼저 디스코드의 프리코스 커뮤니티를 통하여 다른 분들의 코드를 리뷰하고, 제 코드의 리뷰를 받을 수 있었습니다. 리뷰를 하며, 또 피드백을 받으며 많은 점을 배워갈 수 있었습니다. 프리코스를 진행하는데 서로의 코드를 리뷰하는 것은 꼭 필요한 과정은 아니지만, 저의 코드를 실력이 우수하신 다른 분들께 검증받을 수 있는 기회는 흔치 않다고 생각했기에 즐겁게 진행할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차 프리코스까지는 &amp;lsquo;프리코스를 해치우지 말자&amp;rsquo;라는 목표를 잘 지키고 있습니다. 현재 프리코스는 제 일상의 많은 부분을 차지하고 있으며 이 면적은 프리코스가 끝날 때까지 줄어들지 않을 것입니다. 남은 프리코스도 몰입하며 더욱 성장하겠습니다.&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;8-2. 지원서에 작성한 목표를 변경해야 한다고 생각하는지에 대한 질문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표를 변경하고 싶다기보다는 목표를 추가하고 싶습니다. 제가 추가하고 싶은 목표는 바로 &amp;lsquo;자바스크립트 문법을 자연스럽게 틀리지 않고 입력하기&amp;rsquo;입니다. 프리코스를 진행하며 이론적으로 제가 알고 있는 내용들을 코드로 적용시키는 일이 쉬운 일이 아님을 느끼고 있습니다. AI를 활용하거나 검색을 통하여 문법을 확인하는 행동이 나쁜 것은 아니지만, 제가 구현하고자 하는 바를 머릿 속에서 생각한 그대로 바로 코드로 작성할 수 있다면 효율성이 많이 상승할 것입니다. 남은 프리코스를 진행하며 많은 코드를 작성하게 될 텐데, 코드를 작성하며 자바스크립트 문법을 손에 익히겠습니다.&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;8-3. 프리코스를 진행하면서 눈에 띄는 변화나 깨달은 점이 있는지에 대한 질문&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 프리코스를 진행하면서 가장 크게 바뀐 것은 커밋 방식입니다. 프리코스 진행 전까지는 커밋을 본문 없이 제목만 작성했었습니다. 기능별로 커밋을 하지도 않았고 PR을 하기 전이나 컴퓨터를 덮기 전에 부랴부랴 커밋을 진행했었습니다. 하지만 이번에 AngularJS commit convention의 사용법을 익히고 적용하기 시작하면서, 저의 커밋은 구체적으로 작성되었으며 가독성도 향상될 수 있었습니다. 이번에 배운 커밋 컨벤션을 프리코스에서 계속 사용할 것이며, 또한 다른 프로젝트 개발을 진행할 때에도 이 커밋 컨벤션을 사용하려 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 단일 책임 원칙을 배우고, 이 원칙에 따라 메서드를 분할했습니다. 이렇게 개발을 진행하니 코드가 이해하기가 편하고 메서드를 다른 곳에서 활용하기도 편했습니다. 사실 다른 프로젝트도 같이 진행하고 있어서 제가 작성했던 코드를 다시 확인을 할 일이 있었는데, 제가 얼마나 코드를 잘 못 짜고 있었는지 보자마자 깨달았습니다. 원칙을 지키며 코딩을 해야함을 느끼며, 해당 코드들을 리팩토링해야겠다는 다짐을 하게 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2주차까지 프리코스를 진행하며 계속 와닿은 점은 바로 일관성이 있게, 가독성이 있는 코딩을 해야한다는 점이었습니다. 다른 사람이 코드를 읽었을 때 이 코드가 무슨 의미를 지니고 있는지 확인할 수 있어야하며, 다른 곳에 있는 코드들도 같은 방식으로 입력이 되어 보는 사람들로 하여금 헷갈리지 않게 만들어야합니다. 기존에 코딩을 하던 습관들이 남아있어 아직은 일관성 있게 코딩을 하지 못하고 서투릅니다. 남은 2주 동안 계속 깔끔하게 코딩을 하려고 훈련을 해가며 클린한 코딩을 체화해 나가고 싶습니다.&lt;/p&gt;</description>
      <category>우아한테크코스(프리코스)</category>
      <category>우아한테크코스</category>
      <category>우아한테크코스 7기</category>
      <category>우아한테크코스 프리코스</category>
      <category>우테코</category>
      <category>우테코 7기</category>
      <category>우테코 프리코스</category>
      <author>퀵차분</author>
      <guid isPermaLink="true">https://quickchabun.tistory.com/146</guid>
      <comments>https://quickchabun.tistory.com/146#entry146comment</comments>
      <pubDate>Mon, 28 Oct 2024 17:34:35 +0900</pubDate>
    </item>
  </channel>
</rss>