<?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[ NodeJS - freeCodeCamp.org ]]>
        </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[ NodeJS - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/korean/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 16 Jun 2026 21:15:08 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/korean/news/tag/nodejs/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[ Ubuntu에 Node.js를 설치하고 npm을 최신 버전으로 업데이트하는 방법 ]]>
                </title>
                <description>
                    <![CDATA[  apt-package manager를 사용하여 최신 버전의 노드를 설치하려고 하면 v10.19.0이 설치됩니다. 이것은 ubuntu 앱스토어의 최신 버전이지만, NodeJS의 최신 버전은 아닙니다. 새로운 버전의 소프트웨어가 출시될 때, Ubuntu 팀이 테스트하고 공식 Ubuntu 스토어에 배포하는데 수개월이 걸릴 수 있기 때문입니다. 따라서 소프트웨어의 최신 버전을 받으려면 개발자가 게시한 비공개 패키지를 사용해야 할 수도 있습니다. ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/how-to-install-node-js-on-ubuntu-and-update-npm-to-the-latest-version/</link>
                <guid isPermaLink="false">64d63da2e7425543c7709486</guid>
                
                    <category>
                        <![CDATA[ NodeJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jeeann K. ]]>
                </dc:creator>
                <pubDate>Mon, 14 Aug 2023 21:47:01 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2023/08/1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/how-to-install-node-js-on-ubuntu-and-update-npm-to-the-latest-version/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Install Node.js on Ubuntu and Update npm to the Latest Version</a>
      </p><h3></h3><!--kg-card-begin: markdown--><h3 id="aptpackagemanagerv10190ubuntunodejs">apt-package manager를 사용하여 최신 버전의 노드를 설치하려고 하면 v10.19.0이 설치됩니다. 이것은 ubuntu 앱스토어의 최신 버전이지만, NodeJS의 최신 버전은 아닙니다.</h3>
<h4 id="ubuntuubuntu">새로운 버전의 소프트웨어가 출시될 때, Ubuntu 팀이 테스트하고 공식 Ubuntu 스토어에 배포하는데 수개월이 걸릴 수 있기 때문입니다. 따라서 소프트웨어의 최신 버전을 받으려면 개발자가 게시한 비공개 패키지를 사용해야 할 수도 있습니다.</h4>
<p>이 튜토리얼에서는 Node의 v12.18.1(LTS - 장기 지원 포함) 또는 v14.4를 설치하려고 합니다. 최신 버전을 설치하려면 nodesource 또는 nvm(노드 버전 관리자)을 사용할 수 있습니다. 두 가지 쓰는 법을 모두 보여드릴게요.</p>
<p>여기서 모든 명령은 Ubuntu CLI/터미널을 사용하여 실행됩니다.</p>
<h2 id="nvm">NVM 사용하기 - 제가 선호하는 방법</h2>
<p>저는 nvm을 좋아합니다. 왜냐하면 여러 프로젝트에 다른 노드 버전을 사용할 수 있기 때문입니다. 가끔 다른 버전의 노드를 사용하는 누군가와 협업하게 될 수 있고 프로젝트가 요구하는 노드 버전으로 전환해야 합니다. 이럴 때 nvm이 최고의 도구입니다.</p>
<h2 id="nvm">NVM 설치</h2>
<pre><code>curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
</code></pre>
<p>nvm이 설치되었는지 확인하기 위해서 <code>nvm --version</code>을 쳐봅시다. <code>0.35.3</code> 같은 버전 숫자가 나온다면, nvm이 성공적으로 설치된 겁니다.<br>
변경 내용을 적용하려면 터미널을 다시 시작하세요.</p>
<h2 id="nodejs">NodeJS 설치</h2>
<p>다음으로 Nodejs 14.4 버전을 설치해봅시다.<br>
간단하게 <code>nvm install 14.4.0</code>를 실행해보세요.<br>
<code>nvm install 12.18.1</code> 같은 명령어를 사용해서 원하는 버전의 노드를 설치할 수 있습니다.<br>
이 명령어는 자동으로 nodejs를 설치하고 'v6.14.5'인 최신 npm 버전도 설치합니다.<br>
만약 노드 버전을 바꿔야 한다면, <code>nvm use &lt;version-number&gt;</code>을 실행하면 됩니다. 예를 들면 <code>nvm use v12.18.1.</code>.<br>
nvm과 함께 설치한 다양한 노드 버전을 열거하려면 <code>nvm ls</code>을 실행하세요.</p>
<h2 id="nodesource">Nodesource 설치</h2>
<p>아래 명령을 실행하여 Nodesource에서 Nodejs 패키지를 설치할 것임을 Ubuntu에 알립니다.</p>
<pre><code>curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
</code></pre>
<p>NB v14.4.0는 노드의 최신 버전이지만 현재 LTS(장기지원)이 제공되지 않습니다. LTS로 Node의 최신 버전을 설치하고 싶다면, dnl 명령어에서 <code>14</code>를 <code>12</code>로 바꾸세요.<br>
root 유저의 비밀번호를 입력하라는 말이 나올 수 있습니다. 입력 후 enter/return를 누르세요.</p>
<h2 id="nodejs">NodeJS 설치</h2>
<p>Nodesource 세팅이 끝나면, 이제 Nodejs v14.4 설치를 할 수 있습니다.<br>
<code>sudo apt-get install -y nodejs</code>를 실행하세요.<br>
완료되면, 최신 버전의 Node가 설치되었는지 확인할 수 있습니다. 간단하게 터미널에 <code>nodejs -v</code>를 쳐보세요. <code>v14.4.0</code>이 나와야 합니다.<br>
이 시점에서 npm이 자동으로 설치되어 있어야 합니다. 어떤 npm 버전이 있는지 확인하려면 <code>npm version</code>을 실행하세요. npm의 최신 버전인 6.14.5를 포함한 오브젝트가 안 나온다면, <code>{ npm: '6.14.5' }</code>, 아래 명령어를 사용해 npm을 수동으로 업데이트 할 수 있습니다.</p>
<pre><code>npm install -g npm@latest
</code></pre>
<p>npm이 설치되지 않아 업데이트할 수 없는 이슈가 뜬다면, <code>sudo apt-get install -y npm</code>를 사용해서 npm을 먼저 설치한 다음 위의 명령어로 업데이트할 수 있습니다.<br>
특정 npm 패키지를 실행하려면 아래 명령도 실행해야 합니다.</p>
<pre><code>sudo apt install build-essential
</code></pre>
<p>이게 다예요!<br>
여러분은 여러분의 Ubuntu 컴퓨터에 최신 버전의 NodeJS와 NPM을 설치했습니다.<br>
이제 멋진 제품들을 만들러 가보세요 :)</p>
<!--kg-card-end: markdown--><p></p><p></p><p></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Node.js 자식 프로세스(Child Process): 알아야 할 모든 것 ]]>
                </title>
                <description>
                    <![CDATA[  spawn(), exec(), execFile(), fork() 사용법 > 업데이트: 이 글은 현재 필자의 책 "Node.js Beyond The Basics"의 일부입니다. > jscomplete.com/node-beyond-basics [https://github.com/samerbuna/efficient-node] 에서 Node에 대한 더 많은 정보와 이 글에 대한 갱신된 내용을 읽을 수 있습니다. Node.js에서 단일 스레드(single thread), 논-블라킹(non-blocking - 타 작업 수행 허용) 성능은 단일 프로세스에서 확실히 뛰어납니다. ]]>
                </description>
                <link>https://www.freecodecamp.org/korean/news/node-js-child-processes-everything-you-need-to-know-e69498fe970a/</link>
                <guid isPermaLink="false">64cba01f36770806964a6e37</guid>
                
                    <category>
                        <![CDATA[ NodeJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ SeunghyunKim ]]>
                </dc:creator>
                <pubDate>Mon, 14 Aug 2023 21:38:13 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/korean/news/content/images/2023/08/nodejs-child-proecss-image.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>기사 원문:</strong> <a href="https://www.freecodecamp.org/news/node-js-child-processes-everything-you-need-to-know-e69498fe970a/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Node.js Child Processes: Everything you need to know</a>
      </p><h4></h4><h2 id="spawn-exec-execfile-fork-">spawn(), exec(), execFile(), fork() 사용법</h2><blockquote>업데이트: 이 글은 현재 필자의 책 "Node.js Beyond The Basics"의 일부입니다.</blockquote><blockquote><a href="https://github.com/samerbuna/efficient-node">jscomplete.com/node-beyond-basics</a>에서 Node에 대한 더 많은 정보와 이 글에 대한 갱신된 내용을 읽을 수 있습니다.</blockquote><p>Node.js에서 단일 스레드(single thread), 논-블라킹(non-blocking - 타 작업 수행 허용) 성능은 단일 프로세스에서 확실히 뛰어납니다. 하지만 결국, 단일 CPU에 단일 프로세스는 어플리케이션의 늘어나는 작업량을 감당하기에는 충분하지 않습니다.</p><p>서버의 성능이 얼마나 좋은지에 상관없이 단일 스레드는 한정된 처리량만을 지원할 수 밖에 없습니다.</p><p>Node.js가 단일 스레드에서 동작하는 것이 다중 프로세스나 다중 기기의 이점을 얻을 수 없다는 것을 의미하지는 않습니다.</p><p>다중 프로세스를 사용하는 것이 Node 어플리케이션의 크기 조정을 하는데 가장 좋은 방법입니다. Node.js는 많은 Node를 가진 분산(distributed) 어플리케이션을 만들기 위해 설계되었습니다. 이름이 Node인 이유는 여기서 찾을 수 있습니다. 확장성(Scalability)은 Node.js에 이미 탑재가 되었으며 어플리케이션의 생애주기 동안은 생각할 필요가 없는 요소입니다.</p><blockquote>이 글은 <a href="https://www.pluralsight.com/courses/nodejs-advanced" rel="nofollow">개인 Pluralsight Node.js 강의</a>의 일부입니다. 이 웹페이지에 비슷한 내용을 비디오로 담았습니다.</blockquote><p>이 글을 읽기 전에 Node.js 이벤트(event)와 스트림(stream)에 대한 이해가 필요합니다. 아직 두 요소에 대한 이해가 부족하시다면 이 글을 읽기 전에 다음의 두 글을 먼저 읽기를 권합니다.</p><p><a href="https://www.freecodecamp.org/news/understanding-node-js-event-driven-architecture-223292fcbc2d/" rel="nofollow">Node.js 이벤트 구동 구조에 대한 이해대부분의 Node 객체 - HTTP 요청, 응답과 스트림(stream) 등 - 는 이벤트 에미터(EventEmitter)를 실행하는데...</a></p><p><a href="https://medium.freecodecamp.com/node-js-streams-everything-you-need-to-know-c9141306be93" rel="nofollow">스트림(Stream): 알아야 할 모든 것</a>Node.js 스트림은 같이 사용하기 어려운 것으로 유명하지만 이해하는 게 더 어려운 걸로도 유명합니다. 하지만 좋은 소식이 있습니다...</p><h2 id="-child-process-">자식 프로세스(Child Process) 모듈</h2><p>자식 프로세스는 Node.js <code>child_process</code> 모듈을 통해 쉽게 만들어질 수 있으며 그 자식 프로세스들은 메시징 시스템(messaging system)을 통해 서로 쉽게 소통할 수 있습니다.</p><p><code>child_process</code> 모듈은 자식 프로세스 안에서 모든 시스템 명령어를 실행함으로써 운영 체제 기능들을 접근하게 해줍니다.</p><p>자식 프로세스의 입력 스트림을 제어하고 이 입력에 대한 출력 스트림을 리슨(Listen)합니다(리슨은 어떠한 대상을 받아 처리할 준비를 하는 것으로 이해하시면 됩니다. - 역주). 또한, 기저 OS 명령어에 전달되는 인자(argument)들을 제어할 수 있고 그 명령어들의 출력을 이용해 우리가 원하는 무엇이든 할 수 있습니다. 예를 들면, 하나의 명령어에 대한 출력을 다른 명령어의 입력으로 연결시킬 수 있습니다(Linux에서 하는 것과 같습니다). 이 명령어들의 입/출력이 Node.js 스트림을 통해서 표현될 수 있기 때문입니다.</p><p><em>이 글에 사용되는 예시들은 모두 Linux가 바탕이라는 것을 알아두시길 바랍니다. 윈도우에서는 필자가 사용하는 명령어의 대체 명령어로 바꿔 주어야 합니다.</em></p><p>Node에서 자식 프로세스를 생성하는 방법은 다음과 같이 4개가 있습니다: <code>spawn()</code>, <code>fork()</code>, <code>exec()</code>, <code>execFile()</code>.</p><p>이 4개의 함수들 간의 차이와 언제 사용하는지에 대하여 알아보겠습니다.</p><h2 id="spawn-">Spawn으로 생성된 자식 프로세스</h2><p><code>spawn</code> 함수는 새 프로세스에서 한 명령어를 시작시키며 그 명령어에 어떤 인자든지 전달하기 위해 이 함수를 사용할 수 있습니다. 예를 들면, 여기 <code>pwd</code> 명령어를 실행할 새 프로세스를 스폰(spawn)하는 코드입니다.</p><pre><code>const { spawn } = require('child_process');

const child = spawn('pwd');</code></pre><p><code>spawn</code> 함수는 간단히 <code>child_process</code> 모듈로부터 구조 분해(destructuring)하여 얻을 수 있으며 첫 번째 인자로 OS 명령어를 넣어 이 함수를 실행시킬 수 있습니다.</p><p>위 <code>spawn</code> 함수를 실행시켜 얻은 결과(위 <code>child</code> 객체)는 <code>ChildProcess</code> 인스턴스이며 <a href="https://medium.freecodecamp.com/understanding-node-js-event-driven-architecture-223292fcbc2d" rel="nofollow">이벤트 에미터(EventEmitter) API</a>를 실행합니다. 이는 그 <code>child</code> 객체에 이벤트를 처리하는 핸들러(handler)를 등록시킬 수 있다는 것을 뜻합니다. 예를 들면, <code>exit</code> 이벤트 핸들러 등록하면 자식 프로세스가 종료될 때 뭔가를 할 수 있습니다.</p><pre><code>child.on('exit', function (code, signal) {
  console.log('child process exited with ' +
              `code ${code} and signal ${signal}`);
});</code></pre><p>위의 핸들러는 자식 프로세스의 종료 <code>code</code>와 종료시킬 때 사용되는 <code>signal</code>를 줍니다. 이 <code>signal</code> 변수는 보통 자식 프로세스가 종료될 때 null입니다.</p><p><code>ChildProcess</code> 인스턴스에 핸들러를 등록할 수 있는 다른 이벤트로는 <code>disconnect</code>, <code>error</code>, <code>close</code> 그리고 <code>message</code>가 있습니다.</p><ul><li><code>disconnect</code> 이벤트는 부모 프로세스가 <code>child.disconnect</code> 함수를 직접 호출할 때 발생합니다(emitted).</li><li><code>error</code> 이벤트는 프로세스가 생성될 수 없거나 어떠한 이유로 멈출 때(killed) 발생합니다.</li><li><code>close</code> 이벤트는 자식 프로세스의 <code>stdio</code> 스트림이 종료될 때 발생합니다.</li><li><code>message</code> 이벤트는 가장 중요한 이벤트입니다. 이 이벤트는 메시지를 보내기 위해 자식 프로세스가 <code>process.send()</code> 함수를 사용할 때 발생합니다. 이 방법으로 부모/자식 프로세스 간에 소통을 할 수 있게 됩니다. 아래에서 이에 대한 예를 살펴보겠습니다.</li></ul><p>모든 자식 프로세스는 세 개의 표준 <code>stdio</code> 스트림도 갖고 있는데 이 스트림들은 <code>child.stdin</code>, <code>child.stdout</code>, <code>child.stderr</code>를 사용하여 접근할 수 있습니다.</p><p>그 스트림들이 종료될 때, 그것들을 사용하던 자식 프로세스가 <code>close</code> 이벤트를 발생시킵니다. 이 <code>close</code> 이벤트는 <code>exit</code> 이벤트와는 다른데 여러 자식 프로세스들이 같은 <code>stdio</code> 스트림을 공유할 지도 모르며 그 중 한 자식 프로세스가 종료(exit)된다는 것이 그 스트림들이 모두 종료(close)되는 것을 의미하지 않기 때문입니다.</p><p>모든 스트림이 이벤트 에미터(Event Emitter)이기 때문에 모든 자식 프로세스에 연결된 이 <code>stdio</code> 스트림들에서 다른 이벤트들을 리슨할 수 있습니다. 그렇지만 보통의 프로세스와 달리 자식 프로세스에서는 <code>stdin</code> 스트림이 쓰기가 가능한 스트림인 반면 <code>stdout</code>/<code>stderr</code> 스트림들은 읽기가 가능한 스트림입니다. 이는 기본적으로 주 프로세스(main process)에서의 스트림들의 유형과 정반대입니다. 이 스트림들에 사용하는 이벤트들이 표준 이벤트들입니다. 가장 중요한 것은 읽기가 가능한 스트림에서는 명령어에 대한 출력과 명령어 실행 시 발생하는 에러가 담긴 <code>data</code> 이벤트를 리슨할 수 있다는 것입니다.</p><pre><code>child.stdout.on('data', (data) =&gt; {
  console.log(`child stdout:\n${data}`);
});

child.stderr.on('data', (data) =&gt; {
  console.error(`child stderr:\n${data}`);
});</code></pre><p>위의 두 핸들러는 두 로그(log)가 각각 주 프로세스 <code>stdout</code>과 <code>stderr</code>에 출력될 것입니다. 위에서 언급했던 <code>spawn</code> 함수를 실행할 때, <code>pwd</code> 명령어에 대한 결과가 출력되고 자식 프로세스는 에러가 일어나지 않았다는 의미인 code <code>0</code>과 함께 종료됩니다.</p><p><code>spawn</code> 함수의 두번째 인자를 사용하여 <code>spawn</code> 함수에 의해 실행되는 명령어에 인자(리눅스 명령어에 넣을 인자 - 역주)를 넣을 수 있는데 이 때, 이 두번째 인자는 명령어에 전달되는 모든 인자의 배열입니다. 예를 들면, <code>find</code> 명령어를 현재 디렉토리(directory)에서 <code>-type f</code> 인자(파일 유형만 찾기)와 함께 실행시키려면 다음과 같이 하면 됩니다:</p><pre><code>const child = spawn('find', ['.', '-type', 'f']);</code></pre><p>명령어 실행 중 에러가 발생한다면, 예를 들어, 유효하지 않은 경로를 <code>find</code>에 입력하면 <code>child.stderr</code>, <code>data</code> 이벤트 핸들러가 동작이 되고 <code>exit</code> 이벤트 핸들러가 에러가 발생했다는 신호로 종료 code <code>1</code>을 알리게 될 것입니다. 이 에러 값들은 실제 호스트(host) OS와 에러 유형에 따라 달라집니다.</p><p>자식 프로세스 <code>stdin</code>은 쓰기가 가능한 스트림입니다. 명령어에 입력을 보내기 위해 <code>stdin</code>을 활용할 수 있습니다. 여느 쓰기가 가능한 스트림처럼 <code>stdin</code>을 사용하는 데 가장 쉬운 방법은 <code>pipe</code> 함수를 사용하는 것입니다. 간단하게 읽기가 가능한 스트림을 쓰기가 가능한 스트림에 연결하면 됩니다. 주 프로세스 <code>stdin</code>이 읽기가 가능한 스트림이므로 이를 자식 프로세스 <code>stdin</code> 스트림에 연결할 수 있습니다. 예를 들면 다음과 같습니다:</p><pre><code>const { spawn } = require('child_process');

const child = spawn('wc');

process.stdin.pipe(child.stdin)

child.stdout.on('data', (data) =&gt; {
  console.log(`child stdout:\n${data}`);
});</code></pre><p>위 예시에서 자식 프로세스는 리눅스에서 줄, 단어와 글자들을 세는 <code>wc</code> 명령어를 호출합니다. 그런 다음에 주 프로세스 <code>stdin</code>(읽기가 가능한 스트림)을 자식 프로세스 <code>stdin</code>(쓰기 가능한 스트림)과 연결시킵니다. 이 조합의 결과는 우리가 타자를 칠 수 있는 표준 입력 모드를 얻는 것이며 <code>Ctrl+D</code>를 누르면 우리가 입력한 것이 <code>wc</code> 명령어의 입력으로 사용될 것입니다.</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/9776f5fedbac7577130549ebb47952ddf480b4138bfe3e95b33347b7ee9b97b1/68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f312a7339645159394764676b6b4966397a4331424c3642672e676966" class="kg-image" alt="Gif captured from my Pluralsight course — Advanced Node.js" width="600" height="400" loading="lazy"></figure><p>또한, 리눅스 명령어에서 하는 것처럼 여러 프로세스의 표준 입/출력을 서로에게 연결시킬 수 있습니다. 예를 들면, 현재 디렉토리에 있는 모든 파일의 수를 파악하기 위해 <code>find</code> 명령어의 <code>stdout</code>을 <code>wc</code> 명령어의 <code>stdin</code>과 연결시킬 수 있습니다.</p><pre><code>const { spawn } = require('child_process');

const find = spawn('find', ['.', '-type', 'f']);
const wc = spawn('wc', ['-l']);

find.stdout.pipe(wc.stdin);

wc.stdout.on('data', (data) =&gt; {
  console.log(`Number of files ${data}`);
});</code></pre><p>필자는 줄의 수만 세기 위해 <code>wc</code> 명령어에 <code>-l</code> 인자를 더했습니다. 위에 있는 코드가 실행이 되면 현재 디렉토리에 있는 모든 디렉토리 안의 파일들의 수를 출력하게 됩니다.</p><h2 id="shell-exec-">Shell 문법과 exec 함수</h2><p>기본적으로 <code>spawn</code> 함수는 전달되는 명령어를 실행하기 위해 <em>shell</em> 을 생성하지 않습니다. 이게 shell을 생성하는 <code>exec</code> 함수보다 효율적인 점입니다. <code>exec</code> 함수는 다른 주요 차이점이 있습니다. 이 함수는 명령어에 의해 생성된 출력을 <em>버퍼(buffer)에 저장</em> 하고 이 전체 출력값을 콜백 함수에 보냅니다(<code>spawn</code> 함수는 대신에 스트림을 사용합니다).</p><p>이전에 살펴본 <code>exec</code> 함수에 적용된 <code>find | wc</code> 예시입니다.</p><pre><code>const { exec } = require('child_process');

exec('find . -type f | wc -l', (err, stdout, stderr) =&gt; {
  if (err) {
    console.error(`exec error: ${err}`);
    return;
  }

  console.log(`Number of files ${stdout}`);
});</code></pre><p><code>exec</code> 함수는 명령어 실행을 위해 shell을 사용하기 때문에 shell <em>pipe</em> 기능을 이용하여 <em>shell 문법</em> 을 바로 사용할 수 있습니다.</p><p>여기서 외부에서 제공되는 동적 입력을 실행한다면 shell 문법 사용이 보안 위험이 수반된다는 것을 알아두셔야 합니다. 사용자가 ;과 $같은 shell 문법 글자들을 사용하여 명령어 주입 공격을 쉽게 할 수 있게 됩니다.(예를 들면, <code>command + ’; rm -rf ~’</code>)</p><p><code>exec</code> 함수 출력을 버퍼에 저장하고 그 출력을 <code>stdout</code> 인자로 콜백 함수(<code>exec</code>의 두 번째 인자)에 보냅니다. 이 <code>stdout</code> 인자가 우리가 출력하기를 원하는 명령어의 결과인 것입니다.</p><p><code>exec</code> 함수는 shell 문법을 사용해야 하고 명령어로부터 예상되는 데이터의 크기가 작을 때 좋은 선택입니다.(<code>exec</code> 함수는 반환(return)하기 전에 전체 데이터를 버퍼에 저장한다는 것을 기억하시면 됩니다)</p><p><code>spwan</code> 함수는 명령어로부터 예상되는 데이터의 크기가 클 때 더 좋은 선택이 됩니다. 이는 그 데이터가 표준 IO 객체와 함께 스트림이 될 것이기 때문입니다.</p><p>필요하다면 spwan으로 생성이 된 자식 프로세스가 그 부모 프로세스의 표준 IO 객체를 상속받게 할 수 있지만 가장 중요한 것은 <code>spawn</code> 함수가 shell 문법을 사용하도록 만들 수 있다는 것입니다. 다음은 <code>spawn</code> 함수에 적용된 같은 <code>find | wc</code> 명령어입니다.</p><pre><code>const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true
});</code></pre><p><code>stdio: 'inherit'</code> 옵션(option)이 있기 때문에 코드를 실행하면 자식 프로세스는 주 프로세스의 <code>stdin</code>, <code>stdout</code>과 <code>stderr</code>을 상속받습니다. 이것이 자식 프로세스의 데이터 이벤트 핸들러들이 주 프로세스의 <code>process.stdout</code> 스트림에서 작동하게 하고 이 스크립트(script)가 결과를 즉시 출력하게 만듭니다.</p><p><code>shell: true</code> 옵션이 있기 때문에 <code>exec</code>함수와 했던 것처럼 전달받은 명령어에서 shell 문법을 사용할 수 있었습니다. 하지만 이 코드로 <code>spawn</code> 함수가 주는 데이터 스트리밍의 이점 또한 살릴 수 있습니다. <em>일거양득이라고 할 수 있겠습니다.</em></p><p><code>shell</code>과 <code>stdio</code> 이외에도 <code>child_process</code> 함수에 마지막 인자로 사용할 수 있는 다른 좋은 옵션들이 있습니다. 예를 들면, 스크립트의 작업 디렉토리를 변경하기 위해 <code>cwd</code> 옵션을 사용할 수 있습니다. 예시로 myDownloads 폴더가 작업 디렉토리로 설정되고 shell을 사용하는 <code>spawn</code> 함수를 이용한 전체 파일의 수 세기 예제가 있습니다. <code>cwd</code> 옵션은 스크립트가 <code>~/Downloads</code>에 있는 모든 파일의 수를 셀 수 있게 해줍니다.</p><pre><code>const child = spawn('find . -type f | wc -l', {
  stdio: 'inherit',
  shell: true,
  cwd: '/Users/samer/Downloads'
});</code></pre><p>사용이 가능한 다른 옵션은 새 자식 프로세스에 보이는 환경 변수를 특정하는 <code>env</code> 옵션이 있습니다. 이 옵션에 대한 기본값은 현재 프로세스 환경에 모든 명령어가 접근하게 해주는 <code>process.env</code>입니다. 이 기본값을 덮어 씌우길 원한다면 간단하게 <code>env</code> 옵션으로 빈 객체를 전달하거나 유일한 환경 변수로 여기도록 새로운 값을 전달하면 됩니다.</p><pre><code>const child = spawn('echo $ANSWER', {
  stdio: 'inherit',
  shell: true,
  env: { ANSWER: 42 },
});</code></pre><p><code>echo</code> 명령어는 부모 프로세스 환경 변수에 접근할 수 없습니다. 예를 들면, 이 명령어는 <code>$HOME</code>에 접근할 수 없지만 <code>$ANSWER</code>에는 접근할 수 있는데 <code>env</code> 옵션을 통해 맞춤(custom) 환경 변수로 전달되었기 때문입니다.</p><p>이 단락에서 마지막으로 설명하려는 중요한 자식 프로세스 옵션은 자식 프로세스를 부모 프로세스와 독립적으로 실행시켜주는 <code>detached</code> 옵션입니다.</p><p>이벤트 루프가 끊임없이 분주한 <code>timer.js</code>이라는 파일이 있다고 가정해봅시다.</p><pre><code>setTimeout(() =&gt; {  
  // keep the event loop busy
}, 20000);</code></pre><p><code>detached</code> 옵션을 사용하여 백그라운드(background)에서 실행시킬 수 있습니다. (백그라운드 실행은 사용자의 개입없이 실행이 되는 것을 의미합니다. - 역주)</p><pre><code>const { spawn } = require('child_process');

const child = spawn('node', ['timer.js'], {
  detached: true,
  stdio: 'ignore'
});

child.unref();</code></pre><p>분리된(detached) 자식 프로세스의 정확한 동작은 OS에 따라 달라집니다. 윈도우에서는 분리된 자식 프로세스가 개별 console 창을 가지는 반면에 리눅스에서는 자식 프로세스가 새 프로세스 그룹과 세션(session)의 리더(leader)가 될 것입니다.</p><p><code>unref</code> 함수가 분리된 프로세스에서 호출이 된다면 부모 프로세스는 자식 프로세스와 별개로 종료될 수 있습니다. 이는 자식 프로세스가 소요 시간이 긴 프로세스를 실행시킬 때 유용하지만 이 자식 프로세스를 백그라운드에서 계속 실행시키기 위해서는 자식 프로세스의 <code>stdio</code> 설정 역시 부모 프로세스와 독립적이어야 합니다.</p><p>위 예시는 부모 프로세스가 자식 프로세스가 백그라운드에서 계속 실행하는 동안 종료될 수 있도록 분리시키고 부모 <code>stdio</code> 파일 설명서를 무시하여 백그라운드에서 node 스크립트(timer.js)를 실행할 것입니다.</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/e5e79744650f13e201e392d8ff077d4919f572d1625b47e78eef12d69493a45f/68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f312a5768764d73387a762d57533676376e44586d44557a772e676966" class="kg-image" alt="Gif captured from my Pluralsight course — Advanced Node.js" width="600" height="400" loading="lazy"></figure><h2 id="execfile-">execFile 함수</h2><p>만약 shell을 사용하지 않고 파일을 실행시켜야 한다면, <code>execFile</code> 함수를 사용하면 됩니다. 이 함수는 <code>exec</code> 함수와 동일하게 동작하지만 <code>exec</code> 함수를 조금 더 효율적으로 만들어주는 shell을 사용하지 않습니다. 윈도우에서는 <code>.bat</code>이나 <code>.cmd</code> 같은 파일은 실행이 되지 않습니다. 이러한 파일들은 <code>execFile</code>으로 실행시킬 수 없으며 <code>exec</code> 혹은 <code>spawn</code> 함수에 shell 사용 옵션을 true로 두어야 실행시킬 수 있습니다.</p><h2 id="-sync-">*Sync 함수</h2><p><code>child_process</code>로부터 나온 <code>spawn</code>, <code>exec</code>과 <code>execFile</code> 함수들은 자식 프로세스가 종료될 때까지 기다리는 동기화 차단 버전들도 있습니다.</p><pre><code>const { 
  spawnSync, 
  execSync, 
  execFileSync,
} = require('child_process');</code></pre><p>이 동기화 버전들은 스크립팅(scripting) 작업이나 시작 프로세스 작업 시 유용할 수 있으나 이 경우들이 아니라면 피해야 합니다.</p><h2 id="fork-">fork() 함수</h2><p><code>fork</code> 함수는 node 프로세스를 생성하기 위한 <code>spawn</code> 함수의 변형입니다. <code>spawn</code>과 <code>fork</code> 함수의 가장 큰 차이는 <code>fork</code>를 사용할 때 소통 통로가 자식 프로세스에 만들어지기 때문에 부모 프로세스와 복사된(forked) 프로세스 간에 메시지를 주고 받기 위해 전역(global) <code>process</code> 객체와 함께 복사된 프로세스에서 <code>send</code> 함수를 사용할 수 있다는 것입니다. 이것은 <code>EventEmitter</code> 모듈을 통해 구현할 수 있습니다. 다음 예시를 들어보겠습니다.</p><p>부모 파일, <code>parent.js</code>:</p><pre><code>const { fork } = require('child_process');

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

forked.on('message', (msg) =&gt; {
  console.log('Message from child', msg);
});

forked.send({ hello: 'world' });</code></pre><p>자식 파일, <code>child.js</code>:</p><pre><code>process.on('message', (msg) =&gt; {
  console.log('Message from parent:', msg);
});

let counter = 0;

setInterval(() =&gt; {
  process.send({ counter: counter++ });
}, 1000);</code></pre><p>위의 부모 파일에서 (<code>node</code> 명령어와 함께 파일을 실행시키는)<code>child.js</code>를 복사(fork)하고 난 다음에 <code>message</code> 이벤트를 리슨합니다. <code>message</code> 이벤트는 자식 프로세스가 <code>process.send</code>를 사용할 때마다, 여기서는 매초마다 발생할 것입니다.</p><p>부모에서 자식으로 메시지를 전달하기 위해서 <code>send</code> 함수를 복사(forked)된 객체에서 실행시킬 있고 자식 스크립트에서 전역 <code>process</code> 객체에 있는 <code>message</code> 이벤트를 리슨할 수 있습니다.</p><p>위 <code>parent.js</code>를 실행시킬 때, 복사된 자식 프로세스에 의해 출력이 될 <code>{ hello: 'world' }</code> 객체를 우선 보내고 난 다음에 복사된 자식 프로세스가 매초마다 부모 프로세스에서 출력이 되는 카운터(counter) 증분값을 보낼 것입니다.</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/f4a84df45d480a7bbd181cf26531a6fb1c0b38e127e0944a25f639864ea9a26d/68747470733a2f2f63646e2d6d656469612d312e66726565636f646563616d702e6f72672f696d616765732f312a474f494f54415a54636e3430715a334a776773724e412e676966" class="kg-image" alt="Screenshot captured from my Pluralsight course — Advanced Node.js" width="600" height="400" loading="lazy"></figure><p><code>fork</code> 함수에 대해 실례를 더 살펴보겠습니다.</p><p>두 개의 엔드포인트(endpoint)를 처리하는 http 서버를 가지고 있다고 가정하겠습니다. 이 중 한 엔드포인트(아래의 <code>/compute</code>)는 과도한 계산이 이루어지며 완료까지 수 초가 걸립니다. 다음과 같이 for 루프를 통해서 모방해볼 수 있습니다.</p><pre><code>const http = require('http');

const longComputation = () =&gt; {
  let sum = 0;
  for (let i = 0; i &lt; 1e9; i++) {
    sum += i;
  };
  return sum;
};

const server = http.createServer();

server.on('request', (req, res) =&gt; {
  if (req.url === '/compute') {
    const sum = longComputation();
    return res.end(`Sum is ${sum}`);
  } else {
    res.end('Ok')
  }
});

server.listen(3000);</code></pre><p>이 프로그램은 큰 문제를 가지고 있습니다; <code>/compute</code> 엔드포인트에 요청이 오면, 긴 시간이 소요되는 for 루프 연산으로 이벤트 루프가 분주하기 때문에 서버가 다른 요청들을 처리할 수가 없게 됩니다.</p><p>장시간 연산의 성질에 영향을 받는 이 문제를 해결하는 몇가지 방법이 있지만 모든 연산에 통하는 해결책은 그 연산을 <code>fork</code>를 이용해서 다른 프로세스로 이동시키는 것입니다.</p><p>우선, <code>longComputation</code> 함수 자체를 한 파일에 이동시키고 주 프로세스로부터 받은 메시지로 명령을 받을 때 해당 함수를 불러오게 합니다.</p><p>새롭게 생성한 <code>compute.js</code>:</p><pre><code>const longComputation = () =&gt; {
  let sum = 0;
  for (let i = 0; i &lt; 1e9; i++) {
    sum += i;
  };
  return sum;
};

process.on('message', (msg) =&gt; {
  const sum = longComputation();
  process.send(sum);
});</code></pre><p>이제 주 프로세스 이벤트 루프에서 장시간 연산을 하는 대신에 <code>compute.js</code>를 복사(fork)하고 서버와 복사된 프로세스 사이에 메시지를 주고 받는 메시지 인터페이스(interface)를 사용할 수 있습니다.</p><pre><code>const http = require('http');
const { fork } = require('child_process');

const server = http.createServer();

server.on('request', (req, res) =&gt; {
  if (req.url === '/compute') {
    const compute = fork('compute.js');
    compute.send('start');
    compute.on('message', sum =&gt; {
      res.end(`Sum is ${sum}`);
    });
  } else {
    res.end('Ok')
  }
});

server.listen(3000);</code></pre><p>위의 코드로 <code>/compute</code> 요청이 일어날 때, 그 장시간 연산을 실행시키기 위해 메시지를 복사된(forked) 프로세스에 보냅니다. 주 프로세스의 이벤트 루프는 차단되지 않을 것입니다.</p><p>복사된 프로세스가 이 장시간 연산을 마무리하면, 해당 프로세스는 <code>process.send</code>를 이용해서 부모 프로세스에 결과를 보낼 수 있게 됩니다.</p><p>부모 프로세스에서 복사된 자식 프로세스에 있는 <code>message</code> 이벤트를 리슨합니다. 이 이벤트를 받을 때, http로 요청을 보낸 사용자에게 보낼 <code>sum</code> 값을 대기시킵니다.</p><p>위의 코드는 물론, 복사할 수 있는 프로세스의 개수에 따라 제한되지만 우리가 코드를 실행하고 http를 통해 이 장시간 연산 엔드포인트에 요청을 보낼 때, 주 서버는 차단되지 않고 더 많은 요청을 받을 수 있습니다.</p><p>다음 기사의 주제인 Node의 <code>cluster</code> 모듈은 자식 프로세스 복사와 여느 시스템 상에서 만들 수 있는 많은 복사물들 간 요청의 부하 조절에 대한 생각에 기반을 둡니다.</p><p>그럼 이만 여기서 마무리 짓겠습니다. 읽어 주셔서 감사합니다! 다음에 또 뵙겠습니다!</p><p>React나 Node에 대하여 배우고 있나요? 제 책을 확인해보세요.</p><ul><li>Learn React.js by Building Games</li><li>Node.js Beyond the Basics</li></ul> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
