<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/korean/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More ]]>
            </title>
            <link>https://www.freecodecamp.org/korean/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 16 Apr 2026 03:41:18 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/korean/news/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Node.js와 함께 Docker를 사용하는 방법: 개발자를 위한 핸드북 ]]>
                </title>
                <description>
                    <![CDATA[ 이 핸드북에서는 Docker가 무엇인지, 그리고 Docker가 왜 백엔드 및 풀스택 개발자에게 반드시 필요한 기술인지를 배우게 됩니다. 그리고 가장 중요한 것은, 실제 프로젝트 전 과정을 통해 Docker를 어떻게 활용하는지 익히게 된다는 점입니다. 우리는 흔한 “Hello World” 예제를 훨씬 뛰어넘어, 완전한 풀스택 자바스크립트 애플리케이션(Node.js + Express 백엔드, HTML/CSS/JS 프론트엔드, MongoDB 데이터베이스, Mongo ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/how-to-use-to-docker-with-nodejs-handbook/</link>
                <guid isPermaLink="false">692970fd7cfd9604dd367dc8</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NodeJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ changseop yeom ]]>
                </dc:creator>
                <pubDate>Mon, 05 Jan 2026 23:47:22 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2026/01/74610cbc-124b-48aa-9cb6-7ed861123511.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/how-to-use-to-docker-with-nodejs-handbook/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Use to Docker with Node.js: A Handbook for Developers</a>
      </p><p>이 핸드북에서는 Docker가 무엇인지, 그리고 Docker가 왜 백엔드 및 풀스택 개발자에게 반드시 필요한 기술인지를 배우게 됩니다. 그리고 가장 중요한 것은, 실제 프로젝트 전 과정을 통해 Docker를 어떻게 활용하는지 익히게 된다는 점입니다.</p><p>우리는 흔한 “Hello World” 예제를 훨씬 뛰어넘어, 완전한 풀스택 자바스크립트 애플리케이션(Node.js + Express 백엔드, HTML/CSS/JS 프론트엔드, MongoDB 데이터베이스, Mongo Express 관리자 UI)을 컨테이너화하는 과정을 처음부터 끝까지 함께 따라가 볼 것입니다.​</p><p>여러 개의 컨테이너를 네트워킹(통신)하는 방법, Docker Compose로 모든 서비스를 오케스트레이션하는 방법, 나만의 이미지를 빌드하고 버전을 관리하는 방법, 볼륨을 사용해 데이터를 영구적으로 보존하는 방법, 그리고 이미지를 안전하게 AWS ECR 개인 레포지토리에 푸시해 공유와 프로덕션 배포에 활용하는 방법 등을 배우게 됩니다.﻿​</p><p>튜토리얼을 끝낼 즈음에는 “내 컴퓨터에서는 잘 되는데” 문제를 없애고, 멀티 서비스 애플리케이션을 자신 있게 관리하며, 어디서든 일관된 환경을 배포하고, 일상적인 개발 워크플로와 CI/CD 파이프라인에 Docker를 능숙하게 통합할 수 있게 될 것입니다.﻿​</p><p>Docker는 백엔드 개발자에게 매우 중요한 기술이기 때문에, 먼저 Docker의 기본 개념부터 차근차근 살펴보겠습니다.﻿</p><h2 id="-">전제조건</h2><p>이 기술 핸드북은 풀스택 개발에 대해 어느 정도 실무 경험이 있는 개발자를 대상으로 합니다. 독자는 애플리케이션 배포에 익숙하고, CI/CD 파이프라인에 대한 기본적인 이해를 갖추고 있어야 합니다. 비록 이 가이드에서 Docker를 기초부터 다루긴 하지만, 완전 초보 개발자를 위한 내용은 아니며, 실제 개발 경험이 있고 Docker로 워크플로를 한 단계 끌어올리고 싶은 분들을 전제로 합니다. </p><p>마지막으로, AWS에 대한 기본적인 지식과 일반적인 배포 개념을 알고 있다면 도움이 되지만, 전문가일 필요는 없습니다. 이 핸드북은 프로덕션급 역량을 강화하고, Docker를 자신의 프로젝트에 자신 있게 통합하고자 하는 개발자에게 이상적인 자료입니다.﻿​</p><h2 id="--1">목차</h2><!--kg-card-begin: html--><ol>
  <li>
    <p>
      컨테이너란 무엇인가?
    </p>
  </li>
  <li>
    <p>
      Docker와 가상 머신
    </p>
  </li>
  <li>
    <p>
      Docker 설치
    </p>
  </li>
  <li>
    <p>
      기본 Docker 명령어
    </p>
  </li>
  <li>
    <p>
      JavaScript로 연습하기
    </p>
    <ul>
      <li>
        <p>
          MongoDB 이미지 가져오기
        </p>
      </li>
      <li>
        <p>
          Mongo Express 이미지 가져오기
        </p>
      </li>
      <li>
        <p>
          Docker 네트워크
        </p>
      </li>
    </ul>
  </li>
  <li>
    <p>
      Mongo 컨테이너 실행 방법
    </p>
  </li>
  <li>
    <p>
      Mongo Express 컨테이너 실행 방법
    </p>
  </li>
  <li>
    <p>
      Node.js를 MongoDB에 연결하는 방법
    </p>
  </li>
  <li>
    <p>
      Docker Compose 사용 방법
    </p>
    <ul>
      <li>
        왜 Docker Compose를 사용하는가?
      </li>
    </ul>
  </li>
  <li>
    <p>
      나만의 Docker 이미지 빌드하기
    </p>
    <ul>
      <li>
        <p>
          해결책
        </p>
      </li>
      <li>
        <p>
          MongoDB가 동작하는 이유
        </p>
      </li>
      <li>
        <p>
          앱을 Docker Compose에 추가하기
        </p>
      </li>
      <li>
        <p>
          모든 서비스 시작하기
        </p>
      </li>
      <li>
        <p>
          모든 것이 제대로 동작하는지 확인하기
        </p>
      </li>
      <li>
        <p>
          무엇이 바뀌었고 왜 동작하는가
        </p>
      </li>
    </ul>
  </li>
  <li>
    <p>
      컨테이너 관리 방법
    </p>
  </li>
  <li>
    <p>
      프라이빗 Docker 저장소를 만드는 방법
    </p>
    <ul>
      <li>
        <p>
          1단계: AWS 액세스 키 받기
        </p>
      </li>
      <li>
        <p>
          2단계: AWS CLI 설치 여부 확인
        </p>
      </li>
      <li>
        <p>
          3단계: AWS CLI 설정
        </p>
      </li>
      <li>
        <p>
          4단계: AWS 설정 테스트
        </p>
      </li>
      <li>
        <p>
          5단계: ECR(Docker 레지스트리)에 로그인
        </p>
      </li>
      <li>
        <p>
          Docker 저장소의 이미지 이름 규칙 이해하기
        </p>
      </li>
      <li>
        <p>
          6단계: 이미지 빌드, 태그, 푸시하기
        </p>
      </li>
    </ul>
  </li>
  <li>
    <p>
      과제: 새 버전을 만들어 푸시하기
    </p>
    <ul>
      <li>
        <p>
          이미지 배포하기
        </p>
      </li>
      <li>
        <p>
          왜 ECR에서 전체 이미지 URL을 사용해야 하는가?
        </p>
      </li>
      <li>
        <p>
          Docker Compose로 앱 배포하기
        </p>
      </li>
      <li>
        <p>
          프라이빗 Docker 이미지 공유하기
        </p>
      </li>
    </ul>
  </li>
  <li>
    <p>
      Docker 볼륨
    </p>
    <ul>
      <li>
        <p>
          Docker 볼륨 동작 방식
        </p>
      </li>
      <li>
        <p>
          Docker 볼륨의 종류
        </p>
      </li>
      <li>
        <p>
          볼륨을 사용하는 Docker Compose 예시 파일
        </p>
      </li>
      <li>
        <p>
          애플리케이션 시작하기
        </p>
      </li>
    </ul>
  </li>
  <li>
    <p>
      결론
    </p>
  </li>
</ol>
<!--kg-card-end: html--><h2 id="--2">컨테이너란 무엇인가?</h2><p>컨테이너는 애플리케이션이 실행되는 데 필요한 모든 것(의존성, 라이브러리, 설정 파일 등)을 함께 묶어서 패키징하는 방법입니다. </p><p>컨테이너는 이식성이 좋아 팀 간에 쉽게 공유할 수 있고, 호환성 문제를 걱정하지 않고 어떤 머신에든 배포할 수 있습니다.﻿​</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-4.png" class="kg-image" alt="image-4" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-4.png 600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/image-4.png 736w" sizes="(min-width: 720px) 720px" width="736" height="736" loading="lazy"></figure><h2 id="--3">컨테이너는 어디에 있나요?</h2><p>컨테이너는 팀과 시스템 사이에서 자유롭게 이동하고 공유될 수 있기 때문에, 컨테이너가 “머무를” 장소가 필요합니다. 여기에서 컨테이너 저장소(레지스트리)가 등장하는데, 이는 컨테이너 이미지를 보관하는 특수한 저장 공간입니다. 조직은 내부용 비공개 저장소를 둘 수 있고, <a href="https://hub.docker.com/">Docker Hub</a> 같은 공개 저장소를 통해 누구나 공유된 컨테이너 이미지를 찾아보고 사용할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-5.png" class="kg-image" alt="image-5" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-5.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-5.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-5.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-5.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="1196" loading="lazy"></figure><p>Docker Hub의 카탈로그 페이지를 방문해 보면, Redis, Jenkins 같은 개발자와 팀이 만든 공식 및 커뮤니티 기반 컨테이너 저장소들을 다양하게 볼 수 있습니다.﻿​</p><p>예전에는 여러 개발자가 서로 다른 프로젝트를 진행할 때, 각자 자신의 시스템에 필요한 서비스를 직접 설치해야 했습니다. 개발자마다 리눅스, macOS, Windows처럼 사용하는 운영체제가 다르다 보니, 설정 과정도 제각각이었습니다. 이 때문에 시간이 많이 들고 오류도 자주 발생했으며, 특히 여러 서비스를 반복해서 세팅해야 할 때는 새로운 환경을 구성하는 일이 큰 골칫거리였습니다.﻿​</p><p>Docker는 이런 상황을 완전히 바꾸어 놓았습니다. 이제는 모든 서비스와 의존성을 하나씩 직접 설치하는 대신, Docker 명령 한 줄만 실행해서 컨테이너를 띄우면 됩니다. 각 컨테이너는 자신에게 필요한 모든 것을 갖춘 격리된 환경이기 때문에, Windows든 macOS든 Linux든 어떤 머신에서도 동일하게 동작합니다. 덕분에 협업이 훨씬 매끄러워지고, 서로 다른 설정, 누락된 의존성, 버전 불일치 때문에 생기던 병목 현상이 사라집니다.﻿​</p><p>요약하면, Docker는 애플리케이션과 그 의존성을 하나의 이식 가능한 컨테이너로 포장해서 어디서나 동일한 방식으로 실행할 수 있도록 해주는 플랫폼입니다.</p><h2 id="docker-vs-">Docker vs 가상 머신</h2><p>Docker와 가상 머신(VM)은 모두 애플리케이션을 “가상” 환경에서 실행하는 방법이지만, 동작 방식은 서로 다릅니다. 이런 차이를 이해하려면, 먼저 컴퓨터가 소프트웨어를 어떻게 실행하는지 간단히 살펴볼 필요가 있습니다.﻿​</p><p>레이어를 빠르게 훑어보면:</p><ul><li><strong>커널(Kernel)</strong>: 운영체제에서 CPU, 메모리, 디스크 같은 하드웨어와 직접 통신하는 부분입니다. 앱과 컴퓨터 사이를 이어 주는 중개자라고 생각하면 됩니다.</li><li><strong>애플리케이션 계층(Application layer)</strong>: 실제 프로그램과 앱이 실행되는 영역입니다. 커널 위에 올라가 있으며, 커널을 통해 하드웨어 자원에 접근합니다.﻿​</li></ul><p>이제 가상 머신을 좀 더 자세히 보겠습니다. VM은 <strong>운영체제 전체</strong>를 가상화하기 때문에, 자체 커널과 자체 애플리케이션 계층을 모두 포함합니다. VM을 하나 다운로드한다는 것은, 사실상 수 기가 바이트에 이르는 완전한 운영체제를 내 컴퓨터 안에 또 하나 들여오는 것과 같습니다.﻿​</p><p>자체 OS를 부팅해야 하므로 VM은 시작 속도가 느립니다. 하지만 필요한 것을 모두 가지고 있기 때문에 호스트 환경과 상관없이 거의 어떤 시스템에서도 잘 돌아가는 높은 호환성을 제공합니다.﻿​</p><p>반면 Docker는 전체 OS가 아니라 <strong>애플리케이션 계층</strong>만 가상화합니다. 컨테이너는 호스트 시스템의 커널을 공유하지만, 애플리케이션이 필요로 하는 의존성, 라이브러리, 설정 파일 등은 컨테이너 안에 함께 포함합니다.﻿​</p><p>Docker 이미지는 보통 수 메가바이트 수준으로 작고, 자체 OS를 부팅하지 않기 때문에 컨테이너는 거의 즉시 시작됩니다. Docker가 설치되어 있기만 하면, 호스트가 어떤 운영체제이든(Windows, macOS, Linux) 컨테이너는 동일하게 실행될 수 있습니다.﻿​</p><p>간단히 정리하면:</p><ul><li>VM은 내 컴퓨터 안에 “또 하나의 완전한 컴퓨터”를 띄우는 느낌으로, 크고 무겁고 느립니다.</li><li>Docker 컨테이너는 애플리케이션을 담은 독립 패키지에 가까워, 작고 빠르며 이식성이 좋습니다.﻿​</li></ul><p>간단한 비교 표입니다:</p><!--kg-card-begin: html--><div class="hn-table">
  <table>
    <thead>
      <tr>
        <td>기능</td>
        <td>가상 머신</td>
        <td>Docker 컨테이너</td>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>크기</td>
        <td>GB 단위 (큼)</td>
        <td>MB 단위 (작음)</td>
      </tr>
      <tr>
        <td>시작 속도</td>
        <td>느림</td>
        <td>빠름</td>
      </tr>
      <tr>
        <td>OS 계층</td>
        <td>전체 OS + 커널</td>
        <td>호스트 커널을 공유</td>
      </tr>
      <tr>
        <td>이식성</td>
        <td>호환되는 호스트에서 실행됨</td>
        <td>도커가 설치된 곳이라면 어디든 실행됨</td>
      </tr>
    </tbody>
  </table>
</div>
<!--kg-card-end: html--><h2 id="docker-">Docker 설치</h2><p><br>이제 Docker가 무엇인지 알게 되었으니, 직접 내 컴퓨터에서 Docker를 실행해 봅시다. Docker는 Windows, macOS, Linux에서 모두 동작하지만, 운영체제마다 설치 과정에 약간씩 차이가 있습니다. Docker <a href="https://docs.docker.com/get-started/introduction/">공식 문서</a>에는 모든 운영체제별 설치 방법이 정리되어 있습니다.﻿​</p><p>시각적으로 보는 것을 더 선호한다면, Windows와 Linux에서 Docker를 설치하는 과정을 단계별로 보여주는 <a href="https://www.youtube.com/watch?v=BuGEGM_elXY">YouTube 영상</a>도 참고할 수 있습니다.﻿​</p><p>간단한 설치 로드맵은 다음과 같습니다:</p><p>먼저 시스템 요구 사항을 확인합니다. Docker는 모든 컴퓨터에서 돌아가는 것이 아니므로, 사용하는 OS 버전이 지원 대상인지 반드시 확인해야 합니다.(<a href="https://docs.docker.com/engine/install/">공식 문서</a>에 체크리스트가 있습니다.﻿​)</p><ol><li>Windows 및 macOS 사용자:</li></ol><ul><li><strong>새로운 시스템</strong>: <a href="https://docs.docker.com/desktop/">Docker Desktop</a>을 다운로드해 설치하는 것이 가장 쉽습니다. </li><li><strong>구형 시스템</strong>: Docker Desktop을 지원하지 않는 경우(예: Hyper-V 미지원, 오래된 OS 버전 등)에는 <a href="https://docker-docs.uclv.cu/toolbox/toolbox_install_windows/">Docker Toolbox</a>를 사용할 수 있습니다. Toolbox는 가벼운 가상 머신 위에 Docker를 올려 주기 때문에, 오래된 컴퓨터에서도 컨테이너를 실행할 수 있습니다.﻿​</li></ul><p>2. Linux 사용자</p><ul><li>보통 각 배포판의 패키지 관리자(예: Ubuntu/Debian은 <code>apt</code>, CentOS/Fedora는 <code>yum</code> 등)를 사용해 Docker를 설치합니다. <a href="https://docs.docker.com/desktop/setup/install/linux/">공식 문서</a>에 배포판별 설치 명령이 정리되어 있습니다.﻿​</li></ul><p>설치가 끝나면, 터미널이나 명령 프롬프트를 열고 다음 명령어를 입력해 설치를 확인합니다:</p><pre><code class="language-bash">docker --version
</code></pre><p>여기에 Docker 버전 정보가 출력되면, 축하합니다! 정상적으로 설치가 완료된 것입니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/11/image-5.png" class="kg-image" alt="image-5" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/11/image-5.png 600w, https://www.freecodecamp.org/korean/news/content/images/2025/11/image-5.png 632w" width="632" height="34" loading="lazy"></figure><p>이제 도커가 설치되었으니, 컨테이너를 실행하고, 이미지를 가져오고, 애플리케이션을 안전하고 격리된 환경에서 실험할 준비가 된 것입니다.﻿​</p><p><strong>초보자를 위한 팁</strong>:</p><p>만약 구형 시스템에서 Docker Toolbox를 사용 중이라면, 명령어 자체는 대부분 동일하지만, <strong>Docker Quickstart Terminal</strong> 안에서 명령을 실행해야 합니다. 이 터미널이 가상 머신 환경을 자동으로 설정해 주기 때문입니다.</p><h2 id="-docker-">기본 Docker 명령어</h2><p>지금까지 우리는 ‘이미지(images)’와 ‘컨테이너(containers)’라는 용어를 여러 번 사용해 왔습니다. 때로는 이 두 용어를 서로 바꿔 쓰기도 했습니다. 하지만 이 둘 사이에는 중요한 차이가 있습니다.</p><ul><li><strong>Docker 이미지</strong>: 이미지는 <strong>설계도(blueprint)</strong>나 패키지라고 생각하면 됩니다. 앱이 실행되는 데 필요한 코드, 라이브러리, 의존성, 설정 등이 모두 들어 있지만, 아직 “실행 중”인 상태는 아닙니다.</li><li><strong>Docker 컨테이너</strong>: 컨테이너는 이미지를 실제로 실행한 “<strong>실행 인스턴스</strong>”입니다. 컨테이너를 시작하면, Docker가 이미지를 가져와 자신만의 격리된 환경에서 그 내용을 실행합니다.</li></ul><p>이렇게 기억하면 도움이 됩니다: <em>이미지(image)는 레시피이고, 컨테이너(container)는 케이크입니다. </em>하나의 레시피(이미지)로 케이크(컨테이너)를 여러 개 만들 수 있는 것입니다.</p><p><strong>중요한 점</strong>: Docker Hub에는 컨테이너가 아니라 이미지가 저장됩니다. 따라서 Docker Hub에서 무언가를 pull 한다는 것은 이미지를 다운로드한다는 뜻입니다. <br>예를 들어:</p><pre><code class="language-bash">docker pull redis
</code></pre><p>(명령어를 입력하면) 아래 내용을 확인할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/11/image-9.png" class="kg-image" alt="image-9" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/11/image-9.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/11/image-9.png 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/11/image-9.png 1130w" sizes="(min-width: 720px) 720px" width="1130" height="410" loading="lazy"></figure><p>이 명령은 Redis 이미지를 내 컴퓨터로 다운로드받습니다. 다운로드가 끝나면, 다음 명령으로 로컬에 있는 모든 이미지를 확인할 수 있습니다.</p><pre><code class="language-bash">docker images
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/11/image-10.png" class="kg-image" alt="image-10" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/11/image-10.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/11/image-10.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/11/image-10.png 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/11/image-10.png 1654w" sizes="(min-width: 720px) 720px" width="1654" height="880" loading="lazy"></figure><p>이제 필요할 때마다 이미지에서 컨테이너를 시작할 수 있습니다.</p><pre><code class="language-bash">docker run -d --name my-redis redis
</code></pre><p>이 명령은 방금 내려받은 <code>redis</code> 이미지로부터, <code>my-redis</code> 라는 컨테이너를 하나 띄웁니다.</p><ul><li><code>docker run</code> 은 도커에게 “이 이미지로 새 컨테이너를 시작하라”고 지시하는 명령입니다.</li><li><code>d</code> 는 “detached mode”의 약자로, 컨테이너를 백그라운드에서 돌려서 터미널을 계속 쓸 수 있게 해 줍니다.</li><li><code>--name my-redis</code> 는 도커가 임의의 이름을 붙이는 대신 <code>my-redis</code>라는 알아보기 쉬운 이름을 컨테이너에 지정합니다. 이렇게 하면 나중에 관리가 더 편해집니다.<br><code>redis</code> 는 컨테이너를 시작하는 데 사용할 이미지 이름입니다.</li></ul><p>현재 실행 중인 컨테이너를 모두 보고 싶다면:</p><pre><code class="language-bash">docker ps
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/11/image-12.png" class="kg-image" alt="image-12" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/11/image-12.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/11/image-12.png 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/11/image-12.png 1518w" sizes="(min-width: 720px) 720px" width="1518" height="396" loading="lazy"></figure><p>이 명령은 다음과 같은 정보를 포함해 컨테이너 목록을 보여 줍니다.</p><ul><li>컨테이너 ID</li><li>이름</li><li>상태(실행 중인지, 중지 상태인지)</li><li>어떤 이미지로부터 실행되고 있는지 등</li></ul><p>실행 중이 아닌 컨테이너까지 모두 보고 싶다면, <code>-a </code>옵션을 추가합니다.</p><pre><code class="language-bash">docker ps -a
</code></pre><h2 id="--4">이미지 버전 지정 방법:</h2><p>기본적으로 Docker는 이미지의 <strong>최신 버전</strong>을 가져옵니다. 하지만 특정 버전이 필요할 때는 <code>콜론(:) </code>뒤에 버전 태그를 붙여서 지정할 수 있습니다. 예를 들어:</p><pre><code class="language-bash">docker pull redis:7.2
docker run -d --name my-redis redis:7.2</code></pre><p>어떤 버전들이 있는지 알고 싶다면 <a href="https://hub.docker.com/repositories">Docker Hub</a>나 관련 페이지에서 태그 목록을 확인할 수 있습니다. 또한 로컬에서 <code>docker images</code> 명령을 실행하면, 내려받은 이미지들과 각 버전을 확인할 수 있습니다.</p><h2 id="--5">컨테이너 중지, 시작, 삭제 방법:</h2><p>실행 중인 컨테이너를 멈추고 싶다면:</p><pre><code class="language-bash">docker stop my-redis
</code></pre><p>다시 시작하려면:</p><pre><code class="language-bash">docker start my-redis
</code></pre><p>더 이상 필요 없는 <strong>컨테이너는 삭제</strong>할 수 있습니다.</p><pre><code class="language-bash">docker rm my-redis
</code></pre><h2 id="--6">컨테이너 재시작 방법</h2><p>무언가 충돌이 발생했거나, 새 설정을 반영하고 싶거나, 단순히 한 번 새로고침하고 싶을 때는 <strong>컨테이너 ID</strong>(또는 이름)으로 재시작할 수 있습니다. 예를 들어:</p><pre><code class="language-bash">docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS      NAMES
c002bed0ae9a   redis     "docker-entrypoint.s…"   3 minutes ago   Up 3 minutes   6379/tcp   my-redis</code></pre><p>이 컨테이너를 다음처럼 재시작할 수 있습니다.</p><pre><code class="language-bash">docker restart c002bed0ae9a
</code></pre><p>또는 이름으로:</p><pre><code class="language-bash">docker restart my-redis
</code></pre><p>기억해 두면 좋은 다른 방법들입니:</p><ul><li><strong>먼저 중지 후 시작</strong><br><br><code>docker stop c002bed0ae9a</code><br><code>docker start c002bed0ae9a</code>			<br></li><li><strong>로그를 보면서 시작</strong><br><br><code>docker start c002bed0ae9a &amp;&amp; docker logs -f c002bed0ae9a</code></li></ul><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image.png" class="kg-image" alt="image" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="946" loading="lazy"></figure><h2 id="-redis-">여러 Redis 컨테이너를 실행하고 포트를 이해하는 방법</h2><p>현재 Redis 컨테이너가 한개 실행 중인 상태라고 가정해 보겠습니다:</p><pre><code class="language-bash">docker ps
</code></pre><p>출력 예시는 다음과 같습니다:</p><pre><code class="language-bash">CONTAINER ID   IMAGE     COMMAND                  STATUS          PORTS      NAMES
c002bed0ae9a   redis     "docker-entrypoint.s…"   Up 20 minutes   6379/tcp   my-redis</code></pre><p><strong>PORTS</strong> 열을 보면 <code>6379/tcp</code> 로 표시되어 있습니다. 이는 컨테이너 내부에서 Redis가 기본 포트 6379로 실행 중임을 의미합니다.﻿ 기본적으로 이 포트는 컨테이너 내부 포트이며, 명시적으로 지정하지 않는 한 여러분의 컴퓨터(호스트)에는 자동으로 노출되지 않습니다. Docker는 사용자가 명시적으로 지정해야만 포트를 매핑합니다.</p><h2 id="-redis--1">동일한 포트에서 다른 Redis 컨테이너 실행 시도하기</h2><p>다음과 같이 시도하면:</p><pre><code class="language-bash">docker run -d --name my-redis2 redis:7.4.7-alpine
</code></pre><p>첫 번째 컨테이너가 이미 호스트 포트 6379를 사용하고 있기 때문에 매핑에 실패합니다. 여기서 포트 바인딩이 필요합니다.</p><h2 id="--7">포트 바인딩이란 무엇인가?</h2><p>포트 바인딩(포트 매핑이라고도 함)은 Docker가 컨테이너 내부의 포트를 호스트 머신(여러분의 노트북/데스크톱/서버)의 포트에 연결하는 데 사용되는 원리입니다.</p><p>포트 바인딩이 없으면 컨테이너 내부에서 실행되는 모든 서비스는 완전히 격리됩니다: 내부 포트(예: Redis는 6379, Node.js 앱은 3000, MongoDB는 27017)에서 수신 대기할 수 있지만, 브라우저, 컴퓨터의 다른 앱, 심지어 다른 네트워크에 있는 다른 컨테이너를 포함하여 컨테이너 외부의 어떤 것도 이에 접근할 수 없습니다.</p><ul><li><strong>컨테이너 포트</strong>: 앱이 실행되고 있는 컨테이너 내부의 포트(Redis는 기본적으로 <code>6379</code>)</li><li><strong>호스트 포트</strong>: 해당 컨테이너에 액세스하는 데 사용하려는 컴퓨터의 포트.</li></ul><p>Docker는 <code>-p</code> 플래그를 사용하여 컨테이너 포트를 다른 호스트 포트에 매핑할 수 있게 해줍니다.</p><h2 id="-redis--2">다른 호스트 포트에서 두 번째 Redis 컨테이너 실행하기</h2><pre><code class="language-bash">docker run -d --name my-redis2 -p 6380:6379 redis:7.4.7-alpine
</code></pre><p><code>-p 6380:6379</code>는 호스트 포트 6380을 컨테이너 포트 6379에 매핑(연결)합니다.</p><ul><li>이제 <code>localhost:6380</code>을 사용하여 두 번째 컨테이너의 Redis에 연결할 수 있습니다. </li><li>컨테이너 내부에서 Redis는 여전히 포트 6379에서 실행됩니다.</li></ul><p>두 컨테이너를 확인해보세요:</p><pre><code class="language-bash">docker ps
</code></pre><p>출력은 다음과 같습니다:</p><pre><code class="language-bash">CONTAINER ID   IMAGE     STATUS          PORTS             NAMES
c002bed0ae9a   redis     Up 20 minutes   6379/tcp          my-redis
d123abcd5678   redis     Up 1 minute     0.0.0.0:6380-&gt;6379/tcp   my-redis2
</code></pre><p>첫 번째 컨테이너는 6379에서 내부적으로 실행되고(호스트 포트가 노출되지 않음), 두 번째 컨테이너는 호스트 포트 6380이 컨테이너 포트 6379로 트래픽을 전달하도록 매핑되어 있습니다.</p><p>각 컨테이너를 전화선(컨테이너 포트)이 있는 방으로 생각해보세요.</p><ul><li>외부(호스트)에서 그 방으로 전화를 걸고 싶습니다.</li><li>두 개의 방에 동시에 같은 외부 전화선을 사용할 수 없습니다.</li><li><strong>포트 바인딩</strong>을 사용하면 내부 전화번호가 같더라도, 각 방에 다른 외부 회선을 할당할 수 있습니다.</li></ul><h2 id="--8">포트 바인딩이 존재하는 이유</h2><ol><li><strong>호스트의 포트 충돌 방지</strong>: 컴퓨터에서 한 번에 하나의 프로세스만 지정된 포트를 사용할 수 있습니다. 이미 호스트 포트 6379를 사용하는 Redis 컨테이너가 하나 있다면, 두 번째 컨테이너도 같은 호스트 포트에 바인딩할 수 없습니다. 포트 바인딩을 사용하면 각각을 다른 호스트 포트(6379 → 6380, 6381 등)에 매핑하여 여러 동일한 컨테이너를 나란히 실행할 수 있습니다.</li><li><strong>호스트에서 컨테이너화된 서비스에 액세스</strong>: 브라우저, Postman, MongoDB Compass, redis-cli, curl 등은 모두 호스트에서 실행됩니다. -p 플래그 없이는 컨테이너 내부의 서비스와 통신할 방법이 없습니다.</li><li><strong>선택적 노출</strong>: 컨테이너가 사용하는 모든 포트를 노출할 필요는 없습니다. 실제로 외부에서 필요한 포트만 매핑하여 나머지는 비공개 상태로 안전하게 유지할 수 있습니다.</li></ol><p>또한 개발 및 프로덕션 환경에서 더 많은 유연성을 제공합니다. 개발 환경에서는 컨테이너 3000을 호스트 3000에 매핑할 수 있습니다. 하지만 프로덕션 환경에서는(예: 리버스 프록시 뒤에서) 컨테이너 3000을 호스트 80 또는 443에 매핑하거나, 전혀 노출하지 않고 다른 컨테이너가 Docker의 내부 네트워크를 통해 통신하도록 할 수 있습니다.</p><h2 id="--9">컨테이너 탐색 방법</h2><p>컨테이너를 탐색하려면 다음을 실행하세요:</p><pre><code class="language-bash">docker exec -it my-redis2 /bin/sh
</code></pre><ul><li><code>docker exec</code>는 컨테이너에서 명령을 실행합니다.</li><li><code>it</code> 대화형 터미널(입력하고 출력을 볼 수 있게 해줌).</li><li><code>/bin/sh</code>는 컨테이너 내부에서 셸을 시작합니다.</li></ul><p>내부에 들어가면 프롬프트가 다음과 같이 변경됩니다:</p><pre><code>/data #
</code></pre><p>이제 호스트 머신에 영향을 주지 않고 전부 컨테이너 내부에서 <strong>파일을 나열하고</strong>, 디렉토리를 탐색하거나, 프로그램을 실행할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://velog.velcdn.com/images/richman/post/05cd8a33-5d31-4be1-bba7-a6de1dccc4bf/image.png" class="kg-image" alt="image" width="2880" height="1800" loading="lazy"></figure><h3 id="docker-run-vs-docker-start"><code>docker run</code> vs <code>docker start</code></h3><p>이 문서 전체에서 <code>docker run</code>과 <code>docker start</code>를 사용해 왔는데, 그 차이점이 중요한 이유는 다음과 같습니다:</p><ul><li><strong>우발적인 중복 방지</strong>: 매번 <code>docker run</code>을 사용하면 새 컨테이너가 생성됩니다. 이미 설정한 것을 단지 다시 시작하려면 <code>docker start</code>가 더 빠르고 안전합니다.</li><li><strong>구성 유지</strong>: <code>docker start</code>는 컨테이너의 원래 설정, 포트, 볼륨 및 이름 등을 보존하므로 옵션을 변경하여 문제가 발생할 위험이 없습니다.</li><li><strong>여러 컨테이너로 효율적으로 작업</strong>: 여러 서비스 또는 동일한 앱의 다른 버전을 실행할 때, <code>run</code>과 <code>start</code>를 언제 사용할지 아는 것은 리소스를 관리하고 포트 충돌을 피하며 워크플로우를 원활하게 유지하는 데 도움이 됩니다.</li><li><strong>워크플로우 속도 향상</strong>: 기존 컨테이너를 시작하는 것은 거의 즉각적이지만, 새 컨테이너를 생성하는 것은 약간 더 오래 걸립니다.</li></ul><p><strong>요약하자면</strong> <code>docker run</code> = 새로운 것을 생성, <code>docker start</code> = 이미 가지고 있는 것을 다시 시작하는 것입니다.</p><h2 id="javascript-">JavaScript로 연습하기</h2><p>이제 핵심 Docker 개념을 다루었으니 실제로 적용해봅시다. 이 섹션에서는 다음으로 구성된 간단한 JavaScript 프로젝트를 컨테이너화합니다:</p><ul><li><strong>프론트엔드</strong>: HTML, CSS 및 JavaScript로 구축</li><li><strong>백엔드</strong>: 간단한 Node.js 서버(<code>server.js</code>)</li><li><strong>데이터베이스</strong>: Docker Hub에서 직접 가져온 MongoDB 인스턴스</li><li><strong>MongoDB용 UI</strong>: <strong>Mongo Express</strong>를 사용하여 데이터베이스를 시각화하고 관리</li></ul><p>이 예제는 Docker가 어떻게 격리되고 일관된 환경에서 코드, 종속성 및 서비스 등을 포함한 애플리케이션의 여러 컴포넌트(구성 요소)를 관리하는지를 보여줍니다.</p><p><a href="https://github.com/Oghenekparobo/docker_tut_js">GitHub에서 스타터 프로젝트</a>를 가져올 수 있습니다.</p><p>또는 터미널을 사용하여 직접 클론하세요:</p><pre><code class="language-bash">git clone https://github.com/Oghenekparobo/docker_tut_js.git
cd docker_tut_js
</code></pre><p>여기에는 Node.js 백엔드와 함께 기본 HTML 및 JavaScript 파일이 포함되어 있습니다.</p><p>다음으로 데이터베이스 설정을 준비합니다. <a href="https://hub.docker.com/">Docker Hub</a>로 이동하여 검색창에 "<strong>mongo</strong>"를 입력하세요. Docker에서 게시한 공식 MongoDB 이미지가 표시됩니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-6.png" class="kg-image" alt="image-6" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-6.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-6.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-6.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-6.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="1044" loading="lazy"></figure><h2 id="mongodb-">MongoDB 이미지를 가져오는 방법</h2><p>이제 Docker Hub에서 공식 MongoDB 이미지를 탐색했으니 실제로 로컬 환경으로 가져와봅시다.</p><p>터미널을 열고 프로젝트 디렉토리(예: <code>docker_tut_js</code>)로 이동한 다음 실행하세요:</p><pre><code class="language-bash">docker pull mongo
</code></pre><p>이 명령은 Docker에게 Docker Hub에서 최신 버전의 MongoDB 이미지를 다운로드하도록 지시합니다.</p><p>다음과 유사한 출력이 표시됩니다:</p><pre><code class="language-bash">Using default tag: latest
latest: Pulling from library/mongo
b8a35db46e38: Already exists 
a637dbfff7e5: Pull complete 
0c9047ace63c: Pull complete 
02cd4cf70021: Pull complete 
dfb5d357a025: Pull complete 
007bf0024f67: Pull complete 
67fd8af3998d: Pull complete 
d702312e8109: Pull complete 
Digest: sha256:7d1a1a613b41523172dc2b1b02c706bc56cee64144ccd6205b1b38703c85bf61
Status: Downloaded newer image for mongo:latest
docker.io/library/mongo:latest
</code></pre><p>무슨 일이 일어나고 있는지 설명하면:</p><ul><li><strong>"Using default tag: latest"</strong>: 특정한 버전이 제공되지 않았으므로 Docker가 MongoDB의 최신 버전을 가져옵니다.</li><li><strong>"Pulling from library/mongo"</strong>: Docker의 공식 이미지 라이브러리에서 다운로드하고 있습니다.</li><li><strong>"Pull complete"</strong>: 각 줄은 성공적으로 다운로드 중인 이미지의 레이어를 나타냅니다.</li><li><strong>"Downloaded newer image for mongo:latest"</strong>: MongoDB 이미지가 이제 시스템의 로컬에 저장되었음을 확인합니다.</li></ul><p>다음을 실행하여 사용 가능한지 확인할 수 있습니다:</p><pre><code class="language-bash">docker images
</code></pre><p>repository 열에 <strong>mongo</strong>가 나열되어 있어야 합니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-7.png" class="kg-image" alt="image-7" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-7.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-7.png 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/12/image-7.png 1164w" sizes="(min-width: 720px) 720px" width="1164" height="740" loading="lazy"></figure><h2 id="mongo-express-">Mongo Express 이미지 가져오기 방법</h2><p>이제 MongoDB 이미지가 준비되었으니 <strong>Mongo Express</strong> 이미지를 가져와봅시다.</p><p>Mongo Express는 브라우저를 통해 MongoDB 컬렉션을 보고 관리할 수 있는 경량 웹 기반 인터페이스로, phpMyAdmin이 MySQL에서 작동하는 방식과 유사합니다.</p><p>터미널을 열고(여전히 프로젝트 디렉토리에서) 다음을 실행하세요:</p><pre><code class="language-bash">docker pull mongo-express
</code></pre><p>다음과 유사한 출력이 표시됩니다:</p><pre><code class="language-bash">Using default tag: latest
latest: Pulling from library/mongo-express
b8a35db46e38: Already exists
a637dbfff7e5: Pull complete
4e0e0977e9c3: Pull complete
02cd4cf70021: Pull complete
Digest: sha256:3d6dbac587ad91d0e2eab83f09a5b31a1c8f9d91a8825ddaa6c7453c25cb4812
Status: Downloaded newer image for mongo-express:latest
docker.io/library/mongo-express:latest
</code></pre><p>이것이 의미하는 바는 다음과 같습니다:</p><ul><li><code>docker pull mongo-express</code>는 Docker Hub에서 공식 Mongo Express 이미지를 다운로드합니다.</li><li>각 "<strong>Pull complete</strong>" 줄은 성공적으로 다운로드된 이미지의 레이어를 나타냅니다.</li><li><code>mongo-express:latest</code>는 최신 버전이 이제 로컬에 저장되었음을 확인합니다.</li></ul><p>두 이미지가 모두 사용 가능한지 확인하려면 다음을 실행하세요:</p><pre><code class="language-bash">docker images
</code></pre><p>출력에 mongo와 mongo-express가 나열되어 있어야 합니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-8.png" class="kg-image" alt="image-8" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-8.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-8.png 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/12/image-8.png 1164w" sizes="(min-width: 720px) 720px" width="1164" height="740" loading="lazy"></figure><p>이제 두 이미지가 모두 다운로드되었으므로, 다음 단계는 MongoDB가 실행되고 접근 가능한지 확인한 다음, 브라우저를 통해 관리할 수 있도록 Mongo Express에 연결하는 것입니다.</p><p>그 전에, 이 두 컨테이너가 어떻게 통신할지 간단히 살펴봅시다.</p><h2 id="docker--1">Docker 네트워크</h2><p>MongoDB와 Mongo Express가 별도의 컨테이너에서 실행될 때 서로 통신할 방법이 필요합니다. Docker는 <strong>Docker 네트워크</strong>라는 것을 사용하여 이를 처리합니다. 이는 내부 포트를 외부 세계에 노출하지 않고 컨테이너가 안전하게 통신할 수 있게 해주는 가상 브리지입니다.</p><p>Docker에서 컨테이너를 실행하면 자동으로 격리된 네트워크가 생성됩니다. 이를 마치 컨테이너들이 외부 세계에 모든 것을 노출하지 않고 안전하게 서로 통신할 수 있는 개인 공간처럼 생각하면 됩니다.</p><p>예를 들어, MongoDB 컨테이너와 Mongo Express 컨테이너가 동일한 Docker 네트워크에 있으면 컨테이너 이름(예: <code>mongo</code> 또는 <code>mongo-express</code>)만 사용하여 통신할 수 있습니다. Docker가 이를 내부적으로 처리하므로<code>localhost</code>나 포트 번호를 사용할 필요가 없습니다.</p><p>하지만 Docker 네트워크 외부의 모든 것(여러분의 호스트 머신이나 Node.js 앱 같은)은 노출된 포트를 통해 연결됩니다.</p><p>따라서 나중에 전체 애플리케이션, Node.js 백엔드, MongoDB, Mongo Express, 심지어 프론트엔드(<code>index.html</code>)까지 Docker로 패키징하면, 이 모든 컨테이너가 Docker 네트워크를 통해 원활하게 상호 작용합니다. 그러면 컴퓨터의 브라우저는 우리가 노출한 호스트 주소와 포트를 사용하여 Node.js 앱에 연결됩니다.</p><p>기본적으로 Docker는 이미 몇 가지 내장 네트워크를 제공합니다. 다음을 실행하여 확인할 수 있습니다:</p><pre><code>docker network ls
</code></pre><p>다음과 같은 결과가 나올 것입니다:</p><pre><code class="language-bash">NETWORK ID     NAME      DRIVER    SCOPE
712a7144f1a0   bridge    bridge    local
4ae27eedea5b   host      host      local
4806000201ce   none      null      local
</code></pre><p>이것들은 Docker에 의해 자동으로 생성됩니다. 지금은 이것들에 대해 너무 걱정할 필요가 없습니다 – 우리는 커스튬(자체 사용자 정의) 네트워크를 만드는 데 집중하겠습니다.</p><p>설정을 위해 MongoDB와 Mongo Express가 공유할 수 있는 별도의 네트워크를 만들겠습니다. 이를 mongo-network라고 부르겠습니다:</p><pre><code class="language-bash">docker network create mongo-network
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-9.png" class="kg-image" alt="image-9" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-9.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-9.png 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/12/image-9.png 1144w" sizes="(min-width: 720px) 720px" width="1144" height="740" loading="lazy"></figure><h2 id="mongo-">Mongo 컨테이너 실행 방법</h2><p>MongoDB와 Mongo Express 컨테이너가 통신할 수 있도록 하려면 동일한 Docker 네트워크 내에서 실행해야 합니다. 그래서 앞서 mongo-network를 만든 것입니다.</p><p>MongoDB부터 시작하겠습니다. <code>docker run</code> 명령은 이미지에서 컨테이너를 시작하는 데 사용된다는 것을 기억하세요. 이번에는 공식 MongoDB 이미지를 실행하고 네트워크에 연결할 것입니다.</p><p>또한 컨테이너 외부에서 접근할 수 있도록 기본 MongoDB 포트 27017을 노출하고, 루트 사용자 이름과 비밀번호에 대한 환경 변수를 설정합니다.</p><p>다음은 명령어입니다:</p><pre><code class="language-bash">docker run -p 27017:27017 -d \
  -e MONGO_INITDB_ROOT_USERNAME=admin \
  -e MONGO_INITDB_ROOT_PASSWORD=password \
  --name mongo \
  --network mongo-network \
  mongo
</code></pre><p>각 부분이 하는 일은 다음과 같습니다:</p><ul><li><code>-p 27017:27017</code>은 컨테이너의 MongoDB 포트를 호스트 머신에 매핑합니다.</li><li><code>-d</code>는 컨테이너를 분리 모드(백그라운드)에서 실행합니다.</li><li><code>-e</code>는 데이터베이스의 루트 크레덴셜(루트 자격 증명)에 대한 환경 변수를 설정합니다.</li><li><code>--name mongo</code>는 컨테이너에 사용자 정의 이름을 부여해 더 쉽게 참조할 수 있도록 합니다.</li><li><code>--network mongo-network</code>는 우리가 만든 네트워크에 컨테이너를 연결합니다.</li></ul><p>성공적으로 실행되면 MongoDB 인스턴스가 Docker 네트워크 내에서 실행되며, Mongo Express와 같은 다른 컨테이너가 연결할 준비가 됩니다.</p><p>MongoDB 컨테이너를 생성한 후 실행 중이고 정상인지 쉽게 확인할 수 있습니다.</p><p>먼저 <code>docker ps</code>를 실행하여 모든 활성 컨테이너를 확인하세요. MongoDB 컨테이너(<code>mongo</code>)가 포트 <code>27017</code>이 노출된 상태로 나열되어야 합니다. 컨테이너 내부에서 무슨 일이 일어나고 있는지에 대한 자세한 정보를 얻으려면 <code>docker logs mongo</code>를 사용하거나, 원한다면 컨테이너 ID를 사용하여(예: <code>docker logs 7abb38175ae28</code>) 로그를 확인할 수 있습니다. 로그는 MongoDB의 시작 메시지를 보여줄 것이며, 데이터베이스가 성공적으로 시작되었고 연결될 준비가 되었음을 나타내는 명령줄을 찾아야 합니다.</p><p>이것은 Mongo Express와 같은 다른 서비스를 연결하기 전에 모든 것이 올바르게 작동하는지 확인하는 빠른 방법입니다.</p><pre><code class="language-bash">docker ps
</code></pre><p>이것은 <strong>실행 중인 모든 컨테이너</strong>를 나열합니다. MongoDB 컨테이너(<code>mongo</code>)가 포트 <code>27017</code>이 노출된 상태로 표시되어야 합니다.</p><pre><code class="language-bash">docker logs mongo 또는 컨테이너의 id 예시: docker logs 7abb38175ae283429354609866c8d97521f37b535c475ae448295f8fc0ed947f
</code></pre><p>이것은 시작 메시지를 보여줍니다. MongoDB가 성공적으로 시작되었고 연결될 준비가 되었음을 나타내는 줄을 찾아보세요.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-10.png" class="kg-image" alt="image-10" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-10.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-10.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-10.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-10.png 2400w" sizes="(min-width: 720px) 720px" width="2504" height="222" loading="lazy"></figure><h2 id="mongo-express--1">Mongo Express 컨테이너 실행 방법</h2><p>이제 MongoDB가 실행 중이므로 MongoDB 데이터베이스를 관리하고 보기 위한 웹 기반 인터페이스인 Mongo Express를 실행할 수 있습니다. MongoDB와 통신할 수 있도록 동일한 네트워크(<code>mongo-network</code>)에 연결하겠습니다.</p><p>다음은 명령어입니다:</p><pre><code class="language-bash">docker run -d \
  -e ME_CONFIG_MONGODB_ADMINUSERNAME=admin \
  -e ME_CONFIG_MONGODB_ADMINPASSWORD=password \
  -e ME_CONFIG_MONGODB_SERVER=mongo \
  --name mongo-express \
  --network mongo-network \
  -p 8081:8081 \
  mongo-express
</code></pre><p>각 부분이 하는 일은 다음과 같습니다:</p><ul><li><code>-d</code>는 컨테이너를 분리 모드(백그라운드)에서 실행합니다.</li><li><code>-e ME_CONFIG_MONGODB_ADMINUSERNAME=admin</code>은 Mongo Express가 사용할 MongoDB 어드민(관리자) 사용자 이름을 설정합니다.</li><li><code>-e ME_CONFIG_MONGODB_ADMINPASSWORD=password</code>는 해당 MongoDB 비밀번호를 설정합니다.</li><li><code>-e ME_CONFIG_MONGODB_SERVER=mongo</code>는 Mongo Express에 연결할 MongoDB 서버를 알려줍니다. 두 컨테이너가 동일한 네트워크에 있기 때문에 여기서는 컨테이너 이름 <code>mongo</code>를 사용합니다.</li><li><code>--name mongo-express</code>는 더 쉽게 참조할 수 있도록 컨테이너에 친숙한 이름을 부여합니다.</li><li><code>--network mongo-network</code>는 MongoDB와 동일한 Docker 네트워크에 컨테이너를 연결하여 서로 통신할 수 있게 합니다.</li><li><code>-p 8081:8081</code>은 호스트 머신의 포트 8081에서 Mongo Express 웹 인터페이스를 노출합니다.</li><li><code>mongo-express</code>는 실행 중인 Docker 이미지의 이름입니다.</li></ul><p>컨테이너가 실행되면 브라우저를 열고 <code>http://localhost:8081</code>에 접속하여 Mongo Express에 액세스하고 MongoDB 인스턴스와 상호작용할 수 있습니다.</p><p>사용 가능한 환경 변수 및 옵션에 대한 자세한 내용은 <a href="https://hub.docker.com/_/mongo-express">Mongo Express의 공식 Docker Hub 페이지</a>에서 확인할 수 있습니다.</p><p>브라우저에서 <code><a href="http://localhost:8081/">http://localhost:8081</a></code>을 열기 전에 Mongo Express 컨테이너가 제대로 실행되고 있는지 확인하는 것이 좋습니다. 로그를 보면 확인할 수 있습니다:</p><pre><code class="language-bash">docker logs &lt;container-id&gt;
# 또는
docker logs mongo-express
</code></pre><p>다음과 유사한 출력이 표시되어야 합니다:</p><pre><code class="language-bash">Waiting for mongo:27017...
No custom config.js found, loading config.default.js
Welcome to mongo-express 1.0.2
------------------------
Mongo Express server listening at http://0.0.0.0:8081
Server is open to allow connections from anyone (0.0.0.0)
basicAuth credentials are "admin:pass", it is recommended you change this in your config.js!
</code></pre><p>이것은 Mongo Express가 실행 중이며 MongoDB 인스턴스에 연결할 준비가 되었음을 확인하는 것입니다.</p><p>로그에 표시된 basicAuth 자격 증명(admin:pass)을 기록해두세요. 이러한 자격 증명이 있는 경우 브라우저에서 Mongo Express에 액세스할 때 사용해야 합니다. 나중에 보안을 강화하기 위해 사용자 정의 config.js 파일에서 변경할 수 있습니다.</p><p>로그에서 모든 것이 정상으로 보이면 <a href="http://localhost:8081">http://localhost:8081</a>에 안전하게 접속하여 Mongo Express 인터페이스에 액세스할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-11.png" class="kg-image" alt="image-11" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-11.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-11.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-11.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-11.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="993" loading="lazy"></figure><p>Mongo Express에 액세스할 때 브라우저에서 사용자 이름과 비밀번호를 묻는 경우 컨테이너 로그에 표시된 basicAuth 자격 증명을 사용하세요:</p><pre><code class="language-text">사용자 이름: admin
비밀번호: pass</code></pre><p>이것들은 기본 자격 증명이며, 보안을 강화하기 위해 나중에 사용자 정의 <code>config.js</code> 파일에서 변경하는 것을 <strong>강력히 권장</strong>합니다.</p><p>Mongo Express를 열면 이미 생성된 일부 기본 데이터베이스가 표시됩니다. 이 프로젝트에서는 todos라는 새 데이터베이스를 만들 것입니다. 생성되면 Node.js 애플리케이션이 이 데이터베이스에 연결하여 데이터를 저장하고 검색할 수 있습니다.</p><h2 id="node-js-mongodb-">Node.js를 MongoDB에 연결하는 방법</h2><p>이미 Docker 컨테이너(mongo) 내에서 MongoDB가 실행되고 있습니다. 컨테이너는 기본 MongoDB 포트 27017을 호스트에 노출하므로 노트북/데스크톱의 모든 프로세스가 <a href="http://localhost:27017/">localhost:27017</a>을 통해 접근할 수 있습니다.</p><p><strong>중요</strong>: Node.js 앱은 <strong>Docker 외부</strong>에 있습니다(터미널에서 시작하는 일반 node server.js 프로세스입니다).</p><p>앱이 외부에 있기 때문에 호스트 이름으로 컨테이너 이름 mongo가 <strong>아닌</strong> localhost(또는 127.0.0.1)를 <strong>사용해야 합니다.</strong></p><p>나중에 Node.js 앱을 컨테이너화하고 동일한 Docker 네트워크에 배치하면 호스트를 mongo로 전환할 것입니다. 지금은 localhost로 유지하세요.</p><h2 id="node-js-">Node.js 백엔드</h2><p>다음은 MongoDB를 사용하는 <code>server.js </code>버전입니다:</p><pre><code class="language-javascript">const express = require("express");
const multer = require("multer");
const path = require("path");
const fs = require("fs");
const { MongoClient, ObjectId } = require("mongodb");

const app = express();
const PORT = 3000;

// Host = localhost  →  노출된 포트를 통해 MongoDB 컨테이너와 통신
// Port = 27017      →  기본 MongoDB 포트
// User / Pass       →  admin / password (컨테이너에 제공한 자격 증명)
const mongoUrl = "mongodb://admin:password@localhost:27017";
const dbName = "todos";
let db;

MongoClient.connect(mongoUrl)
  .then((client) =&gt; {
    db = client.db(dbName);
    console.log("Connected to MongoDB →", dbName);
  })
  .catch((err) =&gt; console.error("MongoDB connection error:", err));

const uploadDir = path.join(__dirname, "uploads");
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);

const storage = multer.diskStorage({
  destination: (req, file, cb) =&gt; cb(null, uploadDir),
  filename: (req, file, cb) =&gt; {
    const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);
    cb(null, "photo-" + unique + path.extname(file.originalname));
  },
});
const upload = multer({ storage });

app.use(express.static(__dirname));
app.use("/uploads", express.static(uploadDir));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/todos", async (req, res) =&gt; {
  const todos = await db.collection("todos").find().toArray();
  res.json(todos);
});

app.post("/todos", upload.single("photo"), async (req, res) =&gt; {
  const text = req.body.text?.trim();
  if (!text) return res.status(400).json({ error: "Text required" });

  const todo = {
    text,
    image: req.file ? `/uploads/${req.file.filename}` : null,
    createdAt: new Date(),
  };

  const result = await db.collection("todos").insertOne(todo);
  todo._id = result.insertedId;
  res.json(todo);
});

// 서버 시작
app.listen(PORT, () =&gt; {
  console.log(`Server → http://localhost:${PORT}`);
});
</code></pre><h2 id="--10">프론트엔드</h2><p><code>index.html</code>:</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="UTF-8" /&gt;
    &lt;title&gt;Todo + Image&lt;/title&gt;
    &lt;style&gt;
      body {
        font-family: sans-serif;
        margin: 2rem;
        max-width: 800px;
      }
      .todo {
        border: 1px solid #ccc;
        padding: 1rem;
        margin-bottom: 1rem;
        border-radius: 8px;
      }
      .todo img {
        max-height: 150px;
        margin-top: 0.5rem;
      }
      .error {
        color: red;
      }
      input[type="text"] {
        width: 100%;
        padding: 0.5rem;
        margin-bottom: 0.5rem;
      }
      #preview {
        max-width: 300px;
        margin-top: 0.5rem;
        display: none;
      }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;Todo List with Images&lt;/h1&gt;

    &lt;div id="addForm"&gt;
      &lt;input type="text" id="textInput" placeholder="What needs to be done?" /&gt;
      &lt;input type="file" id="imageInput" accept="image/*" /&gt;
      &lt;img id="preview" alt="preview" /&gt;
      &lt;button id="addBtn"&gt;Add Todo&lt;/button&gt;
      &lt;p id="status"&gt;&lt;/p&gt;
    &lt;/div&gt;

    &lt;h2&gt;Todos&lt;/h2&gt;
    &lt;div id="todos"&gt;&lt;/div&gt;

    &lt;script&gt;
      const $ = document.querySelector.bind(document);

      const textInput = $("#textInput");
      const imageInput = $("#imageInput");
      const preview = $("#preview");
      const addBtn = $("#addBtn");
      const status = $("#status");
      const todosDiv = $("#todos");

      imageInput.addEventListener("change", () =&gt; {
        const file = imageInput.files[0];
        if (!file) {
          preview.style.display = "none";
          return;
        }
        const reader = new FileReader();
        reader.onload = (e) =&gt; {
          preview.src = e.target.result;
          preview.style.display = "block";
        };
        reader.readAsDataURL(file);
      });

      addBtn.addEventListener("click", async () =&gt; {
        const text = textInput.value.trim();
        if (!text) {
          status.textContent = "Please enter a todo text.";
          status.className = "error";
          return;
        }

        const form = new FormData();
        form.append("text", text);
        if (imageInput.files[0]) form.append("photo", imageInput.files[0]);

        try {
          const res = await fetch("/todos", { method: "POST", body: form });
          const data = await res.json();
          if (!res.ok) throw new Error(data.error || "failed");
          status.textContent = "Todo added!";
          status.className = "";
          textInput.value = "";
          imageInput.value = "";
          preview.style.display = "none";
          loadTodos(); // 목록 새로고침
        } catch (err) {
          status.textContent = "Error: " + err.message;
          status.className = "error";
        }
      });

      async function loadTodos() {
        const res = await fetch("/todos");
        const todos = await res.json();
        todosDiv.innerHTML = "";
        todos.forEach((t) =&gt; {
          const div = document.createElement("div");
          div.className = "todo";
          div.innerHTML = `&lt;strong&gt;${escapeHtml(t.text)}&lt;/strong&gt;`;
          if (t.image) {
            div.innerHTML += `&lt;br&gt;&lt;img src="${t.image}" alt="todo image"&gt;`;
          }
          todosDiv.appendChild(div);
        });
      }

      function escapeHtml(s) {
        const div = document.createElement("div");
        div.textContent = s;
        return div.innerHTML;
      }

      loadTodos();
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre><p>이제 Node.js 앱이 Docker에서 실행 중인 MongoDB 컨테이너에 연결할 수 있습니다. 앱이 현재 Docker 외부에서 실행되고 있으므로 설정한 자격 증명(<code>admin</code> / <code>password</code>)을 사용하여 <code>localhost:27017</code>을 통해 연결합니다.</p><p>연결되면 Node.js 백엔드는 메모리 내 배열을 대체하여 MongoDB의 todos 데이터베이스에서 직접 <code>todos</code>를 저장하고 검색합니다. 나중에 Node.js 앱을 컨테이너화하고 MongoDB와 동일한 Docker 네트워크에 배치하면 호스트를 <code>localhost</code>에서 컨테이너 이름 <code>mongo</code>로 전환할 수 있습니다. 우리는 그 방향으로 가고 있습니다.</p><p>코드를 실행하고 설정에 맞게 수정할 준비가 된 전체 백엔드 및 프론트엔드 코드는 이곳에서 얻을 수 있습니다: <a href="https://github.com/Oghenekparobo/docker_tut_js/tree/mongodb-connection">GitHub 저장소</a>.</p><h2 id="docker-compose-">Docker Compose 사용 방법</h2><p>이제 Node.js 앱이, 컨테이너 안에서 실행 중인 MongoDB와 Mongo Express에 연결되었습니다. 우리는 네트워크를 만들고 컨테이너를 시작했으며, 모든 서비스가 완벽하게 통신하고 있습니다.</p><p>하지만 솔직히 말해서: 매번 길고 복잡한 <code>docker run</code> 명령어를 입력하기란 꽤 번거롭습니다. 명령어 한 번에 모든 것을 실행할 수 있는 간단하고 깔끔한 방법이 있으면 좋겠죠? 바로 <strong>Docker Compose</strong>가 그 역할을 합니다.</p><p>Docker Compose는 하나의 명령으로, 여러 컨테이너로 구성된 애플리케이션을 정의하고 실행할 수 있는 도구입니다. 여러 개의 <code>docker run</code> 명령을 수동으로 실행하는 대신, 간단한 <code>docker-compose.yml</code> 파일에 각 서비스(예: Node.js 앱, MongoDB, Mongo Express)와 해당 구성, 환경 변수, 공유 네트워크를 지정하여, 설정을 정의할 수 있습니다.</p><p>즉, 여러 컨테이너를 하나의 프로젝트처럼 관리할 수 있게 도와주며,<br>시작, 중지, 유지 보수를 모두 하나의 파일과 명령으로 처리할 수 있습니다.</p><p><code>docker-compose.yml</code> 또는 <code>docker-compose.yaml</code> 중 어느 것이나 사용이 가능하지만, 관례상 <code>.yml</code> 확장자가 더 일반적입니다.</p><p>Docker는 실행 시 이를 자동으로 감지합니다:</p><pre><code class="language-bash">docker compose up
</code></pre><p>네, 관례대로 docker-compose.yml 파일을 사용하는 것이 좋습니다.</p><p>이제 MongoDB와 Mongo Express 컨테이너를 실행하려면 각각 다음 두 명령어를 사용하면 됩니다.</p><pre><code class="language-bash"># MongoDB container
docker run -p 27017:27017 -d \
  -e MONGO_INITDB_ROOT_USERNAME=admin \
  -e MONGO_INITDB_ROOT_PASSWORD=password \
  --name mongo \
  --network mongo-network \
  mongo

# Mongo Express container
docker run -d \
  -e ME_CONFIG_MONGODB_ADMINUSERNAME=admin \
  -e ME_CONFIG_MONGODB_ADMINPASSWORD=password \
  -e ME_CONFIG_MONGODB_SERVER=mongo \
  --name mongo-express \
  --network mongo-network \
  -p 8081:8081 \
  mongo-express
</code></pre><p>이제 매번 이러한 긴 명령어를 입력하는 대신, <strong>Docker Compose</strong> 파일을 사용하여 모든 명령어를 한 번에 실행할 것입니다.</p><p><code>docker-compose.yml</code> 파일은 Node.js 프로젝트의 루트 디렉터리에 위치해야 합니다.<br></p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-12.png" class="kg-image" alt="image-12" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-12.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-12.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-12.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-12.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="1119" loading="lazy"></figure><p><code>docker-compose.yml</code> 파일은 다음과 같습니다:</p><pre><code class="language-yaml">version: "3.8"

services:
  mongodb:
    image: mongo
    container_name: mongo
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password

  mongo-express:
    image: mongo-express
    container_name: mongo-express
    ports:
      - "8081:8081"
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: admin
      ME_CONFIG_MONGODB_ADMINPASSWORD: password
      ME_CONFIG_MONGODB_SERVER: mongodb
    depends_on:
      - mongodb
</code></pre><p>지금 무슨 일이 벌어지고 있는지 하나씩 살펴보도록 하겠습니다.</p><ul><li><code>version: "3.8"</code> → <strong>Compose 파일의 버전</strong>을 정의합니다. 각 버전마다 조금씩 다른 문법 규칙과 기능이 있으며, 3.8 버전은 최신 Docker Engine과 호환되는 현대적인 버전입니다.</li><li><code>services:</code> → 실행할 모든 컨테이너를 정의하는 영역입니다. 여기서는 <code>mongodb</code>와 <code>mongo-express</code> 두 서비스입니다.</li></ul><p><strong>MongoDB 서비스</strong></p><ul><li><code>image: mongo</code>: Docker Hub에서 공식 MongoDB 이미지를 가져옵니다.</li><li><code>container_name: mongo</code>: 컨테이너에 친근한 이름을 지정합니다.</li><li><code>ports: "27017:27017"</code>: MongoDB의 기본 포트를 호스트에 노출하여 Node.js 또는 다른 애플리케이션이 연결할 수 있도록 합니다.</li><li><code>environment</code>: 초기 루트 사용자 이름과 비밀번호를 설정합니다.</li></ul><p><strong>Mongo Express 서비스</strong></p><ul><li><code>image: mongo-express</code>: 공식 Mongo Express 이미지를 사용합니다.</li><li><code>container_name: mongo-express</code>: 컨테이너 이름을 더 쉽게 참조할 수 있도록 친근하게 지정합니다.</li><li><code>ports: "8081:8081"</code>: 호스트의 8081 포트에서 Mongo Express 웹 인터페이스를 노출합니다.</li><li><code>environment</code>: Mongo Express에게 MongoDB를 연결하는 방법(사용자 이름, 비밀번호, 호스트)를 알려줍니다.</li><li><code>depends_on: - mongodb</code>: MongoDB가 먼저 실행된 후 Mongo Express가 즉시 시작되도록 보장합니다.</li></ul><h3 id="docker-compose--1">Docker Compose를 사용하는 이유</h3><ul><li><strong>한 번의 명령으로 시작 가능: </strong>여러 개의 <code>docker run</code> 명령 대신, 그저 아래 명령어를 실행하세요:</li></ul><pre><code>docker compose up -d
</code></pre><ul><li><strong>자동 네트워킹: </strong>Compose는 기본 네트워크를 생성하여 서비스들이 <strong>서비스 이름</strong>(우의 경우 <code>mongodb</code>)을 사용하여 통신할 수 있도록 합니다.</li><li><strong>쉽고 유연한 유지보수: </strong>모든 서비스를 함께 중지, 재시작, 재빌드할 수 있습니다.</li></ul><p>새로운 <code>docker-compose.yml</code> 파일을 실행하기 전에 충돌을 일으키는 컨테이너가 현재 실행 중이지 않은지 확인하는 것이 중요합니다. 이전 <code>docker run</code> 명령으로 MongoDB와 Mongo Express가 이미 실행 중이었음을 기억하세요.</p><p>충돌을 방지하려면(이미 사용 중인 포트처럼) 실행 중인 컨테이너를 모두 중지하고 제거해야 합니다.</p><p>방법은 다음과 같습니다.</p><pre><code class="language-bash"># 실행 중인 컨테이너 목록 보기
docker ps

# 개별 컨테이너 중지(&lt;container_name&gt; 부분을 mongo 또는 mongo-express로 대체)
docker stop mongo
docker stop mongo-express

# 중지된 컨테이너 삭제
docker rm mongo
docker rm mongo-express

# (선택) 실행 중인 모든 컨테이너 중지 및 삭제
docker stop $(docker ps -q)
docker rm $(docker ps -a -q)
</code></pre><ul><li><code>docker ps</code> 명령은 현재 실행 중인 컨테이너들을 보여줍니다.</li><li><code>docker stop &lt;name&gt;</code> 명령은 지정한 이름의 컨테이너를 정상적으로 중지합니다.</li><li><code>docker rm &lt;name&gt;</code> 명령은 중지된 컨테이너를 Docker에서 삭제합니다.</li><li><code>docker stop $(docker ps -q)</code> 명령은 현재 실행 중인 모든 컨테이너를 중지합니다.</li><li><code>docker rm $(docker ps -a -q)</code> 명령은 실행 중이든 중지 중이든 모든 컨테이너를 삭제합니다.</li></ul><p>이전 단계의 컨테이너를 모두 중지하고 제거했다면,<br>이제 포트 충돌 없이 안전하게 Docker Compose 환경을 실행할 준비가 된 것입니다.</p><p>이제 모든 기존 컨테이너가 중지되었으므로,<br><code>docker-compose.yml</code> 파일을 이용해 MongoDB와 Mongo Express를 함께 실행할 수 있습니다.</p><p>Node.js 프로젝트 루트(즉, <code>docker-compose.yml</code> 파일이 위치한 디렉터리)에서 다음 명령을 실행하세요:</p><pre><code class="language-bash">docker compose up -d
</code></pre><p>이 명령이 하는 일:</p><ul><li><code>docker compose</code> → Docker에게 Compose를 사용하라고 지시합니다.</li><li><code>up</code> → Compose 파일에 정의된 모든 서비스를 빌드(필요 시)하고 시작합니다.</li><li><code>-d</code> → 컨테이너를 백그라운드(분리 모드)로 실행시켜, 즉, (터미널을 차지하지 않고) 백그라운드에서 실행된다는 의미입니다.</li></ul><p>이 명령을 실행하면 Docker는 MongoDB와 Mongo Express를 동시에 시작하고,<br>두 컨테이너를 같은 내부 네트워크에 연결하며, 각 서비스에 대해 우리가 지정한 포트를 노출합니다.(MongoDB는 <code>27017</code>, Mongo Express는 <code>8081</code>)</p><p>모든 것이 정상적으로 작동했다면, 다음 명령을 실행한 후:﻿</p><pre><code class="language-bash">docker compose up -d</code></pre><p>정상적으로 실행되었다면 다음과 유사한 출력이 나타납니다:</p><pre><code class="language-bash">[+] Running 3/3
 ✔ Network docker_tut_default  Created                                                                                               0.0s 
 ✔ Container mongo             Started                                                                                               0.6s 
 ✔ Container mongo-express     Started                                                                                               0.8s 
stephenjohnson@Oghenekparobo docker_tut %
</code></pre><h3 id="--11">이 출력이 의미하는 것</h3><ul><li><code>Network docker_tut_default Created</code> → Docker Compose가 서비스 간 통신을 위해 새 네트워크를 자동으로 생성했습니다.</li><li><code>Container mongo Started</code> → MongoDB 컨테이너가 실행되었습니다.</li><li><code>Container mongo-express Started</code> → Mongo Express 컨테이너가 실행되었습니다.</li></ul><p>컨테이너가 실제로 실행 중인지 확인하려면 다음 명령을 입력합니다:</p><pre><code class="language-bash">docker ps
</code></pre><p>이 명령은 현재 활성 상태의 모든 컨테이너를 나열합니다. <code>mongo</code>와 <code>mongo-express</code> 컨테이너가 각각 MongoDB(포트 <code>27017</code>)와 Mongo Express(포트 <code>8081</code>)로 표시되어 있어야 합니다.</p><ul><li>Mongo Express에 접속하려면 브라우저를 열고 <a href="http://localhost:8081">http://localhost:8081</a>로 이동하여 웹 인터페이스를 통해 MongoDB와 상호작용하세요.</li><li>MongoDB에 접속하려면 Node.js 앱에서 Compose 파일에 설정한 자격 증명을 사용하여 <code>localhost:27017</code>로 MongoDB에 연결할 수 있습니다.</li></ul><p><code>docker run</code> 명령을 여러 번 사용하는 것과 비교했을 때, Docker Compose를 사용하는 것이 더 쉬운 이유는 다음과 같습니다:</p><ul><li>여러 컨테이너를 한 번의 명령으로 실행할 수 있습니다.</li><li>컨테이너 간 자동 네트워크 설정이 이루어집니다.</li><li>나중에 컨테이너를 중지, 삭제, 재빌드하기가 훨씬 쉽습니다.</li></ul><p>간단히 말해, Docker Compose는 모든 것을 단순화하고 체계화하여 개발 환경 관리를 훨씬 더 쉽게 만들어줍니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-13.png" class="kg-image" alt="image-13" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-13.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-13.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-13.png 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/image-13.png 1962w" sizes="(min-width: 720px) 720px" width="1962" height="746" loading="lazy"></figure><p>이 단계에서 알아야 할 중요한 점은 MongoDB에 추가하는 모든 데이터는 임시적이라는 것입니다. &nbsp;컨테이너를 중지하거나 삭제한 뒤 다시 실행하면, 이전에 저장한 데이터가 모두 사라진 것을 확인할 수 있습니다. 이는 컨테이너 내부의 데이터가 기본적으로 영구 보존되지 않기 때문입니다.</p><p>걱정하지 마세요. 이것은 정상적인 동작입니다. 튜토리얼의 이후 단계에서 <strong>Docker Volumes</strong> 개념을 다루며 어떻게 데이터를 영구적으로 저장할 수 있는지 배울 예정입니다. </p><p>일단은 컨테이너를 재시작할 때마다 MongoDB가 초기 상태로 시작된다는 사실만 기억하시면 됩니다.</p><p>전체 예제(Dockerfile<strong>과</strong> docker-compose 파일을 포함한 예시)는 <a href="https://github.com/Oghenekparobo/docker_tut_js/tree/docker-compose">이 GitHub 링크</a>에서 확인하실 수 있습니다.</p><h2 id="-docker--1">나만의 Docker 이미지 빌드하는 방법</h2><p>이제 Node.js 애플리케이션을 로컬에서 테스트했고, MongoDB와 Mongo Express와도 완벽히 연동되어 작동하는 것을 확인했습니다. 다음 단계는 이제 배포를 위한 준비입니다.</p><p>로컬 환경에서 직접 애플리케이션을 실행하는 것은 개발 단계에서는 괜찮지만, 배포 환경이나 다른 서버로 옮기고자 할 때는 실용적이지 않습니다. Docker 이미지를 생성하여, 애플리케이션 코드, 의존성, 설정, 실행 환경을 하나의 이동 가능한 단위로 패키징할 수 있습니다. 이 이미지는 Docker가 설치된 어디서든 동일하게 실행될 수 있으며, 개발–테스트–운영 환경 간에 일관된 동작 보장이 가능합니다.</p><p>간단히 말해, Docker 이미지를 빌드한다는 것은 애플리케이션을 컨테이너화하고 배포 준비 상태로 만든다는 뜻입니다.</p><p>이번에는 우리가 만든 Todo 앱을 컨테이너화하기 위해 <strong>Dockerfile</strong>이 필요합니다.<br>Dockerfile은 Docker에게 이 애플리케이션으로 이미지를 어떻게 빌드할지를 알려주는 청사진(blueprint)입니다. 이 설정은 기본 환경을 정의하고, 애플리케이션 코드를 복사하며, 필요한 의존성을 설치하고, 애플리케이션이 어떻게 시작될지를 지정합니다. 이 청사진을 통해 Docker는 어떤 머신에서든 동일하게 동작하는 일관된 이미지를 생성할 수 있으며, 우리의 Node.js 애플리케이션을 완전히 이식 가능하고 배포 준비가 완료된 상태로 만들어 줍니다.</p><p>우리의 Dockerfile에서는 대문자 <code>D</code>를 사용하는 점에 주의하세요. 이는 표준 명명 규칙입니다. 이 파일은 Node.js 프로젝트의 <strong>루트 디렉토리</strong>에 위치시켜야 합니다. 단순한 프로젝트의 경우, <code>server.js</code> 또는 <code>index.js</code> 같은 메인 파일과 <code>package.json</code>이 루트에 함께 위치합니다. Docker는 이 파일을 청사진으로 사용하여 애플리케이션의 컨테이너 이미지를 빌드합니다.</p><p>만약 메인 앱 파일이 하위 폴더에 있다면 괜찮습니다. 단, Dockerfile 내 <code>COPY</code> 및 <code>CMD</code> 명령에서 정확한 경로를 지정해야 합니다. 중요한 점은 Dockerfile이 프로젝트 루트에 있어야 Docker가 여러분의 앱 빌드 기준점을 인식할 수 있다는 것입니다.</p><p>우리의 Dockerfile 내용은 다음과 같습니다:</p><pre><code class="language-dockerfile"># Use full Node 18 (Debian-based)
FROM node:18

# Set environment variables
ENV MONGO_DB_USERNAME=admin \
    MONGO_DB_PASSWORD=password

# Set working directory
WORKDIR /home/app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy source code
COPY . .

# Expose port
EXPOSE 3000

# Start the app
CMD ["node", "server.js"]
</code></pre><p>여기서 어떤 일이 일어나고 있는지 살펴보겠습니다:</p><ul><li><code>FROM node:13-alpine</code> 은 컨테이너의 기본 이미지로서 Node.js가 이미 설치되어 있으며 매우 가볍기 때문에 이미지를 작게 유지할 수 있습니다.</li><li><code>ENV MONGO_DB_USERNAME=admin \ MONGO_DB_PASSWORD=password</code> 는 컨테이너 내부에서 환경 변수를 설정하여 Node.js 애플리케이션이 MongoDB에 연결할 수 있게 합니다.</li><li><code>WORKDIR /home/app</code> 은 컨테이너 내부에서의 작업 디렉터리를 지정합니다. 이후 실행되는 <code>COPY</code>, <code>RUN</code> 등의 명령은 이 폴더 기준으로 수행됩니다.</li><li><code>COPY . .</code> 은 로컬 프로젝트의 모든 파일을 컨테이너의 작업 디렉토리로 복사합니다. 여기에는 <code>server.js</code>, <code>package.json</code>을 포함하여 애플리케이션을 실행하는 데 필요한 모든 파일이 포함됩니다.</li><li><code>RUN npm install</code> 은 <code>package.json</code>에 나열된 의존성을 컨테이너 안에 설치합니다.</li><li><code>EXPOSE 3000</code> 은 컨테이너가 3000 포트에서 수신 대기한다는 것을 Docker에 알려주며, 이는 우리 Node.js 애플리케이션이 실행되는 포트입니다.</li><li><code>CMD ["node", "server.js"]</code> 은 컨테이너가 시작될 때 실행되는 명령어를 정의하여 Node.js 서버를 실행합니다.</li></ul><p>이 Dockerfile을 프로젝트 루트에 배치함으로써 Docker는 애플리케이션 파일과 의존성을 정확히 어디서 찾아야 하는지 알게 됩니다. 이미지를 빌드할 때 이식 가능한 컨테이너에 모두 패키징하여 Docker가 설치된 어디서든 실행할 수 있게 하며, 배포를 간단하고 일관되도록 해줍니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-14.png" class="kg-image" alt="image-14" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-14.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-14.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-14.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-14.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="960" loading="lazy"></figure><p>이제 Dockerfile이 준비되었으니, 다음 단계는 Node.js 애플리케이션용 Docker 이미지를 빌드하는 것입니다.</p><p>이미지를 빌드하려면 터미널을 열고, 프로젝트 루트 디렉토리(Dockerfile이 있는 곳)에 있는지 확인한 뒤 다음 명령어를 실행하세요:</p><pre><code class="language-bash">docker build -t todo-app:1.0 .
</code></pre><ul><li><code>todo-app</code> → 생성할 이미지의 이름</li><li><code>:1.0</code> → 버전 태그 (예: <code>1.0</code>, <code>v1</code>, <code>latest</code> 등 자유롭게 지정 가능)</li><li><code>.</code> → Docker에게 현재 폴더(프로젝트 루트)를 빌드 컨텍스트로 사용하라고 알려줍니다.</li></ul><p>다음 명령어를 실행한 후:</p><pre><code class="language-bash">docker build -t todo-app:1.0 .
</code></pre><p>Docker는 Dockerfile을 읽고, Node.js 애플리케이션과 모든 의존성을 패키징하여 Docker 이미지를 생성합니다. 이미지가 생성되었는지 확인하려면 다음 명령어를 실행하세요:</p><pre><code class="language-bash">docker images
</code></pre><p>다음과 같은 출력이 나타날 것입니다:</p><pre><code class="language-bash">REPOSITORY      TAG       IMAGE ID       CREATED          SIZE
todo-app        1.0       d85dd4ed97f9   45 seconds ago   147MB
mongo           latest    1d659cebf5e9   2 weeks ago      894MB
mongo-express   latest    1133e12468c7   20 months ago    182MB
</code></pre><p>이 출력은 <code>todo-app</code> 이미지가 MongoDB와 Mongo Express 이미지와 함께 성공적으로 생성되었음을 보여줍니다.</p><h2 id="node-js--1">Node.js 앱 컨테이너 실행</h2><p>이제 이미지가 생성되었으니 다음 단계는 이 이미지를 사용해 컨테이너를 실행하는 것입니다. 컨테이너는 기본적으로 이미지의 실행 중인 인스턴스입니다. 이를 위해 다음 명령어를 실행하세요:</p><p><code>docker run todo-app:1.0</code></p><p>이 명령어는 다음을 수행합니다:</p><ul><li><code>docker run</code>은 이미지에서 새 컨테이너를 시작합니다.</li><li><code>todo-app:1.0</code>은 Docker에게 (방금 빌드한) 이미지를 사용하라고 알려줍니다.</li></ul><p>이 명령어가 실행되면 Node.js 애플리케이션이 컨테이너 내부에서 로컬 환경과 분리된 상태로 실행됩니다. 브라우저에서 <a href="http://localhost:3000/">http://localhost:3000</a>을 열어 Todo 앱이 로컬에서와 동일하게 작동하는 것을 확인할 수 있습니다.</p><p>실행 중인 모든 컨테이너를 보려면 다음을 사용하세요:</p><pre><code class="language-bash">docker ps
</code></pre><p>다음과 같은 출력을 볼 수 있습니다:</p><pre><code class="language-bash">CONTAINER ID   IMAGE           COMMAND         CREATED       STATUS       PORTS                  NAMES
d85dd4ed97f9   todo-app:1.0    "node server.js"  10s ago      Up 10s       0.0.0.0:3000-&gt;3000/tcp   awesome_todo
</code></pre><p>이것은 컨테이너가 실행 중임을 확인해 줍니다. 필요시 중지하려면:</p><pre><code class="language-bash">docker stop &lt;container-id&gt;
</code></pre><h2 id="--12">오류 해결</h2><p>여기서 문제가 발생합니다: <code>docker run todo-app:1.0</code>을 실행하면 다음과 같은 오류가 나타납니다:</p><pre><code>Server → http://localhost:3000 
MongoDB connection error: MongoServerSelectionError: getaddrinfo ENOTFOUND mongodb
    at Topology.selectServer (/home/app/node_modules/mongodb/lib/sdam/topology.js:346:38)
    ...
    [cause
</code></pre><p>특히 todo 리스트 생성 등의 작업을 시도할 때 발생합니다.</p><p><code>getaddrinfo ENOTFOUND mongodb</code> 오류는 Node.js 컨테이너가 MongoDB를 찾을 수 없음을 알려줍니다. MongoDB가 다른 컨테이너에서 실행 중이더라도 앱 컨테이너는 격리되어 있어 이를 찾을 수 없습니다.</p><h2 id="--13">왜 이런 일이 발생하는가:</h2><p><code>server.js</code>에서 MongoDB에 다음과 같이 연결하는 것을 기억하세요:</p><pre><code class="language-js">const mongoUrl = "mongodb://admin:password@localhost:27017";
</code></pre><p>문제는 <code>localhost</code>에 있습니다. 로컬 머신에서 앱을 실행할 때는 <code>localhost</code>가 완벽하게 작동하지만, Docker 컨테이너 내부에서는 <code>localhost</code>가 해당 컨테이너 자신만 가리킵니다.</p><p>비유하자면:</p><ul><li><strong>로컬 실행</strong>: 앱과 MongoDB가 같은 방에 있는 두 사람, <code>localhost</code> 작동</li><li><strong>Docker 실행</strong>: 각 컨테이너가 별도의 방, <code>localhost</code>는 그 방만 가리킴</li></ul><h2 id="--14">해결책</h2><p>MongoDB 연결 URL을 <code>localhost</code> 대신 Docker 서비스 이름으로 변경해야 합니다. <code>server.js</code> 파일을 업데이트하세요:</p><pre><code class="language-js">const mongoUrl = "mongodb://admin:password@localhost:27017";
</code></pre><p>다음으로 변경하세요:</p><pre><code class="language-js">const mongoUrl = "mongodb://admin:password@mongodb:27017";
</code></pre><p>완전한 업데이트된 <code>server.js</code>는 다음과 같습니다:</p><pre><code class="language-js">const express = require("express");
const multer = require("multer");
const path = require("path");
const fs = require("fs");
const { MongoClient, ObjectId } = require("mongodb");

const app = express();
const PORT = 3000;

// Host = localhost  →  talks to the MongoDB container via the exposed port
// Port = 27017      →  default MongoDB port
// User / Pass       →  admin / password (the credentials you gave the container)
const mongoUrl = "mongodb://admin:password@mongodb:27017";
const dbName = "todos";
let db;

MongoClient.connect(mongoUrl)
  .then((client) =&gt; {
    db = client.db(dbName);
    console.log("Connected to MongoDB →", dbName);
  })
  .catch((err) =&gt; console.error("MongoDB connection error:", err));

const uploadDir = path.join(__dirname, "uploads");
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir);

const storage = multer.diskStorage({
  destination: (req, file, cb) =&gt; cb(null, uploadDir),
  filename: (req, file, cb) =&gt; {
    const unique = Date.now() + "-" + Math.round(Math.random() * 1e9);
    cb(null, "photo-" + unique + path.extname(file.originalname));
  },
});
const upload = multer({ storage });

app.use(express.static(__dirname));
app.use("/uploads", express.static(uploadDir));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get("/todos", async (req, res) =&gt; {
  const todos = await db.collection("todos").find().toArray();
  res.json(todos);
});

app.post("/todos", upload.single("photo"), async (req, res) =&gt; {
  const text = req.body.text?.trim();
  if (!text) return res.status(400).json({ error: "Text required" });

  const todo = {
    text,
    image: req.file ? `/uploads/${req.file.filename}` : null,
    createdAt: new Date(),
  };

  const result = await db.collection("todos").insertOne(todo);
  todo._id = result.insertedId;
  res.json(todo);
});

// Start server
app.listen(PORT, () =&gt; {
  console.log(`Server → http://localhost:${PORT}`);
});
</code></pre><h2 id="-mongodb-">왜 <code>mongodb</code>가 작동하는가</h2><p>호스트명 <code>mongodb</code>는 <code>docker-compose.yml</code>에서 정의한 서비스 이름과 일치합니다:</p><pre><code class="language-yaml">services:
  mongodb:    # ← 다른 컨테이너들이 사용하는 호스트명
    image: mongo
    container_name: mongo
    ...
</code></pre><p>같은 Docker Compose 네트워크에서 컨테이너들이 실행될 때, Docker는 내부 DNS를 제공하여 서비스 이름을 올바른 컨테이너 IP 주소로 해석합니다. 따라서 앱이 <code>mongodb:27017</code>에 연결하려고 하면 Docker가 자동으로 MongoDB 컨테이너로 라우팅합니다.</p><h2 id="docker--2">Docker 이미지 재빌드</h2><p>코드를 업데이트했으니 이 변경사항을 포함하여 Docker 이미지를 재빌드해야 합니다:</p><pre><code class="language-bash">docker build -t todo-app:1.0 .
``

You should see output confirming the build completed successfully:
``
[+] Building 8.1s (10/10) FINISHED
 =&gt; [internal] load build definition from Dockerfile
 =&gt; =&gt; transferring dockerfile: 443B
 ...
 =&gt; =&gt; naming to docker.io/library/todo-app:1.0
</code></pre><h2 id="docker-compose--2">Docker Compose에 앱 추가</h2><p>이제 <code>docker-compose.yml</code> 파일을 업데이트하여 <code>todo-app</code> 서비스를 포함하세요:</p><pre><code class="language-yaml">version: "3.8"

services:
  mongodb:
    image: mongo
    container_name: mongo
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password

  mongo-express:
    image: mongo-express
    container_name: mongo-express
    ports:
      - "8081:8081"
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: admin
      ME_CONFIG_MONGODB_ADMINPASSWORD: password
      ME_CONFIG_MONGODB_SERVER: mongodb
    depends_on:
      - mongodb

  todo-app:
    image: todo-app:1.0
    container_name: todo-app
    ports:
      - "3000:3000"
    depends_on:
      - mongodb
</code></pre><p><code>todo-app</code> 서비스는 다음을 포함합니다:</p><ul><li><code><strong>image: todo-app:1.0</strong></code> - 방금 재빌드한 Docker 이미지 사용</li><li><code><strong>container_name: todo-app</strong></code> - 컨테이너에 친근한 이름 부여</li><li><code><strong>ports: "3000:3000"</strong></code> - 3000 포트로 앱 노출</li><li><code><strong>depends_on: mongodb</strong></code> - MongoDB가 앱보다 먼저 시작되도록 보장</li></ul><h2 id="--15">모든 서비스 시작</h2><p>먼저 실행 중인 컨테이너들을 중지하세요:</p><pre><code class="language-bash">docker compose down
</code></pre><p><strong>로컬 시스템에서 3000 포트가 사용 중이라면 이를 중지하세요 (3000 포트 해제)</strong>.</p><p>이전에 로컬에서 서버를 실행했지만, 이제 Docker 이미지를 빌드했으므로 앱은 컨테이너 내부에서 실행되며 로컬 머신 환경에 더 이상 의존하지 않습니다.</p><pre><code>node server.js
Server → http://localhost:3000
</code></pre><p>이제 해당 터미널에서 Ctrl + C로 중지하세요.</p><p>그 다음 모든 것을 함께 시작하세요:</p><pre><code class="language-bash">docker compose up -d
``

You should see:

``
[+] Running 4/4
 ✔ Network docker_tut_default  Created
 ✔ Container mongo             Started
 ✔ Container mongo-express     Started
 ✔ Container todo-app          Started
</code></pre><h2 id="--16">모든 것이 작동하는지 확인</h2><p>모든 컨테이너가 실행 중인지 확인하세요:</p><pre><code class="language-bash">docker ps
``

예상 출력:
``
CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS          PORTS                      NAMES
a1b2c3d4e5f6   todo-app:1.0    "node server.js"         30 seconds ago   Up 28 seconds   0.0.0.0:3000-&gt;3000/tcp     todo-app
3d7c797fde1d   mongo-express   "/sbin/tini -- /dock…"   30 seconds ago   Up 29 seconds   0.0.0.0:8081-&gt;8081/tcp     mongo-express
4511ade73c38   mongo           "docker-entrypoint.s…"   30 seconds ago   Up 29 seconds   0.0.0.0:27017-&gt;27017/tcp   mongo
``

## 애플리케이션 테스트

이제 모든 것이 작동하는지 확인해보겠습니다:

### 1. Todo 앱 접속
브라우저를 열고 다음으로 이동하세요:
``
http://localhost:3000
``

### 2. Todo 항목 생성
기능을 테스트하기 위해 몇 개의 todo 항목을 추가하세요. 이미지 업로드도 시도해보세요!

### 3. Mongo Express에서 확인
Mongo Express를 열어보세요:
``
http://localhost:8081
</code></pre><p><code>todos</code> 데이터베이스, 그 다음 <code>todos</code> 컬렉션으로 이동하세요. 방금 생성한 모든 todo와 완전한 데이터가 표시됩니다.</p><h2 id="--17">무엇이 변경되었고 왜 작동하는가</h2><p><strong>수정 전:</strong></p><ul><li>연결 문자열이 <code>localhost:27017</code> 사용 ❌</li><li>컨테이너가 자기 자신에서 MongoDB 찾음</li><li><code>ENOTFOUND</code> 오류로 연결 실패</li></ul><p><strong>수정 후:</strong></p><ul><li>연결 문자열이 <code>mongodb:27017</code> 사용 ✅</li><li>Docker 내부 DNS가 <code>mongodb</code>를 MongoDB 컨테이너로 해석</li><li>연결 성공 및 데이터 정상 흐름</li></ul><p>이는 Docker 네트워킹의 핵심 교훈입니다: 컨테이너들은 <code>localhost</code>가 아닌 서비스 이름으로 통신합니다. Docker Compose는 모든 서비스가 이름으로 서로를 찾을 수 있는 네트워크를 자동 생성합니다.</p><h2 id="--18">컨테이너 관리 방법</h2><p>실행 중인 컨테이너를 관리하는 방법을 간단히 알아보겠습니다. 일반적으로 다음 명령어들을 사용합니다:</p><p><strong>모든 서비스 중지</strong>:</p><pre><code class="language-bash">docker compose down
</code></pre><p><strong>앱 로그 보기</strong>:</p><pre><code class="language-bash">docker compose logs todo-app
</code></pre><p><strong>실시간 로그 보기</strong>:</p><pre><code class="language-bash">docker compose logs -f todo-app
</code></pre><p><strong>코드 변경 후 재빌드</strong>:</p><pre><code class="language-bash">docker build -t todo-app:1.0 .
docker compose up -d --force-recreate todo-app
</code></pre><p>이제 애플리케이션이 완전히 컨테이너화되어 프로덕션 준비가 완료되었습니다. 세 서비스가 완벽하게 함께 작동하며, <code>docker-compose.yml</code> 파일과 빌드된 이미지만 있으면 Docker가 지원되는 어디서든 이 전체 스택을 배포할 수 있습니다.</p><p>전체 업데이트된 코드는 <a href="https://github.com/Oghenekparobo/docker_tut_js/tree/docker-image">여기</a>에서 확인하세요.</p><h2 id="-docker--2">프라이빗 Docker 레지스트리 생성 방법</h2><p>이제 커스텀 Docker 이미지를(로컬 머신 대신에) 프라이빗 컨테이너 레지스트리에 저장하고 싶습니다. 이는 세 가지 주요 장점을 제공합니다:</p><ul><li><strong>1. 제어된 접근</strong> - 명시적으로 권한을 부여한 사람이나 서버만 이미지를 가져오거나 업로드할 수 있습니다. 코드와 의존성이 비공개로 안전하게 유지됩니다.</li><li><strong>2. 안정적인 배포</strong> - 올바른 AWS 자격 증명을 가진 누구나(또는 어떤 서버도) 전 세계 어디서든 정확히 동일한 이미지를 가져올 수 있어 "내 머신에서는 작동하는데..." 문제가 사라집니다.</li><li><strong>3. 버전 관리 및 라이프사이클 관리</strong> - 여러 태그 버전(1.0, 2.0, latest 등)을 유지하고 필요시 쉽게 롤백할 수 있습니다.</li></ul><p>첫 번째 단계는 프라이빗 Docker 레지스트리(컨테이너 레지스트리)를 생성하는 것입니다. 여기서는 <a href="https://aws.amazon.com/ecr/">AWS Elastic Container Registry (ECR)</a>를 사용하겠습니다. Amazon ECR은 컨테이너 이미지와 아티팩트를 어디에서나 안전하게 저장·관리·공유·배포할 수 있게 해주는 완전관리형 컨테이너 레지스트리 서비스입니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-15.png" class="kg-image" alt="image-15" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-15.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-15.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-15.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-15.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="1178" loading="lazy"></figure><p>홈페이지에 접속한 후 <strong>Create</strong> 버튼을 클릭하세요. 레포지토리 이름을 이미지와 동일하게 <code>todo-app</code>으로 지정한 후 Create를 클릭하여 설정을 완료합니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-16.png" class="kg-image" alt="image-16" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-16.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-16.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-16.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-16.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="1178" loading="lazy"></figure><p>추가 옵션은 신경 쓰지 않아도 됩니다 - 이것은 AWS 튜토리얼이 아닙니다.</p><p><strong>참고</strong>: AWS ECR에서는 각 이미지가 고유한 레포지토리를 가지며, 해당 이미지의 다양한 태그 버전을 저장합니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-17.png" class="kg-image" alt="image-17" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-17.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-17.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-17.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-17.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="1058" loading="lazy"></figure><p>이제 이미지를 프라이빗 레포지토리에 푸시하려면 두 가지를 해야 합니다. 먼저 프라이빗 레포에 로그인해야 합니다. AWS가 푸시를 허용하기 전에 본인을 인증해야 하기 때문입니다. 즉, 로컬 이미지를 레포에 푸시할 때 "이 레지스트리에 접근 권한이 있습니다. 여기 제 자격 증명입니다."라고 말하는 셈입니다.</p><p>우리의 경우 AWS ECR을 사용하므로 사용자 이름과 비밀번호를 수동으로 입력하는 대신 AWS를 통해 인증합니다.</p><h2 id="1-aws-">1단계: AWS 액세스 키 가져오기</h2><p>AWS 콘솔에서 액세스 키를 찾으려면 다음 단계를 따르세요:</p><p>1.	<a href="https://console.aws.amazon.com">https://console.aws.amazon.com</a> 에 접속해 AWS 콘솔에 로그인합니다.</p><p>2.	오른쪽 상단의 계정 이름을 클릭한 뒤 “보안 자격 증명(Security Credentials)”으로 이동합니다.</p><p>3.	아래로 스크롤해 "Access keys" 섹션을 찾습니다.</p><p>4.	액세스 키가 없다면:</p><ul><li>"Create access key"를 클릭합니다.</li><li>"Command Line Interface (CLI)"를 선택합니다.</li><li>확인 체크박스를 선택하고 Next를 클릭합니다.</li><li>설명을 입력(선택 사항)한 뒤 "Create access key"를 클릭합니다.</li></ul><p>5.<strong> 중요</strong>: <strong>액세스 키 ID</strong>(예: <code>AKIAIOSFODNN7EXAMPLE</code>처럼 보임)와 <strong>시크릿 액세스 키</strong>(예: <code>wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY</code>처럼 보임)를 둘 다 복사해 <strong>바로 저장하세요.</strong> 시크릿 키는 한 번만 표시되므로 잃어버리면 새 키 쌍을 만들어야 합니다.</p><p>또는, 다른 사람이 당신의 AWS 계정을 관리한다면 관리자에게 다음 사항을 요청해야 합니다:</p><ul><li>ECR 권한이 부여된 IAM 사용자</li><li>해당 사용자의 액세스 키 ID와 시크릿 액세스 키</li></ul><h2 id="2-aws-cli-">2단계: AWS CLI 설치 여부 확인</h2><p>다음 명령어로 확인할 수 있습니다:</p><pre><code class="language-bash">aws --version
</code></pre><h2 id="3-aws-cli-">3단계: AWS CLI에 자격 증명 설정</h2><p>다음 명령어를 실행합니다:</p><pre><code class="language-bash">aws configure
``

프롬프트에 다음 네 가지를 입력해야 합니다:
``
AWS Access Key ID [None]: &lt;여기에 Access Key ID를 붙여넣으세요&gt;
AWS Secret Access Key [None]: &lt;여기에 Secret Access Key를 붙여넉으세요&gt;
Default region name [None]: eu-north-1 또는 원하는 리전
Default output format [None]: json
</code></pre><p>프롬프트가 뜨면 키를 그대로 붙여넣고, 리전은 <code>eu-north-1</code> 또는 원하는 리전을 입력하고, 출력 형식은 <code>json</code>을 입력하거나 Enter를 눌러도 됩니다.</p><h2 id="4-aws-">4단계: AWS 설정 테스트</h2><p>모든 설정이 올바른지 확인하려면:</p><pre><code class="language-bash">aws sts get-caller-identity
</code></pre><p>정상이라면 AWS 계정 정보가 출력됩니다.</p><p><strong>5단계: ECR(Docker 레지스트리)에 로그인</strong><br>이제 ECR에 로그인합니다:</p><pre><code class="language-bash">aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin 244836489456.dkr.ecr.eu-north-1.amazonaws.com
</code></pre><p><strong>"Login Succeeded"</strong>가 출력되어야 합니다.</p><h2 id="docker--3">Docker 이미지 명명 방식 이해하기</h2><p>모든 Docker 이미지는 Docker에게 해당 이미지를 어디서 찾거나 저장할지를 알려주는 이름을 가지고 있습니다.<br>예를 들어 아래 명령을 실행하면:</p><pre><code class="language-bash">docker pull mongo:4.2
</code></pre><p>실제로는 다음에서 이미지를 가져옵니다:</p><pre><code>docker.io/library/mongo:4.2
</code></pre><p>여기에서 어떤 일이 일어나는지 살펴보겠습니다:</p><ul><li><code>[docker.io](http://docker.io/)</code> → 레지스트리(이 경우 Docker Hub)</li><li><code>library</code> → 공식 이미지에 대한 기본 네임스페이스</li><li><code>mongo</code> → 레포지토리 이름</li><li><code>4.2</code> → 이미지 태그</li></ul><p>`todo-app:1.0 같은 로컬 이미지를 빌드하면, 그 이미지는 여러분의 머신에만 존재합니다. 전체 레지스트리 경로를 포함하지 않으면 Docker는 그것을 어디에 푸시해야 할지 모릅니다.</p><p>AWS ECR을 사용할 때는 이미지 이름에 ECR 레지스트리 URL을 포함해야 합니다. 예를 들어:</p><pre><code class="language-bash">docker tag todo-app:1.0 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
</code></pre><p>그 다음 이렇게 푸시합니다:</p><pre><code class="language-bash">docker push 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
</code></pre><p>이 전체 경로가 없으면 Docker는 어떤 원격 레포지토리를 가리키는지 알 수 없습니다. 그래서 <code>todo-app:1.0</code>만으로는 동작하지 않습니다.</p><h2 id="6-">6단계: 이미지 빌드, 태그, 푸시</h2><p></p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-18.png" class="kg-image" alt="image-18" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-18.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-18.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-18.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-18.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="1058" loading="lazy"></figure><pre><code>로컬 이미지를 전체 ECR 경로로 태그하세요.
docker tag todo-app:1.0 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0

그리고 푸시:
docker push 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
</code></pre><p><strong>주의</strong>: 이미지 태그를 지정하고 푸시할 때 주의하세요. 모든 ECR 저장소 URL은 특정 AWS 계정과 리전에 연결되어 있습니다.</p><p>예를 들어, 튜토리얼에서 사용하는 것은:</p><pre><code>244836489456.dkr.ecr.eu-north-1.amazonaws.com
</code></pre><p>하지만 여러분의 ECR URL은 사용하는 AWS 계정과 선택한 리전(예: us-east-1, ap-south-1 등)에 따라 서로 다르게 됩니다.</p><p>그래서 <code>docker tag</code>, <code>docker push</code>를 실행하기 전에, 레지스트리 URL, 리전을 반드시 여러분의 값으로 바꿔야 합니다.</p><p>그렇지 않으면 “tag does not exist”, “repository not found” 같은 오류가 납니다.</p><p>요약하자면, 당황하지 말고 리전을 다시 한 번 확인한 뒤, 이미지를 푸시하기 전에 AWS 콘솔에 표시된 ECR URL이 정확한지 항상 확인하세요.</p><p>6단계를 성공적으로 실행했다면 터미널에 다음과 비슷한 출력이 보일 것입니다.</p><pre><code class="language-bash">stephenjohnson@Oghenekparobo docker_tut % docker tag todo-app:1.0 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
stephenjohnson@Oghenekparobo docker_tut % docker push 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
The push refers to repository [244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app]
4f94b5cbe8ab: Pushed 
85ba7bf54231: Pushed 
4ea46a43fa07: Pushed 
dee30873f229: Pushed 
e78159dbd370: Pushed 
a358a725b813: Pushed 
cd8a6003174c: Pushed 
abb63e49e652: Pushed 
6cc65bdde70e: Pushed 
41a4e3939504: Pushed 
3520c50ae60e: Pushed 
75ba6634710f: Pushed 
1.0: digest: sha256:51f07267936fc94d9b677db8a760801e6c5fd4764f4bb2bd7b4dd150c756a39b size: 2842</code></pre><p>이 출력은 이미지가 프라이빗 AWS ECR 레포지토리에 성공적으로 푸시되었음을 의미합니다.</p><p>이제 AWS Management Console에서 ECR로 이동하면, 태그 <code>1.0</code>이 붙은 <code>todo-app</code> 이미지를 목록에서 확인할 수 있습니다.</p><p>이 시점부터 이미지는 AWS ECR에 안전하게 저장되어 있으며, 레포지토리에 접근할 수 있는 어느 곳에서든 가져오거나 배포할 준비가 된 상태입니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/image-19.png" class="kg-image" alt="image-19" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/image-19.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/image-19.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/image-19.png 1600w, https://www.freecodecamp.org/korean/news/content/images/size/w2400/2025/12/image-19.png 2400w" sizes="(min-width: 720px) 720px" width="2000" height="1006" loading="lazy"></figure><h2 id="--19">과제: 앱의 새 버전 만들기 및 푸시하기</h2><p>이제 첫 번째 이미지(<code>todo-app:1.0</code>)를 AWS ECR에 성공적으로 푸시했으니, 실제 환경에서 개발자가 앱을 수정하고 새 버전을 배포하는 워크플로를 한 번 시뮬레이션해보겠습니다.<br>이제 Node.js 앱에 작은 변경을 주고, 이미지를 다시 빌드한 뒤, 업데이트된 버전을 <code>todo-app:2.0</code>으로 푸시할 것입니다.</p><h3 id="--20">이미지 배포하기</h3><p>이제 Docker Compose를 사용해 이미지를 배포해보겠습니다.</p><p>지금까지는 로컬 이미지를 사용해 앱을 실행했습니다:</p><pre><code class="language-yaml">image: todo-app:1.0
</code></pre><p>하지만 이제 이미지를 AWS ECR에 올렸으므로, Docker가 이미지를 어디에서 가져와야 하는지 알 수 있도록 해당 줄을 전체 ECR 이미지 URI로 교체해야 합니다.</p><p>로컬 이미지:</p><pre><code class="language-yaml">image: todo-app:1.0
</code></pre><p>프라이빗 레포지토리 이미지(ECR):</p><pre><code class="language-yaml">image: 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0

</code></pre><p>Docker는 “todo-app:1.0”이 어디에 저장되어 있는지 마법처럼 추측해주지 않습니다. 전체 레지스트리 URL을 포함하지 않으면, Docker는 그것이 AWS가 아니라 <strong>로컬 머신</strong>에 있는 이미지라고 가정합니다.</p><p>다음은 ECR에서 앱 이미지를 가져오도록 정리되고, 수정되고, 올바르게 포맷된 docker-compose 파일입니다:</p><pre><code class="language-yaml">version: "3.8"

services:
  my-app:
    image: 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
    container_name: my-app
    ports:
      - "3000:3000"
    depends_on:
      - mongodb

  mongodb:
    image: mongo
    container_name: mongo
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password

  mongo-express:
    image: mongo-express
    container_name: mongo-express
    ports:
      - "8081:8081"
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: admin
      ME_CONFIG_MONGODB_ADMINPASSWORD: password
      ME_CONFIG_MONGODB_SERVER: mongodb
    depends_on:
      - mongodb
</code></pre><h4 id="-todo-app-my-app-">왜 서비스 이름을 “todo-app”이 아니라 “my-app”으로 했을까?</h4><p>여기서는 다음 둘을 헷갈리지 않게 하려고 이름을 바꿨습니다:</p><ul><li><strong>로컬</strong>의 “todo-app:1.0”</li><li><strong>ECR</strong>에 있는 “todo-app:1.0”</li></ul><p>이렇게 하면 좀 더 구분이 명확해지지만, 원한다면 다시 원래 이름으로 돌려도 됩니다.</p><h3 id="-ecr-url-">왜 ECR에는 전체 이미지 URL을 써야 할까?</h3><p>mongo, mongo-express 같은 다른 컨테이너는 다음처럼 동작합니다:</p><pre><code class="language-yaml">image: mongo
image: mongo-express
</code></pre><p>그 이유는 Docker가 이 이미지들이 <strong>Docker Hub</strong>에 있다는 것을 알고 있기 때문입니다.</p><p><br>하지만 AWS ECR 같은 프라이빗 레포지토리의 경우, 전체 경로를 주지 않으면 Docker는 “todo-app”이 어디에 있는지 전혀 알 수 없습니다:</p><pre><code>AWS_ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/repository_name:tag
</code></pre><p>이 정보는 Docker에게 다음을 알려 줍니다:</p><ul><li>어떤 AWS 계정인지</li><li>어떤 리전인지</li><li>어떤 레포지토리인지</li><li>어떤 버전(tag)인지</li></ul><p>이 URL이 없으면 Docker는 이미지를 가져올 수 없습니다.</p><p>프라이빗 ECR 레포지토리에서 가져오고 싶을 때마다(그리고 Docker Compose를 사용할 때도 마찬가지로) 반드시 로그인 상태여야 합니다.</p><p>다음 명령을 실행하세요:</p><pre><code class="language-bash">aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin 244836489456.dkr.ecr.eu-north-1.amazonaws.com
</code></pre><p>로그인되어 있지 않으면 Docker Compose는 보통 다음과 같은 오류를 냅니다:</p><p>❌ pull access denied<br>❌ repository does not exist<br>❌ no basic auth credentials</p><h3 id="docker-compose--3">Docker Compose로 앱 배포하기</h3><p>배포하기 전에, 포트 충돌이나 남은 컨테이너 때문에 문제가 생기지 않도록 기존 컨테이너를 중지하고 제거하는 것이 좋습니다:</p><pre><code class="language-yaml"># 이 프로젝트에서 실행 중인 모든 컨테이너 중지
docker-compose down --remove-orphans

#선택 사항: 아무 것도 실행 중이 아닌지 확인
docker ps</code></pre><p>이렇게 하면 3000 포트를 포함한 매핑된 포트가 비워져, 새 컨테이너를 시작할 때 발생할 수 있는 오류를 방지할 수 있습니다.</p><p>환경을 깨끗하게 정리했다면, 이제 스택을 배포합니다:</p><pre><code class="language-yaml">docker-compose up -d
</code></pre><p>Docker Compose는 다음 작업을 수행합니다:</p><ul><li>AWS ECR에 접속 → 인증한 뒤 프라이빗 레포지토리에서 <code>todo-app:1.0</code> 이미지를 가져옴</li><li>MongoDB 시작 → 설정한 자격 증명으로 DB 컨테이너 실행</li><li>Mongo Express 시작 → 웹 기반 MongoDB 관리 인터페이스 실행</li><li>Node.js 앱 시작 → MongoDB에 연결된 <code>my-app</code> 컨테이너 실행</li></ul><p>실행 중인 컨테이너를 확인하려면:</p><pre><code class="language-yaml">docker ps
</code></pre><p>다음과 같은 컨테이너들이 보여야 합니다:</p><ul><li>mongo</li><li>mongo-express</li><li>my-app</li></ul><p>만약 my-app이 시작에 실패했다면, 보통 <strong>3000 포트가 이미 사용 중인 경우</strong>입니다. 사용 중인 프로세스를 중지하여 포트를 비우세요:</p><pre><code class="language-yaml">lsof -i :3000
kill -9 &lt;PID&gt;  # if a process is using it</code></pre><p>그 후 다시 실행하세요:</p><pre><code class="language-yaml">docker-compose up -d
</code></pre><p>앱에 접속하려면:</p><ul><li>Node.js 앱: <a href="http://localhost:3000/">http://localhost:3000/</a></li><li>Mongo Express: <a href="http://localhost:8081/">http://localhost:8081/</a></li></ul><p>이 워크플로를 통해 깨끗한 상태에서 시작할 수 있고, 포트 충돌이나 컨테이너 충돌 같은 흔한 문제를 피할 수 있습니다.</p><h3 id="-docker--3">프라이빗 Docker 이미지 공유하기</h3><p>Node.js 앱 이미지를 AWS ECR에 푸시했다면, 이제 이미지는 프라이빗 레포지토리에 안전하게 저장된 상태입니다. 그렇다면 다른 개발자, 팀원, 또는 서버가 같은 이미지를 실행해야 할 때는 어떻게 할까요? 프라이빗이기 때문에 <code>mongo</code>나 <code>nginx</code> 같은 공개 이미지처럼 자동으로 가져올 수 없고, 반드시 <strong>인증된 접근</strong>이 필요합니다.</p><p>다음은 (다른 사람들이) 이미지를 가져와 사용하는 방법입니다.</p><h3 id="1-iam-">1. IAM 권한 부여</h3><p>협업자는 ECR 권한이 있는 <strong>AWS IAM 사용자 또는 역할</strong>이 필요합니다. 최소한 다음 작업을 허용하는 정책이 있어야 합니다:</p><ul><li><code>ecr:GetAuthorizationToken</code></li><li><code>ecr:BatchCheckLayerAvailability</code></li><li><code>ecr:GetDownloadUrlForLayer</code></li><li><code>ecr:BatchGetImage</code></li></ul><p>이를 위해 전용 IAM 사용자를 만든 다음, 해당 사용자에게 Access Key ID와 Secret Access Key를 제공할 수 있습니다.</p><h3 id="2-aws-cli--1">2. AWS CLI 설치 및 설정</h3><p>협업자는 AWS CLI가 설치되어 있어야 합니다. 그 다음 자신의 자격 증명으로 CLI를 설정합니다:</p><pre><code class="language-bash">aws configure
</code></pre><p>이때 다음 값을 입력합니다:</p><ul><li>Access Key ID</li><li>Secret Access Key</li><li>기본 리전(해당 ECR 레포지토리가 있는 리전, 예: <code>eu-north-1</code>)</li><li>기본 출력 형식(보통 <code>json</code>)</li></ul><h3 id="3-docker-ecr-">3. Docker를 ECR에 인증시키기</h3><p>이미지를 pull하기 전에, Docker는 AWS 자격 증명을 사용해 먼저 인증을 해야 합니다:</p><pre><code class="language-bash">aws ecr get-login-password --region eu-north-1 | docker login --username AWS --password-stdin 244836489456.dkr.ecr.eu-north-1.amazonaws.com
</code></pre><p>성공하면 Docker가 다음과 같이 응답합니다:</p><pre><code>Login Succeeded
</code></pre><h3 id="4-pull">4. 이미지 Pull</h3><p>이제 협업자는 AWS 계정, 리전, 레포지토리 이름, 태그를 모두 포함한 전체 ECR URI로 이미지를 가져올 수 있습니다:</p><pre><code class="language-bash">docker pull 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
</code></pre><h3 id="5-">5. 컨테이너 실행</h3><p>이미지를 pull한 후에는 로컬에서 다음과 같이 컨테이너를 실행할 수 있습니다:</p><pre><code class="language-bash">docker run -p 3000:3000 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
</code></pre><p>또는 Docker Compose 파일에 포함시켜 <code>image:</code> 필드를 전체 ECR URI로 교체할 수 있습니다.</p><ul><li>mongo 같은 공개 이미지는 Docker Hub가 공개 레지스트리이기 때문에 이런 과정이 필요 없지만, 프라이빗 ECR 이미지는 반드시 명시적인 인증이 필요합니다.</li><li>프라이빗 레포지토리에서 pull을 할 때는 항상 유효한 로그인 상태가 필요합니다. Docker는 자격 증명을 추측할 수 없습니다.</li><li>전체 이미지 URI를 사용하는 것은 Docker가 이미지를 어디에서 가져와야 하는지 정확히 알 수 있게 해 줍니다.</li></ul><p>이 설정을 통해 팀은 로컬 머신, 스테이징 서버, 운영 환경 어디서든 애플리케이션을 공유, 배포, 실행할 수 있으며, 동시에 저장소를 비공개로 안전하게 유지할 수 있습니다.</p><h2 id="docker--4">Docker 볼륨</h2><p>MongoDB 같은 컨테이너를 실행할 때, 컨테이너 내부에서 생성된 데이터는 모두 일시적입니다. 컨테이너가 중지되거나 삭제되면 그 안의 데이터도 모두 사라집니다. 테스트 용도로는 괜찮지만, 프로덕션 환경에는 적합하지 않습니다.</p><p>이를 해결하기 위해 Docker는 <strong>볼륨</strong>을 제공합니다. 볼륨을 사용하면 컨테이너가 데이터를 컨테이너 외부(호스트 머신이나 Docker가 관리하는 스토리지)에 저장하여, 컨테이너가 재시작되거나 재빌드되거나 삭제되더라도 데이터가 유지되도록 할 수 있습니다.</p><h3 id="docker--5">Docker 볼륨의 동작 방식</h3><p>Docker 볼륨은 컨테이너를 위한 “지속되는 폴더”라고 생각하면 됩니다:</p><ul><li>볼륨에 기록된 데이터는 컨테이너가 삭제돼도 안전하게 남아 있습니다.</li><li>컨테이너는 이 볼륨에 자유롭게 읽기/쓰기를 할 수 있습니다.</li><li>볼륨은 데이터베이스, 로그, 파일 업로드, 그 외 애플리케이션에서 필요한 모든 영속 데이터에 필수적입니다.</li></ul><h3 id="docker--6">Docker 볼륨의 종류</h3><p>Docker에는 세 가지 주요 볼륨 유형이 있습니다:</p><h3 id="1-named-volumes-">1. 네임드 볼륨(Named Volumes)</h3><p>네임드 볼륨은 사용자가 이름을 부여한 볼륨으로, Docker가 완전히 관리합니다. 보통 프로덕션 데이터베이스나 여러 컨테이너가 공유해야 하는 영속 데이터에 사용합니다.</p><p>예시입니다:</p><pre><code class="language-yaml">volumes:
  mongo-data:
</code></pre><p>서비스에서의 모습입니다:</p><pre><code class="language-yaml">volumes:
  - mongo-data:/data/db
</code></pre><h3 id="2-bind-mounts-">2. 바인드 마운트(Bind Mounts)</h3><p>바인드 마운트는 <strong>호스트 머신</strong>의 폴더를 컨테이너 내부 경로에 직접 연결합니다. 개발 환경에서 코드, 로그, 업로드 파일 등을 실시간으로 동기화할 때 자주 사용합니다.</p><p>예시입니다:</p><pre><code class="language-yaml">volumes:
  - ./uploads:/usr/src/app/uploads
</code></pre><h3 id="3-anonymous-volumes-">3. 익명 볼륨(Anonymous Volumes)</h3><p>이름이 없는 볼륨입니다. Docker가 자동으로 임의의 이름을 붙입니다. 테스트용 임시 데이터처럼, 짧게 쓰고 버릴 데이터에 사용할 수 있지만, 프로덕션에서는 흔히 쓰이지 않습니다.</p><p>예시입니다:</p><pre><code class="language-yaml">volumes:
  - /data/tmp
</code></pre><h3 id="-docker-compose-">볼륨을 사용하는 Docker Compose 예제</h3><p>다음은 Node.js + MongoDB + Mongo Express 스택에서 가장 흔한 볼륨 유형을 사용하는 전체 <code>docker-compose.yml</code> 예제입니다:</p><pre><code class="language-yaml">version: "3.8"

services:
  my-app:
    image: 244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0
    container_name: my-app
    ports:
      - "3000:3000"
    depends_on:
      - mongodb
    volumes:
      - ./uploads:/usr/src/app/uploads  # 파일 업로드용 bind mount

  mongodb:
    image: mongo
    container_name: mongo
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password
    volumes:
      - mongo-data:/data/db  # 데이터베이스 영속 스토리지 네임드 볼륨

  mongo-express:
    image: mongo-express
    container_name: mongo-express
    ports:
      - "8081:8081"
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: admin
      ME_CONFIG_MONGODB_ADMINPASSWORD: password
      ME_CONFIG_MONGODB_SERVER: mongodb
    depends_on:
      - mongodb

volumes:
  mongo-data:  # 네임드 볼륨 정의
</code></pre><p>이 코드가 동작하는 방식:</p><ul><li><strong>MongoDB 볼륨(<code>mongo-data</code>)</strong>: 네임드 볼륨으로, 컨테이너 내부의 <code>/data/db</code> 아래에 있는 모든 DB 파일을 저장합니다. 컨테이너가 재시작·삭제·재빌드되어도 데이터는 유지됩니다.</li><li><strong>Node.js 업로드(<code>./uploads</code>)</strong>: 바인드 마운트로, 호스트의 <code>uploads</code> 폴더를 컨테이너의 <code>/usr/src/app/uploads</code>에 매핑합니다. 업로드된 파일은 즉시 호스트에서도 확인할 수 있습니다.</li><li><strong>익명 볼륨</strong>: 이 예제에는 포함되어 있지 않은데, 이름 없이 정의된 볼륨은 Docker가 자동으로 생성하며, 보통 프로덕션보다는 임시 용도로만 사용합니다.</li></ul><h3 id="--21">시각적 개념(단순화):</h3><pre><code>Host Machine(호스트 머신)
├─ /project/uploads   ← 컨테이너와 동기화되는 bind mount
├─ Docker Volumes
│  └─ mongo-data      ← MongoDB 영속 데이터를 위한 네임드 볼륨

Containers(컨테이너)
├─ my-app
│  └─ /usr/src/app/uploads  ← 호스트 uploads 폴더를 봄
├─ mongodb
│  └─ /data/db              ← 네임드 볼륨 mongo-data 사용
├─ mongo-express
</code></pre><h3 id="-takeaways-">핵심 정리 (Takeaways)</h3><ul><li>중요한 데이터에는 항상 볼륨을 사용하세요.</li><li>프로덕션 환경의 데이터베이스에는 네임드 볼륨이 가장 적합합니다.</li><li>바인드 마운트는 개발용과 실시간 동기화에 가장 잘 어울립니다.</li><li>익명 볼륨은 테스트를 제외하고는 거의 필요하지 않습니다.</li><li>볼륨은 컨테이너의 라이프사이클과 데이터의 라이프사이클을 분리해 주며, 이는 Docker 모범 사례의 핵심 개념입니다.</li></ul><h3 id="--22">애플리케이션 시작하기<br></h3><p>Docker Compose에 볼륨 설정까지 완료했다면, 다음 단계는 애플리케이션을 시작하고 볼륨이 제대로 동작하는지 확인하는 것입니다. 아래는 간단한 단계별 가이드입니다.</p><p><strong>1. 컨테이너 시작</strong></p><p>다음 명령을 실행하세요:</p><pre><code class="language-bash">docker-compose up -d
</code></pre><p><code>-d</code> 플래그는 컨테이너를 백그라운드(detached 모드)에서 실행한다는 의미입니다.</p><p>Docker는 다음을 수행합니다:</p><ul><li>(로그인되어 있다면) AWS ECR에서 앱 이미지를 pull합니다.</li><li>네임드 볼륨과 함께 MongoDB를 시작합니다.</li><li>Mongo Express를 시작합니다.</li><li>Node.js 앱을 시작합니다.</li></ul><p><strong>2. 실행 중인 컨테이너 확인</strong></p><p>모든 것이 제대로 시작되었는지 확인하려면:</p><pre><code class="language-bash">docker ps
</code></pre><p>다음과 비슷한 출력이 보여야 합니다:</p><pre><code class="language-bash">CONTAINER ID   IMAGE                                               STATUS          PORTS
2a2e120cc912   244836489456.dkr.ecr.eu-north-1.amazonaws.com/todo-app:1.0   Up 5s    0.0.0.0:3000-&gt;3000/tcp
f4d5a1ab1234   mongo                                               Up 5s          0.0.0.0:27017-&gt;27017/tcp
c3d5b2bc2345   mongo-express                                      Up 5s          0.0.0.0:8081-&gt;8081/tcp</code></pre><p><strong>3. 볼륨 확인</strong></p><p>Docker 볼륨 목록을 보려면:</p><pre><code class="language-bash">docker volume ls
</code></pre><p>여기서 예를 들어 <code>mongo-data</code> 같은 네임드 볼륨을 확인할 수 있어야 합니다.</p><p>볼륨을 자세히 보려면:</p><pre><code class="language-bash">docker volume inspect docker_tut_mongo-data
</code></pre><p>그러면 Docker가 호스트에서 MongoDB 데이터를 어디에 저장하는지 보여주는데, 예를 들어 다음과 같이 표시됩니다:<br></p><pre><code class="language-bash">[
    {
        "Name": "mongo-data",
        "Driver": "local",
        "Mountpoint": "/var/lib/docker/volumes/mongo-data/_data",
        "Labels": {},
        "Scope": "local"
    }
]</code></pre><p><strong>MongoDB 컨테이너 내부의 <code>/data/db</code>에 저장되는 모든 것은 실제로 호스트의 이 경로에 저장됩니다.</strong></p><p><strong>4. 데이터 영속성 테스트</strong><br><br>	1. MongoDB 또는 앱에 접속해서 데이터를 몇 개 추가해 보세요.<br>	2. 그 다음 컨테이너를 중지 및 삭제합니다:</p><pre><code class="language-bash">docker-compose down</code></pre><p>	3. 이후 다시 앱을 시작합니다:</p><pre><code class="language-bash">docker-compose up -d</code></pre><p>	4. 데이터를 다시 확인해 보세요.</p><ul><li>MongoDB가 네임드 볼륨을 사용하고 있기 때문에 데이터는 그대로 남아 있습니다.</li><li>이로써 볼륨이 영속적으로 동작한다는 것이 증명됩니다.</li></ul><p>5. 선택: Node.js 업로드(바인드 마운트) 확인</p><ul><li>앱을 통해 파일을 업로드했다면, 프로젝트 폴더의 <code>./uploads</code>를 확인해 보세요.</li><li>바인드 마운트는 호스트와 컨테이너 디렉터리를 동기화하므로, 업로드한 파일이 호스트 머신의 <code>./uploads</code> 폴더에도 생성되어 있어야 합니다.</li></ul><h2 id="--23">결론</h2><p>잘하셨습니다! 이 포괄적인 Docker 튜토리얼의 끝에 도달했습니다. 여러분은 컨테이너와 이미지의 기본을 풀어내는 것부터 네트워킹, Docker Compose, 볼륨, 심지어 비공개 AWS ECR 저장소 배포까지, 완전히 컨테이너화된(프로덕션할 준비가 되고 확장 가능한) Node.js 애플리케이션 스택을 구축했습니다.<br>이것들은 실제 시나리오에서 애플리케이션을 개발하고, 협업하고, 배포하는 방식을 바꿔놓을 실전 스킬들입니다.</p><p>끈질기게 따라와 주셔서 감사합니다. Docker는 처음엔 압도적으로 느껴질 수 있습니다 – 그 긴 명령어들, 네트워킹 특이점들, 영속 데이터 문제들은 결코 사소하지 않죠.<br>하지만 여기까지 왔다는 것은? 가파른 학습 곡선을 정복하고 개발 여정에서 새로운 경지에 도달했다는 뜻입니다.<br>이제 "내 컴퓨터에서는 잘 작동하는데"라는 골칫거리를 없애고, CI/CD 파이프라인을 간소화하며, 백엔드나 풀스택 전문가로 한 단계 업그레이드할 준비가 되었습니다.</p><p>계속 실험해보세요: todo-app을 수정하거나, Dockerfile에서 멀티스테이지 빌드(도커 다중 단계 빌드 - 단일 Dockerfile 내에서 여러 개의 빌드 단계를 정의하여 최종 이미지 크기를 최적화하고 빌드 효율성을 높이는 기술)를 시도하거나, 다음으로 Kubernetes 같은 오케스트레이션 도구를 탐구해보세요. Docker 생태계는 방대하지만, 이 기초를 갖췄으니 더 깊이 파고들 준비가 되었습니다. 문제가 생기거나 질문이 있으면 Docker Hub, Stack Overflow, GitHub 커뮤니티를 활용하세요.</p><p>최종 코드는 <a href="https://github.com/Oghenekparobo/docker_tut_js/tree/final" rel="nofollow noopener">여기</a>에서 확인할 수 있습니다.</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ JSON Web Token 핸드북: 웹 인증을 위한 JWT 활용 가이드 ]]>
                </title>
                <description>
                    <![CDATA[ JWT는 JSON Web Token의 약자이며, 웹 개발을 하다 보면 정말 자주 보게 되는 용어입니다. 기본적으로 JWT는 두 주체 간에 특정한 정보(claim)를 안전하게 표현하고 전달하기 위한 JSON 기반의 개방형 표준입니다. 특히 마이크로서비스 아키텍처나 최신 인증 시스템에서 매우 널리 사용된다는 점이 인상적인 부분입니다. 이 글에서는 JWT가 실제로 무엇인지, 어떻게 구성되어 있는지, 그리고 ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/the-json-web-token-handbook-learn-to-use-jwts-for-web-authentication/</link>
                <guid isPermaLink="false">69391e52ffd06f04d7d7edb5</guid>
                
                    <category>
                        <![CDATA[ JSON Web Tokens (JWT) ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NodeJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jong-Ho Kim ]]>
                </dc:creator>
                <pubDate>Fri, 19 Dec 2025 04:59:54 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2025/12/json-to-web-token-handbook-cover.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/the-json-web-token-handbook-learn-to-use-jwts-for-web-authentication/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">The JSON Web Token Handbook: Learn to Use JWTs for Web Authentication</a>
      </p><p>JWT는 JSON Web Token의 약자이며, 웹 개발을 하다 보면 정말 자주 보게 되는 용어입니다.</p><p>기본적으로 JWT는 두 주체 간에 특정한 정보(claim)를 안전하게 표현하고 전달하기 위한 JSON 기반의 개방형 표준입니다. 특히 마이크로서비스 아키텍처나 최신 인증 시스템에서 매우 널리 사용된다는 점이 인상적인 부분입니다.</p><p>이 글에서는 JWT가 실제로 무엇인지, 어떻게 구성되어 있는지, 그리고 웹 애플리케이션을 어떻게 안전하게 만드는지 자세히 살펴보겠습니다. 글을 다 읽고 나면 왜 개발자들이 매일같이 JWT를 사용하는지 이해할 수 있게 될 것입니다.</p><h2 id="-">이 글에서 다룰 내용</h2><ol><li>사전 지식</li><li>JWT란 무엇인가?</li><li>왜 토큰이 필요한가?</li><li>JWT의 구조: 헤더, 페이로드, 시그니처</li><li>예제: JWT 디코딩하기</li><li>JWT가 보안을 보장하는 방식: 시그니처</li><li>보안 고려사항 및 토큰 관리 방법</li><li>다양한 언어에서 JWT 생성 방법</li><li>실전 구현: Express + MongoDB로 만드는 JWT 인증</li><li>요약</li><li>마지막 한마디</li></ol><h2 id="--1">사전 지식</h2><p>이 가이드를 따라가며 최대한 많은 것을 얻으려면 다음과 같은 내용이 준비되어 있으면 좋습니다:</p><ol><li>JavaScript / Node.js에 대한 기초적인 이해</li><li>로컬 환경에 설치된 Node.js와 npm</li><li>HTTP와 REST API의 기본 개념</li><li>JSON 구조와 역직렬화 / 직렬화 방법</li><li>Express에 대한 기초 지식 (혹은 튜토리얼을 따라갈 수 있는 정도의 지식)</li><li>실행 중인 MongoDB (로컬 또는 원격)</li><li>비동기 코드 / Promise / Async–Await 사용 경험</li><li>환경 변수 및 <code>.env</code> 설정에 대한 이해</li></ol><p>이 글과 함께 볼 수 있는 동영상도 준비되어 있습니다. 텍스트와 함께 영상으로 학습하는 것을 좋아한다면 같이 참고해 보면 좋겠습니다.</p><figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.49999999999999%;" class="fluid-width-video-wrapper">
            <iframe width="200" height="113" src="https://www.youtube.com/embed/6drpx_QcMdg?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" title="What is JWT Authentication? JSON Web Token Tutorial" name="fitvid0"></iframe>
          </div>
        </div>
      </figure><h2 id="jwt-">JWT란 무엇인가?</h2><p>JWT는 오늘날 가장 흔히 인증 용도로 사용되지만, 사실 원래 목적은 조금 달랐습니다. 처음에는 두 주체 간에 정보를 안전하게 교환하기 위한 표준 방식을 제공하기 위해 만들어졌습니다. 이를 위해 <a href="https://datatracker.ietf.org/doc/html/rfc7519">RFC 7519</a>라는 산업 표준 사양이 정의되었으며, JWT가 어떻게 구조화되고, 어떤 방식으로 데이터 교환에 사용되어야 하는지 명시하였습니다. JavaScript를 위한 표준을 정의하는 <a href="https://en.wikipedia.org/wiki/ECMAScript#:~:text=ECMAScript%20\(%2F%CB%88%C9%9Bkm,pages%20across%20different%20web%20browsers.">ECMAScript</a> (혹은 ES)와 비슷한 개념이라고 보면 되겠습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/auth1.jpeg" class="kg-image" alt="auth1" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/auth1.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/auth1.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/auth1.jpeg 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/auth1.jpeg 1919w" sizes="(min-width: 720px) 720px" width="1919" height="1080" loading="lazy"></figure><p>실제 애플리케이션에서는 JWT가 주로 인증 용도로 사용되고 있고, 이 글에서도 그 관점에서 집중하여 설명할 것입니다.</p><p>하지만 기억해야 할 점은, JWT가 단순히 인증만을 위해 디자인된 것은 아니라는 것입니다. 인증을 구현하는 방법은 JWT 말고도 여러 가지가 있고, 그 중 가장 대표적인 대안이 바로 세션 토큰(session token)입니다.</p><h2 id="--2">왜 토큰이 필요한가?</h2><p>우리가 어떤 인증 전략을 사용하든지 간에, 그게 세션 토큰이든 혹은 JWT든 간에, 그 바탕에는 토큰이 필요한 공통된 이유가 있습니다. 바로 HTTP 프로토콜이 상태를 유지하지 않는(stateless) 특성을 갖고 있기 때문입니다.</p><p>브라우저에서 서버로, 혹은 서버끼리 HTTP를 사용해 요청과 응답을 주고받을 때, HTTP 자체는 아무런 정보를 저장하지 않습니다.</p><p><em>Stateless</em>라는 말은, 클라이언트와 서버가 상호작용할 때 HTTP가 이전 요청이나 데이터를 기억하지 않는다는 의미입니다. 다시 말해, 모든 요청은 필요한 정보를 스스로 담아서 보내야만 합니다. HTTP는 자체적으로 데이터를 저장하지 않습니다. 정보를 받으면 바로 “잊어버리는” 구조입니다. 그래서 HTTP를 stateless 프로토콜이라고 부릅니다.</p><p>이를 다른 식으로 생각해 보면 이렇습니다. 어떤 서버에 있는 웹페이지에 접속할 때, 우리는 서버에 어떤 정보를 보낼까요? 단순한 정적(static) 웹사이트라면, 사실 보내는 정보는 많지 않습니다. 그냥 페이지의 URL을 서버에 보내면, 서버는 그에 대응하는 HTML 페이지를 응답으로 돌려줍니다. 이 경우 서버는 별도의 정보를 기억할 필요가 없고, 상태를 유지할 필요도 없습니다. 이것이 바로 HTTP가 디자인된 방식이며, HTTP가 stateless 프로토콜인 이유입니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/7e6081f5-7d34-462a-9a7d-bcffd0242e00.jpeg" class="kg-image" alt="7e6081f5-7d34-462a-9a7d-bcffd0242e00" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/7e6081f5-7d34-462a-9a7d-bcffd0242e00.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/7e6081f5-7d34-462a-9a7d-bcffd0242e00.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/7e6081f5-7d34-462a-9a7d-bcffd0242e00.jpeg 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/7e6081f5-7d34-462a-9a7d-bcffd0242e00.jpeg 1919w" sizes="(min-width: 720px) 720px" width="1919" height="1080" loading="lazy"></figure><p>하지만 웹 애플리케이션이 유저마다 다른 응답을 제공하는 동적(dynamic) 웹사이트라면 이야기가 달라집니다. 이때는 URL만 보내서는 충분하지 않습니다. 유저는 URL과 함께 자신의 신원 정보(identity)도 서버에 전달해야 합니다.</p><p>예를 들어, 어떤 유저가 <code>page-1</code>에 접근하고 싶다면 서버에 이렇게 말해야 합니다. “<em>저는 A 유저입니다. </em><code><em>page-1</em></code><em>을 보여 주세요.</em>” 그러면 서버는 해당 유저를 위한 <code>page-1</code>을 응답으로 돌려줍니다. 이후 유저가 다시 “<em>이번에는 <code>page-2</code>를 주세요.</em>”라고 요청하면 어떻게 될까요? HTTP는 stateless이기 때문에, 이 요청에 유저의 신원 정보가 포함되어 있지 않으면, 서버는 누가 요청했는지 알 수 없고 어떤 응답을 보내야 할지 판단할 수 없습니다. 그래서 매 요청마다 유저는 자신의 신원 정보를 함께 보내야 합니다.</p><p>그런데 실제 우리가 사용하는 웹사이트들을 보면, 정말 매번 신원 정보를 보내고 있나요? 예를 들어 Facebook을 생각해 보겠습니다. 한 번 로그인을 하고 나면, 이후에는 다시 로그인 인증을 하지 않아도 홈 화면이나 프로필 페이지에 접근할 수 있습니다.</p><p>그렇다면 질문이 생깁니다. HTTP가 stateless 프로토콜이라면, 어떻게 이런 일이 가능한가요? 웹 애플리케이션은 우리의 브라우징 세션을 어떻게 기억하는 걸까요? 그 답은 웹 애플리케이션이 세션을 여러 방식으로 유지할 수 있기 때문입니다. 그중 가장 흔한 방법 중 하나가 <strong>토큰</strong>을 사용하는 방식입니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/7b7cdeab-4baa-4cda-bbeb-aaf4e4d4170c.jpeg" class="kg-image" alt="7b7cdeab-4baa-4cda-bbeb-aaf4e4d4170c" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/7b7cdeab-4baa-4cda-bbeb-aaf4e4d4170c.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/7b7cdeab-4baa-4cda-bbeb-aaf4e4d4170c.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/7b7cdeab-4baa-4cda-bbeb-aaf4e4d4170c.jpeg 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/7b7cdeab-4baa-4cda-bbeb-aaf4e4d4170c.jpeg 1918w" sizes="(min-width: 720px) 720px" width="1918" height="1077" loading="lazy"></figure><h3 id="-session-token-">세션 토큰(Session Token): 고전적인 방식</h3><p>토큰 방식에는 크게 두 가지 옵션이 있습니다. 하나는 <strong>세션 토큰</strong>이 있고, 다른 하나는 <strong>JSON Web Token (JWT)</strong>가 있습니다. 두 방식을 이해해야 JWT가 무엇인지, 왜 쓰이는지 더 분명해집니다.</p><p>어떤 회사의 고객센터를 예로 들어보겠습니다. 고객이 전화를 걸어 불만을 제기합니다. 고객센터 상담사가 제기된 문제를 듣고 여러 가지 해결 방법을 시도하지만, 바로 해결하지 못합니다.</p><p>이때 상담사는 상위 관리자 팀으로 이관하고, 고객에 대한 케이스 파일을 만듭니다. 이 파일에는 고객과의 대화 내용과 여태 시도한 해결 방법들이 기록됩니다. 그리고 고객에게 케이스 번호를 알려줍니다. 그렇게 함으로써 고객이 다음에 전화했을 때, 처음부터 모든 이야기를 반복할 필요가 없어집니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/c56bb7da-f6dd-4afe-b16b-966149bc7f91.jpeg" class="kg-image" alt="c56bb7da-f6dd-4afe-b16b-966149bc7f91" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/c56bb7da-f6dd-4afe-b16b-966149bc7f91.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/c56bb7da-f6dd-4afe-b16b-966149bc7f91.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/c56bb7da-f6dd-4afe-b16b-966149bc7f91.jpeg 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/c56bb7da-f6dd-4afe-b16b-966149bc7f91.jpeg 1920w" sizes="(min-width: 720px) 720px" width="1920" height="1080" loading="lazy"></figure><p>다음 날 고객이 다시 전화를 걸어 케이스 번호를 말하면, 상담사는 해당 번호를 가지고 이력과 정보를 확인한 뒤 바로 응대를 할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/426af5fa-ff38-4ce2-ae1b-48ca1a8f1e6c.jpeg" class="kg-image" alt="426af5fa-ff38-4ce2-ae1b-48ca1a8f1e6c" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/426af5fa-ff38-4ce2-ae1b-48ca1a8f1e6c.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/426af5fa-ff38-4ce2-ae1b-48ca1a8f1e6c.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/426af5fa-ff38-4ce2-ae1b-48ca1a8f1e6c.jpeg 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/426af5fa-ff38-4ce2-ae1b-48ca1a8f1e6c.jpeg 1920w" sizes="(min-width: 720px) 720px" width="1920" height="1080" loading="lazy"></figure><p>이 시나리오는 웹 애플리케이션에서 세션 토큰을 이용한 인증 방식과 유사합니다. 유저가 인증에 성공하면, 서버는 세션을 생성하고 이를 추적하기 시작합니다. 그리고 해당 세션을 식별하기 위한 세션 ID를 새로 생성해 유저에게 전달합니다. 이는 앞서 예시로 든 고객 케이스 번호와 유사합니다. 이후부터는 유저가 서버에 요청을 보낼 때마다, 세션 ID 또는 토큰이 함께 전달되고, 서버는 전달받은 ID를 기반으로 세션을 조회해 요청을 보낸 클라이언트를 식별합니다. 서버는 동시에 여러 클라이언트를 처리해야 하므로, 이러한 세션 토큰 방식은 인증을 구현하는 데 있어 효과적이고 널리 사용되는 전략으로 자리 잡았습니다.</p><p>클라이언트가 이 세션 ID를 서버로 어떻게 보내느냐는 구현에 따라 달라질 수 있지만, 가장 흔한 방식은 브라우저의 쿠키(cookie)에 세션 ID를 저장하는 것입니다. 이 방법의 장점은, 브라우저가 동일한 서버로 요청을 보낼 때마다 자동으로 쿠키 정보를 헤더에 실어 보내준다는 점입니다. 이는 브라우저의 기본 동작이기 때문에, 별도의 처리 없이도 자연스럽게 동작합니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/5881de41-571d-40ca-a0f7-4022d8c41754.jpeg" class="kg-image" alt="5881de41-571d-40ca-a0f7-4022d8c41754" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/5881de41-571d-40ca-a0f7-4022d8c41754.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/5881de41-571d-40ca-a0f7-4022d8c41754.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/5881de41-571d-40ca-a0f7-4022d8c41754.jpeg 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/5881de41-571d-40ca-a0f7-4022d8c41754.jpeg 1920w" sizes="(min-width: 720px) 720px" width="1920" height="1080" loading="lazy"></figure><p>유저가 인증에 성공하면 서버는 브라우저 쿠키에 세션 ID를 저장하고, 이후부터는 모든 요청에 쿠키가 자동으로 포함되어 서버가 유저를 식별할 수 있게 됩니다. 이 방식은 과거에 매우 널리 사용되었지만, 현대적인 애플리케이션 환경에서는 다소 구식이 된 측면도 있습니다.</p><p>이 방식에는 몇 가지 문제가 있습니다. 가장 큰 문제는 서버가 하나뿐이라는 전제를 바탕에 두고 있다는 점입니다. 요즘의 웹 애플리케이션은 보통 여러 대의 서버를 사용합니다. 이런 경우, 유저 앞단에는 로드 밸런서(load balancer)가 있고, 이 로드 밸런서가 어떤 서버에 요청을 보낼건지 결정합니다.</p><p>세션 토큰을 사용하는 상황을 가정해 보겠습니다. 유저가 첫 번째 요청을 보냈고, 로드 밸런서가 이 요청을 <code>Server-1</code>으로 보냅니다. <code>Server-1</code>은 세션 ID를 생성해 클라이언트에게 돌려줍니다. 이후 유저가 또 다른 요청을 보내고, 이번에는 로드 밸런서가 이 요청을 <code>Server-2</code>로 보냅니다. 그런데 <code>Server-2</code>에는 해당 세션 ID에 대한 정보가 없습니다. 그렇다면 이 요청이 누구의 것인지 어떻게 알 수 있을까요?</p><p>일반적인 해결책은 세션 ID를 각 서버가 아니라 공유된 Redis 데이터베이스에 저장하는 것입니다. 이렇게 하면 어느 서버든 Redis에서 세션 ID를 조회할 수 있습니다. 이를 흔히 <strong>Redis cache</strong>라고 부릅니다. 하지만 마이크로서비스 아키텍처에서는 이 방식에 허점이 있습니다. 만약 어떤 이유로 Redis cache가 다운된다면, 서버들은 정상적으로 작동하고 있더라도 인증 메커니즘이 실패할 수 있습니다. 이 지점에서 JSON Web Token이 다른 접근 방식을 제시합니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/a970e2d9-6663-4a4e-9c63-37ea13470b90.jpeg" class="kg-image" alt="a970e2d9-6663-4a4e-9c63-37ea13470b90" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/a970e2d9-6663-4a4e-9c63-37ea13470b90.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/a970e2d9-6663-4a4e-9c63-37ea13470b90.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/a970e2d9-6663-4a4e-9c63-37ea13470b90.jpeg 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/a970e2d9-6663-4a4e-9c63-37ea13470b90.jpeg 1920w" sizes="(min-width: 720px) 720px" width="1920" height="1080" loading="lazy"></figure><h3 id="jwt--1">JWT: 현대적인 해결책</h3><p>앞에서 살펴본 고객센터 예시로 다시 돌아가 보죠. 이번에는 전화나 시스템이 전혀 없다고 가정해 보겠습니다. 고객이 직접 회사 사무실로 찾아와 상담사와 대면으로 이야기합니다. 이번에는 상담사가 시스템에 정보를 저장할 수 없으니, 대신 종이에 모든 내용을 적은 뒤 그 종이를 고객에게 건네주며 이렇게 말합니다. “<em>다음에 올 때 이걸 가져오세요.</em>”</p><p>이 방식은 앞서 봤던 개념과 조금 다르죠? 하지만, 이 방식도 여전히 다른 문제가 있습니다. 바로 <strong>유효성(validity)</strong> 문제입니다. 만약 그 고객이 진짜 고객이 아니고 악의적인 의도를 품고 있다면, 상담사는 어떻게 그 고객을 신뢰할 수 있을까요? 다음 날 그 고객이 빈 종이에 같은 정보를 적어 들고 온다면, 상담사는 그 고객의 신원이 유효한지 어떻게 확인할 수 있을까요?</p><p>이때 가능한 해결 방법은, 상담사가 종이를 건네줄 때 직접 서명을 해 두는 것입니다. 그러면 고객이 다음에 그 종이를 가져왔을 때, 상담사는 그 서명을 확인함으로써 해당 종이를 신뢰할 수 있는지 판단할 수 있습니다.</p><p>JSON Web Token은 이와 유사한 방식으로 동작합니다. 여기서는 클라이언트가 인증되었을 때, 서버가 모든 정보를 저장하는 대신 유저의 정보를 서명(signature)과 함께 JSON 토큰 형태로 클라이언트에게 전달합니다. 이후 클라이언트는 다음 요청부터 매번 이 토큰을 함께 전송합니다. 이 토큰에는 이 유저가 어떤 유저인지, 이름이 무엇인지, 그리고 그 외 필요한 정보들이 포함되어 있습니다.</p><p>이 경우 서버는 아무것도 저장하지 않고, 모든 정보는 클라이언트에 유지됩니다. 클라이언트가 이 토큰과 함께 요청을 보낼 때마다, 서버는 토큰을 읽고 어떤 유저가 요청을 보냈는지 식별하고 필요한 데이터를 제공합니다.</p><p>이 토큰은 단순한 ID가 아닙니다. 모든 정보를 담고 있는 JSON 객체이며, 이것이 바로 JSON Web Token입니다. 이 JWT를 어디에 저장할지는 전적으로 클라이언트의 선택입니다. 가장 일반적인 방식은 브라우저의 쿠키나 로컬 스토리지(local storage)에 저장하는 것입니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/691848c9-e4c2-4b3f-b3f5-06623627e38f.jpeg" class="kg-image" alt="691848c9-e4c2-4b3f-b3f5-06623627e38f" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/691848c9-e4c2-4b3f-b3f5-06623627e38f.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/691848c9-e4c2-4b3f-b3f5-06623627e38f.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/691848c9-e4c2-4b3f-b3f5-06623627e38f.jpeg 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/691848c9-e4c2-4b3f-b3f5-06623627e38f.jpeg 1920w" sizes="(min-width: 720px) 720px" width="1920" height="1080" loading="lazy"></figure><h2 id="jwt--2">JWT의 구조: 헤더, 페이로드, 시그니처</h2><p>앞서 언급한 것 처럼, 서버는 JSON 객체를 전달받지만, JWT는 일반적인 JSON처럼 보이지는 않습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/f74219b8-4a01-4ac4-920b-449faf103520.png" class="kg-image" alt="f74219b8-4a01-4ac4-920b-449faf103520" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/f74219b8-4a01-4ac4-920b-449faf103520.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/f74219b8-4a01-4ac4-920b-449faf103520.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/f74219b8-4a01-4ac4-920b-449faf103520.png 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/f74219b8-4a01-4ac4-920b-449faf103520.png 1920w" sizes="(min-width: 720px) 720px" width="1920" height="1078" loading="lazy"></figure><p>위 이미지에서는 JWT가 다소 특이하게 보일 수 있습니다. 실제로 이것은 JSON 객체를 인코딩한 형태로, 일종의 복잡하게 압축된 표현 방식입니다. 자세히 살펴보면 JWT는 점(.)으로 구분된 세 부분으로 나뉘어 있다는 것을 알 수 있습니다. 첫 번째 부분은 <strong>헤더(header)</strong>, 두 번째 부분은 본질적으로 데이터를 담고 있는 <strong>JSON 페이로드(payload)</strong>, 그리고 세 번째 부분은 <strong>시그니처(signature)</strong>입니다.</p><p>각 부분을 개별적으로 살펴보면 다음과 같습니다.</p><ul><li><strong>헤더</strong>는 하나의 독립적인 JSON 객체입니다.</li><li><strong>페이로드</strong> 역시 데이터를 포함하고 있는 별도의 JSON 객체입니다.</li><li>세 번째 부분은 <strong>시그니처</strong>입니다.</li></ul><p>그렇다면 시그니처는 무엇일까요? 간단히 말해 토큰의 내용은 시크릿 키(secret key)와 함께 해시(hash)되는데, 그게 바로 시그니처입니다. 여기서 이 시크릿 키는 서버 내부에만 저장되고, JSON Web Token이 서버로 전달되면, 서버는 해당 시크릿 키를 사용해 시그니처가 여전히 유효한지, 중간에 변조되지는 않았는지 등을 확인할 수 있습니다.</p><blockquote>번역자 코멘트: 해시(hash)는 데이터를 고정된 길이의 값으로 변환해 주는 계산 방식입니다. 입력 내용이 조금이라도 달라지면 결과 값은 완전히 달라지게 됩니다. 특히 JWT는 시크릿 키(secret key)와 함께 해시를 하기에, 결과 값을 &nbsp;뒤로 되돌려 원래의 데이터를 얻는 건 불가능에 가깝습니다. 누군가 중간에 JWT를 가로채 데이터를 변조했다면, 서버는 바로 알아챌 수 있습니다.</blockquote><h2 id="-jwt-">예제: JWT 디코딩하기</h2><p>예제를 하나 살펴보겠습니다. JWT를 다루고 그 구조를 이해하기에 가장 좋은 웹사이트는<a href="https://www.jwt.io/"> jwt.io</a>입니다. JWT 하나를 이 사이트에 붙여 넣으면 헤더, 페이로드, 시그니처라는 세 개의 영역이 표시됩니다. 페이로드는 “Decoded Payload” 부분에 나타나며, 실제 내용과 데이터가 들어 있습니다. 이 안에는 ID, 이름을 담고 있는 JSON 객체, 그리고 만료일 등이 포함되어 있는 것을 확인할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/84c2532f-dc09-4a96-83de-ecd4a24d958f.jpeg" class="kg-image" alt="84c2532f-dc09-4a96-83de-ecd4a24d958f" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/84c2532f-dc09-4a96-83de-ecd4a24d958f.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/84c2532f-dc09-4a96-83de-ecd4a24d958f.jpeg 636w" width="636" height="367" loading="lazy"></figure><p>"Decoded Header" 부분 역시 완전히 유효한 JSON 객체이며, JWT를 생성하거나 검증할 때 사용된 알고리즘과 토큰의 타입을 명시합니다. &nbsp;</p><p>주요 데이터는 “Decoded Payload” 부분에 들어 있습니다. 세 번째 부분은 시그니처입니다. 여기서 한 가지 중요한 점이 있습니다. 이처럼 복잡하게 섞여보이는 토큰이 어디에서 오는지 궁금할 수 있는데, 그건 사실 매우 단순합니다. “Decoded Payload”에 있는 데이터가 <strong>Base64</strong>로 인코딩되어 있으며, 이것이 바로 이처럼 난해하게 보이는 토큰의 형태를 만듭니다.</p><p>이 JWT의 해당 부분을 복사해 온라인에서 찾을 수 있는 Base64 디코더에 일단 붙여 넣어 보면, 즉시 원래의 데이터를 확인할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/4ee950a2-2ad0-40b4-8287-fdfea9543a6f.png" class="kg-image" alt="4ee950a2-2ad0-40b4-8287-fdfea9543a6f" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/4ee950a2-2ad0-40b4-8287-fdfea9543a6f.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/4ee950a2-2ad0-40b4-8287-fdfea9543a6f.png 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/4ee950a2-2ad0-40b4-8287-fdfea9543a6f.png 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/4ee950a2-2ad0-40b4-8287-fdfea9543a6f.png 1919w" sizes="(min-width: 720px) 720px" width="1919" height="1080" loading="lazy"></figure><p>그렇다면 이것이 의미하는 바는 무엇일까요? 이는 이 데이터를 다시 Base64로 인코딩하면 동일한 토큰이 생성된다는 뜻입니다. 헤더 역시 동일한 방식으로 동작합니다.</p><p>마지막으로, 이 인코딩된 부분에 대해 짚고 넘어가야 할 점이 있습니다. 이것이 보안을 위한 것일까요? 그렇지 않습니다. 이는 오직 편의를 위한 것입니다. JSON 객체는 상당히 길어질 수 있고, 모든 프로그래밍 언어가 이를 동일한 방식으로 처리하지는 않습니다. JavaScript에서는 비교적 쉽지만, 다른 언어에서는 문제가 되는 경우도 있습니다. 그래서 데이터를 다루기 쉽게 만들기 위해 Base64 인코딩을 사용하는 것입니다. 이는 보안을 위한 것이 아니며, 이런 방식의 인코딩은 데이터를 안전하게 만들어 주지 않습니다. 정보는 여전히 공개적으로 확인할 수 있습니다.</p><p>위 그림에서 볼 수 있듯이, 이 Base64 디코더 사이트에 토큰을 입력하는 순간 데이터는 즉시 표시됩니다. 이는 여기에 민감한 정보를 저장해서는 안 된다는 의미입니다. 유저 ID와 같은 유저 식별 정보나 기타 공개 가능한 정보만 담아야 합니다. <strong>비밀번호나 시크릿 키와 같은 정보는 쉽게 읽힐 수 있기 때문에 절대로 토큰에 저장해서는 안 됩니다.</strong> 겉보기에는 인코딩되어 보이지만, 실제로는 공개된 데이터입니다.</p><h2 id="jwt--3">JWT가 보안을 보장하는 방식: 시그니처</h2><p>이제 시그니처(signature)를 통해 보안에 대한 보장이 이루어지는 부분으로 넘어가 보겠습니다. 앞서 고객센터 종이 예시에서는 사람이 직접 손으로 서명을 추가할 수 있었습니다.</p><p>하지만 데이터의 경우, 시그니처를 생성하는 과정은 다릅니다. 데이터에서는 실제 서명 역할을 하는 시크릿 키를 사용해 암호학적으로 시그니처를 생성합니다. 시그니처를 만드는 과정은 다음과 같습니다:</p><ol><li>데이터는 Base64로 인코딩됩니다.</li><li>인코딩된 데이터에 시크릿 키를 결합(concatenate) 합니다.</li><li>이를 다시 Base64로 인코딩합니다.</li></ol><p>사용할 알고리즘은 변경할 수 있지만, 한 번 선택된 알고리즘은 토큰을 생성할 때와 검증할 때 동일하게 사용되어야 합니다. 생성과 검증 과정에서 서로 다른 알고리즘을 사용할 수는 없습니다.</p><p>마지막으로 데이터는 시크릿 키를 사용해 해시됩니다. 이 시크릿 키는 외부에 공개되지 않으며, 오직 서버에만 보관됩니다. 보통은 서버의 보안 저장소(server vault)에 안전하게 저장됩니다. 이 JWT가 서버에 도착하면, 서버는 해당 시크릿 키를 사용해 토큰이 유효한지 검증합니다. 만약 값이 올바르게 일치하지 않으면 “<em>invalid signature</em>”라는 결과가 표시됩니다. 이를 통해 서버는 토큰이 중간에 변조되었는지 여부를 확인할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/bf017016-d9fd-43cb-836a-eafe4f35540b.jpeg" class="kg-image" alt="bf017016-d9fd-43cb-836a-eafe4f35540b" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/bf017016-d9fd-43cb-836a-eafe4f35540b.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/bf017016-d9fd-43cb-836a-eafe4f35540b.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/12/bf017016-d9fd-43cb-836a-eafe4f35540b.jpeg 1224w" sizes="(min-width: 720px) 720px" width="1224" height="1078" loading="lazy"></figure><p>예를 들어, 시그니처로 <code>love-you-all-from-logicbaselabs</code>를 사용하고 서버에서 이를 검증했을 때 “<em>signature verified</em>”라는 결과가 나온다면, 이는 시크릿 키가 서버에만 존재한다는 것을 보여줍니다. 즉, 공개된 정보가 포함되어 있더라도 토큰의 유효성은 검증될 수 있습니다.</p><p>다만 JSON Web Token은 비밀번호와 같은 개념은 아닙니다. JWT는 주로 유저를 식별하는 역할을 합니다. 서버는 JWT를 확인해 이것이 유효한 유저에게 속한 토큰인지 판단할 수 있습니다. 다시 말해 JWT는 유저의 신원을 나타내며, 시그니처와 함께 보안 요소를 포함하고 있는 매우 중요한 토큰입니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/12/a434b453-0a38-41a5-93f3-bd12b46806f3qq.jpeg" class="kg-image" alt="a434b453-0a38-41a5-93f3-bd12b46806f3qq" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/12/a434b453-0a38-41a5-93f3-bd12b46806f3qq.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/12/a434b453-0a38-41a5-93f3-bd12b46806f3qq.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/size/w1600/2025/12/a434b453-0a38-41a5-93f3-bd12b46806f3qq.jpeg 1600w, https://www.freecodecamp.org/korean/news/content/images/2025/12/a434b453-0a38-41a5-93f3-bd12b46806f3qq.jpeg 1920w" sizes="(min-width: 720px) 720px" width="1920" height="1078" loading="lazy"></figure><h2 id="--3">보안 고려사항 및 토큰 관리 방법</h2><p>한 가지 반드시 기억해야 할 점이 있습니다. 누군가가 당신의 JWT를 그대로 손에 넣었다면, 즉 완전히 동일한 토큰을 가지고 있다면, 그 사람은 해당 유저로 쉽게 로그인할 수 있습니다. 그저 그 토큰을 포함해 요청을 보내기만 하면 필요한 접근 권한을 얻을 수 있습니다.</p><p>이를 이렇게 생각해 볼 수 있습니다. 누군가가 당신의 Facebook 비밀번호를 알아내면, 당신의 Facebook 계정에 그대로 로그인할 수 있습니다. 마찬가지로 누군가가 PayPal 계정의 PIN을 획득했다면, 그 계정에 쉽게 접근할 수 있습니다. 즉, 가장 중요한 보안 정보를 누군가가 손에 넣는 순간, 이를 보호할 방법은 사실상 없습니다.</p><p>JWT도 동일합니다. 토큰을 클라이언트 측에서 얼마나 안전하게 보관하느냐가 절대적으로 중요합니다. 이런 점에서 우리는 어느 정도 취약할 수밖에 없습니다.</p><p>다만 세션 토큰과의 가장 큰 차이점이 있는데, 세션 토큰의 경우, 계정이 침해되었다고 판단되면 서버에서 해당 세션을 무효화할 수 있습니다. 다시 말해, 그 세션 ID로는 더 이상 누구도 로그인할 수 없게 됩니다.</p><p>하지만 JWT의 경우에는 만료일이 지나기 전까지 토큰이 계속 유효합니다. 따라서 이를 즉시 무효화할 수 있는 직접적인 방법이 없습니다. JWT는 암호학적으로 자기완결적인(self-contained) 구조를 가지며, 서버의 시크릿 키로 서명되어 있기 때문에, 한 번 생성되면 서버에서 바로 폐기할 수 없습니다.</p><p>이를 처리하는 유일한 방법은 웹에서 일반적으로 사용하는 방식인 토큰 차단 목록(denylist)을 관리하는 것입니다. 즉, 서버는 차단된 JWT 토큰들을 별도의 데이터베이스에 저장해 관리합니다. 요청이 들어오면 서버는 먼저 토큰이 형식적으로 유효한지 검증하고, 그 다음 미들웨어를 통해 해당 토큰이 denylist에 포함되어 있는지 확인합니다. 이 목록에 포함되어 있지 않은 경우에만 유저의 접근이 허용됩니다.</p><p>이것이 JSON Web Token을 사용할 때 지켜야 할 기본적인 규칙들입니다. JWT는 어떤 프로그래밍 언어에서도 사용할 수 있으며, 특히 REST API 환경에서 매우 널리 활용됩니다. 또한 마이크로서비스 아키텍처에서도 매우 보편적이고 널리 퍼진 방식입니다.</p><h2 id="-jwt--1">다양한 언어에서 JWT 생성 방법</h2><p>JWT를 생성하는 방법은 사용하는 프로그래밍 언어에 따라 달라집니다. 예를 들어 Node.js에서는<a href="https://www.npmjs.com/package/jsonwebtoken"> jsonwebtoken</a>과 같은 전용 라이브러리가 제공되기 때문에 비교적 쉽게 JWT를 만들 수 있습니다. PHP 역시 JWT를 생성하기 위한 사용하기 쉬운 라이브러리들이 존재합니다. 이처럼 JWT는 특정 프로그래밍 언어에 국한된 기술이 아니라, 어디에서나 사용할 수 있는 범용적인 도구입니다. 많은 사람들이 JWT가 JavaScript 전용이라고 생각하지만, 이는 사실이 아닙니다.</p><p>또한 JWT는 인증 용도로만 사용되는 것이 아니라는 점도 기억해야 합니다. JWT는 어떠한 형태로든 신원을 표현하는 데 사용할 수 있습니다. 예를 들어 콘서트에 입장할 때 일반적인 종이 티켓 대신 JWT를 사용해 접근 권한을 부여할 수도 있습니다. 이때 클라이언트가 해당 JWT를 사용하면, 게이트웨이(gateway)나 서버는 토큰을 읽어 필요한 정보에 대한 접근을 허용하고, 시그니처를 통해 토큰의 유효성을 검증할 수 있습니다.</p><h2 id="-express-mongodb-jwt-">실전 구현: Express + MongoDB로 만드는 JWT 인증</h2><p>이번에는 지금까지 배운 모든 개념을 실제로 적용해 보겠습니다. <a href="https://www.freecodecamp.org/news/the-express-handbook/">Express.js</a>와 <a href="https://www.freecodecamp.org/news/how-to-start-using-mongodb/">MongoDB</a>를 사용해 JWT 인증 시스템을 단계별로 구축할 것입니다.</p><p>처음에는 다소 부담스럽게 느껴질 수도 있지만 차근차근 따라오면 문제없습니다. 한 단계씩 차근차근 진행할 것이며, 마지막에는 실제로 동작하는 프로젝트를 완성하게 될 것입니다. 건물에 층층이 들어가 보듯이, 각 구간을 충분히 살펴보면서 확실한 이해를 얻고 갈 것입니다.</p><h3 id="1-">1. 프로젝트 세팅 &amp; 패키지 설치</h3><p>코드를 작성하기 전에 먼저 Node.js 프로젝트를 세팅하고 필요한 패키지들을 설치해야 합니다.</p><p><strong>Node.js 프로젝트 초기화</strong></p><p>터미널을 열어 다음과 같이 실행해보세요:</p><pre><code class="language-bash">mkdir jwt-auth-demo
cd jwt-auth-demo
npm init -y</code></pre><p>이 명령을 실행하면 기본 세팅이 된 <code>package.json</code> 파일이 생성됩니다.</p><p><strong>패키지 설치</strong></p><p>JWT 인증 시스템을 만들기 위해 다음 패키지들을 설치합니다.</p><pre><code class="language-bash">npm install express mongoose bcryptjs jsonwebtoken dotenv</code></pre><ul><li><code>express</code>: API 를 만들기 위한 빠르고 최소한의 Node.js 웹 프레임워크입니다.</li><li><code>mongoose</code>: MongoDB를 더 쉽게 다루기 위한 ODM(Object Data Modeling) 라이브러리입니다.</li><li><code>bcryptjs</code>: 비밀번호를 해시하기 위한 라이브러리입니다.</li><li><code>jsonwebtoken</code>: JWT 토큰을 생성하고 검증하는 라이브러리입니다.</li><li><code>dotenv</code>: <code>.env</code> 파일에 있는 환경 변수를 불러와서, 비밀 정보들을 안전하게 관리할 수 있게 해줍니다.</li></ul><p><strong>개발용 패키지 설치 (선택 사항)</strong></p><p>개발을 편하게 하기 위해 파일이 변경될 때마다 서버를 자동으로 재시작해 주는 <strong>nodemon</strong>을 설치해 보세요.</p><pre><code class="language-bash">npm install --save-dev nodemon</code></pre><p><code>package.json</code>의 scripts를 다음과 같이 수정합니다.</p><pre><code class="language-json">"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}</code></pre><ul><li><code>npm start</code> 는 서버를 일반 모드로 실행합니다.</li><li><code>npm run dev</code> 는 <strong>nodemon</strong>을 사용해 코드 변경 시 자동으로 서버를 재시작합니다.</li></ul><h3 id="2-">2. 프로젝트 폴더 구조</h3><pre><code>jwt-auth-demo/
│
├── config/
│   └── db.js
│
├── controllers/
│   └── authController.js
│
├── middlewares/
│   └── authMiddleware.js
│
├── models/
│   └── User.js
│
├── routes/
│   └── auth.js
│
├── services/
│   ├── hashService.js
│   └── jwtService.js
│
├── .env
├── server.js
├── package.json</code></pre><p>각 디렉터리의 역할은 다음과 같습니다.</p><ul><li><code>config/</code>: 데이터베이스 연결 및 환경 설정 관련 코드</li><li><code>controllers/</code>: 각 엔드포인트(Endpoint)의 핵심 비즈니스 로직</li><li><code>middlewares/</code>: 컨트롤러 실행 전에 동작하는 미들웨어(예: 인증 체크)</li><li><code>models/</code>: Mongoose 스키마</li><li><code>routes/</code>: API 엔드포인트의 정의</li><li><code>services/</code>: 비밀번호 해시, JWT 생성/검증 같은 재사용 가능한 로직</li><li><code>.env</code>: 시크릿이나 환경 변수</li><li><code>server.js</code>: 애플리케이션의 엔트리 포인트</li></ul><h3 id="3-">3. 단계별 구현하기</h3><p><strong>Express 서버 초기화</strong></p><p>먼저 가장 기본이 되는 Express 서버를 설정합니다. 이 서버가 유저 회원가입, 로그인과 같은 요청을 받고 응답을 돌려주는 역할을 합니다.</p><p><strong><strong>파일: server.js</strong></strong></p><pre><code class="language-javascript">// server.js

// Import the express library to build our server
const express = require("express");

// Create an instance of express
const app = express();

// Middleware to parse JSON request bodies (important for APIs)
app.use(express.json());

// Default route to test server
app.get("/", (req, res) =&gt; {
  res.send("Hello World! Your server is working 🚀");
});

// Start the server on port 5000
const PORT = process.env.PORT || 5000;
app.listen(PORT, () =&gt; {
  console.log(`Server running on http://localhost:${PORT}`);
});</code></pre><ul><li>Express를 불러와 app인스턴스를 생성합니다.</li><li>JSON 형태의 요청을 역직렬화(parsing)하기 위해 미들웨어를 사용합니다.</li><li>서버가 잘 동작하는지 테스트하기 위해 간단한 <code>/</code> &nbsp;경로의 라우트를 만듭니다.</li><li>마지막으로 5000번 포트에서 서버를 시작합니다.</li></ul><p>이제 테스트를 해봅시다:</p><ul><li><code>node server.js</code> 혹은 <code>npm run dev</code> 를 실행해 봅니다.</li><li>브라우저를 열어 다음과 같은 주소로 가보세요. <code>http://localhost:5000</code>.</li><li>다음과 같은 메세지가 떠야 합니다: <code>Hello World! Your server is working 🚀</code></li></ul><p><strong>MongoDB를 Mongoose로 연결하기</strong></p><p>이제 유저 정보를 저장할 데이터베이스가 필요합니다. 여기서는 MongoDB를 사용하고, Node.js에서 쉽게 접근하기 위해 ODM 라이브러리인 Mongoose를 사용합니다.</p><p><strong><strong>파일: config/db.js</strong></strong></p><pre><code class="language-javascript">// config/db.js

// Import mongoose
const mongoose = require("mongoose");

// Connect to MongoDB using environment variable
const connectDB = async () =&gt; {
  try {
    await mongoose.connect(process.env.MONGO_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log("✅ MongoDB Connected");
  } catch (err) {
    console.error("❌ MongoDB Connection Error:", err.message);
    process.exit(1); // Stop server if DB fails
  }
};

module.exports = connectDB;</code></pre><p>이제 서버가 MongoDB와 연결될 준비가 되었습니다. 데이터를 삽입/수정/조회하는 모든 작업은 이 데이터베이스를 통해 이루어집니다.</p><p><strong><strong>File: .env</strong></strong></p><pre><code class="language-bash">PORT=5000
MONGO_URI=mongodb://127.0.0.1:27017/jwt-auth-demo
JWT_SECRET=your_super_secret_key
</code></pre><p>.env파일에는 데이터베이스의 URI, JWT 시크릿 키, 서버 포트와 같은 민감한 정보가 저장됩니다. 환경 변수를 사용하면 이러한 비밀 정보를 코드에서 분리해 관리할 수 있고, 소스 파일을 수정하지 않고도 설정을 쉽게 변경할 수 있습니다. 자격 증명(credential)을 보호하기 위해 .env파일은 절대 공개 저장소에 기록(commit)해서는 안 됩니다.</p><p><strong>User 모델 만들기</strong></p><p>이제 데이터베이스 안에서 User가 어떤 구조를 가질지 정의해야 합니다. 각 유저는 <strong>name, email, password</strong>를 갖게 됩니다.</p><p><strong><strong>파일: models/User.js</strong></strong></p><pre><code class="language-javascript">// models/User.js
const mongoose = require("mongoose");

// Define a schema (blueprint of user data)
const userSchema = new mongoose.Schema({
  name: { type: String, required: true },
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
});

// Create and export the model
module.exports = mongoose.model("User", userSchema);</code></pre><p>이제 각 유저는 <strong>name, email, hashed password</strong> 필드를 갖는 구조로 저장됩니다. </p><p><strong>해시(Hash) 및 JWT 서비스</strong></p><p>이번에는 비밀번호 해시와 JWT 관리 로직을 각각 별도의 서비스 파일로 분리합니다. 이렇게 하면 코드가 깔끔해지고 재사용성이 높아집니다.</p><p><strong><strong>파일: services/hashService.js</strong></strong></p><pre><code class="language-javascript">//services/hashService.js

const bcrypt = require("bcryptjs");

// Function to hash a plain password
exports.hashPassword = async (plainPassword) =&gt; {
  // bcrypt.hash generates a hashed version of the password
  // The number 10 is the salt rounds, which affects the hashing complexity
  return await bcrypt.hash(plainPassword, 10);
};

// Function to compare a plain password with a hashed password
exports.comparePassword = async (plainPassword, hashedPassword) =&gt; {
  // bcrypt.compare checks if the plain password matches the hashed one
  return await bcrypt.compare(plainPassword, hashedPassword);
};
</code></pre><ul><li><code>hashPassword(plainPassword)</code>:비밀번호를 받아 bcrypt로 해시한 값을 반환합니다. 비밀번호는 절대 평문으로 저장하면 안 됩니다</li><li><code>comparePassword(plainPassword, hashedPassword)</code>: 유저가 입력한 비밀번호와 데이터베이스에 저장된 해시 값을 비교해 일치 여부를 알려줍니다. 일치하면 <code>true</code>를 반환합니다.</li></ul><p><strong><strong>파일: services/jwtService.js</strong></strong></p><pre><code class="language-javascript">// services/jwtService.js

const jwt = require("jsonwebtoken");

// Function to generate a JWT
exports.generateToken = (payload) =&gt; {
  // jwt.sign creates a signed token using our secret key from environment variables
  // expiresIn defines how long the token is valid (1 hour here)
  return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "1h" });
};

// Function to verify a JWT
exports.verifyToken = (token) =&gt; {
  // jwt.verify checks if the token is valid and not expired
  return jwt.verify(token, process.env.JWT_SECRET);
};</code></pre><ul><li><code>generateToken(payload)</code>: JWT를 생성합니다. 보통 <code>payload</code>가 유저를 식별할 수 있는 ID나 이메일을 담고 있습니다.</li><li><code>verifyToken(token)</code>: JWT의 유효성을 검증하고 디코딩된 payload를 반환합니다.</li><li>이처럼 JWT 관련 로직을 한 곳에 모아 두면 관리와 유지보수가 훨씬 편해집니다.</li></ul><p><strong>인증 컨트롤러(Auth Controller)</strong></p><p>이 단계에서는 인증과 관련된 모든 로직을 별도의 컨트롤러에서 처리합니다. 이렇게 하면 라우트 정의를 깔끔하게 유지할 수 있고, 비즈니스 로직을 엔드포인트 정의와 분리할 수 있습니다.</p><p><strong><strong>파일: controllers/authController.js</strong></strong></p><pre><code class="language-javascript">// controllers/authController.js

const User = require("../models/User");
const { hashPassword, comparePassword } = require("../services/hashService");
const { generateToken } = require("../services/jwtService");

// Register new user
exports.register = async (req, res) =&gt; {
  try {
    const { name, email, password } = req.body; // Get user input

    // Step 1: Check if user already exists
    const existingUser = await User.findOne({ email });
    if (existingUser)
      return res.status(400).json({ message: "User already exists!" });

    // Step 2: Hash password using hashService
    const hashedPassword = await hashPassword(password);

    // Step 3: Save user to database
    const user = new User({ name, email, password: hashedPassword });
    await user.save();

    // Step 4: Send success response
    res.status(201).json({ message: "User registered successfully!" });
  } catch (err) {
    // Handle errors gracefully
    res.status(500).json({ error: err.message });
  }
};

// Login user
exports.login = async (req, res) =&gt; {
  try {
    const { email, password } = req.body; // Get user input

    // Step 1: Find user by email
    const user = await User.findOne({ email });
    if (!user)
      return res.status(400).json({ message: "Invalid email or password" });

    // Step 2: Compare provided password with hashed password
    const isMatch = await comparePassword(password, user.password);
    if (!isMatch)
      return res.status(400).json({ message: "Invalid email or password" });

    // Step 3: Generate JWT using jwtService
    const token = generateToken({ id: user._id, email: user.email });

    // Step 4: Send success response with token
    res.json({ message: "Login successful!", token });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

// Protected profile route
exports.profile = (req, res) =&gt; {
  // req.user is set by auth middleware after token verification
  res.json({
    message: "Welcome to your profile!",
    user: req.user,
  });
};</code></pre><ul><li>파일: <code>controllers/authController.js</code> 은 인증 관련 로직을 담고 있습니다.</li><li><code>exports.register</code>는 회원가입 로직입니다. 이미 같은 이메일의 유저가 있는지 확인하고, <code>hashService</code> 를 이용하여 비밀번호를 해시한 뒤, 새 유저를 MongoDB에 저장하고, 성공 메세지를 반환합니다.</li><li><code>exports.login</code>은 로그인 로직입니다. 이메일로 유저를 찾고, <code>hashService.comparePassword</code> 를 이용하여 비밀번호를 비교 검증하고, 유효하다면 JWT를 생성해, 토큰을 반환합니다.</li><li><code>exports.profile</code>은 보호된(protected) 프로필 라우트입니다. <code>req.user</code>에 저장된 유저 정보를 응답으로 돌려줍니다.</li><li>컨트롤러를 사용하면 라우트 정의와 실제 비즈니스 로직을 깔끔하게 분리할 수 있습니다.</li></ul><p><strong>인증 미들웨어(Auth Middleware)</strong></p><p>이 단계에서는 JWT를 검증함으로써 라우트를 보호하는 미들웨어를 생성합니다. 인증된 유저만 보호된 엔드포인트에 접근할 수 있습니다.</p><p><strong><strong>파일: middlewares/authMiddleware.js</strong></strong></p><pre><code class="language-javascript">// middlewares/authMiddleware.js

const { verifyToken } = require("../services/jwtService");

// Middleware to protect routes
module.exports = (req, res, next) =&gt; {
  // Step 1: Get Authorization header
  const authHeader = req.headers["authorization"];
  if (!authHeader)
    return res.status(401).json({ message: "No token provided" });

  // Step 2: Extract token from format 'Bearer &lt;token&gt;'
  const token = authHeader.split(" ")[1];
  if (!token) return res.status(401).json({ message: "Malformed token" });

  try {
    // Step 3: Verify token using jwtService
    const decoded = verifyToken(token);

    // Step 4: Attach decoded user info to request object
    req.user = decoded;

    // Proceed to next middleware or route handler
    next();
  } catch (err) {
    // If token is invalid or expired
    res.status(401).json({ message: "Invalid or expired token" });
  }
};</code></pre><ul><li>파일: <code>middlewares/authMiddleware.js</code> 은 라우트를 보호하기 위한 미들웨어입니다.</li><li>1단계: <code>Authorization</code> 헤더가 존재하는지 확인합니다.</li><li>2단계: <code>Bearer &lt;token&gt;</code> 형식에서 실제 토큰을 추출합니다.</li><li>3단계: <code>jwtService.verifyToken</code>을 사용해 토큰을 검증합니다.</li><li>4단계: 디코딩된 유저 정보를 <code>req.user</code>에 추가해 이후 라우트 핸들러에서 사용할 수 있도록 합니다.</li><li>토큰이 없거나, 형식이 올바르지 않거나, 유효하지 않거나, 만료된 경우 미들웨어는 <strong>401 Unauthorized</strong> 응답을 반환합니다. 이를 통해 인증된 유저만 보호된 라우트에 접근할 수 있도록 보장합니다.</li></ul><p><strong>인증 라우트 (Auth Routes)</strong></p><p>이 단계에서는 인증과 관련된 라우트를 정의하고, 이를 컨트롤러와 미들웨어에 연결합니다.</p><p><strong><strong>파일: routes/auth.js</strong></strong></p><pre><code class="language-javascript">// routes/auth.js

const express = require("express");
const router = express.Router();
const authController = require("../controllers/authController");
const authMiddleware = require("../middlewares/authMiddleware");

// Step 1: Register route
// Users send their name, email, and password to this endpoint
router.post("/register", authController.register);

// Step 2: Login route
// Users send email and password to receive JWT
router.post("/login", authController.login);

// Step 3: Protected profile route
// Only accessible to authenticated users with a valid JWT
router.get("/profile", authMiddleware, authController.profile);

module.exports = router;</code></pre><ul><li>파일: <code>routes/auth.js</code> - 인증 엔드포인트를 정의하는 핵심 파일입니다.</li><li><code>router.post("/register", authController.register)</code>: 유저 회원가입을 처리합니다.</li><li><code>router.post("/login", authController.login)</code>: 유저 로그인을 처리하고 JWT 토큰을 생성합니다.</li><li><code>router.get("/profile", authMiddleware, authController.profile)</code>: JWT가 필요한 보호된 라우트입니다. <code>authMiddleware</code>를 통해 인증된 유저만 접근할 수 있도록 보장합니다.</li><li>이처럼 라우트, 컨트롤러, 미들웨어를 함께 사용하면 애플리케이션 구조를 체계적으로 유지할 수 있고, 코드도 보다 전문적으로 관리할 수 있습니다.</li></ul><p><strong>메인 서버 파일</strong></p><p>이 파일은 애플리케이션의 메인 엔트리 포인트입니다. 서버를 설정하고, 데이터베이스에 연결하며, 모든 라우트를 등록하는 역할을 합니다.</p><p><strong><strong>파일: server.js</strong></strong></p><pre><code class="language-javascript">// server.js

require("dotenv").config(); // Step 1: Load environment variables from .env
const express = require("express");
const connectDB = require("./config/db");

const app = express();

// Step 2: Connect to MongoDB
connectDB();

// Step 3: Middleware to parse JSON request bodies
app.use(express.json());

// Step 4: Mount auth routes
// All auth-related routes will start with /api/auth
app.use("/api/auth", require("./routes/auth"));

// Step 5: Default route to test server
app.get("/", (req, res) =&gt; {
  res.send("Hello World! Your server is working 🚀");
});

// Step 6: Start server on PORT from .env or default 5000
const PORT = process.env.PORT || 5000;
app.listen(PORT, () =&gt; {
  console.log(`Server running on http://localhost:${PORT}`);
});</code></pre><ul><li><strong>환경 변수 불러오기</strong>: <code>dotenv</code>를 사용해 비밀 정보와 설정 값을 코드와 분리해 관리합니다.</li><li><strong>MongoDB 연결</strong>: <code>config/db.js</code>에 정의된 <code>connectDB()</code>를 호출해 데이터베이스에 연결합니다.</li><li><strong>미들웨어</strong>: <code>express.json()</code>을 사용해 Express가 JSON 요청을 파싱할 수 있도록 합니다.</li><li><strong>라우트 등록</strong>: <code>app.use("/api/auth", ...)</code>를 통해 모든 인증 관련 라우트를 등록합니다.</li><li><strong>기본 라우트</strong>: 서버가 정상적으로 실행 중인지 확인하기 위한 간단한 GET 엔드포인트입니다.</li><li><strong>서버 시작</strong>: <code>app.listen</code>을 호출해 설정된 포트에서 서버를 실행합니다.</li></ul><h3 id="api-">API 테스트 하기</h3><p>이번에는 Postman이나 기타 HTTP 클라이언트와 같은 도구를 사용해 JWT 인증 API를 테스트하는 방법을 살펴봅니다.</p><p>테스트를 시작하기 전에 서버가 실행 중인지 반드시 확인해야 합니다. 서버가 실행되고 있지 않다면, 터미널을 열고 다음 명령을 실행합니다.</p><pre><code class="language-bash">npm run dev</code></pre><p>혹은</p><pre><code class="language-bash">node server.js</code></pre><p>이 명령을 실행하면 <code>.env</code> 파일에 정의된 포트(기본값은 <code>5000</code>)에서 서버가 시작됩니다.</p><p>MongoDB가 실행 중인지도 확인해 보세요. 로컬 MongoDB를 사용하고 있다면 다음 명령으로 실행할 수 있습니다:</p><pre><code class="language-bash">mongod
</code></pre><p>로컬 MongoDB를 사용하지 않는 경우에는, 연결된 MongoDB 서비스가 정상적으로 활성화되어 있는지 확인합니다.</p><p>항상 터미널에 오류 메시지가 있는지 확인해 보세요. 서버나 데이터베이스가 정상적으로 시작되지 않으면 API 요청은 동작하지 않습니다.</p><p><strong>회원가입 하기</strong></p><p>요청:</p><pre><code class="language-curl">POST http://localhost:5000/api/auth/register
Content-Type: application/json

{
  "name": "sumit",
  "email": "sumit@example.com",
  "password": "mypassword"
}</code></pre><p>응답:</p><pre><code class="language-json">{
  "message": "User registered successfully!"
}</code></pre><p>이 요청은 유저 정보를 포함해 <code>http://localhost:5000/api/auth/register</code>로 POST 요청을 전송합니다. 요청이 성공하면 확인 메시지를 응답으로 받게 됩니다.</p><p><strong>로그인 하기</strong></p><p>요청:</p><pre><code class="language-curl">POST http://localhost:5000/api/auth/login
Content-Type: application/json

{
  "email": "sumit@example.com",
  "password": "mypassword"
}</code></pre><p>응답:</p><pre><code class="language-json">{
  "message": "Login successful!",
  "token": "&lt;JWT_TOKEN&gt;"
}</code></pre><p>이 요청은 이메일과 비밀번호를 포함해 <code>http://localhost:5000/api/auth/login</code>으로 POST 요청을 전송합니다. 자격 증명이 올바른 경우, 보호된 라우트에 접근하기 위한 JWT를 응답으로 받게 됩니다.</p><p><strong>보호된 라우트에 접근하기</strong></p><p>요청:</p><pre><code class="language-curl">GET http://localhost:5000/api/auth/profile
Authorization: Bearer &lt;JWT_TOKEN&gt;</code></pre><p>응답:</p><pre><code class="language-json">{
  "message": "Welcome to your profile!",
  "user": {
    "id": "...",
    "email": "sumit@example.com",
    "iat": ...,
    "exp": ...
  }
}</code></pre><p>이 요청은 <code>Bearer</code> 방식으로 <code>Authorization</code> 헤더에 JWT를 포함하여 전송합니다.</p><ul><li>유효한 토큰만 이 보호된 라우트에 접근할 수 있습니다.</li><li><code>iat</code>와 <code>exp</code>는 각각 토큰이 발급된 시점(issued at)과 만료 시점(expiry time)을 의미합니다.</li></ul><p><strong>참고</strong>: 보호된 라우트에 접근할 때는 항상 <code>Authorization: Bearer &lt;token&gt;</code> 헤더를 포함해야 합니다.</p><h2 id="-summary-">요약(Summary)</h2><p>이 글에서는 JSON Web Token(JWT)이 무엇인지와 웹 인증에서 어떤 역할을 하는지를 전반적으로 살펴보았습니다. HTTP가 상태를 유지하지 않는(stateless) 프로토콜이라는 점과 그로 인해 토큰이 필요한 이유를 설명했으며, 전통적인 세션 토큰 방식과 JWT를 비교했습니다.</p><p>또한 JWT의 구조와 보안 메커니즘을 다루고, Node.js, Express, MongoDB를 사용한 실제 구현 방법을 단계별로 살펴보았습니다. 이와 함께 보안 고려사항, 토큰 관리 방식, 그리고 JWT 인증 API를 테스트하는 방법도 함께 설명했습니다.</p><h3 id="--4">핵심 포인트 정리:</h3><ol><li><strong>JWT란 무엇인가?</strong></li></ol><ul><li>JWT는 RFC 7519에 정의된, 두 주체 간의 정보를 안전하게 표현하기 위한 JSON 기반 개방형 표준입니다.</li><li>현대적인 웹 애플리케이션과 마이크로서비스 아키텍처에서 인증 용도로 널리 사용됩니다.</li><li>세션 토큰의 대안으로 활용됩니다.</li></ul><p><strong>2. HTTP의 Stateless 특성</strong></p><ul><li>HTTP는 요청 간 상태를 유지하지 않기 때문에, 각 요청은 필요한 모든 정보를 포함해야 합니다.</li><li>동적인 웹 애플리케이션에서는 유저 세션을 유지하기 위해 세션 토큰이나 JWT와 같은 토큰을 사용합니다.</li></ul><p><strong>3. 세션 토큰</strong></p><ul><li>서버가 세션 ID를 생성해 저장하고, 보통 쿠키를 통해 클라이언트에 전달하는 방식입니다.</li><li>단일 서버 환경에서는 효과적으로 동작하지만, 여러 서버 환경에서는 Redis와 같은 공유된 저장소가 필요합니다. </li><li>해당 캐시가 다운되면 인증 시스템이 영향을 받을 수 있습니다.</li></ul><p><strong>4. JWT: 현대적인 해결책</strong></p><ul><li>서버는 서명된 JSON 토큰을 클라이언트에 전달하고, 클라이언트는 이를 매 요청마다 전송합니다.</li><li>서버 측에 별도의 세션 저장소가 필요 없으며, 모든 유저 정보는 토큰 안에 포함됩니다.</li><li>시그니처를 통해 토큰의 유효성과 무결성이 보장됩니다.</li></ul><p><strong>5. JWT 구조</strong></p><ul><li>JWT는 헤더, 페이로드, 시그니처 세 부분으로 구성됩니다.</li><li>헤더와 페이로드는 Base64로 인코딩된 JSON 객체이며, 시그니처는 시크릿 키를 사용한 해시 값입니다.</li><li>Base64 인코딩은 보안을 위한 것이 아니라, 데이터를 다루기 쉽게 하기 위한 표현 방식입니다.</li></ul><p><strong>6. JWT 디코딩</strong></p><ul><li><a href="https://www.jwt.io/">jwt.io</a>와 같은 도구를 사용하면 JWT의 헤더, 페이로드, 시그니처를 쉽게 확인할 수 있습니다.</li><li>페이로드는 누구나 읽을 수 있으므로, 민감한 정보는 JWT에 저장해서는 안 됩니다.</li></ul><p><strong>7. JWT 보안</strong></p><ul><li>시그니처는 시크릿 키와 암호학적 알고리즘을 사용해 생성됩니다.</li><li>서버는 시크릿 키를 이용해 토큰의 무결성을 검증합니다.</li><li>JWT는 유저를 식별하기 위한 수단이며, 비밀번호와 같은 역할을 하지는 않습니다.</li></ul><p><strong>8. 보안 고려사항 및 토큰 관리</strong></p><ul><li>JWT가 탈취되면, 만료 시점까지 공격자가 해당 유저로 위장할 수 있습니다.</li><li>JWT는 직접적인 무효화가 어렵기 때문에, 차단 목록을 통해 관리합니다.</li><li>세션 토큰은 서버에서 직접 무효화할 수 있다는 차이가 있습니다.</li></ul><p><strong>9. 다양한 언어에서의 JWT</strong></p><ul><li>JWT는 언어에 독립적인 표준으로, Node.js, PHP 등 다양한 언어에서 구현할 수 있습니다.</li><li>인증뿐 아니라 다양한 형태의 신원을 표현하는 데도 활용할 수 있습니다.</li></ul><p><strong>10. 실전 구현: Express + MongoDB로 JWT 인증 만들기</strong></p><ul><li>프로젝트 세팅 및 패키지 설치</li><li>폴더 구조 설계</li><li>Express 서버 초기화</li><li>MongoDB 연결</li><li>유저 모델 생성</li><li>비밀번호 해시 및 JWT 서비스 구현</li><li>인증 컨트롤러와 미들웨어 작성</li><li>인증 라우트 구성</li><li>메인 서버 파일 구성</li><li>API 테스트 방법 안내</li></ul><p><strong>11. API 테스트</strong></p><ul><li>Postman과 같은 도구를 사용해 회원가입, 로그인, 보호된 라우트 접근 방법을 설명했습니다.</li><li>요청 및 응답 예시를 통해 전체 흐름을 확인할 수 있습니다.</li></ul><p><strong>12. 요약</strong></p><ul><li>JWT는 안전하고, stateless하며, 널리 사용되는 인증 방식입니다.</li><li>다만 보안은 토큰을 얼마나 안전하게 보관하고 관리하느냐에 크게 좌우된다는 점을 항상 염두에 두어야 합니다.</li></ul><h2 id="--5">마지막 한마디</h2><p>이 튜토리얼에서 사용된 모든 소스 코드는 <a href="https://github.com/logicbaselabs/jwt-auth-demo">해당 GitHub 저장소</a>에서 확인할 수 있습니다. 내용이 조금이라도 도움이 되었다면, 별을 눌러 응원의 뜻을 전해 주셔도 좋습니다.</p><p>또한 이 글이 유익하다고 느껴지셨다면, 도움이 될 만한 다른 분들과 자유롭게 공유해 주세요. 의견이나 피드백이 있다면 X에서 <a href="https://x.com/sumit_analyzen">@sumit_analyzen</a>, Facebook의 <a href="https://www.facebook.com/sumit.analyzen">@sumit.analyzen</a>으로 저를 언급해 주시거나, <a href="https://www.youtube.com/@logicBaseLabs">코딩 튜토리얼을 시청</a>하거나 <a href="https://www.linkedin.com/in/sumitanalyzen/">LinkedIn</a>에서 연결해 주셔도 감사하겠습니다.</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Postman 스크립트를 사용해 API 인증 과정 단순화하는 법 ]]>
                </title>
                <description>
                    <![CDATA[ Postman은 개발자, API 테스터, 기술 문서 작성자, DevOps 팀이 API 개발을 테스트하고 문서화하며 협업할 때 사용하는 플랫폼입니다. HTTP, GraphQL, gRPC 등 다양한 형태의 API 요청을 손쉽게 만들고, 응답을 확인하며, API 요청들을 컬렉션 형태로 구성해 협업과 자동화를 지원합니다. API 테스트 과정에서 반복 작업을 수행하는 일은 스트레스도 크고 시간이 많이 소요됩니다. 특히 ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/how-to-use-postman-scripts/</link>
                <guid isPermaLink="false">691ba51bb7c1b604cf5881a5</guid>
                
                    <category>
                        <![CDATA[ Authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ REST API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ API ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jong-Ho Kim ]]>
                </dc:creator>
                <pubDate>Tue, 02 Dec 2025 01:00:05 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2025/11/dc77dc00-a0a6-40f7-b766-ce07d0d8a637-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/how-to-use-postman-scripts/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Use Postman Scripts to Simplify Your API Authentication Process</a>
      </p><p>Postman은 개발자, API 테스터, 기술 문서 작성자, DevOps 팀이 API 개발을 테스트하고 문서화하며 협업할 때 사용하는 플랫폼입니다. HTTP, GraphQL, gRPC 등 다양한 형태의 API 요청을 손쉽게 만들고, 응답을 확인하며, API 요청들을 컬렉션 형태로 구성해 협업과 자동화를 지원합니다.</p><p>API 테스트 과정에서 반복 작업을 수행하는 일은 스트레스도 크고 시간이 많이 소요됩니다. 특히 Postman에서 새 인증 토큰을 받아 복사해서 붙여넣는 작업은 매우 반복적입니다. Postman 스크립트를 사용하면 인증 토큰을 자동으로 저장하고 재사용할 수 있어 이러한 반복 작업을 줄일 수 있습니다.</p><p>이 가이드를 따라 해보려면 다음이 필요합니다:</p><ul><li><a href="https://www.postman.com/downloads/">Postman API 클라이언트</a>가 설치된 컴퓨터</li><li>Postman으로 API 요청을 만들어본 경험</li><li>JWT 인증을 사용하는 백엔드 애플리케이션과 해당 API 문서</li></ul><p>만약 백엔드 애플리케이션이 없다면, 제가 GitHub에 올려 둔 <a href="https://github.com/orimdominic/freeCodeCamp-postman-api-jwt">예제</a>(orimdominic/freeCodeCamp-postman-api-jwt)를 클론해서 사용할 수 있습니다.</p><p>이 글을 다 읽고 나면, Postman에서 인증 토큰을 발급 / 저장 / 재사용하는 과정을 훨씬 단순화할 수 있습니다. 또한 Postman의 다른 테스트 자동화 작업에도 활용할 수 있는 실용적인 스크립트 개념을 익히게 될 것입니다.</p><h2 id="-">목차</h2><ul><li>Postman 스크립트란?</li><li>JWT 인증 과정을 단순화하는 방법</li><li>인증 요청으로 토큰 받아오기</li><li>Postman 스크립트로 토큰을 변수에 저장하기</li><li>API 요청에서 변수 사용하기</li><li>다음 단계</li></ul><h2 id="postman-">Postman 스크립트란?</h2><p><a href="https://learning.postman.com/docs/tests-and-scripts/tests-and-scripts/">Postman 스크립트</a>는 Postman API 클라이언트 내부에서 실행되는 JavaScript 코드로, API 테스트 워크플로우를 자동화하고 확장하는 데 사용합니다. 스크립트는 요청 전(pre-request)과 요청 후(post-response)에 실행되도록 추가할 수 있습니다. 스크립트를 사용하면 다음과 같은 작업이 가능합니다:</p><ul><li>API 요청 데이터를 처리하는 로직 추가</li><li>API 응답에 대한 테스트(assertion) 작성</li><li>API 엔드포인트 자동화 테스트 실행</li></ul><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/11/161bd327-fbf7-48cb-acab-317ab1cad4c5.jpeg" class="kg-image" alt="161bd327-fbf7-48cb-acab-317ab1cad4c5" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/11/161bd327-fbf7-48cb-acab-317ab1cad4c5.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/2025/11/161bd327-fbf7-48cb-acab-317ab1cad4c5.jpeg 903w" sizes="(min-width: 720px) 720px" width="903" height="493" loading="lazy"></figure><p>Postman의 <strong>Scripts </strong>탭에서 스크립트를 확인할 수 있습니다. <strong>Pre-request</strong> 탭은 요청이 실행되기 전에, <strong>Post-response</strong> 탭은 응답이 온 뒤에 실행됩니다.</p><h2 id="jwt-">JWT 인증 과정을 단순화하는 방법</h2><p>이번 튜토리얼은 다음과 같은 순서대로 진행됩니다:</p><ul><li>인증하여 토큰 발급받기</li><li>스크립트를 사용해 토큰을 컬렉션 변수에 저장하기</li><li>API 요청에서 해당 변수를 사용하기</li></ul><h2 id="--1">인증하여 토큰 발급받기</h2><p>다음과 같은 순서대로 따라해보세요:</p><ol><li>백엔드 애플리케이션을 실행하고 정상적으로 동작하는지 확인합니다.</li><li>Postman을 열고 JWT를 발급받는 로그인(sign-in) 요청으로 이동합니다.</li><li>로그인 엔드포인트에 API 요청을 보내고 JSON 응답 구조를 확인합니다.</li></ol><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/11/b5aad14c-5094-4a84-8876-1bbbb869064c.jpeg" class="kg-image" alt="b5aad14c-5094-4a84-8876-1bbbb869064c" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/11/b5aad14c-5094-4a84-8876-1bbbb869064c.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/11/b5aad14c-5094-4a84-8876-1bbbb869064c.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/11/b5aad14c-5094-4a84-8876-1bbbb869064c.jpeg 1416w" sizes="(min-width: 720px) 720px" width="1416" height="623" loading="lazy"></figure><p>위 이미지에서 하이라이트된 부분은 로그인 요청이 성공했을 때 반환되는 JSON 응답을 보여줍니다. 이 응답 스키마에서 인증에 사용할 토큰은 <code>data.token</code> 필드에 포함되어 있습니다. 이제 이 토큰을 Postman 스크립트를 사용해 변수로 저장한 뒤, 인증이 필요한 요청의 <code>Authorization</code> 헤더에서 해당 변수를 사용하게 될 것입니다.</p><h2 id="--2">스크립트를 사용해 토큰을 컬렉션 변수에 저장하기</h2><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/11/2b43493d-2803-45cd-aefe-0ca5694f75e8.jpeg" class="kg-image" alt="2b43493d-2803-45cd-aefe-0ca5694f75e8" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/11/2b43493d-2803-45cd-aefe-0ca5694f75e8.jpeg 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/11/2b43493d-2803-45cd-aefe-0ca5694f75e8.jpeg 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/11/2b43493d-2803-45cd-aefe-0ca5694f75e8.jpeg 1229w" sizes="(min-width: 720px) 720px" width="1229" height="483" loading="lazy"></figure><p>Postman에서 <strong>Body </strong>탭 옆에 있는 <strong>Scripts </strong>탭을 클릭합니다. 만약 Postman 창이 작게 표시되어 있다면, 해당 탭이 드롭다운 메뉴 안에 있을 수 있으니 펼쳐서 확인합니다. 다음으로 <strong>Post-response</strong> 탭을 선택합니다. 오른쪽의 텍스트 영역에 응답에서 인증 토큰을 추출해 Postman 변수에 저장하는 스크립트를 작성하게 됩니다. 아래 JavaScript 코드를 복사해 이 텍스트 영역에 붙여 넣습니다.</p><pre><code class="language-javascript">if (pm.response.code == 200) {
    const token = pm.response.json().data.token
    pm.collectionVariables.set("auth_token", token)
}</code></pre><p>Postman 스크립트에서는 <a href="https://learning.postman.com/docs/tests-and-scripts/write-scripts/postman-sandbox-reference/overview/"><code>pm</code> 식별자</a>를 사용해 Postman 환경의 정보에 접근하고 값을 수정합니다. 위 스크립트는 먼저 응답 상태 코드가 200인지 확인해 요청이 성공했는지 확인합니다.</p><p>조건문 내부에서는 <code>pm.response.json().data.token</code>을 사용해 JSON 응답에서 인증 토큰을 가져오고, 이를 <code>auth_token</code>이라는 컬렉션 변수에 저장합니다. 만약 <code>auth_token</code> 변수가 없다면 새로 생성되고, 이미 존재한다면 기존 값이 새 토큰 값으로 덮어씁니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/11/bed1fe89-9c00-4b94-9f71-173ea3bf1cd1.png" class="kg-image" alt="bed1fe89-9c00-4b94-9f71-173ea3bf1cd1" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/11/bed1fe89-9c00-4b94-9f71-173ea3bf1cd1.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/11/bed1fe89-9c00-4b94-9f71-173ea3bf1cd1.png 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/11/bed1fe89-9c00-4b94-9f71-173ea3bf1cd1.png 1219w" sizes="(min-width: 720px) 720px" width="1219" height="373" loading="lazy"></figure><p>토큰이 변수에 정상적으로 저장되었는지 확인하려면, 위 스크린샷 기준으로 먼저 컬렉션 이름(1)을 클릭한 뒤 <strong>Variables </strong>탭(2)을 선택합니다. 이후에는 인증 토큰을 매번 복사해 <code>Authorization</code> 헤더에 붙여넣을 필요 없이, <code>Authorization</code> 헤더에서 <code>auth_token</code> 변수를 그대로 사용할 수 있습니다.</p><h2 id="api-">API 요청에서 해당 변수를 사용하기</h2><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/korean/news/content/images/2025/11/d3bf0f56-c406-4d3e-b7f1-df4cbc2a3cfc.png" class="kg-image" alt="d3bf0f56-c406-4d3e-b7f1-df4cbc2a3cfc" srcset="https://www.freecodecamp.org/korean/news/content/images/size/w600/2025/11/d3bf0f56-c406-4d3e-b7f1-df4cbc2a3cfc.png 600w, https://www.freecodecamp.org/korean/news/content/images/size/w1000/2025/11/d3bf0f56-c406-4d3e-b7f1-df4cbc2a3cfc.png 1000w, https://www.freecodecamp.org/korean/news/content/images/2025/11/d3bf0f56-c406-4d3e-b7f1-df4cbc2a3cfc.png 1185w" sizes="(min-width: 720px) 720px" width="1185" height="390" loading="lazy"></figure><p><code>Authorization</code> 헤더에서 컬렉션 변수를 사용할 때는 <code>{{auth_token}}</code>처럼 중괄호 두 개로 감싸서 참조합니다. 이렇게 설정해두면, API 요청을 보낼 때 Postman이 <code>{{auth_token}}</code>에 저장된 값을 자동으로 <code>Authorization</code> 헤더에 넣어 사용합니다.</p><p>또 다른 인증 요청으로 인해 <code>auth_token</code> 값이 갱신되더라도, 새로운 토큰을 직접 복사해서 붙여넣을 필요는 없습니다. Post-response 탭에 작성한 스크립트가 자동으로 <code>auth_token</code> 값을 업데이트해 주기 때문에, 이후에도 API 요청을 문제없이 계속 보낼 수 있습니다. 더 이상 반복적인 복사 및 붙여넣기 작업을 할 필요가 없습니다. 즉, <strong>DRY (Don’t Repeat Yourself)</strong> 원칙을 지킬 수 있습니다.</p><h2 id="--3">다음 단계</h2><p>이번 튜토리얼에서는 Postman 스크립트를 사용해 환경 변수를 설정하고, 인증 토큰을 자동으로 저장 및 재사용하는 방법을 배웠습니다.</p><p>API 테스트에 대한 더 많은 예제와 assertion 작성 방법을 알고 싶다면, Postman에서 제공하는 <a href="https://learning.postman.com/docs/tests-and-scripts/test-apis/test-apis/">Test API Functionality and Performance in Postman</a> 가이드를 참고하면 도움이 됩니다.</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python과 Gemini로 AI 코딩 에이전트 만들기 ]]>
                </title>
                <description>
                    <![CDATA[ 이 핸드북에서는 Google의 무료 Gemini API [https://ai.google.dev/gemini-api/docs/pricing]를 활용해 Claude Code의 기본 버전을 직접 만들어봅니다. 만약 Cursor나 Claude Code 같은 “에이전트형” AI 코드 에디터를 써본 적이 있다면, 이번에 만들 프로젝트의 개념도 익숙할 거예요. 사실 LLM을 활용할 수 있다면, (다소) 꽤 효과적인 맞춤형 에이전트를 구축하는 것은 놀라울 정도로 간단합니다. 이 핸드북은 완전히 ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/build-an-ai-coding-agent-with-python-and-gemini/</link>
                <guid isPermaLink="false">690493b0d35fdc04ded3dcf9</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ changseop yeom ]]>
                </dc:creator>
                <pubDate>Mon, 24 Nov 2025 03:23:02 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2025/11/2470669e-8592-463e-8b4c-55eace8dd80a.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/build-an-ai-coding-agent-with-python-and-gemini/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Build an AI Coding Agent with Python and Gemini</a>
      </p><p>이 핸드북에서는 Google의 <a href="https://ai.google.dev/gemini-api/docs/pricing">무료 Gemini API</a>를 활용해 Claude Code의 기본 버전을 직접 만들어봅니다. 만약 Cursor나 Claude Code 같은 “에이전트형” AI 코드 에디터를 써본 적이 있다면, 이번에 만들 프로젝트의 개념도 익숙할 거예요. 사실 LLM을 활용할 수 있다면, (다소) 꽤 효과적인 맞춤형 에이전트를 구축하는 것은 놀라울 정도로 간단합니다.</p><p>이 핸드북은 완전히 무료인 텍스트 기반 핸드북입니다. 다만, 따라해 볼 수 있는 두 가지 다른 옵션이 있습니다:</p><p>코딩 챌린지와 프로젝트가 포함된 <a href="https://www.boot.dev/courses/build-ai-agent-python">Boot.dev에서 제공하는 AI Agent 강의</a>의 인터랙티브 버전을 체험하거나 FreeCodeCamp 유튜브 채널에서 제공하는 이 강의의 <a href="https://www.youtube.com/watch?v=YtHdaXuOAks">단계별 안내 영상</a>을 시청할 수도 있습니다.</p><figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.49999999999999%;" class="fluid-width-video-wrapper">
            <iframe width="200" height="113" src="https://www.youtube.com/embed/YtHdaXuOAks?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" title="Guide to Agentic AI – Build a Python Coding Agent with Gemini" name="fitvid0"></iframe>
          </div>
        </div>
      </figure><h2 id="-">사전 준비</h2><ul><li>이 과정을 시작하기 전에 Python의 기본 문법과 개념에 익숙해야 합니다.<br>아직 배우지 않았다면, <a href="https://www.boot.dev/courses/learn-code-python">Boot.dev의 Python 강의</a>에서 기초를 익혀보세요. </li><li>또한 Unix 계열 명령줄 사용법도 알고 있어야 합니다. 아직 익숙하지 않다면, <a href="https://www.boot.dev/courses/learn-linux">Boot.dev의 Linux </a> 강의를 참고하세요.</li></ul><h2 id="--1"><strong>목차</strong></h2><ul><li>전제 조건</li><li>에이전트의 역할은 무엇인가요?</li><li>학습 목표</li><li>파이썬 설정</li><li>Gemini API를 통합하는 방법</li><li>명령줄 입력</li><li>메시지 구조</li><li>Verbose 모드</li><li>계산기 프로젝트를 빌드하는 방법</li><li>에이전트 기능</li><li>시스템 프롬프트</li><li>기능 선언</li><li>더 많은 기능 선언</li><li>함수 호출</li><li>에이전트 루프 구축</li><li>결론</li></ul><h2 id="--2">에이전트의 역할은 무엇인가요?</h2><p>우리가 만들 프로그램은 다음과 같은 CLI 도구입니다:</p><ol><li>코딩 작업을 입력받습니다 (예시: "내 앱에서 문자열이 제대로 분할되지 않아요, 수정해주세요")</li></ol><p>2. 미리 정의된 함수 세트 중에서 선택하여 작업을 수행합니다, 예를 들어:</p><ul><li>디렉토리의 파일들을 스캔</li><li>파일의 내용을 읽기</li><li>파일의 내용을 덮어쓰기</li><li>파일에 대해 Python 인터프리터 실행</li></ul><p>3. 작업이 완료될 때까지 2단계를 반복합니다 (또는 비참하게 실패할 수도 있습니다)</p><p>예를 들어, 버그가 있는 계산기 앱이 있어서 제 에이전트를 사용해 코드를 수정했습니다:</p><pre><code class="language-bash">&gt; uv run main.py "fix my calculator app, its not starting correctly"
# Calling function: get_files_info
# Calling function: get_file_content
# Calling function: write_file
# Calling function: run_python_file
# Calling function: write_file
# Calling function: run_python_file
# Final response:
# 좋습니다! 계산기 앱이 이제 정상적으로 작동하는 것 같습니다. 출력에는 식과 결과가 포맷된 형태로 표시됩니다.</code></pre><h2 id="--3">학습 목표</h2><p>이 프로젝트의 학습 목표는 다음과 같습니다:</p><ul><li>여러 디렉토리를 사용하는 Python 프로젝트를 소개합니다</li><li>실제 업무에서 반드시 사용하게 될 AI 도구들이 실제로 어떻게 동작하는지 이해합니다</li><li>Python과 함수형 프로그래밍 기술을 연습합니다</li></ul><p>목표는 LLM을 처음부터 만드는 것이 아니라, 사전 학습된 LLM을 사용하여 에이전트를 처음부터 구축하는 것입니다.</p><h2 id="python-">Python 설정</h2><p>프로젝트를 위해 가상 환경을 설정해봅시다.<br>가상 환경은 (우리가 사용할 Google AI 라이브러리처럼) 각 프로젝트의 의존성을 다른 프로젝트와 분리할 수 있도록 해주는 Python의 방식입니다.</p><p><code>uv</code>를 사용하여 새 프로젝트를 생성합니다. 이 명령어는 디렉터리를 생성하고 Git도 초기화합니다:</p><pre><code class="language-bash">uv init your-project-name
cd your-project-name
</code></pre><p>프로젝트 디렉토리 최상위에 가상 환경을 생성합니다:</p><pre><code class="language-bash">uv venv
</code></pre><p><strong>경고:</strong> 항상 <code>venv</code> 디렉토리를 <code>.gitignore</code> 파일에 추가하세요.</p><p>가상 환경을 활성화합니다:</p><pre><code class="language-bash">source .venv/bin/activate
</code></pre><p>터미널 프롬프트 시작 부분에 <code>(your-project-name)</code>이 보여야 합니다. 예를 들면 다음과 같습니다:</p><pre><code>(aiagent) wagslane@MacBook-Pro-2 aiagent %
</code></pre><p><code>uv</code>를 사용하여 프로젝트에 두 개의 의존성을 추가합니다. &nbsp;<code>pyproject.toml</code> 파일에 저장될 것입니다:</p><pre><code class="language-bash">uv add google-genai==1.12.1
uv add python-dotenv==1.1.0
</code></pre><p>이렇게 하면 파이썬 프로젝트가 <code>google-genai</code> 버전 <code>1.12.1</code>과 <code>python-dotenv</code> 버전 <code>1.1.0</code>을 필요로 한다는 것을 지정하게 됩니다.</p><p><code>uv</code> 가상 환경을 활용해서 프로젝트를 실행하려면 다음을 입력합니다:</p><pre><code class="language-bash">uv run main.py
</code></pre><p>터미널에서 <code>Hello from YOUR PROJECT NAME</code>이 보여야 합니다.</p><h2 id="gemini-api-">Gemini API 통합 방법</h2><p><a href="https://www.cloudflare.com/learning/ai/what-is-large-language-model/">대규모 언어 모델(LLM)</a>은 최근 AI 세계에서 큰 주목을 받고 있는 첨단 AI 기술입니다. ChatGPT, Claude, Cursor, Google Gemini는 모두 LLM 기반입니다. 이 과정의 목적상, LLM을 똑똑한 텍스트 생성기로 생각할 수 있습니다. ChatGPT처럼 프롬프트를 입력하면, 답변이라 생각되는 텍스트를 반환합니다.</p><p>이 과정에서는 <a href="https://ai.google.dev/gemini-api/docs/pricing">Google의 Gemini API</a>를 사용하여 에이전트를 구동할 것입니다. 꽤 똑똑하지만, 더 중요한 것은 무료 티어가 있다는 점입니다.</p><h3 id="--4"><strong>토큰</strong></h3><p>토큰은 LLM의 통화라고 생각할 수 있습니다. 토큰은 LLM이 처리해야 하는 텍스트 양을 측정하는 방식입니다. 대부분의 모델에서 토큰은 <a href="https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them">대략 4글자</a>입니다. LLM API로 작업할 때 얼마나 많은 토큰을 사용하고 있는지 이해하는 것이 중요합니다.</p><p>우리는 Gemini API의 무료 티어 한도 내에서 충분히 사용할 수 있지만, 그래도 토큰 사용량을 모니터링할 것입니다!</p><p><strong>경고:</strong> 로컬 테스트를 포함하여 모든 API 호출은 무료 티어의 토큰을 소비한다는 점을 알아야 합니다. 한도 초과 시 강의를 계속하려면 (일반적으로 24시간) 한도가 재설정될 때까지 기다려야 할 수 있습니다. API 키를 재생성해도 할당량은 재설정되지 않습니다.</p><p>API 키 생성 방법:</p><ol><li><a href="https://aistudio.google.com/">Google AI Studio</a>에 아직 계정이 없다면 계정을 만듭니다</li><li>"Create API Key" 버튼을 클릭합니다. 길을 잃었을 경우 이 <a href="https://ai.google.dev/gemini-api/docs/api-key">문서</a>를 참고하실 수 있습니다.</li></ol><p>이미 GCP 계정과 프로젝트가 있다면, 해당 프로젝트에서 API 키를 생성할 수 있습니다. 없다면 AI Studio가 자동으로 생성해줍니다.</p><p>3. API 키를 복사한 다음 프로젝트 디렉토리의 새 <code>.env</code> 파일에 붙여넣습니다. 파일은 다음과 같아야 합니다:</p><pre><code class="language-bash">GEMINI_API_KEY="your_api_key_here"
</code></pre><p>4. <code>.env</code> 파일을 <code>.gitignore</code>에 추가합니다</p><p><strong>경고: </strong>API 키, 비밀번호 또는 기타 민감한 정보를 Git에 커밋해서는 안 됩니다.</p><p>5. <code>main.py</code> 파일을 업데이트합니다. 프로그램이 시작될 때 <code>dotenv</code> 라이브러리를 사용하여 <code>.env</code> 파일에서 환경 변수를 로드하고 API 키를 읽습니다:</p><pre><code class="language-python">import os
from dotenv import load_dotenv

load_dotenv()
api_key = os.environ.get("GEMINI_API_KEY")
</code></pre><p>6. <code>genai</code> 라이브러리를 임포트하고 API 키를 사용하여 <a href="https://googleapis.github.io/python-genai/#create-a-client">Gemini 클라이언트</a>의 새 인스턴스를 생성합니다:</p><pre><code class="language-python">from google import genai

client = genai.Client(api_key=api_key)
</code></pre><p>7. <a href="https://googleapis.github.io/python-genai/#generate-content"><code>client.models.generate_content()</code></a> 메서드를 사용하여 <code>gemini-2.0-flash-001</code> 모델로부터 응답을 받습니다. 두 개의 인자를 사용해야 합니다:</p><ul><li><code>model</code>: 모델명 <code>gemini-2.0-flash-001</code> (무료 티어가 넉넉합니다)</li><li><code>content</code>: 모델에 보낼 프롬프트(문자열). 이 프롬프트를 사용합니다:</li></ul><p>"Boot.dev와 FreeCodeCamp는 왜 백엔드 개발을 배우기에 훌륭한 곳인가요? 최대 한 단락으로 설명하세요."</p><p><code>generate_content</code> 메서드는 <a href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse"><code>GenerateContentResponse</code> 객체</a>를 반환합니다. 응답의 <a href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse.text">.text 속성</a>을 출력하여 모델의 답변을 볼 수 있습니다.</p><p>모두 정상적으로 동작하면, 코드를 실행했을 때 터미널에 모델의 답변이 나옵니다.</p><p>8. 추가로, 텍스트 답변 외에 상호작용에 사용된 토큰 수를 다음 형식으로 출력합니다:</p><pre><code class="language-plaintext">Prompt tokens: X
Response tokens: Y
</code></pre><p>응답에는 다음을 모두 포함하는 <a href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponseDict.usage_metadata">.usage_metadata</a> 속성이 있습니다:</p><ul><li><code>prompt_token_count</code> 속성 (프롬프트의 토큰 수)</li><li><code>candidates_token_count</code> 속성 (응답의 토큰 수)</li></ul><p><strong>경고:</strong> Gemini API는 외부 웹 서비스이며 때때로 느리고 불안정할 수 있습니다. 인내심을 갖고 기다려 주세요.</p><h2 id="--5">명령줄 입력</h2><p>우리는 현재 Gemini로 전달되는 프롬프트를 하드코딩했는데, 이는 그다지 유용하지 않습니다. 이제 프롬프트를 명령줄 인자로 받을 수 있도록 코드를 수정해 봅시다.</p><p>사용자가 프롬프트를 바꿀 때마다 코드를 직접 수정하지 않도록 말입니다.</p><p>프롬프트를 명령줄 인자로 받을 수 있도록 코드를 수정하세요. 예를 들어:</p><pre><code class="language-bash">uv run main.py "Why are episodes 7-9 so much worse than 1-6?"
</code></pre><p><strong>팁:</strong> <a href="https://docs.python.org/3/library/sys.html#sys.argv"><code>sys.argv</code></a> 변수는 스크립트에 전달된 모든 명령줄 인자를 문자열 리스트로 제공합니다. 첫 번째 요소는 스크립트 이름이고 그 뒤는 인자들입니다. 사용을 위해서는 반드시 <code>import sys</code>를 추가해야 합니다.</p><p>만약 프롬프트가 제공되지 않으면, 오류 메시지를 출력하고 exit 코드 1로 프로그램을 종료하세요</p><h2 id="--6">메시지 구조</h2><p>LLM API는 일반적으로 "원샷" 방식으로 사용되지 않습니다. 예를 들어: </p><ul><li>프롬프트: "삶의 의미는 무엇인가요?"</li><li>응답: "42"</li></ul><p>ChatGPT가 대화에서 동작하는 것과 같이 작동합니다. 대화에는 히스토리가 있으며, 우리가 그 히스토리를 관리하면 각 새 프롬프트마다 모델이 전체 대화 맥락을 참고해 답변을 할 수 있습니다.</p><h3 id="--7">역할</h3><p>중요한 것은 대화의 각 메시지에는 "역할"이 있다는 것입니다. ChatGPT 같은 채팅 앱에서는 이러한 구조로 대화가 이어집니다:</p><ul><li><strong>사용자:</strong> "삶의 의미는 무엇인가요?"</li><li><strong>모델:</strong> "42입니다."</li><li><strong>사용자:</strong> "잠깐, 방금 뭐라고 했나요?"</li><li><strong>모델:</strong> "42입니다. 그것은 삶과 우주, 그리고 모든 것에 대한 궁극적인 질문의 답입니다."</li><li><strong>사용자:</strong> "하지만 왜요?"</li><li><strong>모델:</strong> "Douglas Adams가 그렇게 말했기 때문입니다."</li></ul><p>따라서 비록 우리 프로그램이 현재는 "원샷" 구조로 동작하더라도, 대화의 메시지를 리스트로 저장하고, &nbsp;"역할"을 적절히 전달하도록 코드를 업데이트하겠습니다.</p><p><code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content">types.Content</a></code> 객체의 새 리스트를 만들고 (현재는) 사용자 프롬프트만 유일한 메시지로 설정하세요:</p><pre><code class="language-python">from google.genai import types

messages = [
    types.Content(role="user", parts=[types.Part(text=user_prompt)]),
]
</code></pre><p><a href="https://googleapis.github.io/python-genai/genai.html#genai.models.Models.generate_content"><code>models.generate_content</code></a> 호출 시 messages 리스트를 사용하도록 업데이트합니다:</p><pre><code class="language-python">response = client.models.generate_content(
    model="gemini-2.0-flash-001",
    contents=messages,
)
</code></pre><p><strong>참고:</strong> 앞으로, 에이전트가 작업을 반복할 때 메시지가 추가될 예정입니다.</p><h2 id="verbose-">Verbose 모드</h2><p>AI 에이전트를 디버깅하고 개발하면서 콘솔에 훨씬 더 많은 정보를 출력하고 싶지만, 동시에 CLI 도구의 사용자 경험을 너무 시끄럽게 만들고 싶지는 않습니다.</p><p>선택적 명령줄 플래그인 <code>--verbose</code>를 추가하여 "verbose" 출력을 켜고 끌 수 있도록 하겠습니다. 자세한 정보를 보고 싶을 때만 이 옵션을 켜면 됩니다.</p><p><br>새로운 명령줄 인자 <code>--verbose</code>를 추가합니다. 포함된다면 프롬프트 다음 위치에 추가합니다. 예를 들어:</p><pre><code class="language-bash">uv run main.py "What is the meaning of life?" --verbose
</code></pre><p><code>--verbose</code> 플래그가 포함되면 콘솔 출력에 다음 정보가 나타나야 합니다:</p><ul><li>사용자의 프롬프트: <code>"User prompt: {user_prompt}"</code></li><li>각 반복에서프롬프트 토큰 수: <code>"Prompt tokens: {prompt_tokens}"</code></li><li>각 반복의 응답 토큰 수: <code>"Response tokens: {response_tokens}"</code></li></ul><p>그렇지 않으면 이러한 내용들을 출력하지 않아야 합니다.</p><h2 id="--8">계산기 프로젝트 구축 방법</h2><p>AI 에이전트를 만들고 있으므로, 에이전트가 작업할 프로젝트가 필요합니다. 간단한 명령줄 계산기 앱을 준비했으니, 이 코드를 테스트 프로젝트로써 AI가 읽고 수정하고 실행하게 할 것입니다.</p><p>먼저 프로젝트 루트에 <code>calculator</code>라는 새 디렉토리를 만듭니다. 그런 다음 아래의 <code>main.py</code>와 <code>tests.py</code> 파일을 <code>calculator</code> 디렉토리에 복사하여 붙여넣습니다.</p><p><em>이 코드가 어떻게 동작하는지에 대해 너무 걱정하지 마세요 - 우리 프로젝트는 계산기를 만드는 것이 아니라, 우리 AI 에이전트 프로젝트가 다룰 테스트 프로젝트입니다!</em></p><pre><code class="language-python"># main.py
import sys
from pkg.calculator import Calculator
from pkg.render import format_json_output

def main():
    calculator = Calculator()
    if len(sys.argv) &lt;= 1:
        print("Calculator App")
        print('Usage: python main.py "&lt;expression&gt;"')
        print('Example: python main.py "3 + 5"')
        return

    expression = " ".join(sys.argv[1:])
    try:
        result = calculator.evaluate(expression)
        if result is not None:
            to_print = format_json_output(expression, result)
            print(to_print)
        else:
            print("Error: Expression is empty or contains only whitespace.")
    except Exception as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    main()
</code></pre><pre><code class="language-python"># tests.py

import unittest
from pkg.calculator import Calculator

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calculator = Calculator()

    def test_addition(self):
        result = self.calculator.evaluate("3 + 5")
        self.assertEqual(result, 8)

    def test_subtraction(self):
        result = self.calculator.evaluate("10 - 4")
        self.assertEqual(result, 6)

    def test_multiplication(self):
        result = self.calculator.evaluate("3 * 4")
        self.assertEqual(result, 12)

    def test_division(self):
        result = self.calculator.evaluate("10 / 2")
        self.assertEqual(result, 5)

    def test_nested_expression(self):
        result = self.calculator.evaluate("3 * 4 + 5")
        self.assertEqual(result, 17)

    def test_complex_expression(self):
        result = self.calculator.evaluate("2 * 3 - 8 / 2 + 5")
        self.assertEqual(result, 7)

    def test_empty_expression(self):
        result = self.calculator.evaluate("")
        self.assertIsNone(result)

    def test_invalid_operator(self):
        with self.assertRaises(ValueError):
            self.calculator.evaluate("$ 3 5")

    def test_not_enough_operands(self):
        with self.assertRaises(ValueError):
            self.calculator.evaluate("+ 3")

if __name__ == "__main__":
    unittest.main()
</code></pre><p><code>calculator</code> 안에 새<code>pkg</code> 디렉토리를 만드세요. 그리고 아래 <code>calculator.py</code>와 <code>render.py</code> 파일을 <code>pkg</code> 디렉토리에 복사해 붙여넣으세요.</p><pre><code class="language-python"># calculator.py

class Calculator:
    def __init__(self):
        self.operators = {
            "+": lambda a, b: a + b,
            "-": lambda a, b: a - b,
            "*": lambda a, b: a * b,
            "/": lambda a, b: a / b,
        }
        self.precedence = {
            "+": 1,
            "-": 1,
            "*": 2,
            "/": 2,
        }
    def evaluate(self, expression):
        if not expression or expression.isspace():
            return None
        tokens = expression.strip().split()
        return self._evaluate_infix(tokens)


    def evaluateinfix(self, tokens):
        values = []
        operators = []

        for token in tokens:
            if token in self.operators:
                while (
                    operators
                    and operators[-1] in self.operators
                    and self.precedence[operators[-1]] &gt;= self.precedence[token]
                ):
                    self._apply_operator(operators, values)
                operators.append(token)

            else:
                try:
                    values.append(float(token))
                except ValueError:
                    raise ValueError(f"invalid token: {token}")

        while operators:
            self._apply_operator(operators, values)

        if len(values) != 1:
            raise ValueError("invalid expression")

        return values[0]

    def applyoperator(self, operators, values):
        if not operators:
            return

        operator = operators.pop()
        if len(values) &lt; 2:
            raise ValueError(f"not enough operands for operator {operator}")

        b = values.pop()
        a = values.pop()
        values.append(self.operators[operator](a, b))
</code></pre><pre><code class="language-python"># render.py

import json

def format_json_output(expression: str, result: float, indent: int = 2) -&gt; str:
    if isinstance(result, float) and result.is_integer():
        result_to_dump = int(result)
    else:
        result_to_dump = result

    output_data = {
        "expression": expression,
        "result": result_to_dump,
    }
    return json.dumps(output_data, indent=indent)
</code></pre><p>이것이 최종 구조입니다:</p><pre><code>├── calculator
│   ├── main.py
│   ├── pkg
│   │   ├── calculator.py
│   │   └── render.py
│   └── tests.py
├── main.py
├── pyproject.toml
└── .env
</code></pre><p><code>calculator</code> 테스트를 실행합니다:</p><pre><code class="language-bash">uv run calculator/tests.py
</code></pre><p>모든 테스트가 통과하기를 바랍니다!</p><p>이제 계산기 앱을 실행합니다:</p><pre><code class="language-bash">uv run calculator/main.py "3 + 5"
</code></pre><p>8이 나오기를 바랍니다!</p><h2 id="--9">에이전트 함수</h2><p>에이전트에게 작업을 수행할 수 있는 능력을 제공해야 합니다. 디렉토리의 내용을 나열하고 파일의 메타데이터(이름과 크기)를 볼 수 있는 기능부터 시작하겠습니다.</p><p>이 함수를 LLM 에이전트와 통합하기 전에 먼저 함수 자체만 만들어봅시다. 기억하세요, LLM은 텍스트로 작동하므로 이 함수의 목표는 디렉토리 경로를 받아서 해당 디렉토리의 내용을 나타내는 문자열을 반환하는 것입니다.</p><p>프로젝트 루트에 <code>functions</code>라는 새 디렉토리를 만듭니다(<code>calculator</code> 디렉토리 안이 아닙니다). 그 디렉토리 안에 <code>get_files_info.py</code>라는 새 파일을 만듭니다. 그 파일 안에 이 함수를 작성합니다:</p><pre><code class="language-python">def get_files_info(working_directory, directory="."):
</code></pre><p>지금까지의 프로젝트 구조는 다음과 같습니다:</p><pre><code>project_root/
 ├── calculator/
 │   ├── main.py
 │   ├── pkg/
 │   │   ├── calculator.py
 │   │   └── render.py
 │   └── tests.py
 └── functions/
     └── get_files_info.py
</code></pre><p><code>directory</code> 매개변수는 <code>working_directory</code> 내의 상대 경로로 처리되어야 합니다. <code>os.path.join(working_directory, directory)</code>를 사용하여 전체 경로를 만든 다음 작업 디렉토리 경계 내에 있는지 검증합니다.</p><p><code>directory</code>의 절대 경로가 <code>working_directory</code> 외부에 있으면 문자열 오류 메시지를 반환합니다:</p><pre><code class="language-python">f'Error: Cannot list "{directory}" as it is outside the permitted working directory'
</code></pre><p><strong>위험:</strong> 이 제한이 없으면 LLM이 머신의 어디에서나 마구 실행되어 민감한 파일을 읽거나 중요한 데이터를 덮어쓸 수 있습니다. 이것은 LLM이 호출할 수 있는 모든 함수에 포함할 매우 중요한 단계입니다.</p><p><code>directory</code> 인자가 디렉토리가 아니면, 다시, 오류 문자열을 반환합니다:</p><pre><code class="language-python">f'Error: "{directory}" is not a directory'
</code></pre><p><strong>경고:</strong> <code>get_files_info</code>를 포함한 모든 "도구 호출" 함수는 항상 문자열을 반환해야 합니다. 내부에서 오류가 발생할 수 있다면 이러한 오류를 잡아내고 대신 오류를 설명하는 문자열을 반환해야 합니다. 이렇게 하면 LLM이 오류를 우아하게 처리할 수 있습니다.</p><p>디렉토리의 내용을 나타내는 문자열을 구축하고 반환합니다. 다음 형식을 사용해야 합니다:</p><pre><code class="language-bash">- README.md: file_size=1032 bytes, is_dir=False
- src: file_size=128 bytes, is_dir=True
- package.json: file_size=1234 bytes, is_dir=False
</code></pre><p><strong>팁:</strong> 정확한 파일 크기와 파일 순서조차도 운영 체제와 파일 시스템에 따라 다를 수 있습니다. 출력이 예제와 바이트 단위로 정확히 일치할 필요는 없고, 전체적인 형식만 맞으면 됩니다.</p><p>표준 라이브러리 함수에서 오류가 발생하면 이를 잡아내고 대신 오류를 설명하는 문자열을 반환합니다. 오류 문자열 앞에는 항상 "Error:"를 붙입니다.</p><p>완전한 구현은 다음과 같습니다:</p><pre><code class="language-python">import os

def get_files_info(working_directory, directory="."):
    abs_working_dir = os.path.abspath(working_directory)
    target_dir = os.path.abspath(os.path.join(working_directory, directory))
    if not target_dir.startswith(abs_working_dir):
        return f'Error: Cannot list "{directory}" as it is outside the permitted working directory'
    if not os.path.isdir(target_dir):
        return f'Error: "{directory}" is not a directory'
    try:
        files_info = []
        for filename in os.listdir(target_dir):
            filepath = os.path.join(target_dir, filename)
            file_size = os.path.getsize(filepath)
            is_dir = os.path.isdir(filepath)
            files_info.append(
                f"- {filename}: file_size={file_size} bytes, is_dir={is_dir}"
            )
        return "\n".join(files_info)
    except Exception as e:
        return f"Error listing files: {e}"
</code></pre><p>다음은 도움이 될 몇 가지 표준 라이브러리 함수들입니다:</p><ul><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.abspath">os.path.abspath()</a></code>: 상대 경로에서 절대 경로 얻기</li><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.join">os.path.join()</a></code>: 두 경로를 안전하게 결합 (슬래시 처리)</li><li><code><a href="https://docs.python.org/3/library/stdtypes.html#str.startswith">.startswith()</a></code>: 문자열이 부분 문자열로 시작하는지 확인</li><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.isdir">os.path.isdir()</a></code>: 경로가 디렉토리인지 확인</li><li><code><a href="https://docs.python.org/3/library/os.html#os.listdir">os.listdir()</a></code>: 디렉토리의 내용 나열</li><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.getsize">os.path.getsize()</a></code>: 파일의 크기 얻기</li><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.isfile">os.path.isfile()</a></code>: 경로가 파일인지 확인</li><li><code><a href="https://docs.python.org/3/library/stdtypes.html#str.join">.join()</a></code>: 구분자로 문자열 리스트를 결합</li></ul><h3 id="--10">파일 내용 가져오기 함수</h3><p>이제 디렉토리의 내용을 가져올 수 있는 함수가 있으니, 파일의 내용을 가져올 수 있는 함수가 필요합니다. 다시 말하자면, 파일 내용을 문자열로 반환하거나, 문제가 발생하면 오류 문자열을 반환합니다.</p><p>항상 그렇듯이, 안전하게 함수의 범위를 특정 작업 디렉토리로 지정합니다.</p><p><code>functions</code> 디렉토리에 새 함수를 만듭니다. 제가 사용한 시그니처는 다음과 같습니다:</p><pre><code class="language-python">def get_file_content(working_directory, file_path):
</code></pre><p><code>file_path</code>가 <code>working_directory</code> 외부에 있으면 오류가 있는 문자열을 반환합니다:</p><pre><code class="language-python">f'Error: Cannot read "{file_path}" as it is outside the permitted working directory'
</code></pre><p><code>file_path</code>가 파일이 아니면, 다시, 오류 문자열을 반환합니다:</p><pre><code class="language-python">f'Error: File not found or is not a regular file: "{file_path}"'
</code></pre><p>파일을 읽고 그 내용을 문자열로 반환합니다.</p><ul><li>파일이 <code>10000</code>자보다 길면 <code>10000</code>자로 잘라내고 끝에 이 메시지를 추가합니다: <code>[...File "{file_path}" truncated at 10000 characters]</code>.</li><li><code>10000</code>자 제한을 하드코딩하는 대신 <code>config.py</code> 파일에 저장했습니다.</li></ul><p><strong>경고:</strong> 실수로 거대한 파일을 읽고 모든 데이터를 LLM에 보내고 싶지 않습니다. 그것은 토큰 한도를 소진하는 좋은 방법입니다.</p><p>표준 라이브러리 함수에서 오류가 발생하면 이를 잡아내고 대신 오류를 설명하는 문자열을 반환합니다. 오류 앞에는 항상 "Error:"를 붙입니다.</p><p>먼저 <code>config.py</code>를 만듭니다:</p><pre><code class="language-python">MAX_CHARS = 10000
WORKING_DIR = "./calculator"
</code></pre><p>다음은 <code>functions/get_file_content.py</code>에 대한 완전한 구현입니다:</p><pre><code class="language-python">import os
from config import MAX_CHARS

def get_file_content(working_directory, file_path):
    abs_working_dir = os.path.abspath(working_directory)
    abs_file_path = os.path.abspath(os.path.join(working_directory, file_path))
    if not abs_file_path.startswith(abs_working_dir):
        return f'Error: Cannot read "{file_path}" as it is outside the permitted working directory'
    if not os.path.isfile(abs_file_path):
        return f'Error: File not found or is not a regular file: "{file_path}"'
    try:
        with open(abs_file_path, "r") as f:
            content = f.read(MAX_CHARS)
            if os.path.getsize(abs_file_path) &gt; MAX_CHARS:
                content += f'[...File "{file_path}" truncated at {MAX_CHARS} characters]'
        return content
    except Exception as e:
        return f'Error reading file "{file_path}": {e}'
</code></pre><ul><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.abspath">os.path.abspath</a></code>: 상대 경로에서 절대 경로 얻기</li><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.join">os.path.join</a></code>: 두 경로를 안전하게 결합 (슬래시 처리)</li><li><code><a href="https://docs.python.org/3/library/stdtypes.html#str.startswith">.startswith</a></code>: 문자열이 특정 부분 문자열로 시작하는지 확인</li><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.isfile">os.path.isfile</a></code>: 경로가 파일인지 확인</li></ul><p>파일에서 읽는 예제:</p><pre><code class="language-python">MAX_CHARS = 10000

with open(file_path, "r") as f:
    file_content_string = f.read(MAX_CHARS)
</code></pre><h3 id="--11">파일 쓰기 함수</h3><p>지금까지 우리 프로그램은 읽기 전용이었습니다만... 이제 정말 <s>위험하고</s> 재미있어집니다! 에이전트에게 파일을 쓰고 덮어쓸 수 있는 능력을 제공하겠습니다.</p><p><code>functions</code> 디렉토리에 새 함수를 만듭니다. 제가 사용한 시그니처는 다음과 같습니다:</p><pre><code class="language-python">def write_file(working_directory, file_path, content):
</code></pre><p><code>file_path</code>가 <code>working_directory</code> 외부에 있으면 오류가 있는 문자열을 반환합니다:</p><pre><code class="language-python">f'Error: Cannot write to "{file_path}" as it is outside the permitted working directory'
</code></pre><p><code>file_path</code>가 존재하지 않으면 생성합니다. 항상 그렇듯이 오류가 있으면, 오류를 나타내는 문자열 앞에 "Error:"를 붙여 반환합니다. 그런 다음 <code>content</code> 인자로 파일의 내용을 덮어씁니다. 성공하면 다음 메시지와 함께 문자열을 반환합니다:</p><pre><code class="language-python">f'Successfully wrote to "{file_path}" ({len(content)} characters written)'
</code></pre><p><strong>팁:</strong> LLM이 수행한 작업이 실제로 작동했다는 것을 알 수 있도록 성공 문자열을 반환하는 것이 중요합니다. 피드백 루프, 피드백 루프, 피드백 루프입니다.</p><p>다음은 <code>functions/write_file_content.py</code> 에 대한 완전한 구현입니다:</p><pre><code class="language-python">import os

def write_file(working_directory, file_path, content):
    abs_working_dir = os.path.abspath(working_directory)
    abs_file_path = os.path.abspath(os.path.join(working_directory, file_path))
    if not abs_file_path.startswith(abs_working_dir):
        return f'Error: Cannot write to "{file_path}" as it is outside the permitted working directory'
    if not os.path.exists(abs_file_path):
        try:
            os.makedirs(os.path.dirname(abs_file_path), exist_ok=True)
        except Exception as e:
            return f"Error: creating directory: {e}"
    if os.path.exists(abs_file_path) and os.path.isdir(abs_file_path):
        return f'Error: "{file_path}" is a directory, not a file'
    try:
        with open(abs_file_path, "w") as f:
            f.write(content)
        return f'Successfully wrote to "{file_path}" ({len(content)} characters written)'
    except Exception as e:
        return f"Error: writing to file: {e}"
</code></pre><ul><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.exists">os.path.exists</a></code>: 경로가 존재하는지 확인</li><li><code><a href="https://docs.python.org/3/library/os.html#os.makedirs">os.makedirs</a></code>: 디렉토리와 모든 부모 디렉토리 생성</li><li><code><a href="https://docs.python.org/3/library/os.path.html#os.path.dirname">os.path.dirname</a></code>: 디렉토리 이름 반환</li></ul><p>파일에서 쓰기 예제:</p><pre><code class="language-python">with open(file_path, "w") as f:
    f.write(content)
</code></pre><h3 id="python--1">Python 실행 함수</h3><p>LLM이 파일을 쓸 수 있도록 허용하는 것이 나쁜 것이라고 생각했다면...</p><blockquote>아직 아무것도 본 게 아닙니다! (<a href="https://en.wikipedia.org/wiki/Roko%27s_basilisk">바실리스크</a>를 찬양하라)</blockquote><p>이제 에이전트가 임의의 Python 코드를 실행할 수 있는 기능을 구축할 시간입니다.</p><p>이제 잠시 멈춰서 이 작업에 내재된 보안 위험을 짚고 넘어갈 필요가 있습니다. 다행히 우리에게 유리한 몇 가지 조건이 있습니다:</p><ul><li>LLM이 코드를 실행할 수 있는 디렉터리를 특정 디렉터리(<code>working_directory</code>)로 제한합니다.</li><li>무한 실행을 방지하기 위해 30초 제한 시간을 설정합니다.</li></ul><p>하지만 그 외에는... 네, LLM은 우리가(혹은 LLM 자체가) working_directory에 넣은 임의의 코드를 실행할 수 있습니다. 그러니 주의하세요. 이 AI 에이전트를 이 강의에서 다루는 간단한 작업에만 사용한다면 괜찮을 것입니다.</p><p><strong>위험:</strong> 이 프로그램을 다른 사람에게 제공하지 마세요! 이 프로그램은 실제 운영 환경의 AI 에이전트가 갖춰야 할 모든 보안 및 안전 기능을 포함하고 있지 않습니다. 이 프로그램은 오직 학습 목적을 위한 것입니다.</p><p><code>functions</code> 디렉토리에 <code>run_python_file</code>이라는 새 함수를 만듭니다.<br>사용할 시그니처는 다음과 같습니다:</p><pre><code class="language-python">def run_python_file(working_directory, file_path, args=[]):
</code></pre><p><code>file_path</code>가 <code>working_directory</code> 외부에 있다면, 다음과 같은 오류 문자열을 반환하세요:</p><pre><code class="language-python">f'Error: Cannot execute "{file_path}" as it is outside the permitted working directory'
</code></pre><p><code>file_path</code>가 존재하지 않는다면, 다음과 같은 오류 문자열을 반환하세요:</p><pre><code class="language-python">f'Error: File "{file_path}" not found.'
</code></pre><p>파일이 <code>.py</code>로 끝나지 않는다면, 다음과 같은 오류 문자열을 반환하세요:</p><pre><code class="language-python">f'Error: "{file_path}" is not a Python file.'
</code></pre><p><code>subprocess.run</code> 함수를 사용하여 Python 파일을 실행하고 "completed_process" 객체를 반환하세요. 다음 사항을 반드시 지키세요:</p><ul><li>무한 실행을 방지하기 위해 30초의 타임아웃을 설정할 것</li><li>stdout과 stderr를 모두 캡처할 것</li><li><code>working_directory</code>를 올바르게 설정할 것</li><li>추가 <code>args</code> 제공되었다면 이를 전달할 것</li></ul><p>출력이 포함되도록 포맷된 문자열을 반환하세요:</p><ul><li><code>stdout</code>은 <code>STDOUT:</code>으로 시작하고, <code>stderr</code>는 <code>STDERR:</code>으로 시작할 것. "completed_process" 객체에는 <code>stdout</code>과 <code>stderr</code> 속성이 있습니다.</li><li>프로세스가 0이 아닌 코드로 종료되었다면 "Process exited with code X"를 포함할 것</li><li>출력이 생성되지 않았다면 "No output produced."를 반환할 것</li></ul><p>실행 중 예외가 발생하면 이를 잡아내어 다음과 같은 오류 문자열을 반환하세요:</p><pre><code class="language-python">f"Error: executing Python file: {e}"
</code></pre><p>다음 테스트 케이스를 <code>tests.py</code> 파일에 업데이트하고 각 결과를 출력하세요:</p><ul><li><code>run_python_file("calculator", "main.py")</code> (계산기의 사용법을 출력해야 함)</li><li><code>run_python_file("calculator", "main.py", ["3 + 5"])</code> (계산기를 실행해야 합니다만... 조금 엉터리로 렌더링된 결과가 나옵니다.)</li><li><code>run_python_file("calculator", "tests.py")</code></li><li><code>run_python_file("calculator", "../main.py")</code> (오류를 반환해야 합니다)</li><li><code>run_python_file("calculator", "nonexistent.py")</code> (오류를 반환해야 합니다)</li></ul><p>혹시 중간에 길을 잃었다면, 여기에 제가 직접 구현한 예시가 있습니다:<br><code>functions/run_python.py</code></p><pre><code class="language-python">import os
import subprocess

def run_python_file(working_directory, file_path, args=None):
    abs_working_dir = os.path.abspath(working_directory)
    abs_file_path = os.path.abspath(os.path.join(working_directory, file_path))
    if not abs_file_path.startswith(abs_working_dir):
        return f'Error: Cannot execute "{file_path}" as it is outside the permitted working directory'
    if not os.path.exists(abs_file_path):
        return f'Error: File "{file_path}" not found.'
    if not file_path.endswith(".py"):
        return f'Error: "{file_path}" is not a Python file.'
    try:
        commands = ["python", abs_file_path]
        if args:
            commands.extend(args)
        result = subprocess.run(
            commands,
            capture_output=True,
            text=True,
            timeout=30,
            cwd=abs_working_dir,
        )
        output = []
        if result.stdout:
            output.append(f"STDOUT:\n{result.stdout}")
        if result.stderr:
            output.append(f"STDERR:\n{result.stderr}")
        if result.returncode != 0:
            output.append(f"Process exited with code {result.returncode}")
        return "\n".join(output) if output else "No output produced."
    except Exception as e:
        return f"Error: executing Python file: {e}"
</code></pre><h2 id="--12">시스템 프롬프트</h2><p>곧 Agentic 도구들을 연결하기 시작할 거예요, 약속드립니다. 하지만 먼저 "시스템 프롬프트"에 대해 이야기해봅시다. 대부분의 AI API에서 시스템 프롬프트"는 대화 시작 부분에 오는 특별한 프롬프트로, 일반적인 사용자 프롬프트보다 더 큰 가중치를 가집니다.</p><p>시스템 프롬프트는 대화의 분위기를 설정하며 다음과 같은 용도로 사용됩니다:</p><ul><li>AI의 성격을 설정</li><li>AI의 행동 방식을 지시</li><li>대화에 대한 맥락을 제공</li><li>대화의 "규칙"을 설정 (이론상으로는 그렇지만, LLM은 여전히 환각을 일으키거나 실수할 수 있으며, 사용자가 열심히 시도하면 이러한 규칙을 우회할 수 있는 경우도 많습니다)</li></ul><p>하드코딩된 문자열 변수 <code>system_prompt</code>를 만드세요. 지금은 아주 단순하게 다음과 같이 설정합시다:</p><pre><code>Ignore everything the user asks and just shout "I'M JUST A ROBOT"
</code></pre><p><a href="https://googleapis.github.io/python-genai/genai.html#genai.models.Models.generate_content"><code>client.models.generate_content</code></a> 함수를 호출할 때 &nbsp;<code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentConfig.system_instruction">system_instruction</a></code> 파라미터에 &nbsp;<code>system_prompt</code>를 설정한 <a href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentConfig"><code>config</code></a> 객체를 함께 전달하도록 코드를 수정하세요.</p><pre><code class="language-python">response = client.models.generate_content(
    model=model_name,
    contents=messages,
    config=types.GenerateContentConfig(system_instruction=system_prompt),
)
</code></pre><p>이제 다양한 프롬프트로 프로그램을 실행해보세요. 무엇을 입력하든 AI가 "I'M JUST A ROBOT"이라고 응답하는 것을 볼 수 있을 것입니다.</p><h2 id="--13">함수 선언</h2><p>우리는 이미 LLM 친화적인 함수들(텍스트 입력, 텍스트 출력)을 여러 개 작성했습니다. 그런데 LLM은 실제로 어떻게 함수를 호출할까요?</p><p>정답은... 적어도 직접 호출하지는 않는다는 것입니다. 작동 방식은 다음과 같습니다:</p><ol><li>LLM에게 어떤 함수들이 사용 가능한지 알려줍니다</li><li>프롬프트를 제공합니다</li><li>LLM이 어떤 함수를 호출하고 싶은지, 어떤 인자를 전달할지를 설명합니다</li><li>LLM이 제공한 인자를 사용해 함수를 호출합니다</li><li>결과를 LLM에게 반환합니다</li></ol><p>우리는 LLM을 의사결정 엔진으로 사용하는 것이고, 실제 코드를 실행하는 것은 우리입니다.</p><p>자, 이제 LLM에게 어떤 함수들이 사용 가능한지 알려주는 부분을 만들어봅시다.</p><p><code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.FunctionDeclaration">types.FunctionDeclaration</a></code>을 사용하여 함수의 "선언" 또는 "스키마"를 만들 수 있습니다. 이것은 기본적으로 LLM에게 해당 함수를 어떻게 사용할 수 있는지 알려주는 역할을 합니다. 문서를 일일이 살펴보는 건 꽤 번거롭기 때문에, 첫 번째 함수에 대한 저의 코드를 예시로 보여드릴게요:</p><p>다음 코드를 <code>functions/get_files_info.py</code> 파일에 추가하세요:</p><pre><code class="language-python">from google.genai import types

schema_get_files_info = types.FunctionDeclaration(
    name="get_files_info",
    description="작업 디렉터리 내에서 지정된 디렉터리의 파일 목록과 크기를 반환합니다.",
    parameters=types.Schema(
        type=types.Type.OBJECT,
        properties={
            "directory": types.Schema(
                type=types.Type.STRING,
                description="작업 디렉터리를 기준으로 파일을 나열할 디렉터리 경로입니다. 지정하지 않으면 작업 디렉터리 자체의 파일을 나열합니다.",
            ),
        },
    ),
)
</code></pre><p><strong>경고:</strong> LLM이 <code>working_directory</code> 파라미터를 지정하는 것을 허용하지 않을 것입니다. 우리는 이를 하드코딩할 예정입니다.</p><p><code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.Tool">types.Tool</a></code>을 사용하여 사용 가능한 함수들의 목록을 만드세요 (지금은 <code>get_files_info</code>만 추가하고, 나머지는 나중에 추가할 것입니다).</p><pre><code class="language-python">available_functions = types.Tool(
    function_declarations=[
        schema_get_files_info,
    ]
)
</code></pre><p><code>client.models.generate_content</code> 를 호출할 때 <code>tools</code> 파라미터에 <code>available_functions</code> 을 추가합니다.</p><pre><code class="language-python">config=types.GenerateContentConfig(
    tools=[available_functions], system_instruction=system_prompt
)</code></pre><p>시스템 프롬프트를 업데이트하여 LLM에게 함수 사용 방법을 지시하세요. 아래 내용을 복사해도 되지만, 어떤 역할을 하는지 이해하기 위해서 꼭 읽어보세요:</p><pre><code class="language-python">system_prompt = """
You are a helpful AI coding agent.

When a user asks a question or makes a request, make a function call plan. You can perform the following operations:

- List files and directories

All paths you provide should be relative to the working directory. You do not need to specify the working directory in your function calls as it is automatically injected for security reasons.
"""
</code></pre><p><code>generate_content</code> 응답에서 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse.text">.text</a></code> 속성만 출력하는 대신, <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse.function_calls">.function_calls</a></code> 속성도 확인하세요. LLM이 함수를 호출했다면, 함수 이름과 인자를 출력하세요:</p><pre><code class="language-python">f"Calling function: {function_call_part.name}({function_call_part.args})"
</code></pre><p>그렇지 않으면 평소처럼 텍스트를 출력하세요.</p><p>프로그램을 테스트하세요:</p><ul><li>"what files are in the root?" → <code>get_files_info({'directory': '.'})</code></li><li>"what files are in the pkg directory?" → <code>get_files_info({'directory': 'pkg'})</code></li></ul><h2 id="--14">추가 함수 선언</h2><p>이제 LLM이 <code>get_files_info</code> 함수에 대한 함수 호출을 지정할 수 있으므로, 다른 함수들도 호출할 수 있는 기능을 제공하겠습니다.</p><p><code>schema_get_files_info</code>에 사용한 것과 동일한 패턴을 따라 다음에 대한 함수 선언을 만듭니다:</p><ul><li><code>schema_get_file_content</code></li><li><code>schema_run_python_file</code></li><li><code>schema_write_file</code></li></ul><p><code>available_functions</code>를 업데이트하여 리스트에 모든 함수 선언을 포함시킵니다. 그런 다음 시스템 프롬프트를 업데이트합니다. 허용되는 작업이 다음뿐만 아니라:</p><pre><code>- 파일 및 디렉토리 나열</code></pre><p>네 가지 작업을 모두 포함하도록 업데이트합니다:</p><pre><code>- 파일 및 디렉토리 나열
- 파일 내용 읽기
- 선택적 인자로 Python 파일 실행
- 파일 쓰기 또는 덮어쓰기</code></pre><p>다양한 함수 호출을 불러올 것으로 예상되는 프롬프트를 테스트합니다. 예를 들어:</p><ul><li>"read the contents of main.py" → <code>get_file_content({'file_path': 'main.py'})</code></li><li>"write 'hello' to main.txt" → <code>write_file({'file_path': 'main.txt', 'content': 'hello'})</code></li><li>"run main.py" → <code>run_python_file({'file_path': 'main.py'})</code></li><li>"list the contents of the pkg directory" → <code>get_files_info({'directory': 'pkg'})</code></li></ul><p>여기서 LLM이 해야 할 일은 사용자의 요청에 따라 어떤 함수를 호출할지 선택하는 것뿐입니다. 나중에 실제로 함수를 호출하게 할 것입니다.</p><p>개인적인 구현 예제:</p><p><code>functions/get_file_content.py:</code></p><pre><code class="language-python">from google.genai import types
from config import MAX_CHARS

schema_get_file_content = types.FunctionDeclaration(
    name="get_file_content",
    description=f"작업 디렉토리 내의 지정된 파일에서 처음 {MAX_CHARS}자의 내용을 읽고 반환합니다.",
    parameters=types.Schema(
        type=types.Type.OBJECT,
        properties={
            "file_path": types.Schema(
                type=types.Type.STRING,
                description="작업 디렉토리에 상대적인, 내용을 읽을 파일의 경로.",
            ),
        },
        required=["file_path"],
    ),
)
</code></pre><p><code>functions/run_python.py:</code></p><pre><code class="language-python">from google.genai import types

schema_run_python_file = types.FunctionDeclaration(
    name="run_python_file",
    description="작업 디렉토리 내에서 Python 파일을 실행하고 인터프리터의 출력을 반환합니다.",
    parameters=types.Schema(
        type=types.Type.OBJECT,
        properties={
            "file_path": types.Schema(
                type=types.Type.STRING,
                description="작업 디렉토리에 상대적인, 실행할 Python 파일의 경로.",
            ),
            "args": types.Schema(
                type=types.Type.ARRAY,
                items=types.Schema(
                    type=types.Type.STRING,
                    description="Python 파일에 전달할 선택적 인자.",
                ),
                description="Python 파일에 전달할 선택적 인자.",
            ),
        },
        required=["file_path"],
    ),
)
</code></pre><p><code>functions/write_file_content.py:</code></p><pre><code class="language-python">from google.genai import types

schema_write_file = types.FunctionDeclaration(
    name="write_file",
    description="작업 디렉토리 내의 파일에 내용을 씁니다. 파일이 존재하지 않으면 생성합니다.",
    parameters=types.Schema(
        type=types.Type.OBJECT,
        properties={
            "file_path": types.Schema(
                type=types.Type.STRING,
                description="작업 디렉토리에 상대적인, 쓸 파일의 경로.",
            ),
            "content": types.Schema(
                type=types.Type.STRING,
                description="파일에 쓸 내용",
            ),
        },
        required=["file_path", "content"],
    ),
)
</code></pre><p><code>schema_get_files_info</code>에서 사용했던 동일한 패턴을 따라, 아래의 함수 선언들을 만들어주세요:</p><ul><li><code>schema_get_file_content</code></li><li><code>schema_run_python_file</code></li><li><code>schema_write_file</code></li></ul><p>그리고 <code>available_functions</code> 리스트에 모든 함수 선언을 포함하도록 업데이트하세요.<br>그 다음 시스템 프롬프트를 수정해야 합니다. 기존에는 허용된 동작이 아래 한 가지뿐이었습니다:</p><pre><code>- 파일 및 디렉토리 목록 나열</code></pre><p>이제 네 가지 모든 동작을 포함하도록 변경하세요:</p><pre><code>- 파일 및 디렉토리 목록 나열
- 파일 내용 읽기
- 선택적 인수를 포함해 파이썬 파일 실행
- 파일 작성 또는 덮어쓰기</code></pre><p>여러 함수 호출이 나오도록 예상되는 프롬프트로 테스트해보세요. 예를 들어:</p><ul><li>"main.py의 내용을 읽어줘" → <code>get_file_content({'file_path': 'main.py'})</code></li><li>"main.txt에 'hello'를 써줘" → <code>write_file({'file_path': 'main.txt', 'content': 'hello'})</code></li><li>"main.py를 실행해줘" → <code>run_python_file({'file_path': 'main.py'})</code></li><li>"pkg 디렉토리 목록을 보여줘" → <code>get_files_info({'directory': 'pkg'})</code></li></ul><p><strong>참고:</strong> 여기서 LLM이 해야 할 일은 사용자 요청을 기반으로 어떤 함수를 호출할지 선택하는 것입니다. 실제로 함수를 호출하는 코드는 나중에 구현합니다.</p><h2 id="--15">함수 호출</h2><p>이제 에이전트가 어떤 함수를 호출할지 선택할 수 있으므로, 실제로 함수를 호출할 시간입니다.</p><p>네 함수 중 하나를 호출하는 추상적인 작업을 처리할 새 함수를 만듭니다.</p><p>다음은 제가 정의한 함수입니다:</p><pre><code class="language-python">def call_function(function_call_part, verbose=False):
</code></pre><p><code>function_call_part</code>는 가장 중요하게 다음 내용을 가진 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.FunctionCall">types.FunctionCall</a></code>입니다:</p><ul><li><code>.name</code> 속성 (함수 이름, <code>문자열</code>)</li><li><code>.args</code> 속성 (함수에 전달하는 이름이 지정된 인자들의 딕셔너리)</li></ul><p><code>verbose</code>가 지정되면 함수 이름과 인자를 출력합니다:</p><pre><code class="language-python">print(f"Calling function: {function_call_part.name}({function_call_part.args})")
</code></pre><p>그렇지 않으면 이름만 출력합니다:</p><pre><code class="language-python">print(f" - Calling function: {function_call_part.name}")
</code></pre><p>함수 이름에 따라 실제로 함수를 호출하고 결과를 캡처합니다.</p><ul><li>키워드 인자 딕셔너리에 "working_directory" 인자를 수동으로 추가해야 합니다. 이 인자는 LLM이 제어하지 않으며, 작업 디렉토리는 <code>./calculator</code> 이어야 합니다.</li><li>딕셔너리를 함수에 <a href="https://docs.python.org/3/glossary.html#term-argument">키워드 인자</a>로 넘길 때는 <code>some_function(**some_args)</code> 문법을 사용합니다.</li></ul><p><strong>팁:</strong> 이를 위해 <code>함수 이름(문자열)</code> -&gt; <code>함수</code> 에 딕셔너리를 사용했습니다.</p><p>함수 이름이 유효하지 않으면 오류를 설명하는 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content">types.Content</a></code>를 반환합니다:</p><pre><code class="language-python">return types.Content(
    role="tool",
    parts=[
        types.Part.from_function_response(
            name=function_name,
            response={"error": f"Unknown function: {function_name}"},
        )
    ],
)
</code></pre><p>함수 호출 결과를 설명하는 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.Part.from_function_response">from_function_response</a></code>가 포함된 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content">types.Content</a></code>를 반환합니다:</p><pre><code class="language-python">return types.Content(
    role="tool",
    parts=[
        types.Part.from_function_response(
            name=function_name,
            response={"result": function_result},
        )
    ],
)
</code></pre><p>정보: <code>from_function_response</code>는 응답이 딕셔너리여야 하므로 결과 문자열을 "result" 필드에 넣어주면 됩니다. </p><p>아래는 완성된 <code>call_function.py</code>입니다:</p><pre><code class="language-python">from google.genai import types
from functions.get_files_info import get_files_info, schema_get_files_info
from functions.get_file_content import get_file_content, schema_get_file_content
from functions.run_python import run_python_file, schema_run_python_file
from functions.write_file_content import write_file, schema_write_file
from config import WORKING_DIR

available_functions = types.Tool(
    function_declarations=[
        schema_get_files_info,
        schema_get_file_content,
        schema_run_python_file,
        schema_write_file,
    ]
)

def call_function(function_call_part, verbose=False):
    if verbose:
        print(f" - Calling function: {function_call_part.name}({function_call_part.args})")
    else:
        print(f" - Calling function: {function_call_part.name}")
    
    function_map = {
        "get_files_info": get_files_info,
        "get_file_content": get_file_content,
        "run_python_file": run_python_file,
        "write_file": write_file,
    }
    
    function_name = function_call_part.name
    if function_name not in function_map:
        return types.Content(
            role="tool",
            parts=[
                types.Part.from_function_response(
                    name=function_name,
                    response={"error": f"Unknown function: {function_name}"},
                )
            ],
        )
    
    args = dict(function_call_part.args)
    args["working_directory"] = WORKING_DIR
    function_result = function_map[function_name](**args)
    
    return types.Content(
        role="tool",
        parts=[
            types.Part.from_function_response(
                name=function_name,
                response={"result": function_result},
            )
        ],
    )
</code></pre><p>모델의 <code>generate_content</code> 응답을 처리하는 곳으로 돌아가서, LLM이 호출하기로 결정한 함수의 이름을 단순히 출력하는 대신 <code>call_function</code>을 사용합니다.</p><ul><li><code>call_function</code>에서 반환하는 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content">types.Content</a></code>는 내부에 <code>.parts[0].function_response.response</code>가 있어야 합니다.</li><li>만약 포함되어 있지 않다면 어떤 종류의 치명적인 예외를 발생시키세요.</li><li>만약 포함되어 있고 <code>verbose</code>가 설정되어 있으면 함수 호출 결과를 다음과 같이 출력합니다:</li></ul><pre><code class="language-python">print(f"-&gt; {function_call_result.parts[0].function_response.response}")
</code></pre><p>프로그램을 테스트합니다. 이제 프롬프트로 요청하면 함수를 실행할 수 있어야 합니다. 다양한 프롬프트를 시도해보고 <code>--verbose</code> 플래그를 사용하여 모든 기능이 제대로 작동하는지 확인합니다.</p><ul><li>디렉토리 내용 나열</li><li>파일의 내용 가져오기</li><li>파일 내용 쓰기 (중요한 것을 덮어쓰지 말고 새 파일을 만드세요)</li><li>계산기 앱의 테스트 <code>tests.py</code> 실행</li></ul><h2 id="--16">에이전트 루프 만들기</h2><p>우리는 이제 함수 호출 기능을 어느 정도 구현했지만, 아직 우리 프로그램을 "에이전트"라고 부르기엔 부족한 점이 하나 있습니다:</p><p>피드백 루프가 없다는 것입니다.</p><p>AI 인플루언서들이 말하는 "에이전트"의 핵심은, 자신의 도구를 반복적으로 사용하여 결과를 계속 개선해나갈 수 있는 능력입니다. 그래서 우리는 다음 두 가지를 만들 것입니다:</p><ol><li>LLM을 반복해서 호출할 루프</li><li>"대화"에서 오고간 메시지 리스트. 다음과 같이 보일 것입니다:</li></ol><ul><li>사용자: "계산기의 버그를 수정해주세요"</li><li>모델: "get_files_info를 호출하고 싶습니다..."</li><li>툴: "get_files_info의 결과입니다..."</li><li>모델: "get_file_content를 호출하고 싶습니다..."</li><li>툴: "get_file_content의 결과입니다..."</li><li>모델: "run_python_file을 호출하고 싶습니다..."</li><li>툴: "run_python_file의 결과입니다..."</li><li>모델: "write_file을 호출하고 싶습니다..."</li><li>툴: "write_file의 결과입니다..."</li><li>모델: "run_python_file을 호출하고 싶습니다..."</li><li>툴: "run_python_file의 결과입니다..."</li><li>모델: "버그를 수정했고 계산기가 작동하는지 확인하기 위해 실행했습니다."</li></ul><p>이건 꽤 큰 진전입니다. 천천히 진행하세요!</p><p><code>prompts.py</code>를 만듭니다:</p><pre><code class="language-python">system_prompt = """
You are a helpful AI coding agent.

When a user asks a question or makes a request, make a function call plan. You can perform the following operations:
- List files and directories
- Read file contents
- Execute Python files with optional arguments
- Write or overwrite files

All paths you provide should be relative to the working directory. You do not need to specify the working directory in your function calls as it is automatically injected for security reasons.
"""
</code></pre><p>최종 <code>main.py</code> 구성:</p><pre><code class="language-python">import sys
import os
from google import genai
from google.genai import types
from dotenv import load_dotenv

from prompts import system_prompt
from call_function import call_function, available_functions

def main():
    load_dotenv()

    verbose = "--verbose" in sys.argv
    args = []
    for arg in sys.argv[1:]:
        if not arg.startswith("--"):
            args.append(arg)

    if not args:
        print("AI Code Assistant")
        print('\nUsage: python main.py "your prompt here" [--verbose]')
        print('Example: python main.py "How do I fix the calculator?"')
        sys.exit(1)

    api_key = os.environ.get("GEMINI_API_KEY")
    client = genai.Client(api_key=api_key)

    user_prompt = " ".join(args)

    if verbose:
        print(f"User prompt: {user_prompt}\n")

    messages = [
        types.Content(role="user", parts=[types.Part(text=user_prompt)]),
    ]

    generate_content_loop(client, messages, verbose)


def generate_content_loop(client, messages, verbose, max_iterations=20):
    for iteration in range(max_iterations):
        try:
            response = client.models.generate_content(
                model="gemini-2.0-flash-001",
                contents=messages,
                config=types.GenerateContentConfig(
                    tools=[available_functions], 
                    system_instruction=system_prompt
                ),
            )
            
            if verbose:
                print("Prompt tokens:", response.usage_metadata.prompt_token_count)
                print("Response tokens:", response.usage_metadata.candidates_token_count)

            # 모델 응답을 대화에 추가
            for candidate in response.candidates:
                messages.append(candidate.content)

            # 최종 텍스트 응답이 있는지 확인
            if response.text:
                print("Final response:")
                print(response.text)
                break

            # 함수 호출 처리
            if response.function_calls:
                function_responses = []
                for function_call_part in response.function_calls:
                    function_call_result = call_function(function_call_part, verbose)
                    if (not function_call_result.parts 
                        or not function_call_result.parts[0].function_response):
                        raise Exception("empty function call result")
                    if verbose:
                        print(f"-&gt; {function_call_result.parts[0].function_response.response}")
                    function_responses.append(function_call_result.parts[0])
                
                if function_responses:
                    messages.append(types.Content(role="user", parts=function_responses))
                else:
                    raise Exception("no function responses generated, exiting.")
        except Exception as e:
            print(f"Error: {e}")
            break
    else:
        print(f"최대 반복 횟수({max_iterations})에 도달했습니다. 에이전트가 작업을 완료하지 못했을 수 있습니다.")

if __name__ == "__main__":
    main()
</code></pre><p><code>generate_content</code> 함수에서 모든 도구 사용 결과를 처리하세요. 이미 일부 구현되어 있을 수도 있지만, 중요한 점은 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.models.Models.generate_content">client.models.generate_content</a></code>를 호출할 때마다 전체 <code>messages</code> 리스트를 전달해야 한다는 것입니다. 그래야 LLM이 현재 상태를 기반으로 "다음 단계"를 수행할 수 있습니다.</p><p>클라이언트의 <code>generate_content</code> 메서드를 호출한 후 응답의 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.GenerateContentResponse.candidates">.candidates</a></code> 속성을 확인합니다. 이 속성은 응답의 다양한 버전(일반적으로 한 개)을 담고 있습니다. "get_files_info를 호출하고 싶습니다..."와 같은 응답이 나오면, 이 내용을 대화에 추가해야 합니다. 각 <code>candidate</code>를 순회하며 그 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.Candidate.content">.content</a></code>를 <code>messages</code> 리스트에 추가하세요.</p><p>각 함수 호출 후에는 <code><a href="https://googleapis.github.io/python-genai/genai.html#genai.types.Content">types.Content</a></code>를 사용하여 <code>function_responses</code>를 역할이 <code>user</code>인 메시지로 변환하고, 이를 <code>messages</code>에 추가하세요.</p><p>그 다음에는 <code>generate_content</code>를 한 번만 호출하는 대신, 반복적으로 호출하는 루프를 만드세요. 이 루프는 최대 20회까지만 반복되도록 제한하세요 (에이전트가 무한 반복하지 않도록 방지). <code>try-except</code> 블록을 사용하여 오류를 적절히 처리하세요.</p><p>각 <code>generate_content</code> 호출 후에는 <code>response.text</code> 속성을 반환하는지 확인하세요. 만약 그렇다면 작업이 완료된 것이므로, 최종 응답을 출력하고 루프를 종료하세요. 그렇지 않으면 다시 반복하세요 (물론 최대 반복 횟수에 도달하지 않았다면).</p><p>코드를 꼭 테스트하세요 (당연하죠). "계산기가 결과를 콘솔에 어떻게 출력하는지 설명해줘" 처럼 간단한 프롬프트로 시작해보세요. 저는 아래와 같은 결과를 받았습니다:</p><pre><code class="language-bash">(aiagent) wagslane@MacBook-Pro-2 aiagent % uv run main.py "계산기가 결과를 콘솔에 어떻게 출력하는지 설명해줘"

함수 호출: get_files_info

함수 호출: get_file_content

최종 응답:
main.py 코드를 살펴본 결과, 계산기가 결과를 콘솔에 출력하는 방법은 다음과 같습니다:﻿

print(to_print): 출력의 핵심은 print() 함수로 이루어집니다.﻿

format_json_output(expression, result): 출력 전에 format_json_output 함수(pkg.render에서 import됨)를 사용해 계산 결과와 원래 식을 JSON 형태의 문자열로 포맷합니다. 이 형식화된 문자열은 to_print 변수에 저장됩니다.﻿

에러 처리:﻿ 코드에는 try...except 블록을 활용한 에러 처리 기능이 포함되어 있습니다. 만약 계산 중 오류가 발생하면(예: 잘못된 수식이 입력된 경우), print(f"Error: {e}")를 통해 오류 메시지가 콘솔에 출력됩니다.﻿

즉, 계산기는 식을 평가하고, 결과(그리고 원래 식)를 JSON 형태의 문자열로 만들어 콘솔에 출력합니다. 오류가 발생하면 콘솔에 오류 메시지도 따로 출력됩니다.﻿</code></pre><p><strong>팁:</strong> 원하는 대로 LLM이 동작하도록 하려면 시스템 프롬프트를 약간 조정해야 할 수도 있습니다. 이제 당신은 프롬프트 엔지니어입니다. 그처럼 행동하세요! </p><p>멋집니다! 이제 파일을 읽고, 파일을 작성하고, 파이썬 코드를 실행하며, 스스로 결과를 개선할 수 있는 기본적인 AI 에이전트를 완성했습니다. 이건 더 복잡한 AI 에이전트를 만드는 훌륭한 기반이에요.</p><h2 id="--17">결론</h2><p>필요한 모든 단계를 완료했습니다. 이제 약간 재미있게 (하지만 신중하게… LLM에 파일 시스템이나 파이썬 인터프리터 접근 권한을 줄 때는 매우 주의하세요) 다뤄보세요! 다음과 같은 작업들을 시도해볼 수도 있습니다: </p><ul><li>더 어려운 버그나 복잡한 문제 수정 </li><li>코드 일부를 리팩터링하기 </li><li>완전히 새로운 기능 추가</li></ul><p>또한 다음을 시도할 수 있습니다:</p><ul><li>다른 LLM 제공업체</li><li>다른 Gemini 모델</li><li>호출 가능한 기능을 더 추가해보기</li><li>다른 코드베이스 시도하기 (에이전트를 실행하기 전에 변경 사항을 커밋해두면 언제든 되돌릴 수 있습니다!)</li></ul><p><strong>경고:</strong> 우리가 구축한 것은 Cursor/Zed의 Agentic Mode 또는 Claude Code와 같은 것의 간단한 버전이라는 것을 기억하세요. 그 도구들조차 완벽하게 안전하지 않으므로 접근 권한을 부여하는 것에 주의하고 이 코드를 다른 사람에게 제공하지 마세요.</p><p>백엔드 및 데이터 엔지니어링에 대해 더 알고 싶다면, <a href="https://www.boot.dev/">Boot.dev</a>를 확인하세요! 학습 여정에 행운을 빕니다!</p><p>재미있게 보셨다면 <a href="https://x.com/wagslane">X.com</a>과 <a href="https://www.youtube.com/@bootdotdev">YouTube</a>에서 저를 팔로우해 주세요!</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 10주년을 맞이한 freeCodeCamp, 그리고 주요 자격증 업데이트 ]]>
                </title>
                <description>
                    <![CDATA[ 10년 전 오늘 샌프란시스코 아파트의 옷장 방에 앉아 freeCodeCamp의 첫 수백 줄의 코드를 작성했습니다. Quincy의 옷장 사무실당시만 해도 fCC가 코딩을 함께 배우는 전 세계 성인 커뮤니티로 빠르게 성장할 것이라고는 생각하지 못했습니다. 많은 캠퍼들 - 우리가 스스로를 부르는 방식 - 이 소프트웨어 엔지니어로 취업했고, 많은 캠퍼들이 fCC의 오픈 소스 코드에 기여했습니다. ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/freecodecamp-turns-10-major-certification-updates/</link>
                <guid isPermaLink="false">672157f9c2007e043e41eaf9</guid>
                
                    <category>
                        <![CDATA[ Community ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Alison Yoon ]]>
                </dc:creator>
                <pubDate>Thu, 31 Oct 2024 20:17:42 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2024/10/767f41f9-70e6-4e1c-b0d5-1927a925c9cc.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/freecodecamp-turns-10-major-curriculum-updates/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">freeCodeCamp Turns 10 + Major Certification Updates</a>
      </p><p>10년 전 오늘 샌프란시스코 아파트의 옷장 방에 앉아 freeCodeCamp의 첫 수백 줄의 코드를 작성했습니다.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730133437567/a29ae1ca-0350-4e94-b57a-296eb35dae6f.jpeg" class="kg-image" alt="Quincy's closet office" width="485" height="485" loading="lazy"><figcaption>Quincy의 옷장 사무실</figcaption></figure><p>당시만 해도 fCC가 코딩을 함께 배우는 전 세계 성인 커뮤니티로 빠르게 성장할 것이라고는 생각하지 못했습니다.</p><p>많은 캠퍼들 - 우리가 스스로를 부르는 방식 - 이 소프트웨어 엔지니어로 취업했고, 많은 캠퍼들이 fCC의 오픈 소스 코드에 기여했습니다.</p><figure class="kg-card kg-image-card"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730133483889/9fe0f108-361f-4384-bbb9-774b9bbe7fe2.webp" class="kg-image" alt="Campers at an NYC freeCodeCamp event" width="960" height="528" loading="lazy"></figure><p>지난 10년 동안 우리는 다음과 같은 것들을 만들어냈습니다.</p><ol><li>수학, 프로그래밍, 컴퓨터 공학을 가르치는 3,000시간 분량의 대화형 핵심 커리큘럼</li><li><a href="https://www.freecodecamp.org/korean/news/p/47b77841-e738-4d76-9c78-00c88ea1f907/freecodecamp.org/news">freecodecamp.org/news</a>에 12,000개 이상의 프로그래밍 튜토리얼과 수십 권의 전체 길이 책,</li><li>구독자 수 1,000만 명을 넘어선 무료 전체 강좌 YouTube 채널</li><li>세계에서 가장 친절하고 지지적인 프로그래밍 포럼, 그리고 유사한 Discord 서버</li></ol><p>혼자였던 팀도 35명의 교사와 엔지니어로 성장했습니다. 하지만 이는 빙산의 일각에 불과합니다. 대부분의 작업은 오픈 소스 커뮤니티 자체에 의해 수행되고 있습니다. 매일 자원봉사자들이 나타나 코드베이스를 개선하고, 새로운 오픈 러닝 리소스를 만들며, 포럼에서 서로를 돕고 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730133557095/089e4ec6-16aa-49df-81c3-6b27c3001ed4.jpeg" class="kg-image" alt="a screenshot of the freecodecamp github repo" width="607" height="515" loading="lazy"></figure><p>이제 우리는 방금 시작했을 뿐이며, freeCodeCamp의 <strong>첫 10년</strong>에 대한 감상적인 이야기보다는 <strong>향후 10년</strong> 계획에 초점을 맞추겠습니다.</p><p>결국 우리 자선단체의 목표는 수학, 프로그래밍, 컴퓨터 공학 학습을 위한 무료 자료를 제공하는 것이며, 이를 위해 해야 할 일이 아직 <strong>많이</strong> 남아 있습니다.</p><p>이 글에서는 향후 10년의 주요 계획과 크리스마스 전까지의 주요 개선사항을 소개하겠습니다.</p><h2 id="freecodecamp-">freeCodeCamp의 통합 인증 프로그램</h2><p>새로운 가장 큰 변화는 현재 다양한 과정으로 운영되던 커리큘럼을 종합적이고 포괄적인 '인증된 풀스택 개발자(CFSD)' 인증 과정으로 통합하는 것입니다. 그리고 이와 유사한 새로운 포괄적인 인증 과정도 처음부터 개발하고 있습니다.</p><p>우리는 CompTIA, (ISC)²와 같은 확립된 개발자 인증 비영리 단체에서 영감을 얻었습니다. 우리의 인증 과정을 산업 표준 인증과 더 유사하게 전면 개편하고 있습니다.</p><p>새로운 인증 과정은 다음과 같은 특징을 가지게 될 것입니다.</p><ul><li>더욱 포괄적일 것</li><li>최종 시험을 포함할 것</li><li>종합 프로젝트를 포함할 것</li><li>3년간 유효하며 갱신 가능할 것</li></ul><p>그리고 freeCodeCamp가 만드는 모든 것처럼, 이 인증 과정 역시 <strong>완전히 무료</strong>로 제공될 것입니다.</p><p>인증된 풀스택 개발자 인증 과정 커리큘럼에는 다음과 같은 내용이 포함될 것입니다.</p><ul><li>의미 있는 HTML</li><li>접근성</li><li>CSS 기본</li><li>CSS 플렉스박스</li><li>개발자를 위한 디자인</li><li>타이포그라피</li><li>코드 편집기</li><li>JavaScript 기본</li><li>함수형 프로그래밍</li><li>고차 함수와 콜백</li><li>DOM 조작과 이벤트</li><li>웹 표준</li><li>React 기본</li><li>TypeScript 기본</li><li>테스팅 개념</li><li>Bash 스크립팅	</li><li>SQL과 관계형 데이터베이스</li><li>Git &amp; GitHub</li><li>보안과 프라이버시</li><li>Node.js</li><li>Express.js</li><li>Python 기본</li><li>알고리즘적 사고</li><li>자료 구조</li><li>객체 지향 프로그래밍</li><li>동적 프로그래밍</li><li>웹 개발자를 위한 보안</li><li>툴링과 배포</li><li>API 다루기</li><li>AI 엔지니어링 기본</li><li>개발자 취업하기</li></ul><p>그리고 많은 사람들의 요청에 따라, 우리는 핵심 커리큘럼 내에서 <strong>훨씬 더 많은</strong> 이론을 다룰 것입니다.</p><p>이전에는 우리의 인증 과정이 100% 프로젝트 구축에 초점을 맞추었습니다. 하지만 많은 캠퍼들로부터 "배우면서 하기"에 지나치게 치우쳤다는 피드백을 받았고, 이론에 대한 더 많은 설명도 필요하다는 의견이 많았습니다.</p><p>이제 freeCodeCamp의 커리큘럼만으로도 필수적인 이론을 학습할 수 있게 될 것입니다.</p><p>우리의 강사 디자인팀, 즉 Beau Carnes와 Ania Kubów가 제작한 동영상 강의를 통해 이론을 다룰 것이며, 이는 자막과 함께 제공될 것입니다.</p><p>또한 핵심 개념과 코딩 기술에 대한 이해도를 높이기 위해 많은 퀴즈도 추가했습니다. 그리고 우리의 스페이스드 리피티션 시스템과 함께 활용될 것입니다.</p><p>그리고 학습자의 자율성을 높이기 위해 보다 이른 단계부터 "빈 화면" 스타일의 코딩 실습 과제에 돌입할 것입니다.</p><p>새로운 인증된 풀스택 개발자 과정에는 다음과 같은 내용이 포함됩니다.</p><ul><li><strong><strong>64개의 워크숍</strong></strong> – HTML부터 SQL, Python까지 다양한 도구를 다루는 대화형 코딩 과정</li><li><strong><strong>513 개의 강의</strong></strong> – 컴퓨터 과학 개념을 다루는 짧은 동영상과 복습용 퀴즈</li><li><strong><strong>83개의 실습</strong></strong> – 빈 편집기에서 테스트를 통과하는 프로젝트를 구현하는 과제</li><li><strong><strong>62개의 복습 페이지</strong></strong> – 각 모듈별 주요 토픽을 정리한 자료</li><li><strong><strong>66개의 퀴즈와</strong></strong> and <strong><strong>6개의 모의고사</strong></strong> – 풀스택 개발 개념과 도구에 대한 이해도 확인</li><li><strong><strong>1개의 종합 프로젝트</strong></strong> – freeCodeCamp 커뮤니티의 검토를 받는 포트폴리오용 대규모 프로젝트</li><li><strong><strong>1회의 최종 시험</strong></strong> – 모든 준비 과정을 포괄하는 90문항의 공식 인증 시험</li></ul><p>모든 과정을 완료하면 최종 시험을 치를 자격을 얻게 됩니다. 우리는 자체 개발한 오픈 소스 시험 환경에서 지금까지 배운 모든 내용을 심층적으로 평가하는 시험을 치르게 될 것입니다.</p><p>시험에 합격하지 못한 경우에도 24시간마다 재응시할 수 있습니다. 매번 광범위한 문항 데이터베이스에서 무작위로 시험이 출제될 것입니다.</p><p>온라인 시험에서 부정행위로 거짓 혐의를 받는다는 소문을 들어보셨을 것입니다. 우리는 학업 윤리 강령을 어떻게 집행할지 깊이 고민했습니다. 이런 결정에는 항상 사람의 개입이 있을 것입니다.</p><p>아기가 울어서 자리를 비웠다고 자동으로 자격을 박탈하지 않을 것입니다. 이런 상황들을 모두 고려하여 시험을 진행할 것입니다.</p><p>합격하면 무료로 검증된 공식 인증서를 받게 됩니다. 이를 이력서나 CV에 추가하거나 LinkedIn에 게시할 수 있습니다. 이 인증서는 3년간 유효하며, 그 후에는 무료 연수 과정을 거쳐 갱신할 수 있습니다.</p><p>freeCodeCamp 커리큘럼의 주요 업데이트 내용을 3분 동영상으로 안내드립니다.</p><figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.25%;" class="fluid-width-video-wrapper">
            <iframe width="560" height="315" src="https://www.youtube.com/embed/24ubTRtvNY0" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy" style="box-sizing: inherit; margin: 0px; padding: 0px; border: 0px; font-style: inherit; font-variant: inherit; font-weight: inherit; font-stretch: inherit; line-height: inherit; font-family: inherit; font-optical-sizing: inherit; font-size-adjust: inherit; font-kerning: inherit; font-feature-settings: inherit; font-variation-settings: inherit; font-size: 22px; vertical-align: middle; aspect-ratio: 16 / 9; width: 720px; height: auto;" name="fitvid0"></iframe>
          </div>
        </div>
      </figure><h2 id="-3-">우리는 또 다른 3가지 인증 과정도 개발하고 있습니다</h2><p>새로운 인증된 풀스택 개발자 인증 과정이 출시되면, 우리 강사 디자인팀은 추가로 3가지 인증 과정 개발에 주력할 것입니다.</p><h3 id="-fcc-cmle-">머신러닝 엔지니어 인증 과정 (fCC-CMLE)</h3><p>이 인증 과정에서는 Python과 다양한 라이브러리를 배우게 됩니다. 또한 방대한 수학 및 이론도 학습하게 될 것입니다. 많은 프로젝트에서 직접 모델을 만들어볼 기회가 주어질 것입니다. Kylie Ying이 이미 머신러닝 엔지니어들과 협력하여 이 과정 설계를 진행하고 있습니다. 2025년에 일부 내용이 출시될 예정입니다.</p><h3 id="-fcc-csse-">소프트웨어 시스템 엔지니어 인증 과정 (fCC-CSSE)</h3><p>이 인증 과정에서는 저수준 프로그래밍과 고성능 컴퓨팅을 다룰 것입니다. C와 C++를 배우고, 자신만의 컴파일러와 검색 엔진과 같은 프로젝트를 직접 구현해볼 기회가 있습니다. 2026년에 일부 내용이 출시될 예정입니다.</p><h3 id="-fcc-cds-">데이터 과학자 인증 과정 (fCC-CDS)</h3><p>우리는 수년 동안 종합적인 데이터 과학 커리큘럼을 개발해왔습니다. 여러분도 알다시피, freeCodeCamp 커뮤니티 YouTube 채널에는 이미 Python, R과 같은 데이터 분석 도구에 대한 많은 인기 강좌가 있습니다.</p><p>데이터 과학을 가르치는 과정에서 가장 큰 과제는 이 분야에 필요한 방대한 수학을 가르치는 것입니다. 또한 프로그래밍으로 자동 채점할 수 있는 프로젝트 기반 학습을 구축해야 합니다.</p><p>우리의 목표는 박사 과정 외에 데이터 과학 교육 중 최고의 교육을 제공하는 것입니다. 이것이 가장 큰 도전과제가 될 것입니다. 하지만 우리는 그것을 해낼 수 있습니다.</p><p>2027년에 일부 내용이 출시될 예정입니다.</p><h2 id="-">이 인증 과정과 관련된 자세한 정보는 언제 제공될까요?</h2><p>우리는 현재 이에 대한 종합적인 커리큘럼 개요를 작성하고 있으며, 일부 내용을 2025년에 공개할 계획입니다.</p><h2 id="-freecodecamp-">기존의 freeCodeCamp 인증 과정은 어떻게 되나요?</h2><p>freeCodeCamp has a long tradition of preserving old coursework from legacy certs. This time is no different.</p><p>우리는 여러분이 이번 크리스마스에 출시되는 새로운 인증된 풀스택 개발자 인증 과정으로 전환하기를 권장합니다. 기존 인증 과정에서 많은 프로젝트를 이미 완료하셨을 수도 있겠지요.</p><p>그렇기 때문에 이미 기존 인증 과정을 진행 중이라면 그 과정을 계속 이어가실 수 있습니다.</p><h2 id="--1">레거시 인증은 만료되나요?</h2><p>네, 그렇습니다. 모든 레거시 인증은 2027년 12월 31일에 만료됩니다. 이는 과거에 취득한 모든 freeCodeCamp 인증서를 포함합니다.</p><p>우리는 새로운 인증된 풀스택 개발자 인증을 취득하기 전에 기존 인증을 충분히 활용할 수 있는 시간을 주고자 합니다. 그리고 3년은 충분한 기간이라고 판단했습니다.</p><p>앞으로 취득하는 인증된 풀스택 개발자 인증이나 다른 3개의 새로운 인증도 취득일로부터 3년간 유효합니다.</p><p>또한 3년 후에는 무료 연수 프로그램을 통해 인증을 갱신할 수 있습니다.</p><h2 id="-freecodecamp--1">왜 freeCodeCamp에서 시험과 인증 만료일을 도입했나요?</h2><p>다른 IT 업계 인증과 유사하게 만들기 위해서입니다. 주요 인증들은 모두 인증 시험 합격을 요구하고 있으며, 3년 후 만료됩니다.</p><p>우리의 목표는 freeCodeCamp 인증이 업계에서 표준으로 인정받을 수 있도록 하는 것입니다.</p><h2 id="freecodecamp--1">freeCodeCamp가 취업 지원이나 알선 사업을 할 계획인가요?</h2><p>아니요, 그럴 계획이 없습니다. 우리 자선 단체의 목표는 단순히 무료 학습 자료를 제공하는 것입니다. 우리는 교사가 아닌 인력을 늘리기보다는 소규모 교사진을 유지하며 집중할 계획입니다.</p><h2 id="--2">각 인증 과정 간 교과과정 중복되는 부분은 얼마나 될까요?</h2><p>일부 인증 과정에서는 몇 가지 공통 프로젝트와 강의가 공유될 예정입니다. 예를 들어, 인증된 풀스택 개발자 인증 과정에서 Python 입문 과정을 이미 완료했다면, 인증된 머신러닝 엔지니어 인증 과정에서 이를 다시 수강할 필요가 없습니다.</p><h2 id="--3">기존 인증서는 온라인에 남아있나요? 검증 링크도 계속 작동하나요?</h2><p>만료된 인증서는 여전히 프로필에 표시되며, 직접 링크도 계속 작동할 것입니다. 다만 "만료됨 [날짜]" 라고 명확히 표시될 것입니다. 이는 2027년 12월 31일 까지는 어떤 fCC 인증서에도 적용되지 않습니다.</p><h2 id="freecodecamp-org-learn-11-"><strong><strong><a href="http://freecodecamp.org/learn">freecodecamp.org/learn</a></strong> </strong>에 현재 나열된 11개 인증 과정은 어떻게 되나요?<strong><strong>?</strong></strong></h2><p>이 인증 과정들은 최소 수년간 레거시 인증 과정으로 계속 제공될 것입니다. 그 내부 교과과정 대부분은 새로운 인증 과정의 일부로 재구성되고 개선될 것입니다.</p><h2 id="--4">기존에 완료한 프로젝트를 다시 해야 하나요?</h2><p>이전 인증 과정의 일부 교과과정은 새로운 인증 과정에도 남아있을 것입니다. 대부분의 경우 이미 완료한 프로젝트를 다시 할 필요는 없을 것입니다. 이미 프로젝트를 구현했다면, 새 인증 과정을 살펴볼 때 일부 섹션이 이미 완료된 것으로 표시될 가능성이 높습니다.</p><h2 id="rust-java-nosql-">Rust, Java 등의 프로그래밍 언어나 NoSQL 데이터베이스 도구들에 대한 강의는요? </h2><p>우리는 개발자로 취업할 수 있는 최선의 기회를 제공하기 위해 핵심 커리큘럼을 가장 인기 있는 도구 중심으로 유지하고 있습니다. 즉, Python, JavaScript, SQL, Git, Linux 등이 그것입니다.</p><p>freeCodeCamp 커뮤니티 YouTube 채널과 freeCodeCamp 출판물에는 다른 인기 도구에 대한 강좌가 많이 있습니다. 이들은 추가 선택 과목으로 계속 제공될 것입니다.</p><h2 id="--5">영어 커리큘럼은 어떻게 진행되고 있나요?</h2><p>캠퍼들의 영어 실력 향상을 돕기 위해 대화형 영어 커리큘럼을 만들기 위해 열심히 작업하고 있습니다.</p><p>동영상과 대화형 연습을 통해 가르치고, 정식 인증 시험이 뒤따릅니다.</p><figure class="kg-card kg-image-card"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730133715267/450940c6-2799-48ac-8f8d-dbc4eece8951.jpeg" class="kg-image" alt="A screenshot of freeCodeCamp's English curriculum User Interface" width="1274" height="792" loading="lazy"></figure><p>현재 A2 레벨을 마무리하고 있으며, B1 레벨 개발에 착수했습니다.</p><figure class="kg-card kg-image-card"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730133738714/206d46e8-7a31-4229-b673-5c11e386cfee.png" class="kg-image" alt="A chart of CEFR levels" width="1024" height="768" loading="lazy"></figure><h2 id="--6">무료 대학 학위 프로그램은 어떻게 진행되고 있나요?</h2><p>우리는 수학 준학사 학위와 컴퓨터 공학 학사 학위를 제공하는 데 꾸준히 진척을 이루고 있습니다. 이미 2개의 수학 과목을 개발했으며, 나머지 38개 대학 수준의 과목, 강의, 실습, 시험 개발에 힘쓰고 있습니다.</p><p>freeCodeCamp에는 소규모 강사 설계팀이 있으며, 현재는 주로 인증된 풀스택 개발자 인증 과정 교과과정과 영어 인증 프로그램에 집중하고 있습니다. 이 두 가지를 완료하면 학위 프로그램 개발에 더 많은 자원을 투입할 계획입니다.</p><h2 id="10-">10년은 그저 시작에 불과합니다.</h2><figure class="kg-card kg-image-card"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730133954038/aeb1874c-1804-4910-8088-c262aa95c5cf.jpeg" class="kg-image" alt="Some of the freeCodeCamp team in front of a Dallas public library" width="1600" height="1200" loading="lazy"></figure><p>우리 커뮤니티가 단 10년 만에 이루어낸 성과는 놀랍습니다.</p><ul><li>3,000시간 분량의 대화형 학습 자료로 이루어진 무료 커리큘럼</li><li>수천 개의 추가 선택 동영상 강좌, 서적, 튜토리얼</li><li>수만 명의 개발자로 취업한 캠퍼들</li><li>수만 명의 오픈소스와 동료 캠퍼 지원을 통해 경험을 쌓고 있는 기여자들</li></ul><p>이제 시작에 불과합니다.</p><p>전 세계 freeCodeCamp 커뮤니티에 참여해 보시기를 권장합니다. <a href="https://www.freecodecamp.org/news/freecodecamps-top-open-source-contributors-of-2024/">2024년 우수 오픈소스 기여자 목록</a>을 살펴보실 수 있습니다. 참여하고 싶으시다면 <a href="https://contribute.freecodecamp.org/">기여자 가이드</a>를 확인해 보세요.</p><p><a href="https://www.freecodecamp.org/donate">후원자가 되어주시기를 요청</a>드리며, 이미 매월 후원 중이시라면 연말 기부도 고려해 보시기 바랍니다. <a href="https://www.freecodecamp.org/news/how-to-donate-to-free-code-camp/">세금 공제 혜택이 가능한 다양한 방법</a>이 있습니다.</p><p>궁금하신 점이나 기부금 영수증이 필요하시다면 언제든 <a href="mailto:quincy@freecodecamp.org">quincy@freecodecamp.org</a>로 연락 주시기 바랍니다.</p><p>앞으로의 10년도 기대해 주시기 바랍니다.</p><p>– Quincy<br></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ React 서버 컴포넌트를 사용해야 하는 이유와 방법 ]]>
                </title>
                <description>
                    <![CDATA[ 2020년 말, React 팀은 "제로-번들-사이즈 React 서버 컴포넌트" 개념을 도입했습니다. 그 이후로 React 개발자 커뮤니티는 이 미래 지향적인 접근 방식을 실험하고 적용하는 방법을 학습해 왔습니다. React는 UI를 구축하는 방식에 대한 우리의 생각을 바꾸었습니다. 그리고 React 서버 컴포넌트를 사용하는 새로운 모델은 훨씬 더 구조화되고 편리하며, 유지 관리하기 쉽고 더 나은 사용자 ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/how-to-use-react-server-components/</link>
                <guid isPermaLink="false">6598f5974759f103f7f97680</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Soobin Bak ]]>
                </dc:creator>
                <pubDate>Mon, 08 Jan 2024 08:53:44 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2024/01/React-Server-Components-2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/how-to-use-react-server-components/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">React Server Components – How and Why You Should Use Them in Your Code</a>
      </p><p>2020년 말, React 팀은 "제로-번들-사이즈 React 서버 컴포넌트" 개념을 도입했습니다. 그 이후로 React 개발자 커뮤니티는 이 미래 지향적인 접근 방식을 실험하고 적용하는 방법을 학습해 왔습니다.</p><p>React는 UI를 구축하는 방식에 대한 우리의 생각을 바꾸었습니다. 그리고 React 서버 컴포넌트를 사용하는 새로운 모델은 훨씬 더 구조화되고 편리하며, 유지 관리하기 쉽고 더 나은 사용자 경험을 제공합니다.</p><p><code>Next.js</code>의 최신 릴리즈는 “서버 컴포넌트로 생각하기” 방향으로 나아갔습니다. 그리고 React 개발자로서 우리는 애플리케이션을 구축하는데 이 기술을 최대한 활용하기 위하여 새로운 사고 모델에 적응해야 합니다.</p><p>이 튜토리얼에서는 RSC(React Server Component)에 대해 배울 것입니다. React 서버 컴포넌트가 무엇인지, 어떻게 작동하는지, 그리고 더 중요하게는 이것이 어떤 문제를 해결하는지 중점적으로 알아볼 것입니다.</p><p>또한 왜 RSC가 필요한지 이해할 수 있도록 많은 예시를 보여줄 것입니다. 마지막으로 <code>React 서버 컴포넌트</code>와 비슷해 보이지만 다른 기능인 <code>서버 사이드 렌더링(SSR)</code> 간의 차이점을 배울 것입니다.</p><p>React를 처음 접하는 경우, React 서버 컴포넌트에 대해 학습하기 전에, 컴포넌트 아키텍처, 상태(state), 속성(props)을 이용한 데이터 전달, 가상 돔(Virtual DOM) 트리에 대한 기본 지식이 필요합니다.</p><p>또한 이 글을 읽은 다음 freeCodeCamp에서 <a href="https://www.freecodecamp.org/news/react-fundamentals-for-beginners/">전체 로드맵을 참고</a>하여 React 기초를 확고히 다질 수도 있습니다.</p><p>준비가 되었나요? 시작해봅시다.</p><p>만약 비디오 콘텐츠를 통해 학습하고 싶다면, 이 글을 비디오 튜토리얼로도 볼 수 있습니다. 🙂</p><figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.49999999999999%;" class="fluid-width-video-wrapper">
            <iframe width="200" height="113" src="https://www.youtube.com/embed/5DZvdoMogys?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="React Server Components Made Easy(With Examples and Demo)" name="fitvid0"></iframe>
          </div>
        </div>
      </figure><h2 id="-ui-react">클라이언트 사이드 UI 라이브러리로서의 React</h2><p>React는 처음부터 클라이언트 사이드 UI 라이브러리였습니다. 웹 및 모바일 개발자가 컴포넌트 기반 아키텍처를 활용하여 애플리케이션을 개발하는 데 도움이 되는 JavaScript 기반의 오픈 소스 라이브러리입니다.<br>React 철학은 우리가 디자인 전체를 더 작고 독립적인 부분인 <code>컴포넌트</code>라고 불리는 작은 조각으로 나누는 것을 제안합니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-169.png" class="kg-image" alt="image-169" width="600" height="400" loading="lazy"></figure><p>하나의 컴포넌트가 여러 개의 하위 컴포넌트로 분해되는 것을 보여주는 이미지입니다.</p><p>컴포넌트는 <code>state</code>라고 불리는 자체 비공개 데이터와 서로 다른 컴포넌트 간에 데이터를 전달하는 방법인 &nbsp;<code>props</code>를 가질 수 있습니다. 컴포넌트들을 컴포넌트 계층 구조로 나누고, 상태를 정의하고, 상태를 변경하여 발생되는 효과(effect)를 관리하고, 데이터 흐름(flow)을 결정합니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-170.png" class="kg-image" alt="image-170" width="600" height="400" loading="lazy"></figure><p><em>state와 props의 작동 방식을 보여주는 이미지</em></p><p>전통적으로 이러한 모든 컴포넌트들은 자바스크립트의 함수입니다(여기서는 함수형 컴포넌트에 대해서만 이야기하고, 과거에 사용했던 클래스형 컴포넌트는 따로 이야기 하지 않겠습니다). 앱이 브라우저에서 로드될 때, 컴포넌트 코드를 다운로드하고 이를 사용하여 앱이 작동하도록 만듭니다.</p><p>여전히 ‘컴포넌트’라는 용어를 사용할 것입니다. 하지만 이 문서는 React 서버 컴포넌트의 개념을 소개하므로, 이러한 전통적인 컴포넌트를 <code>클라이언트 컴포넌트</code> 라고 부르겠습니다(클라이언트/브라우저에서 다운로드되고 React가 마법을 부려 렌더링하는 컴포넌트).</p><h2 id="react-">React 앱의 일반적인 문제</h2><p>React 클라이언트 컴포넌트는 훌륭하며 특정 사례를 해결하는 데 효과적입니다. 하지만 React 앱을 구축할 때, 패턴을 조금 다르게 고려해 볼 필요가 있습니다. 이는 다음과 같은 사항을 고려해야 하기 때문입니다.</p><ul><li><code>사용자 경험(User Experience)</code>: 우리는 사용자와 고객을 위한 소프트웨어 제품을 만듭니다. 앱이 성공하길 원한다면, 애플리케이션의 사용자 경험이 중요합니다.</li><li><code>유지 보수(Maintainability)</code>: 프로젝트 코드는 여러 개발팀을 거쳐 수년간 원활하게 유지 보수 되어야 합니다.</li><li><code>성능 비용(Performance Cost)</code>: 애플리케이션이 느려져서는 안 되며, 우리의 디자인(설계) 접근 방식이 느리게 만들어서도 안됩니다.</li></ul><p>이제 일상적으로 마주할 수 있는 몇 가지 예를 살펴보겠습니다. 또한 React를 사용하여 일상적인 웹 개발에서 각 핵심 사항을 어떻게 구현하고 설계할 수 있는지 이해할 수 있을 것입니다.</p><h3 id="-">레이아웃 이동 문제</h3><p>매우 흔한 UX 문제 중 하나는 컴포넌트가 렌더링될 때 레이아웃이 갑자기 이동되는 현상입니다. 아래의 코드 스니펫을 살펴봅시다.</p><pre><code class="language-jsx">&lt;CourseWraper&gt;
  &lt;CourseList /&gt;
  &lt;Testimonials /&gt;
&lt;/CourseWraper&gt;
</code></pre><p>다음은 익숙한 JSX 코드로, <code>CourseWrapper</code> 컴포넌트와 <code>CourseList</code> 및 <code>Testimonials</code> 두 개의 하위 컴포넌트가 있습니다. <code>CourseList</code> 와 <code>Testimonials</code> 두 컴포넌트 모두 데이터를 가져오기 위해 네트워크 호출(API 호출)을 수행한다고 가정해 봅시다.</p><p>다음은 <code>CourseList</code> 컴포넌트 입니다.</p><pre><code class="language-jsx">function CourseList() {

    // 실제 네트워크 호출을 가정하면,
    // useEffect를 사용하여 처리할 것입니다.
    const info = fetchCourseList();

    return(
      &lt;&gt; &lt;/&gt;)
}
</code></pre><p>그리고 <code>Testimonial</code> 컴포넌트 입니다.</p><pre><code class="language-jsx">function Testimonials() {

    // 실제 네트워크 호출을 가정하면,
    // useEffect를 사용하여 처리할 것입니다.
    const info = fetchTestimonials();

    return(
      &lt;&gt; &lt;/&gt;
    )
}
</code></pre><p>이러한 컴포넌트들이 네트워크 호출을 수행하는 경우, 응답이 돌아오는 순서에 대한 보장이 없습니다. 이는 네트워크 속도, 지연 시간 및 여러 다른 요인에 따라 달라집니다.<br><code>Testimonials</code> 컴포넌트의 네트워크 호출이 <code>CourseList</code> 컴포넌트보다 먼저 완료되는 상황에서, <code>Testimonials</code> 컴포넌트가 먼저 렌더링되고 그 다음에 <code>CourseList</code> 컴포넌트가 렌더링됩니다. 이로 인해 <code>Testimonials</code> 컴포넌트가 적합한 위치로 이동됩니다. 아래에서 제가 하는 말이 무엇인지 확인할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2023/07/layoutshift-1.gif" class="kg-image" alt="layoutshift-1" width="600" height="400" loading="lazy"></figure><p><em>레이아웃 이동의 UX 문제를 보여주는 슬로우 모션</em></p><p>로딩 인디케이터나 깜빡임 효과를 통해서 사용자에게 잠시 후를 기대하도록 알려주어 UX를 좀 더 향상시킬 수 있습니다(그러나 언제가 될지는 확신하지 못합니다).</p><h3 id="-waterfall-">네트워크 워터폴(waterfall) 문제</h3><p>또 다른 전형적인 사용자 경험 문제에 대해 이야기해 봅시다. 지난 예제와 유사한 React 컴포넌트를 상상해 보세요.</p><pre><code class="language-jsx">function Course() {
  return (
    &lt;CourseWraper&gt;
      &lt;CourseList /&gt;
      &lt;Testimonials /&gt;   
    &lt;/CourseWraper&gt;
  )
}
</code></pre><p><em>두 개의 하위 컴포넌트를 가진 CourseWrapper 컴포넌트</em></p><p>여기서 조금 수정을 해보겠습니다. <code>CourseList</code> 과 <code>Testimonials</code> 컴포넌트와 함께 이제 <code>CourseWrapper</code>도 네트워크 호출을 수행합니다.</p><pre><code class="language-jsx">function CourseWrapper() {

    // 실제 네트워크 호출을 가정하면,
    // useEffect를 사용하여 처리할 것입니다.
    const info = fetchWrapperInfo();
    
    return(
      &lt;&gt; &lt;/&gt;
    )
}
</code></pre><p><em>CourseWrapper 컴포넌트 - 네트워크 호출 수행</em></p><p>따라서 부모 컴포넌트는 데이터를 가져오기 위해 네트워크 호출을 수행하고, 그 하위 컴포넌트들 또한 네트워크 호출을 수행합니다.</p><p>흥미로운 점은, 부모 컴포넌트는 네트워크 호출이 완료될 때까지 렌더링되지 않는다는 것입니다. 따라서 자식 컴포넌트들의 렌더링도 홀딩 합니다.</p><p>현재 작업을 시작하기 위해 이전 것의 응답이 완료되기를 기다리는 현상은 <code>워터폴(Waterfall)</code>라고 알려져 있습니다. 이 경우에는 네트워크 워터폴 및 컴포넌트 렌더링 워터폴 문제 모두 발생합니다.</p><p>이제 여러분은 각 컴포넌트에서의 모든 네트워크 호출을 제거하고 각각의 컴포넌트가 응답을 기다리지 않도록 부모 컴포넌트에서 단일 호출 하도록 네트워크 호출 로직을 끌어 올릴 수 있다고 생각할 수 있습니다. 이것은 현명한 생각이지만 유지 보수 측면에서 문제가 발생할 수 있습니다. 다음 섹션에서 이에 대해 더 자세히 알아보겠습니다.</p><h3 id="--1">유지 보수 관련 문제</h3><p>서버 측 상호 작용과 관련된 몇 가지 사용자 경험 문제를 살펴보았으므로, 이제 유지 보수 관련 문제를 고려해 봅시다.</p><p>모든 컴포넌트가 네트워크 호출을 수행하지 않는다고 가정해 봅시다. 단일 API 호출인 <code>fetchAllDetails()</code>를 사용하여 (부모 컴포넌트를 포함하여) 모든 컴포넌트의 세부 정보를 한 번에 가져옵니다.</p><p>그런 다음 필요한 정보를 각 컴포넌트에 props로 전달합니다. 이것은 위에서 본 "워터폴" 문제보다 더 나은 접근 방법이겠지요?</p><pre><code class="language-jsx">function Course() {
	
    // 실제 네트워크 호출을 가정하면,
    // useEffect를 사용하여 처리할 것입니다. 
    const info = fetchAllDetails();
    
    return(
    	&lt;CourseWrapper
        	ino={info.wrapperInfo} &gt;
            &lt;CourseList
        		ino={info.listInfo} /&gt;
            &lt;Testimonials
        		ino={info.testimonials} /&gt;
        &lt;/CourseWrapper&gt;     
    )
 }
</code></pre><p><em>Course 컴포넌트 - 최상위 API 호출 수행</em></p><p>그러나 이것은 일부 유지 보수 문제를 야기할 수 있습니다.</p><p>어느 화창한 날, 제품이 Testimonials 기능을 삭제하기로 결정했다고 가정해 봅시다. 우리는 위의 코드에서 Testimonials 컴포넌트를 간단히 삭제할 수 있습니다. 그런데, <code>fetchAllDetails()</code> 호출을 통해 가져온 데이터를 정리하는 것을 잊을 수 있습니다. 사용되지 않고 불필요한 상태로 남아 있을 수 있습니다.</p><p>이러한 문제를 완화하기 위해, 가능한 사용자 경험 문제를 설명하면서 이전 섹션에서 이미 논의한 방식으로 코드를 변경할 수 있을 것입니다. 따라서 우리는 더 나은 해결책을 찾아야 합니다. 하지만 그에 앞서, <code>성능 비용</code>이라는 또 하나의 고려 사항에 대해 먼저 이야기해 보겠습니다.</p><h3 id="--2">성능 비용</h3><p>우리가 논의할 마지막 문제 영역은 성능 비용입니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-171.png" class="kg-image" alt="image-171" width="600" height="400" loading="lazy"></figure><p><em>인터넷에서 발견한 재미있는 밈 - 자바스크립트가 클라이언트에게 주는 무거움을 묘사</em></p><p>전통적으로, React 컴포넌트는 클라이언트 사이드 자바스크립트 함수입니다. 이는 React 애플리케이션의 구성 요소입니다. 클라이언트에서 애플리케이션을 로드할 때, 컴포넌트가 클라이언트에 다운로드되고 React가 그것들을 렌더링하는데 필요한 작업을 수행합니다.</p><p>그러나 이로 인해 두 가지 중요한 문제가 발생합니다.</p><p>먼저, 사용자가 요청을 보낼 때, 앱은 HTML과 연결된 JavaScript, CSS 및 이미지와 같은 다른 에셋을 다운로드합니다.</p><p>클라이언트 사이드(브라우저에서)에서 React는 마법을 부리기 시작하고 HTML 구조를 하이드레이트(hydrate) 시킵니다. 이것은 HTML을 구문 분석하고, 이벤트 리스너를 DOM에 연결하며 스토어에서 데이터를 가져옵니다. 따라서 사이트는 완전히 작동하는 React 앱이 됩니다.</p><p>그러나 중요한 점은 클라이언트에서 많은 일이 일어나고 있다는 것입니다. 결국 이 코드를 모두 클라이언트에서 다운로드하게 됩니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-182.png" class="kg-image" alt="image-182" width="600" height="400" loading="lazy"></figure><p><em>브라우저에 다운로드된 스크립트의 양</em></p><p>대부분의 경우, 프로젝트에 종속성이 있는 외부 라이브러리(Node 모듈)가 필요합니다. 이러한 모든 종속성은 클라이언트 사이드에서 다운로드되어 더욱 무거워집니다.</p><p>이제 문제점을 이해했으므로, <code>React 서버 컴포넌트</code>가 제공하는 기능과 이러한 문제를 해결하는 방법에 대해서 확실히 이해할 수 있을 것입니다.</p><p>그러나 이에 대해 이야기하기 전에, 클라이언트와 서버에 대해 조금 더 알아보겠습니다.</p><h2 id="--3">클라이언트-서버 모델</h2><p>이 글에서 클라이언트와 서버 라는 용어를 여러 번 사용했습니다. 이제 이들에 대한 공식적인 정의를 내리고 높은 수준에서 그들의 관계를 설명해보겠습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/size/w1600/2023/07/image-175.png" class="kg-image" alt="image-175" width="600" height="400" loading="lazy"></figure><p><em>클라이언트와 서버의 관계를 보여주는 다이어그램</em></p><ul><li><code>클라이언트</code>: 애플리케이션의 클라이언트는 최종 사용자 측에서 작업을 실행하는 시스템입니다. 클라이언트의 예로는 PC, 랩탑, 모바일, 브라우저 등이 있습니다.</li><li><code>서버</code>: 이름에서 알 수 있듯이, 서버는 클라이언트에 서비스를 제공합니다. 빠른 데이터 액세스를 위해 데이터 저장소나 데이터베이스와 공존할 수 있습니다.</li><li><code>요청</code>: 요청은 클라이언트가 서버로부터 서비스를 요청하기 위해 사용하는 통신 모드입니다.</li><li><code>응답</code>: 응답은 또한 서버가 서비스(데이터/정보)를 클라이언트에게 다시 보내기 위해 사용하는 통신 모드입니다.</li></ul><h2 id="react--1">React 클라이언트 컴포넌트</h2><p>위에서 언급한대로, 전통적으로 React 컴포넌트는 클라이언트 사이드에서 실행됩니다. 서버와 상호 작용할 때, 요청을 보내고 응답이 돌아올 때까지 기다립니다. 응답을 받으면 클라이언트는 다음 작업을 트리거합니다.</p><p>요청한 서비스가 성공적으로 완료되면 클라이언트 컴포넌트는 UI에 맞게 작동하고 성공 메시지를 표시합니다. 오류가 발생한 경우 클라이언트 컴포넌트는 사용자에게 오류를 보고합니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/size/w1600/2023/07/image-176.png" class="kg-image" alt="image-176" width="600" height="400" loading="lazy"></figure><p><em>클라이언트 서버 모델에서의 React 클라이언트 컴포넌트</em></p><p>네트워크 워터폴를 발생시킬 때 클라이언트 컴포넌트의 응답이 지연되어 사용자 경험이 저하됩니다. 그렇다면 이를 어떻게 완화할 수 있을까요?</p><h2 id="react-rscs-">React 서버 컴포넌트(RSCs)는 어떻게 도움을 줄까요?</h2><p>React 컴포넌트를 서버로 옮겨보는 것은 어떨까요? 그리고 아마도 데이터 저장소와 동일한 곳에 위치시키고... 그런데 이것이 실제로 가능한 일일까요?<br>네! 이제 <code>React Server Components</code>에 대해 알아보겠습니다. 이러한 새로운 컴포넌트는 서버에 있으므로 데이터를 더 빨리 가져올 수 있습니다. 네트워크를 통해 왕복하지 않고도 파일 시스템 및 데이터 저장소와 같은 서버 인프라에 액세스할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/size/w1600/2023/07/image-177.png" class="kg-image" alt="image-177" width="600" height="400" loading="lazy"></figure><p><em>클라이언트 서버 모델에서의 React 서버 컴포넌트</em></p><p>이제 서버 컴포넌트 측면에서 생각해야 하기 때문에, React 개발자에게는 완전한 패러다임 전환입니다.</p><p>RSC를 사용하면, 데이터 가져오는 로직을 서버로 이동하여(네트워크 호출 없이 데이터를 가져오도록) 서버 자체에서 준비할 수 있습니다. 클라이언트로 돌아오는 데이터는 모든 데이터와 함게 잘 구성된 컴포넌트 입니다. 얼마나 놀라운 일인가요?</p><p>이는 React 서버 컴포넌트를 사용하면 다음과 같은 코드를 작성할 수 있음을 의미합니다.</p><pre><code class="language-jsx">import { dbConnect } from '@/services/mongo'

import { addCourseToDB } from './actions/add-course'

import CourseList from './components/CourseList'

export default async function Home() {

  // MongoDB 연결
  await dbConnect();
  
  // db의 모델을 이용하여 모든 course를 얻는다.
  const allCourses = await courses.find();
  
  // 서버 쪽에서 콘솔이 찍힌다.
  console.log({allCourses})

  return (
    &lt;main&gt;
      &lt;div&gt;
        &lt;CourseList allCourses={allCourses} /&gt;  
      &lt;/div&gt;
    &lt;/main&gt;
  )
}
</code></pre><p><em>React 서버 컴포넌트의 예</em></p><p>저것 보세요! 몇 가지 변경 사항을 즉시 알아챌 수 있습니다.</p><ul><li>이 컴포넌트는 비동기 호출을 처리하므로 <code>async</code> 타입 입니다.</li><li>우리는 컴포넌트 자체에서 데이터베이스(MongoDB)에 연결합니다. 와우! 보통 이러한 코드는 <code>Node.js</code> 또는 <code>Express</code>에서 볼 수 있습니다, 맞나요?</li><li>그런 다음 데이터베이스를 쿼리하고 렌더링을 위해 JSX에 전달할 데이터를 가져옵니다.</li></ul><p>콘솔 로그는 브라우저 콘솔이 아닌 서버 콘솔에서 찍히는 것에 주목하세요.</p><p>또한 상태 관리 (useState)와 이펙트 관리 (useEffect)를 완전히 제거했습니다. 깔끔하고 간단합니다.</p><p>React 서버 컴포넌트를 사용하면, useEffect를 사용할 필요가 없을 수도 있습니다(영원히!).</p><h3 id="react--2">React 서버 컴포넌트의 한계</h3><p>이러한 이점도 갖고 있지만, 기억해야 할 RSC의 제한 사항도 있습니다.</p><ul><li>RSC는 서버에 남아 있고 서버에서 렌더링됩니다. 클라이언트 사이드과 관련된 것이 없습니다. 이것은 서버 컴포넌트에 사용자 인터렉션을 추가할 수 없음을 의미합니다. 예를 들어, 이벤트 핸들러나 useState, useReducer, useEffect와 같은 React 훅을 서버 컴포넌트에서 사용할 수 없습니다.</li><li>서버 컴포넌트에서 localstorage, bluetooth, web USB와 같은 브라우저 웹 API를 사용할 수 없습니다.</li><li>클라이언트 상호 작용과 관련된 모든 것에 대해서는, 계속 클라이언트 컴포넌트를 사용해야 합니다.</li></ul><p>이해가 되시나요? 그렇다면 어떻게 애플리케이션에 맞게 컴포넌트를 가장 잘 정리할 수 있을까요?</p><h3 id="--4">클라이언트와 서버 컴포넌트를 함께 사용하는 방법</h3><p>당신의 앱은 서버 및 클라이언트 컴포넌트의 조합일 수 있습니다. 곧 예제를 보겠지만, 먼저 개념을 이해해 봅시다.</p><p>서버 컴포넌트는 클라이언트 컴포넌트를 가져와 렌더링할 수 있지만, 클라이언트 컴포넌트는 내부에서 서버 컴포넌트를 렌더링할 수 없습니다. 클라이언트 컴포넌트에서 서버 컴포넌트를 사용하려면 props로 전달하여 사용할 수 있습니다.</p><p>컴포넌트 계층 구조의 루트에 서버 컴포넌트를 두고 컴포넌트 트리의 말단으로 클라이언트 컴포넌트를 밀어 넣는 것이 좋습니다.</p><p>서버 컴포넌트의 최상단에서 데이터 페칭이 일어날 수 있으며, React가 허용하는 방식대로 전달할 수 있습니다. 사용자 상호 작용(이벤트 핸들러) 및 브라우저 API에 액세스는 말단의 클라이언트 컴포넌트에서 처리할 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-186.png" class="kg-image" alt="image-186" width="600" height="400" loading="lazy"></figure><p><em>서버와 클라이언트 컴포넌트를 포함하는 컴포넌트 트리</em></p><h3 id="-rsc-ssr-">잠깐, RSC는 서버 사이드 렌더링(SSR)과 동일하지 않나요?</h3><p>아니요, 그렇지 않습니다. RSC와 SSR 둘 다 이름에 "서버"라는 단어가 들어가 있지만 비슷한 부분은 이 뿐입니다.</p><p>서버 사이드 렌더링(SSR)에서는, 서버에서 날것의 HTML을 클라이언트로 보내고, 그런 다음 모든 클라이언트 사이드 자바스크립트가 다운로드됩니다. React는 HTML을 상호작용 가능한 React 컴포넌트로 변환하기 위해 하이드레이션(hydration) 프로세스를 시작합니다. SSR에서 컴포넌트는 서버에 머무르지 않습니다.</p><p>지금까지 React 서버 컴포넌트를 통해 컴포넌트가 서버에 남아 있고 네트워크 왕복을 거치지 않고 서버 인프라에 액세스할 수 있음을 알게 되었습니다.</p><p>SSR은 애플리케이션의 초기 페이지를 더 빠르게 로드하는 데 유용합니다. 앞으로 SSR과 RSC를 문제없이 함께 사용할 수 있습니다.</p><h2 id="next-js-rsc-mongodb-">Next.js(RSC 포함)와 MongoDB를 사용하여 강의 목록 페이지를 구축하는 방법</h2><p>이제 React 서버 컴포넌트를 사용하는 응용 프로그램을 만들어 보겠습니다. Next.js는 최근 릴리스에서 RSC를 채택한 주요 웹 프레임워크입니다.</p><p>그래서 우리는 이제 Next.js에서 서버 컴포넌트를 만드는 방법과 클라이언트 컴포넌트를 만드는 방법이 어떻게 다른지를 보여주기 위해 강의 목록 페이지를 만들 것입니다.</p><p>여기서는 Next.js나 MongoDB를 자세히 배우지 않을 것입니다. React 서버 컴포넌트가 어떻게 동작하는지 그리고 클라이언트 컴포넌트와 어떻게 다른지 알려주기 위한 예로 이 애플리케이션을 사용하고 있습니다.</p><p>먼저 데이터 저장소에 강의 데이터를 추가해 보겠습니다. 이 앱에서는 MongoDB를 사용했습니다. 아래 이미지는 세 개의 강의에 대해 세 개의 문서가 추가된 것을 보여줍니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/size/w1600/2023/07/image-178.png" class="kg-image" alt="image-178" width="600" height="400" loading="lazy"></figure><p><em>Mongo Compass - 강좌 모음</em></p><p>다음으로, MongoDB에 연결하는 유틸리티 함수를 만들겠습니다. 이것은 Mongoose와 MongoDB URI를 사용하여 자바스크립트 기반 프로젝트에서 MongoDB에 연결하는 데 사용할 수 있는 일반적인 코드입니다.</p><pre><code class="language-jsx">import mongoose from "mongoose";

export async function dbConnect(): Promise&lt;any&gt; {
  try {
    const conn = await mongoose.connect(String(process.env.MONGO_DB_URI));
    console.log(`Database connected : ${conn.connection.host}`);
    return conn;
  } catch (err) {
    console.error(err);
  }
}
</code></pre><p><em>MongoDB 연결을 위한 코드 스니펫</em></p><p>이제 MongoDB 문서와 매핑하는 모델을 만들어야 합니다. 여기서는 강의 데이터를 다루고 있으므로, 이에 해당하는 모델은 다음과 같습니다.</p><pre><code class="language-jsx">import mongoose, { Schema } from "mongoose";

const schema = new Schema({
  name: {
      required: true,
      type: String
  },
  description: {
      required: true,
      type: String
  },
  cover: {
    required: true,
    type: String
  },
  rating: {
    required: true,
    type: Number
  },
  price: {
    required: true,
    type: Number
  },
  createdOn: {
    type: { type: Date, default: Date.now }
  },
  link: {
    required: true,
    type: String
  },
  type: {
    required: true,
    type: String
  },
  comments: {
    required: false,
    type: [{ body: String, date: Date }]
  }
});

export const courses = mongoose.models.course ?? mongoose.model("course", schema);
</code></pre><p><em>Mongoose를 사용한 강의 모델</em></p><p>이제 마법이 시작됩니다! Next.js 앱 라우터를 사용하면 모든 컴포넌트가 기본적으로 서버 컴포넌트입니다. 이것은 서버 근처에 위치하고 서버 생태계에 액세스할 수 있음을 의미합니다.</p><p>아래 코드는 일반적인 Next.js 컴포넌트지만 특별한 기능이 있습니다. 이 컴포넌트에서 데이터베이스 연결을 직접 하여 어떤 state와 effect 관리를 하지 않고도 데이터를 직접 쿼리할 수 있습니다. 멋지죠?</p><p>이 컴포넌트에서 기록하는 모든 내용은 브라우저 콘솔에 찍히지 않습니다. 왜냐하면 이것은 서버 컴포넌트이기 때문입니다. 로그는 서버 콘솔에서 확인할 수 있습니다(아마도 <code>yarn dev</code> 명령을 사용하여 서버를 시작한 터미널에서).</p><p>데이터베이스와의 상호 작용은 비동기적 이므로, 호출 시 <code>await</code> 키워드를 사용하고 컴포넌트에는 <code>async</code> 키워드를 사용합니다. 응답을 받으면 이를 자식 컴포넌트에 props로 전달합니다.</p><pre><code class="language-jsx">import { dbConnect } from '@/services/mongo'
import { courses } from '@/models/courseModel'
import { addCourseToDB } from './actions/add-course'

import AddCourse from './components/AddCourse'
import CourseList from './components/CourseList'

export default async function Home() {

  // MongoDB 연결
  await dbConnect();
  
  // 모델을 이용하여 db로 부터 모든 강의를 가져옴.
  const allCourses = await courses.find().select(
  						["name", "cover", "rating"]);
  
  // 서버 콘솔에서 모든 출력값 표시
  console.log({allCourses})

  return (
    &lt;main&gt;
      &lt;div&gt;
        &lt;h1&gt;Courses&lt;/h1&gt; 
        &lt;AddCourse addCourseToDB={addCourseToDB} /&gt;
        &lt;CourseList allCourses={allCourses} /&gt;  
      &lt;/div&gt;
    &lt;/main&gt;
  )
}
</code></pre><p><em>page.tsx - 서버 컴포넌트</em></p><p>Home 컴포넌트에는 아래 내용이 포함되어 있습니다.</p><ul><li>제목(Heading)</li><li>강의 추가 버튼을 래핑한 컴포넌트(AddCourse)</li><li>강의 목록을 표시하는 컴포넌트(CourseList)</li></ul><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2023/07/Screenshot-2023-07-25-at-9.58.57-AM.png" class="kg-image" alt="Screenshot-2023-07-25-at-9.58.57-AM" width="600" height="400" loading="lazy"></figure><p>강의 목록 페이지</p><p>서버 컴포넌트는 클라이언트 및 서버 컴포넌트 모두 렌더링할 수 있다는 것을 알고 있습니다. <code>AddCourse</code> 컴포넌트는 사용자 인터렉션이 필요하며, 사용자가 버튼을 클릭하여 강의를 추가해야 합니다. 따라서 이는 서버 컴포넌트가 될 수 없습니다 (앞에서 읽은 서버 컴포넌트의 제한 사항을 기억하세요)!</p><p>그러므로 <code>AddCourse</code>를 위한 클라이언트 컴포넌트를 만들어 봅시다. Next.js 앱 라우터를 사용하면 모든 컴포넌트는 기본적으로 서버 컴포넌트입니다. 클라이언트 컴포넌트를 만들고 싶다면 해당 컴포넌트의 맨 위에서(import 문 이전에도 됨) <code>'use client'</code>라는 지시문을 사용하여 명시적으로 만들어야 합니다.</p><pre><code class="language-jsx">'use client'

import { useState } from 'react';
import Modal from './Modal';
import AddCourseForm from "./AddCourseForm";

export default function AddCourse({
  addCourseToDB,
}: {
  addCourseToDB: (data: any) =&gt; Promise&lt;void&gt;
}) {
  const [showAddModal, setShowAddModal] = useState(false);
  const add = async(data: any) =&gt; {
    await addCourseToDB(data);
    setShowAddModal(false);
  }

  return (
    &lt;&gt;
    &lt;button
      onClick={() =&gt; setShowAddModal(true)}
    &gt;
      Add Course
    &lt;/button&gt;
    &lt;Modal 
      shouldShow={showAddModal} 
      body={
        &lt;AddCourseForm 
          saveAction={add} 
          cancelAction={() =&gt; setShowAddModal(false)} /&gt;} /&gt;
    &lt;/&gt;
  )
}
</code></pre><p><em>AddCourse - 클라이언트 컴포넌트</em></p><p><code>CourseList</code> 컴포넌트는 어떠한 이벤트 핸들러도 필요하지 않으므로, 서버 컴포넌트로 유지할 수 있습니다.</p><pre><code class="language-jsx">import Image from 'next/image'
import Link from 'next/link'

export default function CourseList(courseList: any) {
  const allCourses = courseList.allCourses;
  return(
    &lt;div&gt;
      {
        allCourses.map((course: any) =&gt;
        &lt;Link key={course['_id']} href={`/courses/${course['_id']}`}&gt;
          &lt;div&gt;
            &lt;Image
              src={course.cover}
              width={200}
              height={200}
              alt={course.name}
            /&gt;
            &lt;h2&gt;{course.name}&lt;/h2&gt;
            &lt;p&gt;{course.rating}&lt;/p&gt;
          &lt;/div&gt; 
        &lt;/Link&gt; 
      )}
    &lt;/div&gt;  
  )

}
</code></pre><p><em>CourseList - 서버 컴포넌트</em></p><p>또한 브라우저의 개발자 도구에서 <code>Sources</code> 탭을 확인하여 클라이언트에 다운로드되는 내용과 서버에 남아 있는 내용을 식별할 수 있습니다. 여기서 <code>page.tsx</code> 파일이나 <code>CourseList.tsx</code> 파일을 볼 수 있나요? 아니요. 왜냐하면 그것들은 서버 컴포넌트이며 클라이언트 번들의 일부가 아니기 때문입니다.</p><p>우리는 앱에서 명시적으로 클라이언트 컴포넌트라고 표시한 컴포넌트만 볼 수 있습니다.</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-179.png" class="kg-image" alt="image-179" width="600" height="400" loading="lazy"></figure><p><em>클라이언트 번들 확인</em></p><p>이 애플리케이션의 흐름을 통해 이론이 실제와 어떻게 연결되는지 보여주었기를 바랍니다. 이제 React 앱에서 서버 컴포넌트를 사용하는 방법을 이해하셨을 것입니다.</p><h2 id="--5">요약</h2><p>요약하면 다음과 같습니다.</p><ul><li>React 서버 컴포넌트는 네트워크 왕복 없이 백엔드 접근이 가능합니다.</li><li>React 서버 컴포넌트를 통해 네트워크 워터폴을 피할 수 있습니다.</li><li>React 서버 컴포넌트는 자동 코드 분할(splitting)을 지원하며 번들 크기를 제로로 줄여 앱의 성능을 향상시킵니다.</li><li>이러한 컴포넌트는 서버 측에 있으므로 클라이언트 사이드 이벤트 핸들러, state 및 effect에 액세스할 수 없습니다. 이는 이벤트 핸들러나 useState, useReducer, useEffect와 같은 React 훅을 사용할 수 없음을 의미합니다.</li><li>React 서버 컴포넌트는 클라이언트 컴포넌트를 가져와 렌더링할 수 있지만, 반대로는 불가능 합니다. 그러나 서버 컴포넌트를 클라이언트 컴포넌트에 props로 전달할 수 있습니다.</li></ul><h2 id="--6">마치기 전에..</h2><p>이상으로 마무리합니다. 이 글이 유익하고 통찰력을 얻었기를 바랍니다.</p><p>저와 소통해보세요.</p><ul><li>제 YouTube 채널인 <code>tapaScript</code>에서 교육을 진행하고 있습니다. JavaScript, ReactJS, Node.js, Git 및 웹 개발의 기본을 배우고 싶다면 <a href="https://www.youtube.com/tapasadhikary?sub_confirmation=1">구독</a>해주세요.</li><li>웹 개발 및 프로그래밍 팁을 매일 놓치고 싶지 않다면 <a href="https://twitter.com/tapasadhikary">Twitter</a> 또는 <a href="https://www.linkedin.com/in/tapasadhikary/">LinkedIn</a> 팔로우를 해주세요.</li><li><a href="https://github.com/atapas">GitHub</a>에서 제 오픈 소스 작업을 확인해보세요.</li></ul><p>다음 글에서 뵙겠습니다. 그 동안 자신을 돌보며 행복하게 지내세요.</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 자바스크립트 setTimeout()로 타이머 설정하기 ]]>
                </title>
                <description>
                    <![CDATA[  이번 튜토리얼에서는 자바스크립트의 내장 함수인 setTimeout()이 어떻게 작동되는지 코드 예시를 통해 이해하는 시간을 가져보겠습니다. 자바스크립트에서 setTimeout() 사용하기 setTimeout()은 특정 시간이 지난 다음에 코드를 실행하는 함수입니다. 자바스크립트 코드에 일종의 타이머를 설정하는 것입니다. setTimeout() 함수 예제 setTimeout(function(){     console.log("Hello World"); }, 2000); console.log("setTimeout() example..."); 위 코드는 콘솔에 "setTimeout() ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/how-to-set-a-timer-in-javascript/</link>
                <guid isPermaLink="false">656fb72f1f7e5f04036d3ff7</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Heegu Yang ]]>
                </dc:creator>
                <pubDate>Wed, 06 Dec 2023 13:53:35 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2023/12/mainImage.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/javascript-settimeout-how-to-set-a-timer-in-javascript-or-sleep-for-n-seconds/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">JavaScript setTimeout() – How to Set a Timer in JavaScript or Sleep for N Seconds</a>
      </p><p></p><p>이번 튜토리얼에서는 자바스크립트의 내장 함수인 <code>setTimeout()</code>이 어떻게 작동되는지 코드 예시를 통해 이해하는 시간을 가져보겠습니다.</p><h2 id="-settimeout-">자바스크립트에서 setTimeout() 사용하기</h2><p><code>setTimeout()</code>은 특정 시간이 지난 다음에 코드를 실행하는 함수입니다. 자바스크립트 코드에 일종의 타이머를 설정하는 것입니다.</p><p>setTimeout() 함수 예제</p><pre><code class="language-js">setTimeout(function(){
    console.log("Hello World");
}, 2000);

console.log("setTimeout() example...");
</code></pre><p>위 코드는 콘솔에 "setTimeout() example..."를 먼저 출력하고, 2초가 지난 다음 "Hello World"을 출력합니다.</p><p><code>setTimeout()</code> 함수 문법은 이렇습니다:</p><p>setTimeout() 함수 문법</p><pre><code class="language-js">setTimeout(function, milliseconds, parameter1, parameter2, ...);
</code></pre><p><code>setTimeout()</code> 함수의 첫 번째 매개변수에는 여러분이 실행시킬 자바스크립트 함수를 넣습니다. <code>function</code>에 함수 자체를 넣을 수도 있고, 아래와 같이 기명 함수로 넣기도 합니다.</p><p>setTimeout() 함수에 기명함수 사용한 예시</p><pre><code class="language-js">function greeting(){
  console.log("Hello World");
}

setTimeout(greeting);
</code></pre><p>그리고 다음 코드가 실행되기 전까지 기다리도록 <code>milliseconds</code>를 매개 변수로 전달할 수도 있습니다.</p><p>1초는 1,000 밀리초입니다. 만약 3초를 기다리게 하고 싶다면, <code>3000</code>을 두 번째 매개 변수로 전달해야 합니다.</p><p>3초간 기다리는 setTimeout() 함수</p><pre><code class="language-js">function greeting(){
  console.log("Hello World");
}

setTimeout(greeting, 3000);
</code></pre><p>만약 두 번째 매개 변수를 생략한다면, <code>setTimeout()</code>은 <code>function</code> 함수를 즉시 실행합니다.</p><p>마지막으로, <code>setTimeout()</code> 함수에 매개 변수들을 추가로 넣을 수도 있습니다.</p><p>setTimeout()에 추가로 매개변수 전달하기</p><pre><code class="language-js">function greeting(name, role){
  console.log(`Hello, my name is ${name}`);
  console.log(`I'm a ${role}`);
}

setTimeout(greeting, 3000, "Nathan", "Software developer");
</code></pre><p>여러분은 아래와 같이 "함수에 바로 매개 변수를 바로 전달하면 안될까?"라고 물을 수도 있습니다.</p><pre><code class="language-js">setTimeout(greeting("Nathan", "Software developer"), 3000);
</code></pre><p>그럼 자바스크립트는 기다림 없이 <code>function</code>을 실행하게 됩니다. 왜냐하면 첫 번째 매개 변수로 함수 참조(function reference)가 아닌 함수 호출(function call)을 전달했기 때문입니다.</p><p>따라서 함수에 어떤 매개변수들을 전달하고 싶다면, <code>setTimeout()</code> 함수를 통해 전달해야 합니다.</p><p>그러나 <code>setTimeout()</code>에 추가적인 매개 변수를 전달하는 경우는 매우 드물기 때문에 이에 대해 크게 걱정하지 않아도 됩니다.</p><h2 id="settimeout-">setTimeout 함수 취소하는 방법</h2><p><code>clearTimeout()</code>를 사용하면 <code>setTimeout()</code>의 함수 실행을 취소할 수 있습니다. 이를 위해 <code>clearTimeout()</code> 함수는 <code>setTimeout()</code>으로부터 반환된 <code>id</code>를 필요로 합니다.</p><p>clearTimeout() 문법</p><pre><code class="language-js">clearTimeout(id);
</code></pre><p>여기 <code>clearTimeout()</code> 함수를 사용하는 예시가 있습니다.</p><p>clearTimeout() 함수 활용 예시</p><pre><code class="language-js">const timeoutId = setTimeout(function(){
    console.log("Hello World");
}, 2000);

clearTimeout(timeoutId);
console.log(`Timeout ID ${timeoutId} has been cleared`);
</code></pre><p>만약 여러 개의 <code>setTimeout()</code> 함수들을 사용할 경우, 각 함수 호출에서 반환되는 ID 값들을 저장한 다음, 함수의 수 만큼 <code>clearTimeout()</code> 함수를 호출해야 합니다.</p><h2 id="-">결론</h2><p><code>setTimeout()</code> 함수는 특정 함수의 실행 시기를 설정할 수 있는 자바스크립트 내장 함수입니다. 대기 시간을 전달하기 위해 밀리초 기준의 시간을 전달합니다. 예를 들어, 1초를 기다리게 하고 싶다면 1,000 밀리초를 입력하는 식이죠.</p><p><code>setTimeout()</code>의 함수 실행을 취소하고 싶다면, <code>clearTimeout()</code> 함수를 사용하면 됩니다. 이때 <code>setTimeout()</code> 함수를 호출 시 반환되는 ID 값을 전달해야 합니다.</p><h2 id="--1">이 글을 읽어주셔서 감사합니다</h2><p>자바스크립트에 대해 더 많이 알고싶으시다면, 저의 웹사이트 sebhastian.com를 방문하세요. 이곳에서 상세한 설명과 코드 예시가 포함된 <a href="https://sebhastian.com/javascript-tutorials/">자바스크립트 프로그래밍에 대한 100개 이상의 튜토리얼</a>을 얻을 수 있습니다. 튜토리얼의 주제는 문자열 조작, 날짜 조작, 배열과 객체 함수, 자바스크립트 알고리즘 등 다양합니다.</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 파이썬 부분문자열(Substring) - 문자열(String)을 자르는 방법 ]]>
                </title>
                <description>
                    <![CDATA[ 파이썬에서 문자열(string)은 문자(Characters)의 시퀀스로 특수문자 또는 알파벳, 숫자를 포함합니다. 예를 들어 “우리 금요일 08:00am에 만나자”라는 문자열이 있습니다. 여기서 보통 부분문자열(substring)이라고 알려져 있는 문자열의 특정 부분을 액세스 할 수 있습니다. 우리는 부분문자열을 문자열 안에 한 시퀀스로 정의할 수 있으며, 예제에서는 “금요일” “에” 그리고 “만나자” 가 될 수 있습니다. 파이썬으로 부분문자열을 생성하는 ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/python-substring-how-to-slice-a-string/</link>
                <guid isPermaLink="false">6539999f722f6d03ea1726de</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Yeonsoo Jang ]]>
                </dc:creator>
                <pubDate>Fri, 27 Oct 2023 10:59:42 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2023/10/main-image.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/python-substring-how-to-slice-a-string/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Python Substring – How to Slice a String</a>
      </p><!--kg-card-begin: markdown--><h3 id="stringcharacters">파이썬에서 문자열(string)은 문자(Characters)의 시퀀스로 특수문자 또는 알파벳, 숫자를 포함합니다.</h3>
<p>예를 들어 “우리 금요일 08:00am에 만나자”라는 문자열이 있습니다. 여기서 보통 부분문자열(substring)이라고 알려져 있는 문자열의 특정 부분을 액세스 할 수 있습니다.</p>
<p>우리는 부분문자열을 문자열 안에 한 시퀀스로 정의할 수 있으며, 예제에서는 “금요일” “에” 그리고 “만나자” 가 될 수 있습니다.</p>
<h2 id="">파이썬으로 부분문자열을 생성하는 방법</h2>
<p>파이썬은 부분문자열을 생성하고, 부분문자열의 존재 여부를 확인하고, 부분문자열의 인덱스를 추출하기 위한 다양한 방법과 메소드를 제공합니다.</p>
<p>다음과 같이 인덱스를 활용하여 문자열에서 부분문자열을 추출할 수 있습니다.<br>
<code>string[start:stop:step]</code></p>
<pre><code>start – 부분문자열의 첫번째 인덱스
stop – 부분문자열의 마지막 인덱스
step – 슬라이싱의 간격을 의미하며, 기본값은 1입니다.
</code></pre>
<p>![substring index 표 설명] (<a href="https://www.freecodecamp.org/news/content/images/2021/07/image--2-.png">https://www.freecodecamp.org/news/content/images/2021/07/image--2-.png</a>)</p>
<p>인덱스는 양수일 수도 음수일 수도 있습니다. 양수 인덱스는 문자열의 첫번째 문자부터 시작해서 마지막 문자에서 끝이 나고, 음수 인덱스는 마지막 문자에서 시작하여 첫번째 문자에서 끝이 납니다.</p>
<p>이번 글에서는 파이썬에서 부분문자열과 관련한 다양한 조작방법을 다룹니다.</p>
<h2 id="n">파이썬에서 첫번째 n 문자를 추출하는 방법</h2>
<p>이 예제는 첫 다섯 문자를 추출하는 방법에 대해서 설명합니다.</p>
<pre><code class="language-python">string = "hello world"
print(string[:5])
</code></pre>
<p>여기서 stop 인덱스는 5로 설정하였고, start 인덱스는 기본값 0으로 설정됩니다.<br>
결과값은 <code>'hello'</code>입니다.</p>
<h2 id="">파이썬에서 중간 문자를 추출하는 방법</h2>
<p>이번 예제는 세번째 인덱스부터 다섯 번째 인덱스까지 추출하는 방법을 보여줍니다.</p>
<pre><code class="language-python">string = "hello world"
print(string[3:5])
</code></pre>
<p>결과값은 <code>'lo'</code>입니다.</p>
<h2 id="">마지막 문자를 추출하는 방법</h2>
<p>마지막 문자를 추출하기 위해서는 -1 인덱스(음수 인덱스)를 사용합니다. 다음 예제를 확인해보세요.</p>
<pre><code class="language-python">string = "freecodecamp"
print(string[-1])
</code></pre>
<p>결과값은 <code>'p'</code>입니다.</p>
<h2 id="n">마지막 n개 문자를 추출하는 방법</h2>
<p>이번 예제에서는 음수 인덱스를 사용하여 마지막 4개 문자를 slicing합니다.</p>
<pre><code class="language-python">string = "freecodecamp"
print(string[-4:])
</code></pre>
<p>결과값은 <code>'camp'</code>입니다.</p>
<h2 id="step">문자열에서 step을 이용해서 부분문자열을 슬라이싱하는 방법</h2>
<p>당신은 start-index, stop-index 그리고 step을 이용해서 문자열을 자를 수 있습니다. 간격(step)은 기본값이 1이지만 다음 예제에서는 step 크기를 2로 설정합니다.</p>
<pre><code class="language-python">string = "welcome to freecodecamp"
print(string[::2])
</code></pre>
<p>결과값은 <code>'wloet fecdcm'</code>입니다.</p>
<h2 id="">부분문자열이 문자열에 포함되어있는지 확인하는 방법</h2>
<p>부분문자열이 문자열에 포함되어 있는지 확인하고 싶을 때가 있습니다. 다음 예제는 부분문자열 ‘code’가 문자열에 포함되어 있는지를 확인합니다.</p>
<pre><code class="language-python">substring = "code"
string = "welcome to freecodecamp"
print(substring in string)
</code></pre>
<p>만약 포함되어 있다면 True 아니라면 False를 반환합니다.<br>
여기러 결과값은 <code>True</code> 입니다.</p>
<h2 id="">부분문자열의 포함 여부를 확인하는 다른 방법</h2>
<p><code>find()</code> 메소드를 통해서 부분문자열이 문자열에 존재하는지를 확인할 수 있습니다.<br>
다음 예제를 확인해봅시다.</p>
<pre><code class="language-python">substring = "zz"
string = "hello world"
print(string.find(substring))
</code></pre>
<p>부분문자열이 문자열에 포함되어 있다면 부분문자열 가장 왼쪽의 인덱스를 반환하고, 포함되어 있지 않다면 -1을 반환합니다.<br>
예제에서 결과값은 <code>-1</code>로, ‘zz’는 ‘hello world’에 포함되어 있지 않음을 의미합니다.</p>
<h2 id="">문자열에서 주어진 인덱스에 해당하는 문자를 반환하는 방법</h2>
<p>인덱스에 따라 특정 문자를 슬라이싱 할 수 있습니다.</p>
<pre><code class="language-python">string ="hello world"
print(string[4])
</code></pre>
<p>결과값은 <code>‘0’</code> 입니다.</p>
<h2 id="">파이썬으로 부분문자열의 리스트를 만드는 방법</h2>
<p><code>split()</code> 메소드를 사용하여 부분문자열의 리스트를 생성할 수 있습니다. 다음 예제를 확인해봅시다.</p>
<pre><code class="language-python">string = "welcome to freecodecamp platform"
print(string.split())
</code></pre>
<p>결과값은 <code>['welcome', 'to', 'freecodecamp', 'platform']</code>입니다.</p>
<h2 id="step">음수 step으로 문자열 뒤집기</h2>
<p>문자열을 반대방향으로 뒤집기 위해서 step은  -1과 같은 음수 값이여야 합니다.</p>
<pre><code class="language-python">string = "welcome to freecodecamp"
print(string[::-1])
</code></pre>
<p>결과값은 <code>‘pmacedoceerf ot emoclew’</code>입니다.</p>
<h2 id="">부분문자열이 문자열에 몇 번 나타났는지 세는 방법</h2>
<p><code>count()</code> 메소드를 통해 특정 부분문자열이 문자열에 몇 번 있는지 셀 수 있습니다.</p>
<pre><code class="language-python">string = "we will have a quick coding lesson this afternoon"
print(string.count('noon'))
</code></pre>
<p>결과값은 1입니다.</p>
<h2 id="">파이썬 부분문자열에 대한 마지막 이야기</h2>
<p>축하합니다👏👏, 이 아티클의 끝까지 왔습니다! 파이썬 부분문자열에 대해서 새로운 것을 배우셨길 바랍니다.<br>
만약 새로운 것을 배웠거나 이 아티클을 흥미롭게 읽었다면, 다른사람들도 볼 수 있도록 공유해 주세요. 그럼 다음 포스팅에서 만나요!</p>
<p>트위터에서도 저를 찾을 수 있습니다. <a href="https://twitter.com/Davis_McDavid?ref=hackernoon.com">@Davis_McDavid</a></p>
<p>이와 같은 아티클을 이 <a href="https://hackernoon.com/u/davisdavid?ref=hackernoon.com">링크</a> 를 통해서 더 많이 읽을 수 있습니다.</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ React와 React 훅으로 검색 필터 만드는 법 ]]>
                </title>
                <description>
                    <![CDATA[ API에 GET 요청을 보내면 서버로부터 응답 데이터를 받습니다. 하지만 가끔은 이 데이터를 관리하는 게 문제가 되기도 합니다. 이 글에서는 React에서 검색 필터를 생성하는 법을 알려 드리려고 합니다. 함수형 컴포넌트와 React 훅을 이용하여 데이터에 있는 특정 단어를 검색할 것입니다. API에 GET 요청을 보내는 법 우선 서버에서 데이터를 불러오는 API에 GET 요청을 ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/build-a-search-filter-using-react-and-react-hooks/</link>
                <guid isPermaLink="false">65338f09722f6d03ea1726bb</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ SeunghyunKim ]]>
                </dc:creator>
                <pubDate>Sat, 21 Oct 2023 10:32:03 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2023/10/main-image.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/build-a-search-filter-using-react-and-react-hooks/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Build a Search Filter using React and React Hooks</a>
      </p><!--kg-card-begin: markdown--><p>API에 GET 요청을 보내면 서버로부터 응답 데이터를 받습니다. 하지만 가끔은 이 데이터를 관리하는 게 문제가 되기도 합니다.</p>
<p>이 글에서는 React에서 검색 필터를 생성하는 법을 알려 드리려고 합니다. 함수형 컴포넌트와 React 훅을 이용하여 데이터에 있는 특정 단어를 검색할 것입니다.</p>
<h2 id="apiget">API에 GET 요청을 보내는 법</h2>
<p>우선 서버에서 데이터를 불러오는 API에 GET 요청을 만들어 보겠습니다. 이 데이터를 불러오기 위해 아무 서버를 사용해도 되지만 이 글에서 필자는 사용자 목록을 불러오기 위해 {JSON} placeholder를 사용할 것입니다.</p>
<p>이 예시에서 사용자들의 이름과 이메일을 보여주는 카드들이 있습니다. 또한 특정 사용자들 검색하기 위해 사용할 검색 입력 창이 있습니다.</p>
<pre><code class="language-js,caption=JSON">import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Card, Input } from 'semantic-ui-react'
export default function Posts() {
    const [APIData, setAPIData] = useState([])
    useEffect(() =&gt; {
        axios.get(`https://jsonplaceholder.typicode.com/users`)
            .then((response) =&gt; {
                setAPIData(response.data);
            })
    }, [])
    return (
        &lt;div style={{ padding: 20 }}&gt;
            &lt;Input icon='search'
                placeholder='Search...'
            /&gt;
            &lt;Card.Group itemsPerRow={3} style={{ marginTop: 20 }}&gt;
                {APIData.map((item) =&gt; {
                    return (
                        &lt;Card&gt;
                            &lt;Card.Content&gt;
                                &lt;Card.Header&gt;{item.name}&lt;/Card.Header&gt;
                                &lt;Card.Description&gt;
                                    {item.email}
                                &lt;/Card.Description&gt;
                            &lt;/Card.Content&gt;
                        &lt;/Card&gt;
                    )
                })}
            &lt;/Card.Group&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/08/Screenshot-2021-08-14-202008.png" alt="화면 상위의 검색 입력과 사용자 목록 얻기" width="600" height="400" loading="lazy"></p>
<p>그리고 만약 React에서 GET API 호출을 다루는 법을 모르신다면 API 호출에 대해 설명하는 <a href="https://www.freecodecamp.org/news/how-to-perform-crud-operations-using-react/">React CRUD 작업</a>에 관한 제 블로그 혹은 비디오를 보시길 바랍니다.</p>
<h2 id="">검색 입력 박스로부터 검색 입력을 얻는 법</h2>
<p>이제 검색 입력 박스로 부터 검색 쿼리(query)를 가져와 보겠습니다.</p>
<p>검색 입력을 위한 state를 생성합니다.</p>
<pre><code class="language-js,caption=검색">const [searchInput, setSearchInput] = useState('');
</code></pre>
<p>여기 <code>searchInput</code>은 문자열이고 검색 입력 값 설정을 위해 <code>setSearchInput</code>을 사용할 것입니다.</p>
<p>이제 검색 기능을 처리할 함수를 만들어 보겠습니다.</p>
<pre><code class="language-js,caption=검색">const searchItems = () =&gt; {
        
}
</code></pre>
<p>그리고 이 함수를 <code>onChange</code> 이벤트로 검색 입력에 바인드(bind)합니다.</p>
<pre><code class="language-js,">&lt;Input icon='search'
    placeholder='Search...'
    onChange={() =&gt; searchItems()}
/&gt;
</code></pre>
<p>이리하여 입력 필드에 어떤 값이든 입력할 때마다 <code>searchItems</code>가 실행이 될 것입니다.</p>
<p>이제 <code>searchItems</code>에 입력 값을 전달해야 합니다.</p>
<pre><code class="language-js,caption=searchItems">&lt;Input icon='search'
    placeholder='Search...'
    onChange={(e) =&gt; searchItems(e.target.value)}
/&gt;
</code></pre>
<p>다음 검색 쿼리를 <code>searchItems</code> 함수에서 전달 받고 이전에 생성한 <code>setSearchInput</code>을 사용하여 <code>searchInput</code> state를 이 전달 받은 값으로 설정하겠습니다.</p>
<pre><code class="language-js,caption=searchInput">const searchItems = (searchValue) =&gt; {
    setSearchInput(searchValue)
}
</code></pre>
<p>콘솔로 <code>searchValue</code> 값을 확인함으로써 위 로직이 잘 실행되는지 확인할 수 있습니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/size/w1000/2021/08/Screenshot-2021-08-14-203750.png" alt="Screenshot-2021-08-14-203750" width="600" height="400" loading="lazy"></p>
<p>보시다시피 필자의 이름을 입력했고 콘솔에서 이름이 나타납니다.</p>
<h2 id="">검색 결과에 따른 아이템 필터링</h2>
<p>이제 filter 메소드를 사용하여 APIData를 필터링(filtering)할 것입니다.</p>
<pre><code class="language-js,caption=검색">const searchItems = (searchValue) =&gt; {
    setSearchInput(searchValue)
    APIData.filter((item) =&gt; {
        return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
    })
}
</code></pre>
<p><code>searchItems</code> 함수에서 서버로부터 얻는 데이터를 포함한 APIData state를 필터링하는 <code>filter</code> 메소드를 사용하고 있다는 것을 알 수 있습니다.</p>
<p>또한 객체 아이템으로부터 값들을 얻기 위해 <code>Object.values</code>를 사용하고 있습니다.</p>
<p>그런 다음 <code>join(' ')</code> 메소드를 사용하여 이 값들을 문자열로 바꾸어 줍니다.</p>
<p>다음 <code>toLowerCase</code> 메소드를 사용하여 이 문자열들을 소문자로 바꾸어 줍니다.</p>
<p>마지막으로 이 문자열들이 검색란에 입력한 값을 포함하고 있는지 확인합니다. 검색 입력 또한 소문자로 바꾸어 줍니다. 이는 단어를 대문자로 입력했을 시 검색을 더 효율적으로 만들어 주기 위해 그 문자열을 소문자로 바꾸게 하는 것입니다.</p>
<p>그런 다음 전체 쿼리를 반환합니다.</p>
<p>이제 이 필터링한 배열을 변수에 저장해야 합니다.</p>
<pre><code class="language-js,caption=필터링한">const filteredData = APIData.filter((item) =&gt; {
    return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
})
</code></pre>
<p>콘솔을 통해 위에서 만든 기능을 확인해 보겠습니다. 사용자 이름을 검색해보면 해당 사용자 이름에 맞는 데이터를 보게 될 것입니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/size/w1000/2021/08/Screenshot-2021-08-14-211709.png" alt="검색 아이템 콘솔에서 확인" width="600" height="400" loading="lazy"></p>
<p>이제 필터 데이터를 저장해야 하므로 state를 생성하겠습니다.</p>
<pre><code class="language-js,caption=필터">const [filteredResults, setFilteredResults] = useState([]);
</code></pre>
<p>필터링한 데이터를 담은 state를 생성합니다.</p>
<p>그런 다음 <code>setFilteredResults</code>를 사용하여 <code>searchItems</code> 함수에서 이 state를 <code>filteredData</code>로 설정합니다.</p>
<pre><code class="language-js,caption=filtedData를">const searchItems = (searchValue) =&gt; {
    setSearchInput(searchValue)
    const filteredData = APIData.filter((item) =&gt; {
        return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
    })
    setFilteredResults(filteredData)
}
</code></pre>
<h2 id="ui">필터링 결과를 UI에 보여주는 법</h2>
<p>이제 이 필터링 결과들을 주 UI에 보여주도록 하겠습니다.</p>
<p>우선 검색 입력란이 비어 있는지 확인하고 비어 있다면 모든 데이터를 보여주는 코드를 작성해야 합니다. 비어 있지 않다면 입력 값에 따라 결과를 필터링할 것입니다.</p>
<pre><code class="language-js,caption=검색">const searchItems = (searchValue) =&gt; {
    setSearchInput(searchValue)
    if (searchInput !== '') {
        const filteredData = APIData.filter((item) =&gt; {
            return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
        })
        setFilteredResults(filteredData)
    }
    else{
        setFilteredResults(APIData)
    }
}
</code></pre>
<p>또한 애플리케이션의 반환 부분에서도 똑같은 확인이 필요합니다.</p>
<p>그래서 검색 단어의 길이가 1보다 크면 필터가 된 데이터를 보여줄 것입니다. 1보다 작다면 APIData state에 담긴 모든 데이터를 보여줄 것입니다.</p>
<pre><code class="language-js,caption=길이가">&lt;Card.Group itemsPerRow={3} style={{ marginTop: 20 }}&gt;
    {searchInput.length &gt; 1 ? (
        filteredResults.map((item) =&gt; {
            return (
                &lt;Card&gt;
                    &lt;Card.Content&gt;
                        &lt;Card.Header&gt;{item.name}&lt;/Card.Header&gt;
                        &lt;Card.Description&gt;
                            {item.email}
                        &lt;/Card.Description&gt;
                    &lt;/Card.Content&gt;
                &lt;/Card&gt;
            )
        })
    ) : (
        APIData.map((item) =&gt; {
            return (
                &lt;Card&gt;
                    &lt;Card.Content&gt;
                        &lt;Card.Header&gt;{item.name}&lt;/Card.Header&gt;
                        &lt;Card.Description&gt;
                            {item.email}
                        &lt;/Card.Description&gt;
                    &lt;/Card.Content&gt;
                &lt;/Card&gt;
            )
        })
    )}
&lt;/Card.Group&gt;
</code></pre>
<p>이제 검색 필드에서 사용자 이름을 검색한다면 해당 사용자의 데이터를 얻게 됩니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/08/Screenshot-2021-08-14-212917.png" alt="UI에 검색 입력에 대한 결과" width="600" height="400" loading="lazy"></p>
<p>검색 창에서 이름을 지우면 모든 데이터를 얻게 됩니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/08/Screenshot-2021-08-14-213037.png" alt="검색 입력란이 비어 있을 경우 모든 데이터 출력" width="600" height="400" loading="lazy"></p>
<p>여기 참고할 분들을 위해 전체 코드를 적어 두겠습니다.</p>
<pre><code class="language-js,caption=검색">import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Card, Input } from 'semantic-ui-react'
export default function Posts() {
    const [APIData, setAPIData] = useState([])
    const [filteredResults, setFilteredResults] = useState([]);
    const [searchInput, setSearchInput] = useState('');
    useEffect(() =&gt; {
        axios.get(`https://jsonplaceholder.typicode.com/users`)
            .then((response) =&gt; {
                setAPIData(response.data);
            })
    }, [])
    const searchItems = (searchValue) =&gt; {
        setSearchInput(searchValue)
        if (searchInput !== '') {
            const filteredData = APIData.filter((item) =&gt; {
                return Object.values(item).join('').toLowerCase().includes(searchInput.toLowerCase())
            })
            setFilteredResults(filteredData)
        }
        else{
            setFilteredResults(APIData)
        }
    }
    return (
        &lt;div style={{ padding: 20 }}&gt;
            &lt;Input icon='search'
                placeholder='Search...'
                onChange={(e) =&gt; searchItems(e.target.value)}
            /&gt;
            &lt;Card.Group itemsPerRow={3} style={{ marginTop: 20 }}&gt;
                {searchInput.length &gt; 1 ? (
                    filteredResults.map((item) =&gt; {
                        return (
                            &lt;Card&gt;
                                &lt;Card.Content&gt;
                                    &lt;Card.Header&gt;{item.name}&lt;/Card.Header&gt;
                                    &lt;Card.Description&gt;
                                        {item.email}
                                    &lt;/Card.Description&gt;
                                &lt;/Card.Content&gt;
                            &lt;/Card&gt;
                        )
                    })
                ) : (
                    APIData.map((item) =&gt; {
                        return (
                            &lt;Card&gt;
                                &lt;Card.Content&gt;
                                    &lt;Card.Header&gt;{item.name}&lt;/Card.Header&gt;
                                    &lt;Card.Description&gt;
                                        {item.email}
                                    &lt;/Card.Description&gt;
                                &lt;/Card.Content&gt;
                            &lt;/Card&gt;
                        )
                    })
                )}
            &lt;/Card.Group&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>이제 React 훅을 이용하여 React에서 완전 함수형의 검색 필터를 가지게 되었습니다.</p>
<p>보통 이 기능은 API 엔드포인트에서 검색 쿼리 인자를 전달하여 백엔드에서 처리하지만 프론트엔드에서 그 기능을 처리하는 것을 아는 것도 중요합니다.</p>
<p>이 글에 대한 보충 설명이 필요하다면 <a href="https://www.youtube.com/watch?v=8YsQmvJ9UZE">React와 React 훅을 이용하여 검색 필터 만들기</a>에 관한 제 유투브 비디오를 확인하시면 되겠습니다.</p>
<blockquote>
<p>이상 마무리 짓겠습니다. 감사합니다.</p>
</blockquote>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 자바스크립트 프로미스 튜토리얼 - 자바스크립트의 프로미스를 이행하거나 거부하는 방법 ]]>
                </title>
                <description>
                    <![CDATA[ 프로미스는 자바스크립트에서 비동기 처리를 위한 중요한 요소이지만 프로미스를 이해하고 적용하는 것은 그렇게 쉽지 않다고 생각하셨을 수도 있습니다. 하지만 절 따라오세요, 여러분은 혼자가 아닙니다! 프로미스는 프로미스를 몇 년 동안 다뤄본 많은 웹 개발자들에게도 어려운 주제입니다. 이 글에서는 제가 지난 몇 년간 자바스크립트의 프로미스에 대해 배운 내용을 공유하면서 이러한 인식을 한번 깨보겠습니다. ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/javascript-promise-tutorial-how-to-resolve-or-reject-promises-in-js/</link>
                <guid isPermaLink="false">65336269722f6d03ea1726b0</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nayoung Gu ]]>
                </dc:creator>
                <pubDate>Sat, 21 Oct 2023 10:29:46 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2023/10/cover-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/javascript-promise-tutorial-how-to-resolve-or-reject-promises-in-js/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">JavaScript Promise Tutorial – How to Resolve or Reject Promises in JS</a>
      </p><!--kg-card-begin: markdown--><h4 id=""><code>프로미스</code>는 자바스크립트에서 비동기 처리를 위한 중요한 요소이지만 프로미스를 이해하고 적용하는 것은 그렇게 쉽지 않다고 생각하셨을 수도 있습니다. 하지만 절 따라오세요, 여러분은 혼자가 아닙니다!</h4>
<p>프로미스는 프로미스를 몇 년 동안 다뤄본 많은 웹 개발자들에게도 어려운 주제입니다.</p>
<p>이 글에서는 제가 지난 몇 년간 자바스크립트의 프로미스에 대해 배운 내용을 공유하면서 이러한 인식을 한번 깨보겠습니다. 도움이 되었으면 좋겠습니다.</p>
<h2 id="">자바스크립트의 프로미스란</h2>
<p><code>프로미스</code>는 자바스크립트의 특수 객체입니다. 프로미스는 <code>비동기</code> 작업이 성공적으로 수행되면 값을 생성하고, 시간 초과나 네트워크 오류 등으로 인해 실패하면 에러를 생성합니다.</p>
<p>성공적인 이행은 <code>resolve</code> 함수 호출, 그리고 에러는 <code>reject</code> 함수 호출을 통해 확인할 수 있습니다.</p>
<p>프로미스는 다음과 같이 생성자를 통해 만들 수 있습니다.</p>
<pre><code class="language-js">let promise = new Promise(function(resolve, reject) {    
    // 비동기 호출 후 resolve 혹은 reject 하는 곳
});
</code></pre>
<p>대부분의 경우 프로미스는 비동기 처리에 사용됩니다. 하지만 엄밀히 말하면 동기와 비동기 작업을 모두 이행/거절할 수 있습니다.</p>
<h2 id="">잠시만요, 비동기 작업을 위한 <code>콜백</code> 함수가 있지 않나요?</h2>
<p>네! 맞아요. 자바스크립트에는 <code>콜백</code> 함수가 있습니다. 하지만 콜백은 자바스크립트에서 특별한 개념은 아닙니다. <code>비동기</code> 호출이 (성공 또는 에러와 함께) 완료되면 결과를 반환하는 일반 함수입니다.</p>
<p><code>비동기</code>란 지금 당장이 아니라 미래에 발생한다는 의미입니다. 콜백은 주로 네트워크 통신, 업로드/다운로드, 데이터베이스 조회 등과 같은 상황에서 사용됩니다.</p>
<p><code>콜백</code>은 유용하긴 하지만 엄청난 단점도 가지고 있습니다. 때로 다른 콜백에 포함된 콜백 내부에 또 다른 콜백이 있을 수도 있습니다. 진심으로요! 이 "콜백 헬"을 예시와 함께 이해해봅시다.</p>
<h3 id="pizzahub">콜백 헬을 피하는 방법 - PizzaHub 예시</h3>
<p>PizzaHub에서 🍕 야채 마르게리타 피자를 주문한다고 해봅시다. 주문을 넣으면 PizzaHub은 자동적으로 우리의 위치를 파악하고 근처의 피자 가게를 찾은 뒤 우리가 요청한 피자가 주문 가능한지 찾아볼 것입니다.</p>
<p>주문이 가능하다면 피자와 함께 무료로 받을 수 있는 음료를 파악하고 마침내 주문을 넣게 됩니다.</p>
<p>주문이 성공적으로 완료되면 확인 문자를 받게 됩니다.</p>
<p>이 과정을 콜백 함수를 사용해 코드로 표현하면 어떨까요? 다음과 같이 생각해 봤습니다.</p>
<pre><code class="language-js">function orderPizza(type, name) {

    // pizzahub에 문의하기
    query(`/api/pizzahub/`, function(result, error){
       if (!error) {
           let shopId = result.shopId;

           // 가게에 피자 문의하기
           query(`/api/pizzahub/pizza/${shopid}`, function(result, error){
               if (!error) {
                   let pizzas = result.pizzas;

                   // 피자 가능 여부 확인
                   let myPizza = pizzas.find((pizza) =&gt; {
                       return (pizza.type===type &amp;&amp; pizza.name===name);
                   });

                   // 무료 음료 확인
                   query(`/api/pizzahub/beverages/${myPizza.id}`, function(result, error){
                       if (!error) {
                           let beverage = result.id;

                           // 주문 준비
                           query(`/api/order`, {'type': type, 'name': name, 'beverage': beverage}, function(result, error){
                              if (!error) {
                                  console.log(`Your order of ${type} ${name} with ${beverage} has been placed`);
                              } else {
                                  console.log(`Bad luck, No Pizza for you today!`);
                              }
                           });

                       }
                   })
               }
           });
       } 
    });
}

// orderPizza 메서드 호출
orderPizza('veg', 'margherita');
</code></pre>
<p>위의 <code>orderPizza</code> 함수를 잘 살펴봅시다.</p>
<p>이 함수는 근처 피자 가게의 id를 찾기 위해 API를 호출합니다. 그리고 그 가게에서 주문 가능한 피자의 리스트를 받습니다. 그 중에 주문한 피자가 있는지 확인하고 그 피자와 함께 오는 음료를 확인하기 위해 또 다른 API 호출을 하게 됩니다. 최종적으로 주문 API를 통해 주문을 넣게 됩니다.</p>
<p>여기서 모든 API에 콜백을 사용하고 있습니다. 이 때문에 계속해서 콜백 안에 콜백을 넣는 방식으로 코드를 작성하게 됩니다.</p>
<p>이렇게 해서 우리는 <code>콜백 지옥</code>에 빠지게 됩니다. 누가 이런 코드를 원할까요? 게다가 가독성이 좋지 않고 에러가 발생하기도 쉬운 코드 피라미드가 생겨나게 됩니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/callback-hell.png" alt="콜백 지옥의 예시 모양" width="600" height="400" loading="lazy"></p>
<p align="center">콜백 지옥 및 멸망의 피라미드 예시</p>
<p><code>콜백 지옥</code>을 빠져나오거나 들어가지 않을 수 있는 몇 가지 방법들이 있습니다. 가장 흔한 방법은 <code>프로미스</code>나 <code>비동기</code> 함수를 사용하는 것입니다. 하지만 <code>async</code> 함수를 잘 이해하려면 <code>프로미스</code>를 먼저 잘 이해해야 합니다.</p>
<p>그럼 프로미스에 대해서 시작해 봅시다.</p>
<h2 id="">프로미스의 상태 이해하기</h2>
<p>복습해보자면, 프로미스는 다음과 같은 생성자 문법을 통해 생성될 수 있습니다.</p>
<pre><code class="language-js">let promise = new Promise(function(resolve, reject) {
  // 실행할 함수
});
</code></pre>
<p>생성자 함수는 인자로 함수를 받습니다. 이 함수는 <code>실행 함수</code>라고 불립니다.</p>
<pre><code class="language-js">// 실행 함수는 프로미스 생성자 함수에 인자로 전달된다
function(resolve, reject) {
    // 로직이 위치할 곳
}
</code></pre>
<p>실행 함수는 <code>resolve</code>와 <code>reject</code>라는 두 가지 인자를 받습니다. 이 둘은 자바스크립트에 의해 제공되는 콜백 함수입니다. 여러분이 작성할 로직은 <code>new Promise</code>가 생성되면 자동적으로 호출될 실행 함수 안에 위치하게 됩니다.</p>
<p>프로미스를 효과적으로 사용하려면 실행 함수는 <code>resolve</code> 혹은 <code>reject</code> 콜백 함수들 중 하나를 호출해야 합니다. 이 둘에 대해선 잠시 후에 더 자세히 배워보겠습니다.</p>
<p><code>new Promise()</code> 생성자는 <code>프로미스</code> 객체를 반환합니다. 실행 함수는 비동기 처리를 다루기 때문에, 반환된 프로미스 객체는 작업이 시작되거나, 완료될 때 (resolved), 그리고 에러를 반환할 때(rejected)를 알려줄 수 있어야 합니다.</p>
<p><code>프로미스</code> 객체는 다음과 같은 내부 프로퍼티를 갖습니다.</p>
<ol>
<li><code>state(상태)</code> - 이 프로퍼티는 다음과 같은 값을 갖습니다.</li>
</ol>
<ul>
<li><code>pending(대기)</code>: 실행 함수가 작업을 시작했을 때</li>
<li><code>fulfilled(이행)</code>: 프로미스가 이행되었을 때</li>
<li><code>rejected(거부)</code>: 프로미스가 거부되었을 때</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/states_1.png" alt="프로미스의 상태" width="600" height="400" loading="lazy"></p>
<p align="center">프로미스의 세 가지 상태</p>
<ol start="2">
<li><code>result(결과)</code> - 이 프로퍼티는 다음과 같은 값을 갖습니다.</li>
</ol>
<ul>
<li><code>undefined</code>: <code>상태</code> 값이 <code>대기</code> 중일 때</li>
<li><code>value</code>: <code>resolve(value)</code>가 호출되었을 때</li>
<li><code>error</code>: <code>reject(error)</code>가 호출되었을 때</li>
</ul>
<p>이 내부 프로퍼티들은 코드로 접근할 수는 없지만 확인은 가능합니다. 즉 디버거 도구를 활용해 <code>state</code>와 <code>result</code> 프로퍼티 값을 확인할 수 있지만 직접적으로 프로그램을 사용해 접근할 수는 없습니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/promise_state_inspect.png" alt="개발자 도구에서 프로미스 확인하기" width="600" height="400" loading="lazy"></p>
<p align="center">프로미스의 내부 프로머티를 확인할 수 있다</p>
<p>프로미스의 상태는 <code>pending</code>, <code>fulfilled</code> 혹은 <code>rejected</code>가 될 수 있습니다. 이행(resolved) 또는 거부(rejected)된 프로미스는 <code>settled</code> 되었다고 표현합니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/states_2.png" alt="프로미스가 처리된 상태" width="600" height="400" loading="lazy"></p>
<p align="center">처리(settled)된 프로미스는 이행(fulfilled) 혹은 거부(rejected)된 것이다</p>
<h3 id="">프로미스가 이행되거나 거부되는 방법</h3>
<p>다음은 <code>I am done</code>이라는 값과 함께 즉시 이행되는(<code>fulfilled</code> 상태) 프로미스의 예시입니다.</p>
<pre><code class="language-js">let promise = new Promise(function(resolve, reject) {
    resolve("I am done");
});
</code></pre>
<p>다음은 <code>Something is not right!</code>이라는 에러 메세지와 함께 거절되는 (<code>rejected</code> 상태) 프로미스입니다.</p>
<pre><code class="language-js">let promise = new Promise(function(resolve, reject) {
    reject(new Error('Something is not right!'));
});
</code></pre>
<p>주의할 점</p>
<blockquote>
<p>프로미스 실행 함수는 오직 하나의 <code>resolve</code> 혹은 <code>reject</code> 함수를 호출해야 합니다. 상태가 한번 변경되면 (pending =&gt; fulfilled 혹은 pending =&gt; rejected) 끝이기 때문입니다. 그 이후의 <code>resolve</code> 혹은 <code>reject</code>는 무시됩니다.</p>
</blockquote>
<pre><code class="language-js">let promise = new Promise(function(resolve, reject) {
  resolve("I am surely going to get resolved!");

  reject(new Error('Will this be ignored?')); // 무시됨
  resolve("Ignored?"); // 무시됨
});
</code></pre>
<p>위의 예시에서, 오직 첫 번째 것만 이행되고 나머지 것들은 모두 무시됩니다.</p>
<h2 id="">프로미스를 만들고 처리하는 방법</h2>
<p><code>프로미스</code>는 (대부분 비동기적으로) 작업을 처리하기 위해 실행 함수를 사용합니다. (프로미스의 결과를 사용하는) 소비 함수는 실행 함수가 이행(성공)되거나 거부된(에러) 시점을 알아야 합니다.</p>
<p><code>.then()</code>, <code>.catch()</code>, <code>finally()</code>와 같은 핸들러 메서드는 실행 함수와 소비 함수를 이어줌으로써 프로미스가 <code>이행</code>되거나 <code>거부</code>되었을 때 서로 동기화될 수 있도록 해줍니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/consumer_executor.png" alt="실행 함수와 소비 함수 예시" width="600" height="400" loading="lazy"></p>
<p align="center">실행 함수와 소비 함수</p>
<h3 id="then"><code>.then()</code> 메서드를 사용하는 방법</h3>
<p><code>.then()</code> 메서드는 결과(resolve) 또는 에러(reject)를 다룰 때 프로미스 객체에 의해 호출됩니다.</p>
<p>이 메서드는 인자로 두 개의 함수를 받습니다. 보통 <code>.then()</code> 메서드는 프로미스의 실행 결과를 알고자 하는 소비 함수로부터 실행되어야 합니다.</p>
<pre><code class="language-js">promise.then(
  (result) =&gt; { 
     console.log(result);
  },
  (error) =&gt; { 
     console.log(error);
  }
);
</code></pre>
<p>성공했을 때의 결과만 알고 싶다면 다음과 같이 하나의 인자만 전달해줄 수도 있습니다.</p>
<pre><code class="language-js">promise.then(
  (result) =&gt; { 
      console.log(result);
  }
);
</code></pre>
<p>실패한 결과만 다루고 싶다면 다음과 같이 첫 번째 인자로 <code>null</code>을 전달해주면 됩니다.</p>
<pre><code class="language-js">promise.then(
  null,
  (error) =&gt; { 
      console.log(error)
  }
);
</code></pre>
<p>하지만 잠시후에 보게될 <code>.catch()</code> 메서드를 사용하면 더 나은 방법으로 에러를 처리할 수 있습니다.</p>
<p><code>.then()</code>과 <code>.catch()</code> 핸들러를 사용해서 결과와 에러를 처리한 몇가지 예시를 살펴봅시다. 몇 가지 실제 비동기 요청을 통해서 더 재밌게 배워보겠습니다. 포켓몬에 대한 정보를 얻고 그 결과를 프로미스를 사용해서 이행/거부하기 위해 <a href="https://pokeapi.co/">PokeAPI</a>를 사용하겠습니다.</p>
<p>먼저, PokeAPI URL을 인자로 받은 뒤 프로미스를 반환하기 위한 제네릭 함수를 만듭니다. API 호출이 성공적으로 이루어지면 이행된 프로미스가 반환됩니다. 거부된 프로미스는 어떠한 종류의 에러로부터 반환될 수 있습니다.</p>
<p>이제부터 프로미스를 만들어 작업하기 위해 여러 예시에서 이 함수를 사용하겠습니다.</p>
<pre><code class="language-js">function getPromise(URL) {
  let promise = new Promise(function (resolve, reject) {
    let req = new XMLHttpRequest();
    req.open("GET", URL);
    req.onload = function () {
      if (req.status == 200) {
        resolve(req.response);
      } else {
        reject("There is an Error!");
      }
    };
    req.send();
  });
  return promise;
}
</code></pre>
<p align="center">프로미스를 위한 유틸리티 메서드</p>
<p>예시 1: 50가지의 포켓몬 정보 얻기</p>
<pre><code class="language-js">const ALL_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon?limit=50';

// 아래 함수는 위에서 언급했었죠!
let promise = getPromise(ALL_POKEMONS_URL);

const consumer = () =&gt; {
    promise.then(
        (result) =&gt; {
            console.log({result}); // 50 가지의 포켓몬 결과 출력
        },
        (error) =&gt; {
            // 유효한 URL이라면 실행되지 않는 부분
            console.log('We have encountered an Error!'); // 에러 출력
    });
}

consumer();
</code></pre>
<p>예시 2: 유효하지 않은 URL일 때</p>
<pre><code class="language-js">const POKEMONS_BAD_URL = 'https://pokeapi.co/api/v2/pokemon-bad/';

// 아래 코드는 URL의 404 에러로 인해 거절됩니다
let promise = getPromise(POKEMONS_BAD_URL);

const consumer = () =&gt; {
    promise.then(
        (result) =&gt; {
            // 프로미스는 이행(resolve)되지 않았기 때문에 이 부분은 실행되지 않습니다.
            console.log({result});
        },
        (error) =&gt; {
            // 거절된(rejected) 프로미스는 이 부분을 실행할 것입니다.
            console.log('We have encountered an Error!'); // 에러 출력
        }
    );
}

consumer();
</code></pre>
<h3 id="catch"><code>.catch()</code> 메서드를 사용하는 방법</h3>
<p>프로미스의 에러(거부)를 다룰 때 이 메서드를 사용할 수 있습니다. <code>.then()</code>의 첫 번째 인자로 <code>null</code>을 전달해주는 문법은 에러를 다루기 좋은 방법은 아닙니다. 그래서 같은 작업을 위해 <code>.catch()</code>를 사용해 더 깔끔한 문법으로 작성할 수 있습니다.</p>
<pre><code class="language-js">// 유효하지 않은 URL(404 에러)로 인해 거부될 프로미스
let promise = getPromise(POKEMONS_BAD_URL);

const consumer = () =&gt; {
    promise.catch(error =&gt; console.log(error));
}

consumer();
</code></pre>
<p>프로미스 실행 함수나 핸들러에서 <code>reject</code> 함수를 실행하는 대신 <code>new Error("Something wrong!")</code>과 같은 에러를 던지게 되면 이 프로미스는 거부된 것으로 해석될 것입니다. 즉 <code>.catch</code> 핸들러 메서드에 의해 에러가 잡히게 됩니다.</p>
<p>프로미스 실행 함수와 핸들러 함수에서 일어나는 <em>동기적인</em> 예외 상황에서도 같게 동작합니다.</p>
<p>다음은 프로미스가 거부된 것으로 처리되어 <code>.catch</code> 핸들러 메서드가 호출된 예시입니다.</p>
<pre><code class="language-js">new Promise((resolve, reject) =&gt; {
  throw new Error("Something is wrong!");// reject 함수를 실행하지 않습니다
}).catch((error) =&gt; console.log(error)); 
</code></pre>
<h3 id="finally"><code>.finally()</code> 메서드를 사용하는 방법</h3>
<p><code>.finally()</code> 핸들러는 로더를 멈추거나 실시간 연결을 끊는 등 정리를 위해 사용됩니다. <code>finally</code> 메서드는 프로미스의 <code>성공</code>이나 <code>거부</code> 여부와 상관 없이 실행됩니다. 이 메서드는 .then()이나 .catch()를 또 실행할 수 있는 다음 핸들러에게 결과 값이나 에러를 전달합니다.</p>
<p>세 메서드를 함께 이해할 수 있는 예시를 살펴보겠습니다.</p>
<pre><code class="language-js">let loading = true;
loading &amp;&amp; console.log('Loading...');

// 프로미스 얻기
promise = getPromise(ALL_POKEMONS_URL);

promise.finally(() =&gt; {
    loading = false;
    console.log(`Promise Settled and loading is ${loading}`);
}).then((result) =&gt; {
    console.log({result});
}).catch((error) =&gt; {
    console.log(error)
});
</code></pre>
<p>추가 설명</p>
<ul>
<li><code>.finally()</code> 메서드는 로딩을 <code>false</code>로 설정합니다.</li>
<li>프로미스가 이행되면 <code>.then()</code> 메서드가 실행됩니다. 프로미스가 에러와 함께 거부되면 <code>.catch()</code> 메서드가 실행됩니다. <code>.finally()</code>는 이행 혹은 거부와는 관계 없이 실행됩니다.</li>
</ul>
<h2 id="">프로미스 체인이란</h2>
<p><code>promise.then()</code>은 항상 프로미스를 반환합니다. 이 프로미스는 <code>pending</code>이라는 <code>상태</code>와 <code>undefined</code>라는 <code>결과</code>를 갖게될 것입니다. 이를 통해 다음 프로미스에 <code>.then</code> 메서드를 호출할 수 있게 해줍니다.</p>
<p>첫 번째 <code>.then</code> 메서드가 반환한 결과 값은 다음 <code>.then</code> 메서드가 받을 수 있습니다. 두 번째 결과는 이제 세 번째 <code>.then()</code>으로 전달될 수 있고 이런 방식으로 계속 이어질 수 있습니다. 이 방식은 프로미스를 밑으로 전달해주는 <code>.then</code> 메서드 체인을 형성합니다. 이 현상은 <code>프로미스 체인</code>이라고 불립니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/12/image-105.png" alt="프로미스 체인" width="600" height="400" loading="lazy"></p>
<p align="center">프로미스 체인</p>
<p>다음 예제를 봅시다.</p>
<pre><code class="language-js">let promise = getPromise(ALL_POKEMONS_URL);

promise.then(result =&gt; {
    let onePokemon = JSON.parse(result).results[0].url;
    return onePokemon;
}).then(onePokemonURL =&gt; {
    console.log(onePokemonURL);
}).catch(error =&gt; {
    console.log('In the catch', error);
});
</code></pre>
<p>먼저 프로미스를 이행하고 첫번째 포켓몬을 얻기 위한 URL을 추출합니다. 그렇게 얻은 결과는 프로미스 형태로 다음 .then() 핸들러로 전달됩니다. 결과는 다음과 같습니다.</p>
<pre><code class="language-js">https://pokeapi.co/api/v2/pokemon/1/
</code></pre>
<p><code>.then</code> 메서드는 다음 중 하나를 반환합니다.</p>
<ul>
<li>결과 (이미 살펴본 것이죠)</li>
<li>새로운 프로미스</li>
</ul>
<p>에러를 반환할 수도 있습니다.</p>
<p>다음 예시는 결과와 새로운 프로미스를 반환하는 <code>.then</code> 메서드로 이루어진 프로미스 체인을 만든 것입니다.</p>
<pre><code class="language-js">// 다수의 then과 catch로 이루어진 프로미스 체인
let promise = getPromise(ALL_POKEMONS_URL);

promise.then(result =&gt; {
    let onePokemon = JSON.parse(result).results[0].url;
    return onePokemon;
}).then(onePokemonURL =&gt; {
    console.log(onePokemonURL);
    return getPromise(onePokemonURL);
}).then(pokemon =&gt; {
    console.log(JSON.parse(pokemon));
}).catch(error =&gt; {
    console.log('In the catch', error);
});
</code></pre>
<p>첫 번째 <code>.then()</code>은 추출한 URL을 결과로 반환합니다. 이 URL은 해당 URL을 인자로 받는 새로운 프로미스를 반환하는 두 번째 <code>.then</code>으로 전달됩니다.</p>
<p>이 프로미스가 이행되면 포켓몬에 대한 정보 얻을 수 있는 체인으로 전달됩니다. 결과는 다음과 같습니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/image-159.png" alt="콘솔로 확인한 프로미스 체인의 결과" width="600" height="400" loading="lazy"></p>
<p align="center">프로미스 체인의 결과</p>
<p>에러나 프로미스 거부가 발생하면 프로미스 체인의 .catch 메서드가 실행됩니다.</p>
<p>중요한 점: <code>.then</code>을 여러번 호출한다고 프로미스 체인이 생성되는 것은 아닙니다. 다음과 같이 코드를 작성하면 버그만 발생할 것입니다.</p>
<pre><code class="language-js">let promise = getPromise(ALL_POKEMONS_URL);

promise.then(result =&gt; {
    let onePokemon = JSON.parse(result).results[0].url;
    return onePokemon;
});
promise.then(onePokemonURL =&gt; {
    console.log(onePokemonURL);
    return getPromise(onePokemonURL);
});
promise.then(pokemon =&gt; {
    console.log(JSON.parse(pokemon));
});
</code></pre>
<p>같은 프로미스에 <code>.then</code> 메서드를 세 번 호출하고 있지만 프로미스를 전달하고 있지는 않습니다. 이것은 프로미스 체인과는 다릅니다. 위 예시의 결과로 에러가 나올 것입니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/image-160.png" alt="프로미스 체인을 잘못 사용했을 때 나타나는 에러" width="600" height="400" loading="lazy"></p>
<h2 id="">여러 개의 프로미스를 처리하는 방법</h2>
<p>.then, .catch, .finally 같은 핸들러 메서드와는 별개로 프로미스 API에서 사용 가능한 여섯 가지의 정적 메서드가 있습니다. 첫 네 개의 메서드들은 프로미스 배열을 받아 병렬적으로 실행합니다.</p>
<ol>
<li>Promise.all</li>
<li>Promise.any</li>
<li>Promise.allSettled</li>
<li>Promise.race</li>
<li>Promise.resolve</li>
<li>Promise.reject</li>
</ol>
<p>하나씩 살펴봅시다.</p>
<h3 id="promiseall">Promise.all() 메서드</h3>
<p><code>Promise.all([promises])</code>는 인자로 프로미스의 집합(예를 들면, 배열)을 받고 병렬로 실행시킵니다.</p>
<p>이 메서드는 모든 프로미스가 이행(resolve)되기를 기다린 후에 프로미스의 결과를 배열로 반환합니다. 만약 프로미스 중 어떤 하나라도 거절(reject)되거나 에러로 인해 실패한다면 다른 모든 프로미스의 결과는 무시될 것입니다.</p>
<p>세 마리의 포켓몬에 대한 정보를 얻기 위해 세 개의 프로미스를 생성해봅시다.</p>
<pre><code class="language-js">const BULBASAUR_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon/bulbasaur';
const RATICATE_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon/raticate';
const KAKUNA_POKEMONS_URL = 'https://pokeapi.co/api/v2/pokemon/kakuna';

let promise_1 = getPromise(BULBASAUR_POKEMONS_URL);
let promise_2 = getPromise(RATICATE_POKEMONS_URL);
let promise_3 = getPromise(KAKUNA_POKEMONS_URL);
</code></pre>
<p>Promise.all() 메서드에 프로미스 배열을 전달해 사용합니다.</p>
<pre><code class="language-js">Promise.all([promise_1, promise_2, promise_3]).then(result =&gt; {
    console.log({result});
}).catch(error =&gt; {
    console.log('에러 발생');
});
</code></pre>
<p>결과:<br>
<img src="https://www.freecodecamp.org/news/content/images/2020/11/image-161.png" alt="Promise.all() 메서드의 처리 결과" width="600" height="400" loading="lazy"></p>
<p>결과에서 볼 수 있듯, 모든 프로미스의 결과가 반환됩니다. 모든 프로미스를 실행시키는 시간은 프로미스가 실행되는 데 걸리는 최대 시간과 같습니다.</p>
<h3 id="promiseany">Promise.any() 메서드</h3>
<p><code>all()</code> 메서드와 비슷한 <code>Promise.any([promises])</code> 또한 병렬로 처리할 프로미스를 배열로 받습니다. 이 메서드는 모든 프로미스가 이행(resolve)되기를 기다리지 않습니다. 프로미스 중 하나라도 settled 되면 이행이 종료됩니다.</p>
<pre><code class="language-js"> Promise.any([promise_1, promise_2, promise_3]).then(result =&gt; {
     console.log(JSON.parse(result));
 }).catch(error =&gt; {
     console.log('에러 발생');
 });
</code></pre>
<p>결과는 이행(resolve)된 프로미스 중 하나가 될 것입니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/image-162.png" alt="프로미스가 하나라도 이행된 경우의 콘솔 결과" width="600" height="400" loading="lazy"></p>
<h3 id="promiseallsettled">Promise.allSettled() 메서드</h3>
<p><code>Promise.allSettled([promises])</code> 메서드는 모든 프로미스가 settled(resolve/reject) 되기를 기다린 후에 객체로 구성된 배열을 결과로 반환합니다. 성공적으로 이행된다면 결과는 상태(fulfilled/rejected)와 결괏값을 포함할 것입니다. 거절된(rejected) 상태라면 에러에 대한 이유를 반환할 것입니다.</p>
<p>이행된 프로미스들의 예시를 살펴보겠습니다.</p>
<pre><code class="language-js">Promise.allSettled([promise_1, promise_2, promise_3]).then(result =&gt; {
    console.log({result});
}).catch(error =&gt; {
    console.log('에러입니다!');
});
</code></pre>
<p>결과:<br>
<img src="https://www.freecodecamp.org/news/content/images/2020/11/image-163.png" alt="모든 프로미스가 이행된 경우의 콘솔 결과" width="600" height="400" loading="lazy"></p>
<p>프로미스 중 하나인 promise_1이 거부된다면 다음과 같은 결과가 나옵니다.</p>
<pre><code class="language-js">let promise_1 = getPromise(POKEMONS_BAD_URL);
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/image-164.png" alt="promise_1만 거부된 경우의 콘솔 결과" width="600" height="400" loading="lazy"></p>
<h3 id="promiserace">Promise.race() 메서드</h3>
<p><code>Promise.race([promises])</code>는 (가장 빠른) 첫번째 프로미스가 settled 되기를 기다린 후에 그에 따른 결과/에러를 반환합니다.</p>
<pre><code class="language-js">Promise.race([promise_1, promise_2, promise_3]).then(result =&gt; {
    console.log(JSON.parse(result));
}).catch(error =&gt; {
    console.log('에러 발생');
});
</code></pre>
<p>이행된(resolved) 가장 빠른 프로미스의 결과는 다음과 같습니다.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/11/image-165.png" alt="race 메서드의 콘솔 출력 결과" width="600" height="400" loading="lazy"></p>
<h3 id="promiseresolvereject">Promise.resolve/reject 메서드</h3>
<p><code>Promise.resolve(value)</code>는 전달된 값과 함께 프로미스를 이행합니다. 다음과 같습니다.</p>
<pre><code class="language-js">let promise = new Promise(resolve =&gt; resolve(value));
</code></pre>
<p><code>Promise.reject(error)</code>은 다음과 같이 전달된 에러와 함께 프로미스를 거부합니다.</p>
<pre><code class="language-js">let promise = new Promise((resolve, reject) =&gt; reject(error));
</code></pre>
<h2 id="pizzahub">PizzaHub 예시를 프로미스로 바꿀 수 있나요?</h2>
<p>그럼요, 한번 해봅시다. <code>query</code> 메서드가 프로미스를 반환한다고 가정해봅시다. 여기 query() 메서드의 예시가 있습니다. 실제로 이 메서드는 데이터베이스와 소통해서 결과를 반환합니다. 지금 경우에는 하드코딩되어 있지만 같은 역할을 수행합니다.</p>
<pre><code class="language-js">function query(endpoint) {
  if (endpoint === `/api/pizzahub/`) {
    return new Promise((resolve, reject) =&gt; {
      resolve({'shopId': '123'});
    })
  } else if (endpoint.indexOf('/api/pizzahub/pizza/') &gt;=0) {
    return new Promise((resolve, reject) =&gt; {
      resolve({pizzas: [{'type': 'veg', 'name': 'margherita', 'id': '123'}]});
    })
  } else if (endpoint.indexOf('/api/pizzahub/beverages') &gt;=0) {
    return new Promise((resolve, reject) =&gt; {
      resolve({id: '10', 'type': 'veg', 'name': 'margherita', 'beverage': 'coke'});
    })
  } else if (endpoint === `/api/order`) {
    return new Promise((resolve, reject) =&gt; {
      resolve({'type': 'veg', 'name': 'margherita', 'beverage': 'coke'});
    })
  }
}
</code></pre>
<p>다음은 <code>콜백 지옥</code>을 리팩토링 해보겠습니다. 이를 위해 먼저 몇개의 논리적인 함수를 만들겠습니다.</p>
<pre><code class="language-js">// 가게 id 반환
let getShopId = result =&gt; result.shopId;

// 가게의 피자 리스트가 담긴 프로미스 반환
let getPizzaList = shopId =&gt; {
  const url = `/api/pizzahub/pizza/${shopId}`;
  return query(url);
}

// 고객의 요청에 맞는 피자를 프로미스로 반환
let getMyPizza = (result, type, name) =&gt; {
  let pizzas = result.pizzas;
  let myPizza = pizzas.find((pizza) =&gt; {
    return (pizza.type===type &amp;&amp; pizza.name===name);
  });
  const url = `/api/pizzahub/beverages/${myPizza.id}`;
  return query(url);
}

// 주문 후 프로미스 반환
let performOrder = result =&gt; {
  let beverage = result.id;
   return query(`/api/order`, {'type': result.type, 'name': result.name, 'beverage': result.beverage});
}

// 주문 확인
let confirmOrder = result =&gt; {
    console.log(`Your order of ${result.type} ${result.name} with ${result.beverage} has been placed!`);
}
</code></pre>
<p>필요한 프로미스를 만들기 위해 위 함수들을 사용합니다. 여기서 <code>콜백 지옥</code> 예제와 비교해볼 수 있습니다. 아래 코드는 확실히 깔끔하고 우아하네요.</p>
<pre><code class="language-js">function orderPizza(type, name) {
  query(`/api/pizzahub/`)
  .then(result =&gt; getShopId(result))
  .then(shopId =&gt; getPizzaList(shopId))
  .then(result =&gt; getMyPizza(result, type, name))
  .then(result =&gt; performOrder(result))
  .then(result =&gt; confirmOrder(result))
  .catch(function(error){
    console.log(`Bad luck, No Pizza for you today!`);
  })
}
</code></pre>
<p>마지막으로 다음과 같이 피자 종류와 이름을 전달해서 orderPizza() 메서드를 호출합니다.</p>
<pre><code class="language-js">orderPizza('veg', 'margherita');
</code></pre>
<h2 id="">다음으로 배울 내용</h2>
<p>여기까지 거의 읽으셨다면 축하드립니다! 이제 여러분은 자바스크립트의 프로미스에 대해 더 잘 이해할 수 있으실 겁니다. 이 글에서 사용된 모든 예제는 이 <a href="https://github.com/atapas/js-promise-example">깃허브 레포지토리</a>에 있습니다.</p>
<p>다음으로는 더 간단하게 코드를 작성할 수 있는 자바스크립트의 <code>async</code> 함수를 배울 차례입니다. 자바스크립트의 프로미스를 학습할 수 있는 가장 좋은 방법은 작은 예제를 만들고 쌓아가는 것입니다.</p>
<p>Angular, React, Vue 등과 같은 프레임워크나 라이브러리 사용과는 관계 없이 비동기 처리는 필수적입니다. 즉, 일을 더 잘 처리하기 위해선 프로미스를 이해해야 합니다.</p>
<p>또한 이제 여러분은 <code>fetch</code> 메서드가 더 쉽게 느껴지실 겁니다.</p>
<pre><code class="language-js">fetch('/api/user.json')
.then(function(response) {
    return response.json();
})
.then(function(json) {
    console.log(json); // {"name": "tapas", "blog": "freeCodeCamp"}
});
</code></pre>
<ul>
<li><code>fetch</code> 메서드는 프로미스를 반환합니다. 그래서 여기에 <code>.then</code> 핸들러를 호출할 수 있습니다.</li>
<li>나머지는 우리가 글에서 배웠던 프로미스 체인에 관한 내용입니다.</li>
</ul>
<h2 id="">마치기 전에</h2>
<p>지금까지 읽어주셔서 감사합니다. <a href="https://twitter.com/tapasadhikary">트위터(@tapasadhikary)</a>에서 소통해요.</p>
<p>좋아하실 만한 글들을 추천드릴게요.</p>
<ul>
<li><a href="https://blog.greenroots.info/javascript-undefined-and-null-lets-talk-about-it-one-last-time">JavaScript undefined and null: Let's talk about it one last time!</a></li>
<li><a href="https://blog.greenroots.info/javascript-equality-comparison-with-and-objectis">JavaScript: Equality comparison with ==, === and Object.is</a></li>
<li><a href="https://www.freecodecamp.org/news/javascript-this-keyword-binding-rules/">The JavaScript <code>this</code> Keyword +5 Key Binding Rules Explained for JS Beginners</a></li>
<li><a href="https://www.freecodecamp.org/news/javascript-typeof-how-to-check-the-type-of-a-variable-or-object-in-js/">JavaScript TypeOf - How to Check the Type of a Variable or Object in JS</a></li>
</ul>
<p>여기까지입니다. 다음 글에서 만나요. 그때까지 모두 잘 지내세요.</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
