<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Jun's TID</title>
    <link>https://til-choonham.tistory.com/</link>
    <description>우상향 라이프</description>
    <language>ko</language>
    <pubDate>Sun, 16 Aug 2026 19:50:35 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>춘햄</managingEditor>
    <image>
      <title>Jun's TID</title>
      <url>https://tistory1.daumcdn.net/tistory/4651033/attach/fadeda62b0b3488abd7691193b6cfd9b</url>
      <link>https://til-choonham.tistory.com</link>
    </image>
    <item>
      <title>[AWS] AWS RDS로 DB 이사하기</title>
      <link>https://til-choonham.tistory.com/578</link>
      <description>&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&quot;야, 너 그러다 큰일 난다.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;3&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;로컬 개발 환경이 편하다고 운영(Production) 서버인 EC2에까지 Docker Compose로 앱의 백단과 웹이랑 DB(PostgreSQL)를 같이 올린 상태에서 알파 테스트를 진행하려다가, 내 AI 시니어 개발자에게 검토를 시켰더니 딱 저 한 마디가 돌아왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;생각해 보니 그렇다. AWS 크레딧도 $1000나 있는데, 왜 굳이 만 원 아끼겠다고 OOM Killer(메모리 부족 시 프로세스 강제 종료)의 공포를 안고 가야 하나. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;나는 개발자지 DBA가 아니다. 새벽 3시에 DB 죽어서 로그 까보고 싶지 않으면, 돈으로 해결할 수 있는 건 해결하는 게 맞다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 오늘은 그 '마음의 평화'를 사기 위해, &lt;b&gt;Docker로 띄운 PostgreSQL을 AWS RDS로 이사 시키는 과정&lt;/b&gt;을 기록한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;왜 굳이 돈 써서 RDS로 가야하나? 안그럼 큰일남?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ㅇㅇ 큰일남.&amp;nbsp; 왜 EC2 내부 도커 DB가 위험하고, RDS가 답인지 원리를 짚고 넘어가자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;OOM Killer와 시끄러운 이웃 (Noisy Neighbor)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;EC2 인스턴스 하나에 앱(Node.js/Spring 등)과 DB를 같이 띄우면 자원 전쟁이 일어난다. 트래픽이 몰려 앱이 메모리를 쳐묵쳐묵 하기 시작하면, 리눅스 커널은 &quot;어? 메모리 없네?&quot; 하고 가장 덩치 큰 녀석을 죽여버린다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;슬프게도 그건 99% 확률로 우리 DB다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;게다가 앱이 CPU를 100% 쓰면 DB 쿼리가 느려지고, 반대로 무거운 쿼리가 돌면 앱이 멈춘다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서로가 서로에게 '시끄러운 이웃'이 되는 셈이다. 이걸 물리적으로 분리하는 게 RDS 도입의 핵심이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;RDS 생성의 핵심: gp3와 스토리지 오토스케일링&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;RDS를 만들 때 아무거나 누르면 안 된다. 가이드에 따르면 다음 설정이 '국룰'이자 모범 사례다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;엔진 버전:&lt;/b&gt; Docker에서 쓰던 것과 맞춰야함. 버전 다르면 덤프 복원할 때 피곤해진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;스토리지 타입:&lt;/b&gt; General Purpose SSD (gp3)를 써라. gp2보다 성능은 좋고 가격은 비슷하거나 더 싸다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;스토리지 오토스케일링:&lt;/b&gt; 이거 켜놔야 한다. 데이터 꽉 찼다고 DB 멈추는 꼴 보기 싫으면 Enable 체크하고 최대 한계(Threshold)를 설정해 두자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;퍼블릭 액세스:&lt;/b&gt; 당연히 No. DB는 방구석(VPC 내부)에 있어야지 대문 열어놓으면 털린다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;데이터 이관의 정석: pg_dump와 옵션의 미학&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;데이터를 옮길 때 그냥 복사 붙여넣기가 아니다. pg_dump를 써야 하는데, 여기서 중요한 건 &lt;b&gt;옵션&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjF7IaVlrqRAxUAAAAAHQAAAAAQ6AE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Docker 내부의 DB를 밖으로 꺼내는 명령어
# -T: TTY 할당 금지 (Docker exec 에러 방지용)
# --no-owner --no-privileges: RDS 마이그레이션 필수 옵션
# (이거 안 하면 권한 에러가 나는데, 상당히 까다롭다.)

docker compose exec -T postgres pg_dump -U postgres -d postgres --no-owner --no-privileges &amp;gt; data/postgres_dump_clean.sql&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 --no-owner가 왜 중요하냐면, 로컬 DB의 주인(Owner) 권한과 RDS의 마스터 사용자 권한 체계가 다르기 때문이다. 이 옵션 없이 덤프를 뜨면 복원할 때 &quot;너 권한 없는데?&quot; 하면서 에러를 뿜어낸다. (경험담이다...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;복원 및 연결 (Restore &amp;amp; Reconnect)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;덤프를 떴으면 RDS에 밀어 넣어야 한다. 로컬이나 EC2에서 RDS 엔드포인트를 바라보고 쏘면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjF7IaVlrqRAxUAAAAAHQAAAAAQ6QE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# 복원 명령어
PGPASSWORD='비밀번호' psql -h &amp;lt;RDS-엔드포인트&amp;gt; -U postgres -d postgres -f data/postgres_dump_clean.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;복원이 끝나면 백엔드 앱의 환경 변수(.env)만 바꿔주면 끝이다. DB_HOST를 localhost에서 RDS 엔드포인트로 변경하고, SSL 연결(DB_SSL=true)을 켜주는 게 좋다.(RDS는 무조건 SSL만 허용하는 거 같다.)&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwjF7IaVlrqRAxUAAAAAHQAAAAAQhwI&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 이제 localhost나 postgres 컨테이너 이름이 아니다.
DB_HOST=RDS 엔드포인트 
DB_PORT=5432
DB_USERNAME=RDS Master User Name
DB_PASSWORD=RDS Master Password
DB_NAME=RDS DB NAME

# [중요] RDS는 보안상 SSL 연결을 권장한다. 이거 안 켜면 연결 거부당할 수도 있음.
DB_SSL=true
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt; Troubleshooting&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 역시나 한 번에 되면 마이그레이션이 아니지. 이번 이사 과정에서 마주친 찐 에러들이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt; [Error] psql: ... OCI runtime exec failed&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[상황]&lt;/b&gt; 덤프 파일을 열어봤더니 SQL 쿼리는 없고 이상한 에러 메시지만 들어있었다. OCI runtime exec failed: exec failed: unable to start container process...&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[원인]&lt;/b&gt; docker compose exec 명령어를 쓸 때 -T 옵션을 안 썼다. 쉘 스크립트나 리다이렉션(&amp;gt;)으로 결과를 파일로 저장할 때, 터미널(TTY)을 할당하려고 하면 도커가 &quot;나보고 어쩌라고&quot;라며 뱉어내는 에러다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[해결책]&lt;/b&gt; 명령어에 -T 옵션을 추가해서 해결. sudo docker compose exec -T postgres ...&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;36&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Error] Connection timed out&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[상황]&lt;/b&gt; 복원하려고 psql을 날렸는데 아무 반응 없이 커서만 깜빡이다가 timeout이 뜬다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;39&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[원인]&lt;/b&gt; 100% 보안 그룹(Security Group) 문제다. RDS를 생성할 때 보안 그룹을 새로 만들었는데, 방화벽 규칙이 비어있어서 내 EC2의 접근조차 막고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;40&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[해결책]&lt;/b&gt; AWS 콘솔 -&amp;gt; RDS 보안 그룹 -&amp;gt; Inbound Rules 편집. 처음엔 EC2 보안 그룹 ID를 넣으려다가, &lt;b&gt;VPC 내 서브넷 IP 대역&lt;/b&gt;을 허용하는 방식으로 해결했다. &lt;b&gt;Type:&lt;/b&gt; PostgreSQL (5432) &lt;b&gt;Source:&lt;/b&gt; 내 VPC의 Private Subnet CIDR (예: 10.0.1.0/24) 이렇게 하면 내 VPC 서브넷 안에 있는 EC2 인스턴스들은 별도 초대 없이도 RDS에 접근할 수 있다. (물론 Public IP인 0.0.0.0/0은 절대 넣으면 안 된다!)&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-path-to-node=&quot;42&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결론 (Outro)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 docker-compose.yml에서 postgres 서비스를 과감히 지웠다. 메모리 그래프가 안정을 되찾았고, 혹시나 DB가 죽을까 봐 노심초사하던 마음도 사라졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;44&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;오늘의 교훈:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;45&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;데이터베이스는 소중하다. 전문가(RDS)에게 맡기자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;pg_dump 할 때는 --no-owner를 잊지 말자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;인프라 아키텍처는 초반에 잡아야 나중에 피눈물 안 흘린다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 DB도 분리했으니, 다음은 스토리지를 S3를 사용하도록 셋팅하고 파일들도 마이그레이션 해야 한다.(산 넘어 산이네)&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;47&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;끝!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/578</guid>
      <comments>https://til-choonham.tistory.com/578#entry578comment</comments>
      <pubDate>Sat, 13 Dec 2025 19:44:27 +0900</pubDate>
    </item>
    <item>
      <title>AWS Activate 신청 후기</title>
      <link>https://til-choonham.tistory.com/577</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;회사 카드로 클라우드 비용을 결제할 때는 절대 이런 생각을 못 했다. 그냥 &quot;인프라가 필요하네? EC2 띄우자.&quot; 하면 끝이었으니까.&amp;nbsp; 하지만 이제 막 퇴사를 결심하고 1인 개발자로 홀로서기를 준비하다 보니, 매달 나가는 고정 비용이 뼈아프게 다가온다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프리티어(Free Tier)? 물론 고맙다. 하지만 1년짜리 시한부인 데다, 조금만 트래픽이 튀어도 과금 폭탄 걱정에 잠을 못 잔다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 스타트업의 빛과 소금, &lt;b&gt;AWS Activate Founders&lt;/b&gt;($1,000 크레딧)를 신청하기로 했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그리고 깨달았다. &quot;아, 그냥 신청한다고 주는 게 아니구나.&quot;&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;오늘은 내가 안일하게 신청했다가 Reject을 당하고, 다시 보완하여 하루 만에 다시 승인 받은 내용을 정리해보려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;처음에는 사업자를 낼 생각도 아니었다. 그냥 1인 개발자로 이것 저것 서비스 올려보다가, 사람들이 좀 쓰겠다 싶으면 사업자를 올리려고 했다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;b&gt;AWS Activate Founders&lt;/b&gt;의 승인 조건 자체가 &quot;사업자 번호&quot;가 들어간 회사 홈페이지길래, 어차피 이러나 저러나 등록할 거 같은데, 빠르게 등록하자는 생각을 했다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;처음엔 자신만만했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AWS가 요구하는 게 '사업의 실체'라면, 그 실체를 만드는 건 일도 아니라고 생각했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마치 npm install 하듯이 필수 요건들을 하나씩 설치해 나갔다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;비상주 사무실: &lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;b&gt;&quot;치킨 한 마리 값의 본사&quot;&lt;/b&gt; &lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;가장 먼저 부딪힌 건 주소지였다. 집 주소로 사업자를 내면 전 국민이 내 집을 알게 된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프라이버시 침해는 물론이고, 나중에 이사 갈 때마다 변경 등기 칠 생각을 하니 벌써 머리가 아팠다.(뭐, 자동으로 해주긴 하던데... 그래도)&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그래서 '비상주 사무실'을 알아봤다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;집 근처 인천 어딘가에 &lt;b&gt;월 15,000원&lt;/b&gt;짜리 업체를 찾았다. &quot;아니, 월 15,000원이면 넷플릭스 프리미엄보다 싼데?&quot; 계약서 도장 찍는 데 10분도 안 걸렸다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇게 내 서류상 본사가 생겼다. 싸구려라 의심받을까? 아니다. AWS 심사관이 이런 부분까지 신경쓸 리 없었다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;개인사업자 등록&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;홈택스(Hometax)는 위대했다. 세무서 갈 필요도 없다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;9&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;업태:&lt;/b&gt; 정보통신업&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;종목:&lt;/b&gt; 소프트웨어 개발 및 공급업 (코드 722000)&amp;nbsp; 이 코드가 찍힌 사업자 등록증이 PDF로 떨어지는 순간, 나는 공식적인 '사장님'이 되었다. 여기까지는 순조로웠다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;@gmail.com? ㄴㄴ 너무 짜침, 메일 서버 등록&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;문제는 여기서부터였다. 신청서에 gmail.com이나 naver.com을 적으면 &quot;너네 진짜 회사 맞아?&quot;라는 소리를 듣기 딱 좋다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;도메인 이메일(admin@company.com)이 필수다. &amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;보통 Google Workspace(구 G-Suite)를 쓰지만, 유료다(월 $6). 아직 매출 0원인 1인 개발자에게 매달 나가는 고정비는 죄악이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 그래서 &lt;b&gt;AWS WorkMail&lt;/b&gt;을 선택했다. (마찬가지로 월 $4지만, 크래딧으로 지불하면 되니까...)&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; 하지만 과정은 순탄치 않았다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;11&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;도메인 구매:&lt;/b&gt; 가비아에서 도메인을 샀다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Organization 생성:&lt;/b&gt; AWS 콘솔에서 WorkMail 조직을 만든다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;DNS 레코드 설정 (The Hell):&lt;/b&gt; 이게 하이라이트다. 메일 하나 보내고 받으려면 도메인 관리 페이지(DNS Management)에 가서 레코드를 심어야 한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;11,2,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;소유권 확인용 TXT 레코드 추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;메일 수신용 MX 레코드 추가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;스팸 취급 안 당하려는 CNAME 레코드 추가 &amp;nbsp; 마치 nginx.conf 설정하듯이 DNS 레코드를 하나하나 박아 넣고 Pending 상태가 Verified로 바뀌길 기다린다. &quot;메일 주소 하나 갖기가 이렇게 힘든 거였나?&quot; &amp;nbsp; 어쨌든 반나절의 삽질 끝에 ceo@slowflowsoft.com이라는 이메일 주소를 얻어냈다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-path-to-node=&quot;12&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;그리고 대망의 신청&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;사업자 등록증: 준비 완료.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;회사 주소: 준비 완료. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이메일: 준비 완료. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;웹사이트: 서비스 알파 테스트를 위한 소개 페이지(alpha.slowflowsoft.com)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;이 정도 서류면 완벽하다.&quot; 자신 있게 제출 버튼을 눌렀다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;결과: 1시간 컷 (넌 못 지나간다.)&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;안돼. 들어줄 생각 없어. 돌아가.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;밤 12시 반에 신청서를 제출하고 바로 침대에서 기절했다. 부푼 기대감과 함께... 하지만 아침에 내가 받은 메일에 적혀 있는 문장은 &lt;b&gt;Sorry&lt;/b&gt;였다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;메일이 온 시간을 보니, 새벽 1시가 조금 넘은 시각이었다. 담당자가 검토한 시간조차 아니었다. 이건 100% 봇(Bot)이 필터링한 거다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내가 반나절 동안 DNS 레코드랑 씨름하고, 부동산 계약서 쓰고 했던 노력이 1시간도 지나지 않아서 부정당했다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;왜 리젝 당했을까? 차근차근, 디버깅해보자.&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;탈락 메일을 멍하니 바라보다가 원인을 분석했다. AWS 입장에서 내 신청서는 '투자할 가치가 있는 스타트업'이 아니라, '크레딧 타먹으려는 학생 과제'로 보였을 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;가장 큰 패착은 '웹사이트'였다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;도메인의 문제:&lt;/b&gt; babplealpha... 이름부터가 &quot;아직 테스트 중이다.&quot;라고 광고를 하고 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;서브 도메인:&lt;/b&gt; 메인 도메인도 아니고 서브 도메인을 냈다. 신뢰도가 바닥이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;콘텐츠:&lt;/b&gt; 회사 소개는 없고, 덩그러니 기능 구현된 앱 페이지만 있었다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;ㅇㅋ, 난 회사가 아니라 서비스를 홍보하고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;AWS Activate 심사 팀은 &quot;이 서비스가 얼마나 훌륭한가&quot;를 보는 게 아니다. &quot;이 팀이 비즈니스를 할 준비(Real Deal)가 되어 있는가&quot;를 본다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;최대한 뭔가 있어보이게&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;접근 방식(Architecture) 자체를 바꿨다. 엔지니어가 아니라, 사장으로.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Step 1. 제대로 된 간판 달기: &lt;/b&gt;알파 테스트용 URL은 알파테스트 할 때 쓰고, 메인 도메인으로 회사 홈페이지를 만들어야 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Step 2. 그럴듯한 랜딩 페이지 구축: &lt;/b&gt;기능은 중요하지 않다. 회사의 비전(Vision)이 보여야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;18&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;디자인:&lt;/b&gt; 개발자들이 환장하는(사실 내가 환장하는) 딥 다크 네이비 테마 적용.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;로고:&lt;/b&gt; Infinite Ripple 컨셉의 세련된 로고 배치.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;카피라이팅:&lt;/b&gt; 몰라, 뭔가 있어 보이는, 회사 이름과 뜻을 같이 하는 그럴 듯한 문구면 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Footer:&lt;/b&gt; 사업자 등록 번호, 대표자 이름, Contact 이메일을 명시해서 '법적 실체'임을 강조.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Step 3. 링크드인(LinkedIn) 업데이트&lt;/b&gt; AWS는 신청자의 신원을 중요하게 본다. 프로필에 Founder &amp;amp; Lead Developer를 박아넣고, 회사 페이지와 웹사이트를 연동하고, 프로필 사진도 회사 로고(물론 AI가 만들어준)로 맞추고 인증 뱃지까지 달았다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-path-to-node=&quot;20&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;재신청&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;모든 리팩토링을 마치고 오후 5시 30분, 다시 신청 버튼을 눌렀다. &amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4시간이 지나도 메일이 안 온다.&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이때 직감했다. &quot;아, 봇(Bot)은 통과했구나.&quot; &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 사람(매니저)이 내 사이트를 클릭해 보고 있다는 뜻이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그리고 다음날 오전, 출근해서 휴대폰을 책상에 두고 화장실을 가려는데 &quot;띵동!&quot; 이메일이 왔다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-path-to-node=&quot;21&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;승인&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;아, 이게 뭐라고... 무슨 대학교 합격 발표마냥 떨렸다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;확인을 해보니,&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGJ1xN/dJMcahbRro3/vTKJJfYTjk0c2HKzhu5tX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGJ1xN/dJMcahbRro3/vTKJJfYTjk0c2HKzhu5tX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGJ1xN/dJMcahbRro3/vTKJJfYTjk0c2HKzhu5tX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGJ1xN%2FdJMcahbRro3%2FvTKJJfYTjk0c2HKzhu5tX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;628&quot; height=&quot;261&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;진짜 어찌나 기쁘던지...ㅋㅋㅋ 이런 설렘. 상당히 오랜만이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1901&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nEkaV/dJMcaioiCy6/WPQyq5HeYjTaK3LbWI7caK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nEkaV/dJMcaioiCy6/WPQyq5HeYjTaK3LbWI7caK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nEkaV/dJMcaioiCy6/WPQyq5HeYjTaK3LbWI7caK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnEkaV%2FdJMcaioiCy6%2FWPQyq5HeYjTaK3LbWI7caK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1901&quot; height=&quot;552&quot; data-origin-width=&quot;1901&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;그냥 보기만 해도 든ㅡ든해서, 한 5분 정도는 바라만 보고 있었다...ㅋㅋㅋㅋ&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 한 6개월 정도의 서버비는 굳었으니, 앞으로 나아가기만 하면 된다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-path-to-node=&quot;22&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결론: 개발자도 '포장'이 필요하다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;우리는 종종 코드 퀄리티나 기능 구현에만 매몰되곤 한다. 하지만 외부의 지원을 받거나 투자를 유치할 때는 '보여지는 신뢰도'가 성능 최적화만큼이나 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;24&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;도메인에 돈 아끼지 마라:&lt;/b&gt; 서브 도메인, 무료 도메인은 필패다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;테스트 페이지 내지 마라:&lt;/b&gt; 심사관은 베타 테스터가 아니다. 회사를 소개하라.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;영어는 비즈니스 톤으로:&lt;/b&gt; 번역기 돌리더라도 있어 보이는 단어(Impact, Connect, Scale)를 써라.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;끝!&lt;/span&gt;&lt;/p&gt;</description>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/577</guid>
      <comments>https://til-choonham.tistory.com/577#entry577comment</comments>
      <pubDate>Wed, 10 Dec 2025 13:55:13 +0900</pubDate>
    </item>
    <item>
      <title>[React] Vite</title>
      <link>https://til-choonham.tistory.com/576</link>
      <description>&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리엑트에 너무 적응 해버린건가?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;JSP로 웹단을 개발할 때는 절대 이런 생각을 못 했는데... &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;요즘은 npm start. 엔터를 치고 물 한 잔 마시고 와도 여전히 로딩 바가 돌고 있으면 참... 너무 느리구나 생각이 든다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;React 개발을 몇 년째 해오면서 CRA(Create React App)는 당연한 시작점인 줄 알았다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 프로젝트 규모가 커질수록 개발 서버 구동 시간은 기하급수적으로 늘어난다. 코드 한 줄 고치고 저장했을 뿐인데 브라우저 반영까지 3초, 5초... 흐름이 뚝뚝 끊긴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 참다못해 Vite(비트)로 갈아탔다. 그리고 깨달았다. &quot;아, 도구가 문제였구나.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 Vite로 바꾸고 개발 서버가 빨라졌다고 해서 끝난 게 아니다. 2023년에 내가 블로그에 썼던 '코드 스플리팅(Code Splitting)' 이슈는 2025년인 지금도 유효하다. 아니, 더 중요해졌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;오늘은 Vite의 압도적인 속도 비결(Why)과, 그 위에서 우리가 놓치지 말아야 할 최적화 전략(Strategy)을 한 번에 정리한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;10&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Webpack은 '성실한 바보'였고, Vite는 '천재'다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Vite가 빠른 건 단순히 최적화를 잘해서가 아니다. 접근 방식(Architecture) 자체가 다르다. 이걸 이해하려면 먼저 Webpack이 어떻게 일하는지 봐야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Webpack의 방식: &quot;전과목 교과서 챙기기&quot; (Bundle-based)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Webpack은 번들러(Bundler)다. 이게 무슨 말이냐면, 개발 서버를 띄울 때 우리가 작성한 자바스크립트 파일, CSS, 이미지 등 모든 리소스를 싹 다 긁어모은다. 그러고 나서 하나의 거대한 파일(Bundle)로 만든 뒤에야 메모리에 올리고 서버를 시작한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;15&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Webpack: 소스 코드 전체 스캔 -&amp;gt; 컴파일 -&amp;gt; 번들링 -&amp;gt; 서버 시작 (Ready)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마치 학교 갈 때, 오늘 안 들은 과목 교과서까지 가방에 다 때려 넣고 나서야 현관문을 나서는 꼴이다. 프로젝트가 커지면 가방 싸는 시간(빌드 시간)이 길어질 수밖에 없다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Vite의 방식: &quot;필요한 책만 꺼내기&quot; (Native ESM)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;반면, Vite는 Native ESM(ECMAScript Modules)을 이용한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최신 브라우저는 script type=&quot;module&quot;을 이해한다. 즉, 브라우저가 import 구문을 해석할 수 있다는 뜻이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Vite는 서버를 일단 켜버린다. (번들링 과정 없음, 0.3초 컷) 그리고 브라우저가 &quot;야, 나 main.js 필요해&quot;라고 요청하면, 그때(On-demand) 해당 파일을 컴파일해서 던져준다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;20&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Vite: 서버 시작 (Ready) -&amp;gt; 브라우저가 요청 -&amp;gt; Vite가 파일 변환 -&amp;gt; 전달&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;브라우저에게 &quot;네가 필요한 것만 그때그때 달라고 해&quot;라고 맡겨버리는 거다. 초기 번들링 과정이 없으니 프로젝트가 아무리 커져도 서버 구동 속도는 항상 빠르다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;22&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.3. HMR(Hot Module Replacement) 속도의 차이&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코드 한 줄 고쳤을 때의 차이는 더 극적이다. Webpack은 파일 하나가 바뀌면, 그 파일이 포함된 번들 전체를 다시 빌드해서 갈아 끼워야 한다. 프로젝트가 클수록 느려진다. Vite는 그냥 바뀐 파일 하나만 새로 교체한다. 나머지는 그대로 둔다. 그래서 수정 사항 반영이 즉시(Instant) 일어난다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;24&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2.4. 설치 팁: SWC를 골라야 하는 이유&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Vite 프로젝트를 생성(npm create vite@latest)할 때 프레임워크를 물어보는데, 여기서 멈칫하게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwilncu0xLCRAxUAAAAAHQAAAAAQzwE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;◆  Select a variant:
│  ● TypeScript
│  ○ TypeScript + SWC  &amp;lt;-- (정답!)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;고민하지 말고 TypeScript + SWC를 골라라. SWC는 Rust로 작성된 컴파일러다. 기존 Babel(JS 기반)보다 20배 이상 빠르다. Vite의 속도(ESM)에 엔진 속도(SWC)까지 더해지면, 말 그대로 날아다닌다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;28&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;29&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. 근데, Vite 쓴다고 코드 스플리팅 안 할 거야?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;여기서 초심자들이 흔히 하는 착각이 있다. &quot;개발 서버 빠르니까 최적화 안 해도 되는 거 아님? 요즘 폰도 좋은데.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;HELL NOPE.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Vite가 빠른 건 '주방(내 컴퓨터)'이지 '배달(네트워크)'이 아니다. 실제 배포(Production Build) 때는 Vite도 결국 Rollup을 사용해 파일을 번들링한다. (브라우저가 파일을 수백 개씩 요청하게 놔두면 네트워크 오버헤드로 더 느려지니까.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;23년도에 내가 썼던 글을 다시 꺼내본다. 그때나 지금이나 &quot;사용자가 당장 보지 않을 페이지는 나중에 불러와라&quot;라는 원칙은 변하지 않았다. 배달 트럭(Vite)이 빨라졌다고 창고(전체 코드)를 통째로 고객에게 던지면 안 된다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;33&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Vite 환경에서의 실전 코드 스플리팅 (2025 ver.)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;과거엔 loadable-components 같은 라이브러리를 썼지만, 지금(React 18 + Vite)은 필요 없다. React 내장 기능과 Route 단위 분할이면 충분하다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwilncu0xLCRAxUAAAAAHQAAAAAQ0AE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;// App.jsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// [핵심] Dynamic Import
// Vite는 이 구문을 만나면 자동으로 별도의 JS 파일(Chunk)로 분리한다.
const Home = lazy(() =&amp;gt; import('./pages/Home'));
const About = lazy(() =&amp;gt; import('./pages/About')); 

function App() {
  return (
    &amp;lt;BrowserRouter&amp;gt;
      {/* Suspense: 로딩되는 찰나의 순간(네트워크 지연)을 메워준다 */}
      &amp;lt;Suspense fallback={&amp;lt;div className=&quot;loading&quot;&amp;gt;Loading...&amp;lt;/div&amp;gt;}&amp;gt;
        &amp;lt;Routes&amp;gt;
          &amp;lt;Route path=&quot;/&quot; element={&amp;lt;Home /&amp;gt;} /&amp;gt;
          &amp;lt;Route path=&quot;/about&quot; element={&amp;lt;About /&amp;gt;} /&amp;gt;
        &amp;lt;/Routes&amp;gt;
      &amp;lt;/Suspense&amp;gt;
    &amp;lt;/BrowserRouter&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이렇게 하고 빌드(npm run build)를 하면, dist/assets 폴더에 Home-xh21.js, About-kz99.js 처럼 파일이 예쁘게 쪼개진 걸 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;37&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;38&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4. 삽질 로그 (Troubleshooting)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;39&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;새로운 도구(Vite)와 최적화(Code Splitting)를 섞다 보면 반드시 마주치는 에러들이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;40&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;#1. &quot;process is not defined&quot; (Vite 마이그레이션 필수 체크)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;41&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[상황] CRA에서 잘 되던 process.env.REACT_APP_API_KEY가 에러를 뱉는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[원인] process는 Node.js 객체다. Vite는 브라우저 표준을 지향하므로 이를 주입하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[해결] import.meta.env를 써야 하며, 접두사도 VITE_로 바뀐다. process.env.REACT_APP_KEY -&amp;gt; import.meta.env.VITE_KEY&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;44&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;#2. &quot;Element type is invalid...&quot; (Lazy의 함정)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[상황] 라우팅 설정 잘 했는데 화면이 하얗게 나오고 콘솔에 빨간 줄이 뜬다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[원인] React.lazy는 무조건 default export만 인식한다. 요즘 유행하는 Named Export (export const About = ...)를 쓰면 React가 컴포넌트를 못 찾는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;47&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[해결] 스플리팅 대상인 페이지 컴포넌트는 반드시 export default를 붙여라.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;48&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;#3. 절대 경로(@) 설정 꼬임&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;49&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[상황] import Button from '@components/Button'이 안 먹힌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[해결] Vite는 vite.config.ts의 alias 설정과, TS의 tsconfig.json의 paths 설정이 둘 다 되어 있어야 한다. 하나만 하면 길을 잃는다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwilncu0xLCRAxUAAAAAHQAAAAAQ0QE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;// vite.config.ts
resolve: {
  alias: { '@': path.resolve(__dirname, './src') },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-path-to-node=&quot;52&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-path-to-node=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CRA에서 Vite로 넘어오면서 개발자 경험(DX)은 비약적으로 좋아졌다. &quot;가방 다 싸야 출발하는&quot; Webpack 시절과는 작별이다. 하지만 사용자 경험(UX)은 도구가 저절로 챙겨주지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;55&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Vite의 속도에 취해 최적화를 잊지 말자. 개발은 Vite로 쾌적하게 하되, 배포는 Code Splitting으로 꼼꼼하게 챙기는 것. 그게 경험 있는 개발자의 품격이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;55&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;56&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;끝!&lt;/span&gt;&lt;/p&gt;</description>
      <category>WEB/React</category>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/576</guid>
      <comments>https://til-choonham.tistory.com/576#entry576comment</comments>
      <pubDate>Tue, 9 Dec 2025 21:52:57 +0900</pubDate>
    </item>
    <item>
      <title>[DevOps] EC2에 Docker와 git action로 배포 파이프라인 구성하기</title>
      <link>https://til-choonham.tistory.com/575</link>
      <description>&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;개발자라면 누구나 한 번쯤 겪는 지긋지긋한 순간이 있다. &quot;아, 배포하기 후달린다... 너무 번거롭고 무섭다..&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;개인 프로젝트를 단순히 공부 목적으로 진행하는 것이 아닌, 내 서비스로 세상에 내놓기 위해서는 단순히 코드만 옮기는 게 끝이 아니다. DB 데이터 이관, SSL 인증서 발급, 그리고 무엇보다 내가 잠든 사이에도 코드가 배포될 수 있는 자동화(CI/CD)가 필요했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(그냥 FTP로 파일을 옮기고, 재시작하기? 뭐 뇌 빼고 진행하기에는 이것만한게 없지만, 그랬다간 나중에 트래픽 몰릴 때 손가락만 빨게 될 게 뻔하다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;오늘은 AWS EC2 인스턴스 하나에 Docker Compose를 박고, Nginx로 대문을 세우고, GitHub Actions로 자동화 도로를 깐 전체 과정을 기록하려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;8&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;야생에서 살아남는 아키텍처 (Core Knowledge)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 배포의 핵심은 '환경의 일관성'과 '자동화'다. EC2에 직접 Node.js나 Postgres를 깔지 않는다. 오직 Docker만 믿고 간다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;11&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;기반 닦기: Docker와 권한 설정&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;EC2(Ubuntu)를 처음 받으면 가장 먼저 할 일은 쉘을 쾌적하게 만드는 것이다. 하지만 더 중요한 건 Docker 권한 설정이다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi-0dPE862RAxUAAAAAHQAAAAAQUQ&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;# Docker 설치 (공식 스크립트가 제일 깔끔하다)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# [중요] 매번 sudo 치기 싫으면 유저를 그룹에 추가해야 한다.
sudo usermod -aG docker $USER
newgrp docker&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-path-to-node=&quot;14&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;14,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[&lt;/b&gt;newgrp docker를 안 하고 &quot;왜 권한이 없지?&quot; 하며 재부팅하는 경우가 많다. 리눅스 그룹 정책은 세션이 갱신되어야 적용된다. 이 한 줄이 꽤나 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;구조적 고민: Git Submodule과 디렉토리&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 프로젝트는 backend, web, app이 분리되어 있고, 이를 docker라는 메인 레포지토리가 감싸는 구조다. 마이크로서비스(MSA) 흉내를 낸 모놀리식 구조랄까.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi-0dPE862RAxUAAAAAHQAAAAAQUg&quot; data-hveid=&quot;0&quot;&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서브모듈은 편하려고 쓰는 건데, 원리를 모르면 **&quot;Detached HEAD&quot;**라는 지옥을 맛보게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;왜 굳이 이 방식을? (Why)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마이크로서비스(MSA)처럼 레포지토리는 나누고 싶은데, 배포는 한 방에 하고 싶을 때 쓴다. 하지만 명심해라. &lt;b&gt;서브모듈은 '폴더'가 아니라 '특정 커밋의 바로가기(Link)'다.&lt;/b&gt; 메인 프로젝트는 서브모듈의 최신 코드를 실시간으로 아는 게 아니라, &quot;내가 마지막으로 기록한 커밋 해시(Commit Hash)&quot;만 기억한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;실전 설정법 (How)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&amp;nbsp;1) .gitmodules 파일의 정체&lt;/b&gt; 처음 git submodule add를 하면 루트에 .gitmodules가 생긴다. 이게 네비게이션 지도다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi-0dPE862RAxUAAAAAHQAAAAAQew&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;[submodule &quot;backend&quot;]
	path = backend
	url = https://github.com/my-org/babple-server.git
[submodule &quot;web&quot;]
	path = web
	url = https://github.com/my-org/babple-web.git&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;i&gt;(Tip: 만약 프라이빗 레포라면 https 대신 ssh 주소를 쓰는 게 CI/CD 설정할 때 인증 처리가 훨씬 수월하다. 여기선 https 기준으로 간다.)&lt;/i&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2) 배포 시 가장 많이 하는 실수: 빈 껍데기만 가져오기&lt;/b&gt; EC2나 CI에서 git clone만 하면 서브모듈 폴더는 텅 비어있다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi-0dPE862RAxUAAAAAHQAAAAAQfA&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;# [나쁜 예] 이렇게 하면 빈 폴더만 덩그러니 있다.
git clone https://github.com/my-org/babple-docker.git

# [좋은 예] 처음부터 재귀적으로 가져와라.
git clone --recursive https://github.com/my-org/babple-docker.git
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3) 업데이트의 함정 (Detached HEAD)&lt;/b&gt; 서브모듈 폴더 안에서 git pull을 하면, 브랜치를 따라가는 게 아니라 특정 커밋(Hash)에 머리가 고정된다. 이걸 풀고 최신으로 맞추려면 아래 명령어를 써야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi-0dPE862RAxUAAAAAHQAAAAAQfQ&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;# 메인 레포지토리 루트에서 실행
# &quot;야, 서브모듈들아. 각자 연결된 원격 저장소(remote)의 최신 브랜치를 가져와라.&quot;
git submodule update --remote --merge
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-path-to-node=&quot;16&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SSL 인증서 발급: 닭이 먼저냐 알이 먼저냐&lt;/span&gt;&lt;/h2&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Nginx에 SSL을 적용할 때 딜레마가 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-path-to-node=&quot;19&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;19,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;Nginx를 켜려면 인증서가 필요한데, 인증서를 발급받으려면(Certbot) 웹 서버(80포트)가 켜져 있어야 검증을 받지!&quot;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 순환 오류를 깨는 가장 확실한 방법은 &lt;b&gt;Standalone 모드&lt;/b&gt;다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;1) 80포트 비우기 (Pre-check)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Certbot이 80포트를 잠깐 빌려 써야 한다. 이미 Nginx가 돌고 있다면 잠시 내려라.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi-0dPE862RAxUAAAAAHQAAAAAQfg&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 혹시 돌고 있는 컨테이너가 있다면 중지
sudo docker compose down
# 혹은 로컬 nginx가 돌고 있다면
sudo systemctl stop nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2) 인증서 발급 (Action)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 Certbot을 직접 실행해서 &quot;나 이 도메인 주인 맞아요&quot;라고 증명서를 받아온다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi-0dPE862RAxUAAAAAHQAAAAAQfw&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;# --standalone 옵션이 핵심이다. (내장 웹서버를 띄워서 검증함)
sudo certbot certonly --standalone -d babplealpha.slowflowsoft.com
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이게 성공하면 /etc/letsencrypt/live/... 경로에 fullchain.pem과 privkey.pem이 생긴다. 이게 우리의 황금열쇠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3) Nginx 설정과 마운트 (Integration)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 Nginx를 켠다. 이때 docker-compose.yml에서 호스트의 인증서 경로를 컨테이너에 꽂아주는 게 핵심이다&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwi-0dPE862RAxUAAAAAHQAAAAAQgAE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# docker-compose.yml
services:
  web:
    image: nginx:latest
    ports:
      - &quot;80:80&quot;
      - &quot;443:443&quot;
    volumes:
      # [중요] 호스트의 인증서 폴더를 통째로 읽기 전용(:ro)으로 넘긴다.
      - /etc/letsencrypt:/etc/letsencrypt:ro
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-path-to-node=&quot;31&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;31,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[Insight]&lt;/b&gt; 왜 ro(Read-Only)냐고? Nginx가 실수로 인증서를 덮어쓰거나 지우면 안 되니까. 보안의 기본은 '최소 권한'이다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-path-to-node=&quot;25&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;데이터 마이그레이션&lt;/span&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;아키텍처가 준비됐으니 이제 로컬에 쌓아둔 데이터를 서버로 옮겨야 한다. (CSV로 내보내기? 그런 아마추어 같은 짓은 하지 말자.)&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;27&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;PostgreSQL 덤프의 정석 (pg_dump)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;28&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;로컬 DB를 운영 서버로 옮길 때 가장 중요한 건 &lt;b&gt;호환성&lt;/b&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwi-0dPE862RAxUAAAAAHQAAAAAQsQE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;# [Local] 데이터 덤프 뜨기
# -F p (Plain SQL): 텍스트 기반이라 용량은 좀 크지만, 버전 호환성 끝판왕이다.
# -F c (Custom): 용량은 작지만, pg_restore 할 때 버전 안 맞으면 에러 뱉고 눕는다.
docker compose exec postgres pg_dump -U postgres -d postgres -F p -f /tmp/backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-path-to-node=&quot;30&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서버로 전송 및 복원 (SCP &amp;amp; Restore)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;파일을 서버로 보내는 건 scp가 답이다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwi-0dPE862RAxUAAAAAHQAAAAAQsgE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# 1. 로컬 -&amp;gt; EC2 파일 전송
scp -i key.pem ./backup.sql ubuntu@ServerIP:~/backup.sql

# 2. EC2 -&amp;gt; 컨테이너 내부로 복사
# (호스트 파일을 바로 컨테이너가 못 읽을 때가 많다)
docker compose cp ~/backup.sql postgres:/tmp/backup.sql

# 3. 데이터 밀어넣기
# psql을 이용해 덤프 파일을 실행시킨다.
docker compose exec -T postgres psql -U postgres -d postgres &amp;lt; /tmp/backup.sql
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-path-to-node=&quot;33&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-path-to-node=&quot;33,0&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[Tip]&lt;/b&gt; 만약 pg_restore: error: input file is too short 같은 에러가 뜬다면, 텍스트 포맷(-F p)을 썼는지 다시 확인해라. 바이너리 포맷은 가끔 깨진다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-path-to-node=&quot;34&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-path-to-node=&quot;35&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;자동화: GitHub Actions 상세 설정 (CI/CD Deep Dive)&lt;/span&gt;&lt;/h2&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;매번 터미널 켜서 git pull, docker build 치는 건 하수다. &lt;b&gt;GitHub Actions&lt;/b&gt;에게 이 지루한 작업을 시킬 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;37&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SSH 키 생성: GitHub가 내 EC2에 들어오려면?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GitHub Actions 러너가 EC2에 접속하려면 '열쇠'가 필요하다. 내 로컬 키를 주는 게 아니라, &lt;b&gt;배포 전용 키&lt;/b&gt;를 새로 파야 안전하다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;39&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Step 1: 키 페어 생성 (로컬 터미널)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwi-0dPE862RAxUAAAAAHQAAAAAQswE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;# -f 옵션으로 파일명을 지정한다 (기본 id_rsa 덮어쓰기 방지)
ssh-keygen -t rsa -b 4096 -C &quot;github-actions&quot; -f ./gh_deploy_key
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;41&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이러면 두 개의 파일이 생긴다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;42&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;gh_deploy_key (비공개 키): &lt;b&gt;GitHub한테 줄 것.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;gh_deploy_key.pub (공개 키): &lt;b&gt;EC2 서버에 설치할 것.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Step 2: EC2에 자물쇠 설치&lt;/b&gt; EC2 서버에 접속해서 authorized_keys 파일 끝에 .pub 내용을 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwi-0dPE862RAxUAAAAAHQAAAAAQtAE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;# EC2 접속 후
vim ~/.ssh/authorized_keys
# (gh_deploy_key.pub 내용을 복사 붙여넣기)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Step 3: GitHub에 열쇠 맡기기&lt;/b&gt; Github Repository -&amp;gt; Settings -&amp;gt; Secrets and variables -&amp;gt; Actions에 등록한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;46&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;EC2_SSH_KEY: 비공개 키 내용 전체 (-----BEGIN... 포함)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;EC2_HOST: EC2의 IP 주소&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;47&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;4.2. Workflow 작성 (deploy.yml)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;48&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 appleboy/ssh-action을 사용해 스크립트를 짠다. 직접 ssh 명령어를 치는 것보다 훨씬 안정적이다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwi-0dPE862RAxUAAAAAHQAAAAAQtQE&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;name: Deploy Service
on:
  push:
    branches: [ main ] # main 브랜치에 푸시되면 발동

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            # 1. 프로젝트 폴더로 이동
            cd ~/babple/dev
            
            # 2. 메인 레포지토리 업데이트
            git pull origin main
            
            # 3. [핵심] 서브모듈 업데이트
            # 서브모듈은 자동으로 pull 되지 않는다. 명시적으로 갱신해줘야 함.
            git submodule update --remote --merge
            
            # 4. Docker 서비스 재배포
            cd docker
            # --build: 코드가 바뀌었으니 이미지 다시 빌드
            # -d: 백그라운드 실행
            sudo docker compose up -d --build
            
            # 5. 불필요한 이미지 정리 (디스크 용량 확보)
            sudo docker image prune -f
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[Insight]&lt;/b&gt; git submodule update --remote가 없으면, GitHub Actions가 백날 돌아도 서버 코드는 그대로다. 서브모듈은 '포인터'라는 걸 절대 잊지 말자.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;32&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;33&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;삽질 로그 (Troubleshooting)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;역시나 세상에 공짜는 없다. 이론적으로 완벽했지만 실전은 달랐다. 피 같은 시간을 태우며 해결한 기록을 남긴다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;35&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;#1. &quot;비밀번호가 맞는데 왜 틀리대?&quot; (Postgres Auth Fail)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[상황]&lt;/b&gt; 서버 배포 후 백엔드가 DB에 붙지 못하고 password authentication failed 에러를 무한히 뱉으며 재시작됨.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[원인 분석]&lt;/b&gt; 이게 진짜 함정이다. docker-compose.yml이나 .env에서 DB_PASSWORD를 바꿨지만, &lt;b&gt;PostgreSQL 컨테이너는 최초 실행 시(데이터 폴더가 비어있을 때)만 환경변수로 유저를 생성한다.&lt;/b&gt; 이미 데이터 볼륨(data/postgres)이 생성된 상태에서는 .env를 백날 바꿔봐야 DB 내부의 비밀번호는 바뀌지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[해결책]&lt;/b&gt; 결국 수동으로 DB에 들어가서 비밀번호를 환경변수와 일치시켜줬다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi-0dPE862RAxUAAAAAHQAAAAAQVg&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;sudo docker compose exec postgres psql -U postgres -c &quot;ALTER USER postgres WITH PASSWORD '진짜_새비밀번호';&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;40&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(이거 때문에 컨테이너를 몇 번을 껐다 켰는지 모른다.)&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;41&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;#2. 이미지가 엑스박스로 뜸 (Nginx Location Priority)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[상황]&lt;/b&gt; 업로드한 이미지가 /uploads/image.jpg 경로로 접근이 안 되고 404가 떴다. 분명 파일은 서버에 있는데 Nginx가 엉뚱한 곳(API 서버)으로 요청을 보내고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[원인 분석]&lt;/b&gt; Nginx의 location 블록 우선순위 문제였다. 기존 설정에 / 나 정규식 표현이 섞여 있었는데, Nginx는 명시적인 우선순위를 주지 않으면 헷갈려한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;44&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[해결책]&lt;/b&gt; ^~ (Caret Tilde)를 사용하여 **&quot;이 경로는 정규식보다 우선순위를 높게 쳐라&quot;**고 명시했다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahcKEwi-0dPE862RAxUAAAAAHQAAAAAQVw&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;# [수정 전] location /uploads { ... } -&amp;gt; 404 (다른 정규식에 먹힘)

# [수정 후]
location ^~ /uploads {
    alias /app/uploads; # 여기가 핵심
    expires 1y;
    add_header Cache-Control &quot;public, immutable&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;단순히 경로만 잡는 게 아니라 Cache-Control 헤더를 추가해 브라우저 캐싱까지 챙겼다. 트래픽 비용 아끼려면 이건 필수다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;47&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;#3. GitHub Actions 타임아웃 (Security Group)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;48&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[상황]&lt;/b&gt; dial tcp ***:22: i/o timeout 에러가 뜨면서 배포가 시작조차 안 됨.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;49&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[원인 분석]&lt;/b&gt; 너무 보안을 철저히 한 나머지, EC2 보안 그룹(Security Group)에서 SSH(22번) 포트를 '내 집 IP'로만 열어뒀었다. GitHub Actions의 러너(Runner)는 마이크로소프트의 클라우드 어딘가에 있으니 당연히 막힌다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;50&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[해결책]&lt;/b&gt; GitHub Actions의 IP 대역을 다 열어주는 건 무식한 짓이고, Secrets에 EC2_HOST와 KEY를 잘 심은 뒤, 보안 그룹에서 &lt;b&gt;일시적으로 테스트를 위해 0.0.0.0/0을 열었다가 배포 확인 후 다시 닫는 방식&lt;/b&gt;을 취하거나(물론 이건 임시방편), Bastion Host를 쓰는 게 정석이다. 이번엔 개발 단계라 융통성 있게 처리했다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;51&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-path-to-node=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;이제 로컬에서 git push만 하면, EC2 서버가 알아서 코드를 당겨오고, 빌드하고, 갈아 끼운다. SSL 인증서도 자동으로 갱신되고, 데이터베이스도 안전하게 안착했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;물론 이게 끝은 아니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;트래픽이 늘어나면 로드 밸런싱(ELB)을 붙여야 할 테고, 로그 모니터링(ELK나 CloudWatch)도 붙여야 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 &lt;b&gt;&quot;수동 배포&quot;라는 노동에서 해방되었다는 것&lt;/b&gt;, 그리고 &lt;b&gt;배포 프로세스를 코드로 제어할 수 있게 되었다는 것&lt;/b&gt;만으로도 오늘 밤은 다리 뻗고 잘 수 있겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;55&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다음엔 무중단 배포(Blue/Green)를 위한 Nginx 설정을 좀 더 깊게 파봐야겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;55&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;55&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;56&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;끝!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/575</guid>
      <comments>https://til-choonham.tistory.com/575#entry575comment</comments>
      <pubDate>Mon, 8 Dec 2025 22:26:36 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 7. CI/CD: GitHub Actions로 배포 자동화하기</title>
      <link>https://til-choonham.tistory.com/574</link>
      <description>&lt;h3 data-path-to-node=&quot;6&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;제발 사람을 믿지 말고 시스템을 믿어라&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;사람은 실수를 한다. 특히 피곤하면 더 한다. 하지만 기계는 지치지 않는다. CI/CD는 거창한 게 아니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;내가 하던 노가다를 스크립트로 짜서 GitHub한테 시키는 것&quot;이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;구현할 프로세스는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;10&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내 컴퓨터에서 GitHub로 코드를 &lt;b&gt;Push&lt;/b&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GitHub Actions가 깨어나서 도커 이미지를 &lt;b&gt;Build&lt;/b&gt;하고 레지스트리에 &lt;b&gt;Push&lt;/b&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GitHub Actions가 내 서버에 &lt;b&gt;SSH로 접속&lt;/b&gt;해서 최신 이미지를 &lt;b&gt;Pull &amp;amp; Run&lt;/b&gt; 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 아름다운 파이프라인을 파일 하나로 만들어보자.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;12&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;본론: 배포 파이프라인 구축 (Core Knowledge)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;14&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GitHub Actions는 .github/workflows 폴더 안에 있는 YAML 파일을 읽어서 동작한다. 프로젝트 루트에 폴더를 만들고 파일을 생성하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;파일 경로:&lt;/b&gt; .github/workflows/deploy.yml&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQ3gk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;name: Deploy to Production

on:
  push:
    branches: [ &quot;main&quot; ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      # ======================================================
      # 1. actions/checkout@v3
      # ======================================================
      # 역할: 깃허브 저장소에 있는 '내 소스 코드'를 가상 컴퓨터(Runner)로 내려받는다.
      # 원리: 이게 없으면 Runner는 텅 빈 깡통이다. 'git clone' 명령어를 아주 정교하게 포장해 둔 것.
      # 버전(@v3)
      - name: Checkout code
        uses: actions/checkout@v3

      # ======================================================
      # 2. docker/login-action@v2
      # ======================================================
      # 역할: Docker Hub(혹은 ECR, GHCR)에 로그인한다.
      # 원리: 'docker login -u ... -p ...' 명령어를 대신 실행한다.
      # 왜 쓰는가?: 직접 run 명령어로 패스워드를 치면 로그에 남을 위험이 있다. 
      #            이 액션은 보안 처리가 되어 있어 비밀번호 유출을 막아준다.
      - name: Login to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # [Tip] 빌드 속도를 높이고 싶다면 이 액션을 추가해라.
      # docker/setup-buildx-action은 도커의 최신 빌드 도구인 'BuildKit'을 활성화한다.
      # 캐싱 기능을 제대로 쓰려면 거의 필수다.
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      # ======================================================
      # 3. docker/build-push-action@v4
      # ======================================================
      # 역할: 이미지를 빌드(Build)하고 레지스트리에 업로드(Push)한다.
      # 원리: 단순 'docker build'보다 훨씬 강력하다. 캐시를 적용하거나, 
      #      여러 플랫폼(amd64, arm64) 이미지를 동시에 만들 수 있다.
      # 버전(@v4): v3보다 캐싱 로직이 개선되었다. 도커 관련 액션은 'docker/' 공식 계정 거라 믿을 만하다.
      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .         # Dockerfile이 있는 위치 (루트)
          push: true         # 빌드 후 자동으로 Push까지 해라 (false면 빌드만 함)
          tags: |            # 태그를 여러 개 달 수 있다.
            ${{ secrets.DOCKER_USERNAME }}/my-server:latest
            ${{ secrets.DOCKER_USERNAME }}/my-server:${{ github.sha }}

      # ======================================================
      # 4. appleboy/ssh-action@master
      # ======================================================
      # 역할: 내 서버(EC2 등)에 SSH로 접속해서 명령어를 실행해준다.
      # 이름 분석: 'appleboy'는 개발자 닉네임이다. (공식 액션 아님)
      #           하지만 워낙 유명하고 스타를 많이 받은 프로젝트라 업계 표준처럼 쓰인다.
      # 주의: 공식(docker, actions)이 아닌 개인 개발자 액션은 신뢰도 확인이 필수다.
      - name: Deploy to Server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: 22
          script: |
            # 여기에 적는 건 내 서버에서 실행될 리눅스 쉘 명령어다.
            docker pull ${{ secrets.DOCKER_USERNAME }}/my-server:latest
            docker stop my-server || true
            docker rm my-server || true
            docker run -d --name my-server -p 80:3000 ${{ secrets.DOCKER_USERNAME }}/my-server:latest&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[중요] Secrets 관리 (보안의 핵심)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위 코드에서 ${{ secrets.XXX }}라고 된 부분은 절대 코드에 직접 적으면 안 된다. GitHub 리포지토리 설정(Settings) -&amp;gt; &lt;b&gt;Secrets and variables&lt;/b&gt; -&amp;gt; &lt;b&gt;Actions&lt;/b&gt; 메뉴에 가서 하나씩 등록해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;19&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;DOCKER_USERNAME: 도커 허브 아이디&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;DOCKER_PASSWORD: 도커 허브 비밀번호 (혹은 Access Token)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SERVER_HOST: 내 서버 IP 주소&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SERVER_USER: 서버 접속 계정명 (예: ubuntu)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SSH_KEY: 서버 접속용 &lt;b&gt;Private Key&lt;/b&gt; (내용 전체)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-path-to-node=&quot;20&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;21&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;SSH 키&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;비밀번호 입력 방식은 자동화에서 쓰기 어렵기 때문에 &lt;b&gt;SSH Key Pair(비대칭 키)&lt;/b&gt; 방식을 쓴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 원리를 모르면 접속이 거부당한다. 정확히 이해하자.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;24&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;키 생성:&lt;/b&gt; 내 컴퓨터 터미널에서 ssh-keygen -t rsa -b 4096을 치면 키가 두 개 나온다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;24,0,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;id_rsa (Private Key): &lt;b&gt;절대 남에게 보여주면 안 됨.&lt;/b&gt; 이게 GitHub Actions Secrets(SSH_KEY)에 들어갈 놈이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;id_rsa.pub (Public Key): 이건 자물쇠다. &lt;b&gt;내 서버&lt;/b&gt;에 등록해야 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;서버 설정:&lt;/b&gt; 내 서버(EC2 등)에 접속해서 ~/.ssh/authorized_keys 파일에 id_rsa.pub의 내용을 한 줄 추가해줘야 한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;24,1,1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;이 자물쇠에 맞는 열쇠(GitHub Actions가 가진 Private Key)를 가져오면 문을 열어줘라&quot;라는 뜻이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 과정이 없으면 GitHub Actions는 하루 종일 Permission denied (publickey)만 외치다가 죽는다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;38&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-path-to-node=&quot;40&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 코드를 수정하고 커밋 버튼만 누르면 된다. 잠시 커피 한 잔 마시고 오면, GitHub Actions가 열심히 일해서 내 서버에 최신 버전을 띄워놨을 것이다. deployment passed라는 초록색 체크 표시를 볼 때의 쾌감, 이건 겪어본 사람만 안다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;41&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우리는 이제:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;42&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Docker로 환경을 격리했고 (컨테이너화)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Compose로 복잡한 실행을 정의했으며 (오케스트레이션 기초)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;GitHub Actions로 배포를 자동화했다 (CI/CD)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 1인 개발자로서 갖춰야 할 '현대적인 인프라'의 기본은 다 갖췄다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;44&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;끝!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Docker</category>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/574</guid>
      <comments>https://til-choonham.tistory.com/574#entry574comment</comments>
      <pubDate>Fri, 5 Dec 2025 15:20:17 +0900</pubDate>
    </item>
    <item>
      <title>Docker] 6. 도커 레지스트리와 배포</title>
      <link>https://til-choonham.tistory.com/573</link>
      <description>&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;지난 글에서 docker-compose로 로컬 개발 환경을 구축하는 방법을 정리했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 내 컴퓨터에서만 잘 돌아가는 서비스는 그냥 '자기만족'일 뿐이다. 이제 이 잘 만든 컨테이너를 서버로 옮겨서 전 세계 사람들이 접속하게 만들어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;1&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;FTP로 소스 코드 올리고 서버에서 npm install 하던 시절은 이제 그만 잊자. (그거 하다가 밤샌 날이 며칠인가..)&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;2&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;로컬에선 되는데 서버에선 안 돼요&quot;의 종말&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;신입 시절, 배포 날만 되면 식은땀이 흘렀다. 로컬에서는 기가 막히게 돌아가던 코드가 서버에만 올리면 에러를 뿜었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;알고 보니 서버의 Node.js 버전이 달랐거나, 실수로 라이브러리 설치를 하나 빼먹었기 때문이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;아니, 내 컴퓨터 환경을 그대로 서버에 복사해 넣을 순 없나?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;도커가 바로 그걸 해준다. &lt;b&gt;'소스 코드'를 배포하는 게 아니라, 실행 환경까지 통째로 얼린 '이미지'를 배포하는 것이다.&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이러면 서버에 Node.js가 깔려 있든 말든 상관없다. 그냥 이미지를 가져와서(Pull) 실행(Run)하면 끝이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그럼 이 이미지를 어디에 저장하고 어떻게 옮길까? &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;USB에 담아서 서버실로 뛰어갈 순 없으니, &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;우리는 '도커 레지스트리(Docker Registry)'라는 이미지 저장소를 사용한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마치 코드를 GitHub에 올리듯, 이미지는 Docker Hub(혹은 ECR 등)에 올리는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 글은 내가 만든 컨테이너를 안전하게 포장해서 배송하는 과정을 뜯어본다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-path-to-node=&quot;12&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;배포의 정석 프로세스 (Build -&amp;gt; Push -&amp;gt; Run)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원리는 간단하다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;14&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;내 컴퓨터:&lt;/b&gt; 이미지를 빌드하고 레지스트리에 업로드한다. (&lt;b&gt;Push&lt;/b&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;레지스트리:&lt;/b&gt; 이미지를 보관하는 창고 (예: Docker Hub, AWS ECR).&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;운영 서버:&lt;/b&gt; 레지스트리에서 이미지를 다운로드 받아 실행한다. (&lt;b&gt;Pull &amp;amp; Run&lt;/b&gt;)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 과정을 쉘 스크립트 한 줄로 끝낼 수 있어야 진정한 개발자다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;16&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 이미지 이름 규칙 (Tagging Strategy)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그냥 docker build -t my-app . 이라고 하면 안 된다. 레지스트리에 올리려면 &lt;b&gt;'누구의 저장소에 넣을지'&lt;/b&gt; 명확한 주소를 적어줘야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;형식: {레지스트리_계정명}/{이미지_이름}:{태그}&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQiwk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;# 잘못된 예 (로컬에서만 쓸 때)
docker build -t my-blog-server .

# 올바른 예 (Docker Hub 계정이 'devchoon'이라고 가정)
# 태그(v1.0)를 안 붙이면 자동으로 latest가 되는데, 실무에선 버전 명시가 생명이다.
docker build -t devchoon/my-blog-server:v1.0 .&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-path-to-node=&quot;20&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 세상 밖으로 업로드 (Push)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 Docker Hub에 로그인하고 이미지를 쏜다. (물론 사전에 Docker Hub 가입은 되어 있어야 한다.)&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQjAk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;# 1. 도커 로그인 (터미널에서 한 번만 하면 된다)
docker login

# 2. 이미지 업로드
docker push devchoon/my-blog-server:v1.0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;터미널에 진행률 바(ProgressBar)가 올라가는 걸 보면 묘한 쾌감이 있다. 여기서 &lt;b&gt;[Tip]&lt;/b&gt; 하나. 두 번째 배포부터는 속도가 엄청 빠르다. 도커는 &lt;b&gt;'레이어(Layer)'&lt;/b&gt; 구조라 변경된 코드 부분만 전송하기 때문이다. (기가 막힌 효율성이다.)&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;24&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;3. 서버에서 실행 (Deployment)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 AWS EC2 같은 서버에 접속했다고 치자. 서버엔 도커만 설치되어 있으면 된다. npm, python, java? 다 필요 없다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQjQk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;# 1. 이미지 다운로드
docker pull devchoon/my-blog-server:v1.0

# 2. 컨테이너 실행 (백그라운드 모드)
docker run -d -p 80:3000 --name blog-server devchoon/my-blog-server:v1.0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;끝이다. 정말로 끝이다. dependencies 설치하느라 에러 날 일도, 환경 변수 꼬일 일도 (제대로 설정했다면) 없다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;28&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;29&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Deep Dive: 보안과 아키텍처 (그냥 넘어가면 털린다)&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-path-to-node=&quot;30&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[중요] .env 파일은 이미지에 굽지 마라&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가장 많이 하는 실수다. COPY . . 할 때 .env 파일까지 이미지 안에 넣어버리는 경우다. 도커 이미지는 docker history 명령어로 내부를 다 까볼 수 있다. 즉, 이미지를 받은 누군가가 내 AWS Secret Key나 DB 비밀번호를 볼 수 있다는 뜻이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;33&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;.dockerignore 파일에 .env를 추가해서 빌드에서 제외한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;환경 변수는 &lt;b&gt;컨테이너를 실행(Run)할 때 주입&lt;/b&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQjgk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;mel&quot;&gt;&lt;code&gt;# 실행 시점에 환경 변수 파일 주입 (--env-file)
docker run -d \
  --env-file ./.env.production \
  -p 80:3000 \
  devhoon/my-blog-server:v1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;35&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이렇게 해야 이미지는 '순수한 로직'만 담고, 설정값은 '서버 환경'에 따라 분리된다. 이게 &lt;b&gt;12-Factor App&lt;/b&gt;의 핵심이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;36&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Tip] 아키텍처 이슈 (Apple Silicon vs Linux)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내가 맥북 M1/M2/M3(ARM 아키텍처)를 쓴다면 주의해야 한다. 내 맥북에서 빌드한 이미지는 기본적으로 linux/arm64용이다. 그런데 대부분의 서버(AWS EC2 등)는 linux/amd64다. 서버에서 실행하려는데 exec format error가 뜬다면 100% 이 문제다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책 (멀티 플랫폼 빌드):&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQjwk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;# 빌드할 때 플랫폼을 명시해준다.
docker build --platform linux/amd64 -t devhoon/my-blog-server:v1.0 .
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;40&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이거 몰라서 3시간 날린 적 있다.&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;41&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;42&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;삽질 로그 (Troubleshooting)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이론은 완벽해도 현실은 시궁창일 때가 많다. 내가 겪은 찐 에러들이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;44&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[상황 1] denied: requested access to the resource is denied&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;에러 메시지:&lt;/b&gt; push를 했는데 권한이 없다며 빨간 줄이 뜬다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQkAk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;denied: requested access to the resource is denied
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;47&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;원인 분석:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;48&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;docker login을 안 했거나.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;[가장 흔함]&lt;/b&gt; 내 아이디와 이미지 태그의 네임스페이스가 다를 때. 내 아이디는 devhoon인데, 이미지를 park/my-server로 만들어서 올리려고 하면, 도커 허브는 &quot;너 park 아니잖아?&quot; 하고 걷어찬다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;49&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt; 이미지 태그 맨 앞이 내 도커 허브 아이디와 일치하는지 확인해라. 틀렸다면 다시 태그를 달자.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQkQk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;# 기존 이미지에 새 이름표(Tag) 붙이기
docker tag my-server devchoon/my-server:v1.0&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-path-to-node=&quot;51&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[상황 2] 서버 디스크가 꽉 찼어요 (No space left on device)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;52&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;상황:&lt;/b&gt; 배포를 몇 달 하다 보니 갑자기 서버가 멈췄다. 용량을 확인하니 도커가 100%를 먹고 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;53&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;원인 분석:&lt;/b&gt; 배포할 때마다 새로운 이미지를 Pull 받으면, **과거의 이미지들(Dangling images)**은 지워지지 않고 서버 구석에 쌓인다. 쓰지도 않는 v0.1, v0.2 ... v9.9가 디스크를 점령한 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;54&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt; 배포 스크립트에 청소 명령어를 추가하거나 주기적으로 실행해준다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQkgk&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# 사용하지 않는 모든 이미지, 컨테이너, 네트워크 한방에 정리
docker system prune -f
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;56&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;(주의: 현재 실행 중인 건 안 건드리지만, 멈춰 있는 건 다 날아가니 조심해서 써야 한다.)&lt;/span&gt;&lt;/p&gt;
&lt;hr data-path-to-node=&quot;57&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-path-to-node=&quot;59&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 &lt;b&gt;'내 컴퓨터'라는 좁은 우물을 벗어났다.&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;59&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;59&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내가 짠 코드가 이미지라는 컨테이너 박스에 담겨, 지구 반대편의 서버에서도 똑같이 돌아간다. 이것이 컨테이너 기술이 가져온 혁명이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;59&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;60&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 매번 코드 수정할 때마다 build, push, 서버 들어가서 pull, run 치는 것도 슬슬 귀찮아질 때가 온다. 사람은 반복을 싫어하니까.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;60&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;61&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다음 글에서는 이 귀찮은 과정을 기계에게 시키는 &lt;b&gt;[Docker] 8. CI/CD 입문: GitHub Actions로 배포 자동화하기&lt;/b&gt;를 정리해보려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;61&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;61&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내가 코드를 push만 하면 알아서 서버에 배포까지 되는 마법을 부려보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;61&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;61&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;62&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;끝!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Docker</category>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/573</guid>
      <comments>https://til-choonham.tistory.com/573#entry573comment</comments>
      <pubDate>Fri, 5 Dec 2025 14:47:42 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 5. Docker Compose: 컨테이너 오케스트라의 지휘자</title>
      <link>https://til-choonham.tistory.com/572</link>
      <description>&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;수동으로 띄우는 건 미친 짓이다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;출근 전&amp;nbsp; 혼자 서비스를 만들 때였다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;아침에 일어나서 코딩 좀 하려는데, DB 띄우고, Redis 띄우고, 백엔드 띄우고, 프론트엔드 띄우는 데만 5분이 걸렸다. 순서라도 틀리면 에러가 뿜어져 나왔다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;이걸 매번 손으로 치라고 만든 건 아닐 텐데.&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Docker Compose는 여러 개의 컨테이너를 정의하고 실행하기 위한 도구다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; docker-compose.yml이라는 파일에 &quot;우리 서비스는 이러이러한 애들이 필요해&quot;라고 적어두면, 도커가 알아서 순서대로 띄워주고, 서로 연결해주고, 죽으면 살려준다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;마치 오케스트라의 지휘자 같다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-path-to-node=&quot;9&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실전 아키텍처 구현 (Core Knowledge)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;말로 설명하는 것보다 코드를 보는 게 빠르다. 가장 전형적인 &lt;b&gt;Web 3-Tier Architecture&lt;/b&gt;를 구현해보자.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;12&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Frontend:&lt;/b&gt; Nginx (정적 파일 서빙 + 리버스 프록시)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Backend:&lt;/b&gt; Node.js API 서버&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Database:&lt;/b&gt; PostgreSQL&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Cache:&lt;/b&gt; Redis&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;13&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;프로젝트 루트에 docker-compose.yml 파일을 만들고 아래 내용을 적는다. 주석을 꼼꼼히 읽어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-hveid=&quot;6&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;version: '3.8'  # 도커 컴포즈 파일 규격 버전

services:
  # ==================================
  # 1. Database Service (PostgreSQL)
  # ==================================
  db:
    image: postgres:15-alpine
    container_name: my-postgres  # 컨테이너 이름 고정 (디버깅할 때 편함)
    restart: always              # 죽으면 다시 살려라
    environment:
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
      POSTGRES_DB: mydb
    volumes:
      # [중요] DB 데이터는 컨테이너가 죽어도 살아야 한다.
      # 호스트의 ./pgdata 폴더와 컨테이너 내부 데이터 폴더를 연결(Bind Mount)
      - ./pgdata:/var/lib/postgresql/data
    networks:
      - app-network

  # ==================================
  # 2. Cache Service (Redis)
  # ==================================
  redis:
    image: redis:alpine
    container_name: my-redis
    networks:
      - app-network

  # ==================================
  # 3. Backend API (Node.js)
  # ==================================
  backend:
    build: 
      context: ./backend      # backend 폴더 안에 Dockerfile이 있다고 가정
      dockerfile: Dockerfile
    container_name: my-api
    depends_on:               # db랑 redis가 먼저 떠야 나도 뜬다
      - db
      - redis
    environment:
      # [핵심] IP 주소 대신 '서비스 이름(db, redis)'을 호스트명으로 쓴다.
      # 도커가 내부 DNS로 알아서 IP를 찾아준다.
      DB_HOST: db
      REDIS_HOST: redis
      DB_USER: myuser
      DB_PASS: mypassword
    volumes:
      # 개발 편의성: 로컬 코드를 컨테이너에 실시간 반영 (Hot Reload)
      - ./backend:/app
      - /app/node_modules # 로컬의 node_modules는 덮어쓰지 않게 방어
    networks:
      - app-network

  # ==================================
  # 4. Frontend &amp;amp; Web Server (Nginx)
  # ==================================
  frontend:
    image: nginx:alpine
    container_name: my-web
    ports:
      - &quot;80:80&quot;  # 내 컴퓨터 80포트로 들어오면 여기 80으로 보냄
    volumes:
      # 1. 리액트 빌드 결과물(dist)을 Nginx가 서빙할 폴더로 연결
      - ./frontend/dist:/usr/share/nginx/html
      # 2. 커스텀 Nginx 설정 파일 연결 (아래 설명 참조)
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - backend
    networks:
      - app-network

# 모든 서비스가 공유할 네트워크 정의
networks:
  app-network:
    driver: bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-path-to-node=&quot;15&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Deep Dive 1: Service Discovery (마법의 도메인 이름)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;초보자가 제일 신기해하는 부분이다. 백엔드 코드에서 DB에 접속할 때 localhost라고 적으면 에러가 난다. 왜? 컨테이너 입장에서 localhost는 자기 자신이지, DB 컨테이너가 아니니까.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Docker Compose는 &lt;b&gt;'서비스 이름'을 호스트명으로 사용&lt;/b&gt;하게 해준다. 즉, db 서비스의 IP가 매번 바뀌어도, 백엔드는 그냥 db:5432로 접속하면 도커가 알아서 연결해준다. 내부 DNS가 자동으로 구성되기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;18&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Deep Dive 2: Nginx 설정 (Reverse Proxy)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;19&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위의 frontend 서비스에서 연결한 ./nginx/default.conf는 이렇게 생겨야 한다. 이게 있어야 &lt;b&gt;CORS 문제&lt;/b&gt; 없이 프론트와 백엔드가 통신한다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-hveid=&quot;8&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server {
    listen 80;

    # 1. 정적 파일 서빙 (React/Vue 빌드 결과물)
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
        try_files $uri $uri/ /index.html; # SPA 라우팅 처리
    }

    # 2. API 요청은 백엔드 컨테이너로 토스 (Reverse Proxy)
    location /api {
        # 여기서 'backend'는 docker-compose의 서비스 이름이다!
        proxy_pass http://backend:3000;
        proxy_set_header Host $host;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 설정을 통해 사용자는 포트 번호 신경 쓸 필요 없이 http://localhost/로 접속하고, API 요청도 http://localhost/api/...로 보내면 된다. Nginx가 내부에서 교통 정리를 다 해준다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;22&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;삽질 로그 (Troubleshooting)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;코드는 완벽해 보이지만 실행해보면 100% 확률로 마주칠 문제들이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;24&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Case 1] DB가 켜지기도 전에 백엔드가 죽어요 (Race Condition)&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;25&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;상황:&lt;/b&gt; docker-compose up을 했는데 백엔드 컨테이너가 시작하자마자 Connection Refused를 뱉고 죽는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;원인 분석:&lt;/b&gt; depends_on: - db 옵션은 &quot;DB 컨테이너가 **실행(Start)**될 때까지 기다린다&quot;는 뜻이지, &quot;DB가 **준비(Ready)**될 때까지 기다린다&quot;는 뜻이 아니다. PostgreSQL 컨테이너는 켜졌지만, 내부적으로 데이터를 로딩하고 접속을 받을 준비가 되기까지 몇 초가 걸린다. 성격 급한 백엔드가 그새를 못 참고 접속을 시도했다가 차인 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt;&lt;/span&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;25,2,1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;재시작 정책:&lt;/b&gt; 백엔드에 restart: on-failure를 넣어두면, 죽었다가 다시 살아나면서 DB가 준비될 때쯤 성공한다. (가장 쉬운 방법)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Healthcheck:&lt;/b&gt; compose 파일에 DB의 상태를 체크하는 healthcheck 옵션을 넣고, 백엔드는 condition: service_healthy를 기다리게 한다. (정석적인 방법)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;26&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Case 2] 볼륨 매핑했더니 권한 에러 (Permission Denied)&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;27&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;상황:&lt;/b&gt; 리눅스 환경에서 DB 데이터를 ./pgdata로 뺐는데, DB 컨테이너가 &quot;데이터 못 쓰겠어!&quot; 라며 에러를 낸다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;원인 분석:&lt;/b&gt; 호스트(내 컴퓨터)의 폴더 권한과 컨테이너 내부의 유저 권한이 안 맞아서 그렇다. 호스트 폴더는 root 혹은 내 계정 소유인데, 컨테이너 내부 프로세스는 postgres 유저로 돌려고 하니 쓰기 권한이 없는 거다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt; Docker가 관리하는 &lt;b&gt;Named Volume&lt;/b&gt;을 쓰는 게 정신 건강에 좋다.이렇게 하면 도커가 알아서 권한 문제를 해결해준다.&lt;/span&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;volumes:
  - db-data:/var/lib/postgresql/data # 경로 대신 이름을 쓴다

volumes: # 파일 맨 아래에 정의
  db-data:
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;28&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 명령어 하나면 끝난다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;docker-compose up -d&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;30&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 한 줄로 DB, Redis, 백엔드, 프론트엔드가 모두 뜬다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;끌 때는 docker-compose down이면 깨끗하게 정리된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Docker Compose는 **'인프라를 코드로 관리(IaC)'**하는 첫걸음이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;내 컴퓨터 환경을 파일 하나로 정의했으니, 이 파일만 동료에게 주면 동료 컴퓨터에서도 똑같이 돌아간다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;다음 글에서는 개발 환경을 넘어서, 도커 레지스트리와 배포&amp;nbsp;방법을 다루겠다. 로컬에서만 잘 돌아가면 뭐하나, 서버에 올려야 돈이 되지.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;끝!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Docker</category>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/572</guid>
      <comments>https://til-choonham.tistory.com/572#entry572comment</comments>
      <pubDate>Fri, 5 Dec 2025 14:02:08 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 4. 이미지 다이어트의 정석: Multi-stage Build</title>
      <link>https://til-choonham.tistory.com/571</link>
      <description>&lt;h3 data-path-to-node=&quot;5&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;건설 장비는 입주 전에 치워야 한다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;집을 짓는다고 상상해보자. 공사할 때는 크레인, 시멘트 트럭, 인부들의 작업복이 필요하다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 집이 다 지어지고 입주할 때, 거실 한복판에 크레인을 놔두고 사는 사람은 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;7&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;소프트웨어 빌드도 똑같다. 애플리케이션을 &lt;b&gt;'만들 때(Build)'&lt;/b&gt; 필요한 도구와, &lt;b&gt;'실행할 때(Run)'&lt;/b&gt; 필요한 도구는 다르다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;8&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;빌드:&lt;/b&gt; 타입스크립트 컴파일러(tsc), C++ 컴파일러(gcc), 전체 라이브러리, 테스트 도구&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;실행:&lt;/b&gt; 컴파일된 결과물(.js, binary), 최소한의 런타임&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;초보 시절엔 이 두 가지를 하나의 이미지에 다 때려 넣는다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그러니 이미지가 뚱뚱해질 수밖에 없다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Multi-stage Build&lt;/b&gt;는 &quot;공사장에서 완성품만 쏙 빼서, 깨끗한 새집으로 옮기는 기술&quot;이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-path-to-node=&quot;10&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원리와 구현 (Core Knowledge)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;핵심은 &lt;b&gt;FROM 명령어를 여러 번 쓰는 것&lt;/b&gt;이다. 하나의 Dockerfile 안에서 이미지를 여러 개 만들고, 앞 단계에서 만든 결과물만 뒤 단계로 복사해오는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가장 극적인 효과를 볼 수 있는 &lt;b&gt;NestJS(혹은 TypeScript)&lt;/b&gt; 프로젝트를 예시로 들어보겠다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;13&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 나쁜 예: 단일 스테이지 빌드&lt;/span&gt;&lt;/h4&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQtAc&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;# 1. 베이스 이미지
FROM node:18

WORKDIR /app

# 2. 의존성 설치 (모든 패키지 다 설치함)
COPY package*.json ./
RUN npm install

# 3. 소스 코드 복사 및 빌드
COPY . .
RUN npm run build

# 4. 실행
CMD [&quot;npm&quot;, &quot;run&quot;, &quot;start:prod&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;문제점:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;16&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;node_modules 안에 개발용 패키지(devDependencies)가 잔뜩 들어있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;타입스크립트 원본 소스(.ts)가 이미지에 남아있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;빌드하느라 생긴 캐시 파일들이 공간을 차지한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 이미지 크기 약 &lt;b&gt;900MB ~ 1.2GB&lt;/b&gt;.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;17&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;2. 좋은 예: 멀티 스테이지 빌드&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 코드는 크게 **Builder(공사장)**와 &lt;b&gt;Runner(입주할 집)&lt;/b&gt; 두 단계로 나뉜다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQtQc&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# ==========================================
# [Stage 1] Builder: 빌드만을 위한 임시 컨테이너
# ==========================================
FROM node:18-alpine AS builder

WORKDIR /app

# 빌드에 필요한 모든 패키지 설치
COPY package*.json ./
RUN npm ci

# 소스 코드 복사 후 빌드 (dist 폴더 생성)
COPY . .
RUN npm run build

# ==========================================
# [Stage 2] Runner: 실제 배포될 경량 컨테이너
# ==========================================
FROM node:18-alpine

WORKDIR /app

# [중요] 운영 환경(Production) 설정
ENV NODE_ENV=production

# 1. 실행에 필요한 패키지 파일만 복사
COPY package*.json ./

# 2. 실행용 의존성만 설치 (devDependencies 제외)
# 이렇게 하면 TS 컴파일러 같은 건 설치되지 않음
RUN npm ci --only=production &amp;amp;&amp;amp; npm cache clean --force

# 3. [핵심] Builder 스테이지에서 '빌드된 결과물'만 가져옴
# --from=builder 옵션이 마법의 키워드다.
COPY --from=builder /app/dist ./dist

# 4. 권한 설정 및 실행
USER node
CMD [&quot;node&quot;, &quot;dist/main.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;개선점:&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;21&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;최종 이미지에는 ts 파일도, webpack도, typescript 컴파일러도 없다. 오직 실행 가능한 js 파일과 최소한의 라이브러리만 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;결과:&lt;/b&gt; 이미지 크기 약 &lt;b&gt;100MB ~ 200MB&lt;/b&gt;. (1/10로 줄었다.)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-path-to-node=&quot;22&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Deep Dive] 왜 보안이 좋아지는가?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;용량이 줄어드는 건 눈에 보이는 이득이고, 보이지 않는 더 큰 이득은 **보안(Security)**이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;24&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;단일 스테이지로 빌드하면 소스 코드 전체가 이미지에 들어간다. 실수로 .env 파일을 복사했거나, 코드에 하드코딩된 API 키가 있다면? 해커가 이미지를 뜯어보는 순간 다 털린다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;멀티 스테이지를 쓰면, Stage 1에서 소스 코드를 지지고 볶든 상관없다. Stage 2(최종 이미지)로 &lt;b&gt;안 가져오면 그만&lt;/b&gt;이다. 최종 결과물에는 컴파일된 난독화 코드만 남으므로 원본 유출 위험이 현저히 줄어든다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;26&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;삽질 로그 (Troubleshooting)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;멀티 스테이지를 처음 적용할 때 자주 겪는 멘붕 상황들이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;28&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Case 1] 경로 실수 (File Not Found)&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;29&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;상황/에러메시지:&lt;/b&gt; 빌드는 성공했는데 실행하자마자 죽는다. Error: Cannot find module '/app/dist/main.js'&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;원인 분석:&lt;/b&gt; COPY --from=builder 할 때 경로를 잘못 지정한 거다. Builder 스테이지의 폴더 구조와 Runner 스테이지의 폴더 구조가 머릿속에서 섞인 탓이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt; Builder 스테이지에서 RUN ls -al /app/dist 같은 명령어를 넣어 실제로 파일이 어디 생성되는지 로그로 확인해라. 대부분 /app을 빼먹거나, dist 폴더 안의 구조가 예상과 달라서 생긴다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;30&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Case 2] 네이티브 모듈 충돌 (bcrypt, sharp)&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;31&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;상황:&lt;/b&gt; Stage 1(Builder)에서는 잘 설치됐는데, Stage 2(Runner)로 넘기니까 실행할 때 에러가 난다. Error: The module '...' was compiled against a different Node.js version using NODE_MODULE_VERSION ...&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;원인 분석:&lt;/b&gt; 만약 Stage 1은 일반 리눅스(Debian)를 쓰고, Stage 2는 알파인(Alpine)을 썼다면? C++로 작성된 라이브러리(bcrypt 등)는 OS 환경에 맞춰 컴파일된다. Debian에서 빌드한 바이너리를 Alpine으로 가져오면 호환이 안 돼서 터진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt; Builder와 Runner의 &lt;b&gt;베이스 이미지 OS를 통일&lt;/b&gt;해라. Builder: node:18-alpine Runner: node:18-alpine 이렇게 맞춰야 바이너리 호환성 문제가 안 생긴다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-path-to-node=&quot;33&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;멀티 스테이지 빌드는 선택이 아니라 필수다. 특히 클라우드 비용을 아끼고 싶다면 더더욱 그렇다. 이미지 용량이 작으면 저장소 비용도 줄고, 오토스케일링 할 때 서버 뜨는 속도도 빨라진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;오늘의 핵심 요약:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;35&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;분리:&lt;/b&gt; 빌드 도구와 실행 환경을 분리해라.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;선택적 복사:&lt;/b&gt; COPY --from을 사용해 결과물만 쏙 빼와라.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;통일:&lt;/b&gt; 두 스테이지의 OS 버전을 맞춰라.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 이미지 하나는 기가 막히게 만들 수 있게 되었다. 하지만 요즘 서비스가 어디 컨테이너 하나로 돌아가나? 프론트엔드, 백엔드, DB, Redis까지 최소 4개는 띄워야 &quot;아, 개발 좀 하겠네&quot; 소리 나온다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;36&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이걸 하나하나 docker run 명령어로 띄우다간 손가락 관절염 온다. 다음 글에서는 이 모든 컨테이너를 한 방에 관리하는 지휘자,&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Docker Compose&lt;/b&gt;를 다루겠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;37&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;38&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;끝!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Docker</category>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/571</guid>
      <comments>https://til-choonham.tistory.com/571#entry571comment</comments>
      <pubDate>Fri, 5 Dec 2025 13:34:24 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 3. Dockerfile 작성법: 한 줄 한 줄의 의미</title>
      <link>https://til-choonham.tistory.com/570</link>
      <description>&lt;h3 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;빌드 스크립트는 단순한 텍스트 파일이 아니다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;구글에 &quot;Node.js Dockerfile&quot;, &quot;Python Dockerfile&quot;을 검색하면 수천 개의 예제가 쏟아진다. 대부분 그걸 복사해서 프로젝트 루트에 넣고 빌드 버튼을 누른다. 운 좋게 돌아가면 다행이지만, 조금만 커스텀하려고 하면 막막해진다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;왜 내 이미지는 1GB가 넘지?&quot; &quot;왜 코드 한 줄 고쳤는데 빌드가 5분이나 걸리지?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 질문의 답은 Dockerfile 안에 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Dockerfile은 이미지를 만드는 설계도이자, 서버 구성을 자동화하는 스크립트다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 설계도의 문법과 순서를 이해해야 효율적인 이미지를 만들 수 있다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;가장 대중적인 Node.js 애플리케이션을 예시로 작성법을 해부한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Dockerfile 문법 해부 (Core Knowledge)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Dockerfile은 기본적으로 &lt;b&gt;명령어 + 인자&lt;/b&gt; 형태로 구성된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;8&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;위에서부터 아래로 순차적으로 실행되며, 각 명령어는 새로운 레이어(Layer)를 만든다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;23&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Dockerfile 라인별 해부 (Deep Dive)&lt;/span&gt;&amp;nbsp;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제 위 코드를 한 줄씩 뜯어보며, 그 속에 숨겨진 의도를 보자.&lt;/span&gt;&lt;/li&gt;
&lt;li data-ved=&quot;0CAAQhtANahgKEwiynuPkgaWRAxUAAAAAHQAAAAAQoQc&quot; data-hveid=&quot;0&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# [1] Base Image: 가볍고 안정적인 버전 선택
FROM node:18-alpine

# [2] 환경 변수 설정 (선택 사항)
ENV NODE_ENV=production

# [3] 작업 디렉토리 생성 및 이동
WORKDIR /app

# [4] 의존성 파일 우선 복사 (캐싱 전략의 핵심)
COPY package*.json ./

# [5] 의존성 설치 (Clean Install)
RUN npm ci --only=production &amp;amp;&amp;amp; npm cache clean --force

# [6] 보안 설정: Root 권한 내려놓기
USER node

# [7] 소스 코드 복사
COPY --chown=node:node . .

# [8] 포트 문서화
EXPOSE 3000

# [9] 컨테이너 실행 명령
CMD [&quot;node&quot;, &quot;app.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;&amp;nbsp;1. FROM node:18-alpine&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;node:18: LTS(Long Term Support) 버전을 사용한다. 최신 기능보다는 안정성이 중요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;alpine: 알파인 리눅스는 초경량판이다. 일반 버전(Debian 기반)이 수백 MB라면, 알파인은 50MB 수준이다. 이미지 크기가 작으면 배포 속도가 빨라지고 비용이 준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;2. 작업 디렉토리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;16&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;루트(/)에 파일을 풀지 마라. 나중에 시스템 폴더랑 섞여서 관리가 안 된다. /app이라는 별도 공간을 만들어서 격리하는 게 국룰이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;3. 의존성 복사 (가장 중요)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;19&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;소스 코드(COPY . .)보다 이걸 먼저 했다는 게 포인트다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;도커는 위에서부터 레이어를 쌓는다. 소스 코드를 고쳐도 package.json이 안 바뀌었다면, 도커는 이 단계까지 캐시(Cache)를 쓴다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;즉, npm install 과정을 건너뛴다는 소리다. 빌드 시간이 1분 걸릴 게 1초로 줄어든다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;4. 의존성 설치&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;22&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;npm install 대신 npm ci를 썼다. package-lock.json을 기준으로 엄격하게 버전을 맞춰 설치한다. 협업 시 버전 충돌을 막아준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;--only=production: 개발용 라이브러리(devDependencies)는 설치하지 않는다. 이미지 가벼워지는 소리 들리지 않나?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;npm cache clean: 설치 후 남은 찌꺼기 파일을 지워서 용량을 더 줄인다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;5. 보안 설정 (User)&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;25&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이거 안 쓰는 사람 많다. 기본적으로 도커 컨테이너는 root 권한으로 실행된다. 만약 해커가 앱을 뚫으면, root 권한까지 탈취당할 수 있다는 뜻이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Node.js 이미지는 node라는 기본 유저를 제공한다. 이걸 사용해서 권한을 제한해야한다. 보안의 기본이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;6. 소스 코드 복사&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;28&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이제야 소스 코드를 복사한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;--chown=node:node: 위에서 유저를 node로 바꿨으니, 복사하는 파일의 주인도 node로 맞춰준다. 안 그러면 권한 에러(Permission Denied)로 고생한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;7. 실행 명령&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;31&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;node app.js라고 그냥 적지 말고, 대괄호[]를 써야한다(Exec form).&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그냥 적으면 쉘(/bin/sh)이 감싸서 실행되는데, 이러면 나중에 컨테이너를 종료하라는 신호(SIGTERM)를 앱이 못 받는다. 즉, 강제 종료되어 데이터가 깨질 수 있다. 대괄호를 써야 신호를 제대로 받아서 안전하게 종료(Graceful Shutdown)된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-path-to-node=&quot;24&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;삽질 로그 (Troubleshooting)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;작성법은 간단해 보이지만, 실제 빌드 과정에서 자주 겪는 문제들이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;26&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Case 1] 불필요한 파일까지 복사됨 (Context 과부하)&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;27&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;상황:&lt;/b&gt; 분명 소스 코드는 몇 MB 안 되는데, 빌드 컨텍스트 전송 시간이 엄청 오래 걸리고 이미지 크기도 기형적으로 크다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;원인 분석:&lt;/b&gt; COPY . . 명령어 때문이다. 내 로컬에 있는 node_modules(이미 설치된 거대 폴더), .git 폴더, 맥북의 .DS_Store 같은 쓰레기 파일까지 몽땅 컨테이너로 복사했기 때문이다. 로컬의 node_modules는 OS가 달라서 컨테이너에서 돌아가지도 않는다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt; .dockerignore 파일을 프로젝트 루트에 만든다. .gitignore와 문법이 같다. 여기에 제외할 파일들을 적어준다.&lt;/span&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;# .dockerignore
node_modules
.git
.env
Dockerfile
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;28&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Case 2] 윈도우 줄바꿈 문자 문제 (CRLF)&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;29&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;상황/에러메시지:&lt;/b&gt; 윈도우에서 작성한 쉘 스크립트(.sh)를 COPY해서 실행하려는데 not found 혹은 이상한 문자가 섞여서 에러가 난다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;원인 분석:&lt;/b&gt; 앞선 글에서도 언급했지만, 윈도우는 줄바꿈을 CRLF(\r\n)로 하고 리눅스는 LF(\n)로 한다. 도커 컨테이너는 리눅스다. 쉘 스크립트 해석기가 \r 문자를 명령어의 일부로 인식해서 오작동하는 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;해결책:&lt;/b&gt; 에디터 설정에서 줄바꿈 형식을 LF로 바꾸거나, COPY 하기 전에 .gitattributes를 설정해서 깃이 알아서 변환하게 해야 한다. 급하면 Dockerfile 안에서 RUN sed -i 's/\r$//' script.sh 같은 명령어로 변환할 수도 있지만, 근본적인 해결책은 원본 파일을 LF로 저장하는 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;34&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Case 3] &quot;분명 코드를 고쳤는데 반영이 안 돼요&quot;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;35&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;상황: app.js를 수정하고 다시 빌드했는데, 컨테이너를 띄워보면 예전 코드가 돌아간다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원인 분석: 도커파일 문제가 아니다. docker build 할 때 태그(이름)를 똑같이 덮어쓰기만 하고, 실행할 때 예전 이미지를 바라보고 있을 확률이 높다. 혹은 브라우저 캐시 문제일 수도 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해결책: 빌드할 때마다 태그를 다르게 붙여라. (예: myapp:v1, myapp:v2). 실무에서는 깃 커밋 해시값을 태그로 붙여서 구분한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-path-to-node=&quot;36&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[Case 4] &quot;bcrypt 설치하다가 터졌어요&quot;&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;37&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;상황: npm install 단계에서 빨간 에러가 좍 뜬다. 특히 node-gyp, python 어쩌고 하는 에러들.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;원인 분석: alpine 이미지는 너무 가벼워서 C++ 컴파일러나 파이썬 같은 빌드 도구가 아예 없다. bcrypt 같은 라이브러리는 설치 과정에서 컴파일이 필요하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;해결책: 방법 1: RUN apk add --no-cache python3 make g++ 명령어로 필요한 도구를 설치한다. 방법 2: 그냥 alpine 말고 slim 버전(node:18-slim)을 쓴다. 조금 무겁지만 호환성은 좋다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;30&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;31&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;32&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;코드 몇 줄 안 되지만, 여기엔&amp;nbsp;&lt;/span&gt;&lt;b&gt;캐싱 전략, 용량 최적화, 보안, 프로세스 관리&lt;/b&gt;의 노하우가 다 들어있다. &lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;33&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;베이스 이미지:&lt;/b&gt; 구체적인 버전을 명시해라. (node:18-alpine)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;순서의 미학:&lt;/b&gt; 변경이 적은 파일(package.json)부터 먼저 복사하고 설치해라.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;다이어트:&lt;/b&gt; .dockerignore는 선택이 아닌 필수다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;40&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;위 예시는 단일 스테이지 빌드다. 하지만 실제 배포 환경에서는 빌드 도구조차 남기기 싫어서, 빌드하는 과정과 실행하는 과정을 나누는 &lt;b&gt;Multi-stage Build&lt;/b&gt;를 쓴다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;34&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;35&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;끝!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Docker</category>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/570</guid>
      <comments>https://til-choonham.tistory.com/570#entry570comment</comments>
      <pubDate>Fri, 5 Dec 2025 12:58:33 +0900</pubDate>
    </item>
    <item>
      <title>[Docker] 2. 설치</title>
      <link>https://til-choonham.tistory.com/569</link>
      <description>&lt;h3 data-path-to-node=&quot;3&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;서론: 편안함과 성능은 반비례한다&lt;/span&gt;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;도커를 처음 접했을 때, 가장 당황스러웠던 건 팬 소음이었다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;도커만 켜면 조용하던 맥북이 비행기 이륙 소리를 내며 뜨거워졌다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;가볍다며? 왜 이렇게 무거워?&quot;&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;4&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이유는 간단했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;도커는 리눅스 기반 기술이다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;윈도우나 맥에서 돌리려면, 눈에 보이지 않는 리눅스 가상 머신(Linux VM)을 하나 띄우고 그 위에서 도커를 돌리기 때문이다. 즉, 도커 자체는 가볍지만, 도커를 돌리기 위한 판(VM)이 무겁다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;5&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;대부분 공식 홈페이지에서 Docker Desktop을 받아 설치하고 끝낸다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;하지만 그렇게 기본 설정으로 쓰다가는 IDE가 버벅이고 브라우저 탭이 멈추는 경험을 하게 된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;6&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이번 글은 내 컴퓨터 자원을 지키면서 도커를 현명하게 설치하는 방법을 정리한다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-path-to-node=&quot;7&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;OS별 최적의 설치 전략 (Core Knowledge)&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-path-to-node=&quot;8&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Windows &amp;amp; Mac 사용자: Docker Desktop (가장 쉽지만 무거운 길)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;9&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;대부분의 입문자가 선택하는 길이다. GUI(그래픽 화면)가 제공되니 컨테이너 상태를 눈으로 보기 편하다. 하지만 이 녀석은 자원 킬러다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치법: 공식 홈페이지 가서 Docker Desktop 다운로드 후 설치.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;[중요] 자원 할당 설정 (Resource Limit) 설치하고 끝내면 안 된다. 기본 설정이 꽤 공격적이라 놔두면 혼자 램 8GB를 먹기도 한다. 개발 좀 하려는데 도커가 자원을 다 가져가면 무슨 소용인가.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;11&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;12&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설정 진입: Docker Desktop &amp;gt; Settings(톱니바퀴) &amp;gt; Resources 권장 값:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;13&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CPUs: 내 코어 수의 절반 이하 (보통 2~4)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Memory: 2GB ~ 4GB (처음엔 2GB로 시작해라. 부족하면 늘려라.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Swap: 1GB 정도 (너무 크면 SSD 수명 갉아먹는다.)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-path-to-node=&quot;14&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Mac 사용자 필독: Colima (가볍고 힙한 길)&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;15&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;나는 현재 Docker Desktop을 지우고 Colima를 쓴다. (만약 네가 맥북 유저라면 강력 추천한다.) Docker Desktop은 유료화 이슈(기업용)도 있고, 무겁다. 반면 Colima는 오픈소스이며 CLI 기반이라 훨씬 가볍다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;16&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치법 (Homebrew 필요): brew install colima docker&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;17&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;실행: colima start --cpu 2 --memory 4&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;18&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;CPU 2개, 메모리 4GB 할당하고 시작하라는 뜻이다. 아주 직관적이다. Docker Desktop 없이도 docker 명령어를 똑같이 쓸 수 있다. 백그라운드에서 조용히 돌기 때문에 팬 소음에서 해방될 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-path-to-node=&quot;19&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Linux 서버 사용자: 자동화 스크립트&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;20&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;리눅스(Ubuntu/CentOS) 실서버에 설치할 때는 GUI가 없다. 터미널로 해야 한다. apt-get install docker.io로 설치하면 구버전이 깔리는 경우가 많다. 최신 버전을 깔끔하게 설치하려면 공식 스크립트를 추천한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;21&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-hveid=&quot;5&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-path-to-node=&quot;23&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;보안상 남이 짠 쉘 스크립트 바로 돌리는 건 위험하지만, 도커 공식 스크립트는 검증됐으니 믿고 쓴다. 실무에서도 빠르게 셋팅할 때 자주 쓴다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-path-to-node=&quot;24&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치 검증: Hello World의 진실&lt;/span&gt;&lt;/h4&gt;
&lt;p data-path-to-node=&quot;25&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치가 끝났으면 국룰 명령어를 쳐봐야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;26&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;docker run hello-world&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;27&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 명령어를 쳤을 때 일어나는 일을 이해하는 게 중요하다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-path-to-node=&quot;28&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Unable to find image...: &quot;로컬에 hello-world 이미지가 없네?&quot; 라고 인식한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Pulling from library...: 도커 허브(원격 저장소)에서 이미지를 다운로드한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Hello from Docker!: 이미지를 컨테이너로 실행해서 텍스트를 출력한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;종료: 할 일 다 했으니 컨테이너는 바로 죽는다(Exited).&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-path-to-node=&quot;29&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 과정이 에러 없이 진행됐다면, 이제 도커 엔진은 정상이다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-path-to-node=&quot;41&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-path-to-node=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;설치는 끝났다. 하지만 명심해라. &lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;도커는 필요한 만큼만 자원을 줘야 한다. 무턱대고 기본 설정으로 쓰다가는 컴퓨터가 느려지는 주범이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-path-to-node=&quot;42&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;43&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;이 글의 핵심 요약:&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-path-to-node=&quot;44&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;자원 제한: Docker Desktop 사용자라면 꼭 메모리와 CPU 제한을 걸어라.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;맥 유저는 Colima: 좀 더 쾌적한 개발 환경을 원한다면 Colima를 시도해봐라.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;검증: hello-world가 돌아가는지 확인하고 넘어가라.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-path-to-node=&quot;45&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;46&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;끝!&lt;/span&gt;&lt;/p&gt;</description>
      <category>Docker</category>
      <author>춘햄</author>
      <guid isPermaLink="true">https://til-choonham.tistory.com/569</guid>
      <comments>https://til-choonham.tistory.com/569#entry569comment</comments>
      <pubDate>Fri, 5 Dec 2025 11:10:48 +0900</pubDate>
    </item>
  </channel>
</rss>