From ffdbada9468d35fde9346097b10256f21b93dc59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=B0=AC=ED=98=81?= Date: Wed, 27 May 2026 23:02:53 +0900 Subject: [PATCH 1/5] =?UTF-8?q?keyword:=209=EC=A3=BC=EC=B0=A8=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyword/chapter09/README.md | 451 ++++++++++++++++++++++++++++++++++++ 1 file changed, 451 insertions(+) create mode 100644 keyword/chapter09/README.md diff --git a/keyword/chapter09/README.md b/keyword/chapter09/README.md new file mode 100644 index 0000000..1427ffb --- /dev/null +++ b/keyword/chapter09/README.md @@ -0,0 +1,451 @@ +# 1. OAuth 2.0 + +## 1-1. 개념 + +OAuth 2.0은 사용자가 Google, Kakao, Naver, GitHub 같은 외부 서비스 계정으로 다른 서비스에 로그인할 수 있게 해주는 인증/인가 표준이다. + +예를 들어 사용자가 우리 서비스에서 “Google로 로그인” 버튼을 누르면, 우리 서비스가 사용자의 Google 비밀번호를 직접 받는 것이 아니다. 대신 Google이 사용자를 인증하고, 우리 서버는 Google로부터 사용자의 이메일, 이름, 프로필 정보 등을 전달받는다. + +즉, OAuth 2.0은 다음과 같은 상황을 가능하게 한다. + +```text +사용자 → Google에 로그인 +Google → 우리 서버에 사용자 정보 전달 +우리 서버 → 사용자 정보 기반으로 로그인 처리 +``` + +--- + +## 1-2. 왜 필요한가? + +사용자가 모든 웹사이트마다 새로운 이메일과 비밀번호를 만들어야 한다면 불편하다. 또한 우리 서버가 직접 비밀번호를 저장하고 관리해야 하므로 보안 부담도 커진다. + +OAuth 2.0을 사용하면 사용자는 기존 Google 계정으로 쉽게 로그인할 수 있고, 우리 서비스는 사용자의 비밀번호를 직접 다루지 않아도 된다. + +| 장점 | 설명 | +| --------- | ------------------------------------------------ | +| 사용자 편의성 | 별도 회원가입 없이 Google 계정으로 로그인 가능 | +| 보안 부담 감소 | 우리 서버가 Google 비밀번호를 직접 저장하지 않음 | +| 사용자 정보 활용 | 이메일, 이름, 프로필 이미지 등을 받아올 수 있음 | +| 표준화 | Google, Kakao, Naver 등 대부분의 소셜 로그인이 OAuth 흐름을 따름 | + +--- + +## 1-3. OAuth 2.0 주요 용어 + +| 용어 | 의미 | +| -------------------- | ---------------------------- | +| Resource Owner | 사용자 | +| Client | 우리 서비스 또는 우리 서버 | +| Authorization Server | Google 로그인과 권한 동의를 처리하는 서버 | +| Resource Server | Google 사용자 정보를 제공하는 서버 | +| Authorization Code | Google 로그인 성공 후 발급되는 일회용 코드 | +| Access Token | Google API를 호출하기 위해 사용하는 토큰 | +| Redirect URI | Google 로그인 후 다시 돌아올 우리 서버 주소 | +| Scope | 우리 서비스가 요청하는 사용자 정보 범위 | + +--- + +## 1-4. OAuth 2.0 흐름 + +Google 로그인 기준으로 OAuth 2.0 흐름은 다음과 같다. + +```text +1. 사용자가 /oauth2/login/google 접속 +2. 서버가 사용자를 Google 로그인 화면으로 이동시킴 +3. 사용자가 Google 로그인 및 권한 동의 +4. Google이 Authorization Code를 Redirect URI로 전달 +5. 우리 서버가 Authorization Code를 Google Access Token으로 교환 +6. 우리 서버가 Google Access Token으로 사용자 프로필 정보 조회 +7. 이메일 기준으로 우리 DB에서 사용자 조회 +8. 사용자가 없으면 회원가입 처리 +9. 우리 서버의 Access Token / Refresh Token 발급 +``` + +이때 중요한 점은 Google의 Access Token과 우리 서버의 Access Token이 다르다는 것이다. + +| 구분 | 의미 | +| ------------------- | ------------------------------- | +| Google Access Token | Google API에서 사용자 정보를 가져오기 위한 토큰 | +| 우리 서버의 Access Token | 우리 서비스 API를 인증하기 위한 JWT | + +--- + +## 1-5. Authorization Code가 필요한 이유 + +OAuth 2.0에서는 Google 로그인 성공 후 바로 Access Token을 전달하지 않고, 먼저 Authorization Code를 전달한다. + +예시: + +```text +http://localhost:3000/oauth2/callback/google?code=abc123 +``` + +이 code는 일회용 코드이다. 우리 서버는 이 code를 Google 서버에 보내서 실제 Access Token으로 교환한다. + +왜 바로 Access Token을 주지 않을까? + +Authorization Code는 브라우저 주소창을 통해 전달되므로 노출될 위험이 있다. 그래서 이 code는 한 번만 사용할 수 있는 중간 단계로 두고, 실제 Access Token 교환은 서버와 Google 서버 사이에서 이루어지도록 한다. + +--- + +## 1-6. Google 로그인 라우트 예시 + +```ts +app.get( + "/oauth2/login/google", + passport.authenticate("google", { session: false }) +); + +app.get( + "/oauth2/callback/google", + passport.authenticate("google", { + session: false, + failureRedirect: "/login-failed", + }), + (req, res) => { + res.status(200).json({ + success: true, + tokens: req.user, + }); + } +); +``` + +### 코드 설명 + +| 코드 | 설명 | +| --------------------------------- | ------------------------------------------- | +| `/oauth2/login/google` | 사용자를 Google 로그인 화면으로 이동시키는 경로 | +| `/oauth2/callback/google` | Google 로그인 성공 후 돌아오는 경로 | +| `passport.authenticate("google")` | Passport의 Google Strategy 실행 | +| `session: false` | 세션 방식이 아니라 JWT 방식을 사용할 것이므로 세션 비활성화 | +| `req.user` | Google 로그인 성공 후 Strategy에서 넘겨준 사용자 또는 토큰 정보 | + + +--- + +# 2. JWT + +## 2-1. 개념 + +JWT는 JSON Web Token의 약자이다. +사용자 정보를 JSON 형태로 담고, 서버의 비밀 키로 서명한 토큰이다. + +로그인에 성공한 사용자에게 JWT를 발급하면, 이후 사용자는 API 요청마다 이 토큰을 보내 자신이 로그인한 사용자임을 증명할 수 있다. + +JWT는 다음과 같은 형태를 가진다. + +```text +xxxxx.yyyyy.zzzzz +``` + +즉, 점(`.`)을 기준으로 3부분으로 나뉜다. + +```text +Header.Payload.Signature +``` + +--- + +## 2-2. JWT 구조 + +| 구성 요소 | 설명 | +| --------- | ----------------- | +| Header | 토큰 타입과 서명 알고리즘 정보 | +| Payload | 사용자 식별 정보 | +| Signature | 토큰 위조 여부를 검증하는 서명 | + +예를 들어 Payload에는 다음과 같은 정보가 들어갈 수 있다. + +```json +{ + "id": 1, + "email": "user@example.com" +} +``` + +하지만 Payload에는 비밀번호 같은 민감한 정보를 넣으면 안 된다. +Header와 Payload는 암호화된 것이 아니라 Base64Url로 인코딩된 것이기 때문에 누구나 디코딩해서 내용을 볼 수 있다. + +JWT에서 중요한 보안 요소는 Signature이다. +서버는 비밀 키를 이용해 Signature를 검증하고, 이 토큰이 위조되지 않았는지 확인한다. + +--- + +## 2-3. JWT를 사용하는 이유 + +기존 세션 방식에서는 서버가 사용자의 로그인 상태를 기억해야 한다. +반면 JWT 방식에서는 클라이언트가 토큰을 가지고 있고, 서버는 요청이 들어올 때마다 토큰의 유효성을 검증한다. + +| 구분 | 세션 방식 | JWT 방식 | +| ------------ | -------------- | ----------------- | +| 로그인 상태 저장 위치 | 서버 | 클라이언트 | +| 서버 상태 관리 | 필요함 | 상대적으로 적음 | +| 확장성 | 서버가 세션을 공유해야 함 | 토큰 검증만 하면 됨 | +| 로그아웃 처리 | 세션 삭제로 비교적 간단 | 토큰 만료/블랙리스트 처리 필요 | +| 보안 관리 | 세션 저장소 보호 중요 | 토큰 탈취 방지 중요 | + +JWT 방식은 서버가 로그인 상태를 직접 저장하지 않아도 되기 때문에 확장성 측면에서 유리하다. +하지만 토큰이 탈취되면 만료 전까지 악용될 수 있으므로 Access Token과 Refresh Token을 나누어 사용한다. + +--- + +## 2-4. Access Token과 Refresh Token + +JWT를 사용할 때는 보통 토큰을 2개로 나눈다. + +| 토큰 | 수명 | 용도 | +| ------------- | -- | ---------------- | +| Access Token | 짧음 | 일반 API 요청 인증 | +| Refresh Token | 김 | Access Token 재발급 | + +Access Token은 API 요청에 사용되는 짧은 수명의 토큰이다. +Refresh Token은 Access Token이 만료되었을 때 새 Access Token을 발급받기 위해 사용하는 긴 수명의 토큰이다. + +이렇게 나누는 이유는 보안과 편의성을 동시에 잡기 위해서이다. + +```text +Access Token이 너무 길면 탈취 시 위험함 +Access Token이 너무 짧으면 사용자가 자주 로그인해야 함 +따라서 Access Token은 짧게, Refresh Token은 길게 사용 +``` + +--- + +## 2-5. Access / Refresh Token 흐름 + +```text +1. 사용자가 로그인한다. +2. 서버가 Access Token과 Refresh Token을 발급한다. +3. 클라이언트는 API 요청 시 Access Token을 보낸다. +4. 서버는 Access Token을 검증한다. +5. Access Token이 유효하면 요청을 처리한다. +6. Access Token이 만료되면 401 Unauthorized를 응답한다. +7. 클라이언트는 Refresh Token으로 새 Access Token을 요청한다. +8. 서버는 Refresh Token을 검증하고 새 Access Token을 발급한다. +``` + +--- + +## 2-6. JWT 생성 코드 예시 + +```ts +import jwt from "jsonwebtoken"; + +export const generateAccessToken = (user: { id: number; email: string }) => { + return jwt.sign( + { + id: user.id, + email: user.email, + }, + process.env.JWT_SECRET!, + { + expiresIn: "1h", + } + ); +}; + +export const generateRefreshToken = (user: { id: number }) => { + return jwt.sign( + { + id: user.id, + }, + process.env.JWT_SECRET!, + { + expiresIn: "14d", + } + ); +}; +``` + +### 코드 설명 + +| 코드 | 설명 | +| ------------------------ | ------------------- | +| `jwt.sign()` | JWT를 생성하는 함수 | +| `{ id, email }` | Payload에 담을 사용자 정보 | +| `process.env.JWT_SECRET` | 서명에 사용할 비밀 키 | +| `expiresIn: "1h"` | Access Token 유효 기간 | +| `expiresIn: "14d"` | Refresh Token 유효 기간 | + +--- + +## 2-7. 주의할 점 + +JWT를 사용할 때 주의할 점은 다음과 같다. + +| 주의점 | 설명 | +| ------------------------ | --------------------------- | +| Payload에 민감 정보 넣지 않기 | 비밀번호, 주민번호 등은 절대 넣으면 안 됨 | +| JWT_SECRET 안전하게 관리 | `.env`에 저장하고 GitHub에 올리지 않기 | +| Access Token 유효 기간 짧게 설정 | 탈취 시 피해 줄이기 | +| Refresh Token은 더 안전하게 관리 | DB 저장, 쿠키 보안 옵션 등 고려 | +| 만료 처리 필요 | 만료된 토큰 요청 시 401 응답 필요 | + + + +--- + +# 3. Bearer Token + +## 3-1. 개념 + +Bearer Token은 HTTP Authorization 헤더에 토큰을 담아 보내는 인증 방식이다. + +형식은 다음과 같다. + +```text +Authorization: Bearer +``` + +여기서 Bearer는 “이 토큰을 가진 사람”이라는 의미로 이해할 수 있다. +즉, 서버는 요청에 포함된 토큰을 검증하고, 유효한 토큰이라면 해당 사용자의 요청으로 처리한다. + +--- + +## 3-2. 왜 필요한가? + +JWT를 발급받았다고 해서 서버가 자동으로 로그인 상태를 아는 것은 아니다. +클라이언트가 API 요청을 보낼 때 JWT를 함께 보내야 서버가 사용자를 인증할 수 있다. + +이때 가장 일반적으로 사용하는 방식이 Authorization 헤더에 Bearer Token을 담는 방식이다. + +```text +클라이언트가 API 요청 + ↓ +Authorization 헤더에 Access Token 포함 + ↓ +서버가 Access Token 검증 + ↓ +사용자 인증 후 요청 처리 +``` + +--- + +## 3-3. 요청 예시 + +Postman이나 프론트엔드에서 요청할 때 Header에 다음 값을 추가한다. + +```text +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6... +``` + +프론트엔드 fetch 예시는 다음과 같다. + +```js +const response = await fetch("http://localhost:3000/mypage", { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + }, +}); +``` + +--- + +## 3-4. JWT Strategy 예시 + +`passport-jwt`를 사용하면 Authorization 헤더의 Bearer Token을 추출하고 검증할 수 있다. + +```ts +import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt"; +import { prisma } from "./db.config.js"; + +export const jwtStrategy = new JwtStrategy( + { + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: process.env.JWT_SECRET!, + }, + async (payload, done) => { + try { + const user = await prisma.user.findFirst({ + where: { id: payload.id }, + }); + + if (!user) { + return done(null, false); + } + + return done(null, user); + } catch (err) { + return done(err, false); + } + } +); +``` + +### 코드 설명 + +| 코드 | 설명 | +| ------------------------------------------ | ---------------------------------- | +| `ExtractJwt.fromAuthHeaderAsBearerToken()` | Authorization 헤더에서 Bearer Token 추출 | +| `secretOrKey` | JWT 서명 검증에 사용할 비밀 키 | +| `payload.id` | JWT Payload에 담긴 사용자 ID | +| `done(null, user)` | 인증 성공 | +| `done(null, false)` | 인증 실패 | + +--- + +## 3-5. 보호된 라우트 예시 + +```ts +const isLogin = passport.authenticate("jwt", { session: false }); + +app.get("/mypage", isLogin, (req, res) => { + res.status(200).json({ + message: "인증 성공", + user: req.user, + }); +}); +``` + +위 코드에서 `/mypage`는 JWT가 있어야 접근할 수 있는 보호된 라우트이다. + +토큰 없이 요청하면 인증에 실패한다. +올바른 Access Token을 Bearer Token 형식으로 보내면 인증에 성공한다. + +--- + +## 3-6. 토큰이 없는 경우와 있는 경우 + +| 요청 상태 | 결과 | +| ------------------- | ---------------- | +| Authorization 헤더 없음 | 401 Unauthorized | +| 잘못된 토큰 | 401 Unauthorized | +| 만료된 토큰 | 401 Unauthorized | +| 올바른 Bearer Token | 요청 성공 | + + +--- + +# 4. 세 키워드의 관계 정리 + +OAuth 2.0, JWT, Bearer Token은 각각 따로 떨어진 개념이 아니라 로그인 흐름 안에서 연결된다. + +```text +OAuth 2.0 + ↓ +Google 계정으로 사용자 인증 + ↓ +우리 서버가 사용자 정보 확보 + ↓ +JWT 발급 + ↓ +클라이언트가 JWT 저장 + ↓ +Bearer Token 형식으로 API 요청 + ↓ +서버가 JWT 검증 후 요청 처리 +``` + +| 단계 | 사용되는 개념 | +| --------------------- | ------------ | +| Google 로그인 화면 이동 | OAuth 2.0 | +| Authorization Code 발급 | OAuth 2.0 | +| Google 사용자 정보 조회 | OAuth 2.0 | +| 우리 서버의 로그인 토큰 발급 | JWT | +| API 요청 시 토큰 전달 | Bearer Token | +| 서버에서 토큰 검증 | JWT | + +--- From 1326dbc1930d75999328570b5f865532f88c187d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=B0=AC=ED=98=81?= Date: Thu, 28 May 2026 01:08:14 +0900 Subject: [PATCH 2/5] =?UTF-8?q?mission:=209=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mission/chapter09/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 mission/chapter09/README.md diff --git a/mission/chapter09/README.md b/mission/chapter09/README.md new file mode 100644 index 0000000..18660e5 --- /dev/null +++ b/mission/chapter09/README.md @@ -0,0 +1,16 @@ +# 9주차 미션 README + +## 1. 미션 목표 + +9주차 미션의 목표는 Google OAuth 로그인과 JWT 인증 시스템을 기존 API에 적용하는 것이다. + +## 2. 공통 미션 내용 + +- 기존 API에서 하드코딩된 사용자 정보 제거 +- Google 로그인 사용자의 추가 정보 입력 또는 수정 방식 구현 +- JWT 인증 미들웨어를 기존 API에 적용 +- 로그인한 사용자만 사용할 수 있는 API 보호 + +## 3. 진행 예정 + +실습 완료 후 구현 과정, 테스트 결과, 트러블슈팅을 정리할 예정이다. \ No newline at end of file From 3ee9746e8c1ca7cf048bac732f63c0b1d9d3fe72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=B0=AC=ED=98=81?= Date: Thu, 18 Jun 2026 22:52:31 +0900 Subject: [PATCH 3/5] =?UTF-8?q?docs:=2010=EC=A3=BC=EC=B0=A8=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20=EB=B0=8F=20=EB=AF=B8=EC=85=98=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keyword/chapter10/README.md | 161 ++++++++++++++++++++++++++++++++++++ mission/chapter10/README.md | 122 +++++++++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 keyword/chapter10/README.md create mode 100644 mission/chapter10/README.md diff --git a/keyword/chapter10/README.md b/keyword/chapter10/README.md new file mode 100644 index 0000000..9b43c97 --- /dev/null +++ b/keyword/chapter10/README.md @@ -0,0 +1,161 @@ +# 10주차 핵심 키워드 + +## 1. CI/CD + +### CI: Continuous Integration + +CI는 개발자가 변경한 코드를 자주 통합하고, 통합 과정에서 빌드와 테스트 같은 검증을 자동으로 수행하는 방식이다. + +```text +코드 push 또는 Pull Request + ↓ +의존성 설치 + ↓ +정적 검사·테스트·빌드 + ↓ +통합 가능한 상태인지 확인 +``` + +CI의 장점은 다음과 같다. + +- 코드 충돌과 결함을 비교적 이른 시점에 발견한다. +- 사람마다 다른 빌드 과정을 하나의 절차로 통일한다. +- 반복적인 검증을 자동화하여 실수를 줄인다. +- 배포 가능한 결과물을 일관되게 만들 수 있다. + +### CD: Continuous Delivery / Continuous Deployment + +CD는 CI를 통과한 결과물을 실제 실행 환경까지 전달하는 과정을 자동화한다. + +| 구분 | 의미 | +| --- | --- | +| Continuous Delivery | 운영 배포 직전까지 자동화하고 최종 배포는 사람이 승인 | +| Continuous Deployment | 검증을 통과한 변경을 운영 환경까지 자동 배포 | + +이번 실습에서는 `main` 브랜치에 push되면 GitHub Actions가 빌드한 뒤 EC2의 systemd 서비스를 재시작하므로 Continuous Deployment에 가까운 흐름을 구성한다. + +### CI와 CD를 분리하는 이유 + +빌드와 배포를 별도 job으로 나누면 실패 위치를 쉽게 구분할 수 있다. 또한 한 번 생성한 아티팩트를 여러 서버에 동일하게 배포할 수 있어 서버마다 다시 빌드하면서 생기는 차이를 줄일 수 있다. + +## 2. GitHub Actions + +GitHub Actions는 GitHub 저장소에서 발생하는 이벤트를 기준으로 자동화 작업을 실행하는 서비스다. 자동화 정의는 `.github/workflows/*.yml`에 작성한다. + +### 주요 구성 요소 + +| 요소 | 설명 | +| --- | --- | +| Workflow | 하나의 자동화 전체 정의 | +| Event | workflow를 실행하는 조건 | +| Job | 같은 runner에서 수행되는 작업 묶음 | +| Step | job 내부의 개별 명령 또는 Action | +| Action | 재사용 가능한 자동화 단위 | +| Runner | workflow 명령을 실행하는 가상 머신 | +| Artifact | job 사이 또는 실행 이후 전달·보관하는 결과물 | +| Secret | SSH 키와 환경변수처럼 공개하면 안 되는 값 | + +### 실행 조건 예시 + +```yaml +on: + push: + branches: + - main + workflow_dispatch: +``` + +- `main` 브랜치에 push가 발생하면 자동 실행된다. +- `workflow_dispatch`가 있으면 Actions 화면에서 수동 실행할 수 있다. + +### build와 deploy 의존 관계 + +```yaml +jobs: + build: + runs-on: ubuntu-latest + + deploy: + runs-on: ubuntu-latest + needs: build +``` + +`needs: build`로 인해 build가 성공해야 deploy가 시작된다. build가 실패하면 불완전한 결과물이 서버로 전달되지 않는다. + +### `npm ci`를 사용하는 이유 + +`npm ci`는 `package-lock.json`에 기록된 버전을 그대로 설치한다. lock file을 수정하지 않으므로 CI 환경에서 빠르고 재현 가능한 설치에 적합하다. + +### Secrets 사용 시 주의점 + +- Secret 값을 코드나 README에 기록하지 않는다. +- 로그에 Secret을 출력하는 명령을 작성하지 않는다. +- SSH 키는 배포 전용 키로 분리하고 필요한 권한만 부여한다. +- GitHub Environment와 승인 규칙을 사용하면 운영 배포를 더 엄격하게 관리할 수 있다. + +## 3. Reverse Proxy + +Reverse Proxy는 클라이언트 요청을 먼저 받은 뒤 내부 애플리케이션 서버로 전달하는 서버다. Node.js 애플리케이션 앞에 Nginx를 두는 구성이 대표적이다. + +```text +Client + ↓ HTTPS :443 +Nginx + ↓ HTTP :3000 +Node.js Application +``` + +### Forward Proxy와의 차이 + +| 구분 | 대신하는 대상 | 목적 | +| --- | --- | --- | +| Forward Proxy | 클라이언트 | 클라이언트 익명화, 접근 제어, 캐시 | +| Reverse Proxy | 서버 | 서버 보호, TLS 종료, 라우팅, 로드밸런싱 | + +### Reverse Proxy의 장점 + +- 애플리케이션의 내부 포트를 외부에 직접 공개하지 않는다. +- HTTPS 인증서를 Nginx에서 일괄 관리할 수 있다. +- 여러 애플리케이션으로 요청을 분배할 수 있다. +- 정적 파일, 압축, 캐시 등을 효율적으로 처리할 수 있다. +- 요청 크기 제한과 속도 제한 같은 방어 정책을 적용할 수 있다. + +단, Reverse Proxy가 단일 장애점이 되지 않도록 상태 확인과 이중화 전략이 필요할 수 있다. + +## 4. HTTPS + +HTTPS는 HTTP 통신을 TLS로 보호한다. 이를 통해 다음 세 가지 보안 특성을 제공한다. + +| 특성 | 의미 | +| --- | --- | +| 기밀성 | 제3자가 통신 내용을 읽기 어렵게 암호화 | +| 무결성 | 전송 중 데이터가 변조되었는지 확인 | +| 인증 | 인증서를 통해 접속한 서버의 신원을 확인 | + +### TLS 연결 흐름 + +1. 클라이언트가 서버에 연결을 요청한다. +2. 서버가 인증서와 공개키 정보를 전달한다. +3. 클라이언트가 인증서의 도메인과 신뢰 체인을 검증한다. +4. 양쪽이 안전하게 세션 키를 합의한다. +5. 이후 HTTP 데이터는 세션 키로 암호화하여 전송한다. + +### 배포 환경에서 HTTPS가 필요한 이유 + +OAuth 로그인에는 인증 코드와 토큰처럼 민감한 데이터가 오간다. HTTP만 사용하면 네트워크 중간에서 정보가 노출되거나 변조될 수 있다. 실제 서비스에서는 도메인, TLS 인증서, Nginx를 구성하고 Google OAuth 콜백도 HTTPS 주소로 등록해야 한다. + +## 5. 네 키워드의 관계 + +```text +GitHub Actions + ├─ CI: 설치·생성·빌드·검증 + └─ CD: 아티팩트를 EC2로 전달하고 프로세스 재시작 + ↓ +Client ── HTTPS ──> Reverse Proxy ──> Node.js API +``` + +CI/CD는 코드를 서버까지 안전하고 반복 가능하게 전달하는 과정이고, Reverse Proxy와 HTTPS는 배포된 서버가 외부 요청을 안전하게 받도록 만드는 운영 구조다. + +## 6. 이번 실습에서 느낀 점 + +CI/CD는 단순히 push 후 자동으로 서버를 재시작하는 기능이 아니다. 같은 의존성, 같은 빌드 명령, 같은 아티팩트를 사용해 배포 결과의 차이를 줄이는 것이 핵심이다. 또한 배포 자동화가 성공했더라도 HTTPS, Secret 관리, 장애 시 롤백과 무중단 배포까지 고려해야 실제 운영에 가까운 파이프라인이 된다. diff --git a/mission/chapter10/README.md b/mission/chapter10/README.md new file mode 100644 index 0000000..0ee591d --- /dev/null +++ b/mission/chapter10/README.md @@ -0,0 +1,122 @@ +# 10주차 미션: CI/CD 배포 자동화 + +## 1. 미션 목표 + +- GitHub Actions와 AWS EC2를 이용해 Node.js API를 자동 배포한다. +- 배포된 EC2 환경에서 Google OAuth 로그인이 동작하도록 수정한다. +- 결과 화면뿐 아니라 설정과 실패·수정 과정을 함께 기록한다. + +## 2. 공통 미션 + +### 2-1. GitHub Actions와 EC2로 CI/CD 구축 + +구성한 파이프라인은 다음과 같다. + +```text +main push + → npm ci + → Prisma·TSOA 코드 생성 + → TypeScript build + → artifact 생성 + → SSH/rsync로 EC2 전송 + → systemd restart + → 서비스 상태와 HTTP 응답 확인 +``` + +구현 파일: + +- `.github/workflows/deploy-main.yml` +- `package.json`의 `build`, `start`, `dev` scripts +- `.env.example` + +진행 상태: + +- [x] 배포용 build/start 스크립트 구성 +- [x] GitHub Actions build/deploy job 작성 +- [x] 로컬 `npm run build` 성공 +- [ ] EC2 인스턴스와 탄력적 IP 준비 +- [ ] GitHub Actions Secrets 등록 +- [ ] `main` 병합 후 Actions 성공 +- [ ] EC2 systemd 서비스 `active` 확인 +- [ ] 외부에서 API 응답 확인 + +### 2-2. 배포 환경에서 Google OAuth 수정 + +기존 코드의 콜백 주소는 localhost로 고정되어 있어 배포 후 사용할 수 없었다. + +```ts +callbackURL: + process.env.PASSPORT_GOOGLE_CALLBACK_URL ?? + "http://localhost:3000/oauth2/callback/google"; +``` + +수정 내용: + +1. 콜백 주소를 `PASSPORT_GOOGLE_CALLBACK_URL` 환경변수로 분리했다. +2. GitHub Secret의 배포용 `.env`에는 EC2 콜백 주소를 넣는다. +3. Google Cloud Console의 승인된 리디렉션 URI에도 같은 주소를 등록한다. + +진행 상태: + +- [x] OAuth 콜백 URL 환경변수화 +- [ ] Google Cloud Console에 EC2 콜백 URI 등록 +- [ ] EC2 주소에서 Google 로그인 성공 확인 +- [ ] 발급된 Access Token으로 `/mypage` 호출 확인 + +## 3. 파이프라인 분석 + +build job과 deploy job을 분리했다. build job이 실패하면 deploy job이 실행되지 않기 때문에 컴파일되지 않는 코드가 서버로 전달되는 것을 막을 수 있다. + +GitHub-hosted runner에서 의존성 설치와 TypeScript 컴파일을 수행하고, EC2에는 실행에 필요한 `dist`와 운영 의존성만 전송한다. 이 방식은 작은 EC2 인스턴스가 직접 빌드할 때 생기는 CPU와 메모리 부담을 줄인다. + +배포 서버에서는 새 파일을 `incoming`에 먼저 받은 뒤 기존 `current`와 교체한다. 이전 배포는 `previous`에 남겨 두어 문제가 발생했을 때 확인하거나 롤백할 수 있도록 했다. + +## 4. 실습 증빙 + +아래 항목은 실제 AWS와 GitHub 작업을 진행하며 추가한다. + +1. EC2 인스턴스와 탄력적 IP +2. Node.js와 MySQL 설치 결과 +3. Secret 값이 가려진 GitHub Actions 설정 +4. build job 성공 화면 +5. deploy job 성공 화면 +6. systemd 서비스 상태 +7. 외부 API 호출 결과 +8. Google Cloud Console 콜백 설정 +9. 배포 주소에서 Google 로그인 성공 결과 + +## 5. 트러블슈팅 + +### 워크북의 workflow를 그대로 사용하면 build가 실패함 + +- **이슈:** `npm run build`와 `dist`를 사용하지만 기존 프로젝트에는 build script가 없었다. +- **문제:** 개발 환경은 `tsx`로 TypeScript를 직접 실행하고 있었다. +- **해결:** Prisma와 TSOA 생성 후 `tsc`로 컴파일하는 build script를 만들고, 운영 start 명령을 `node dist/index.js`로 분리했다. + +### TSOA 생성 파일이 ESM 규칙과 충돌함 + +- **이슈:** 생성된 controller import에 `.js` 확장자가 없어 NodeNext 컴파일이 실패했다. +- **문제:** 프로젝트의 생성 코드 방식과 ESM 모듈 해석 규칙이 일치하지 않았다. +- **해결:** 서버 출력 형식을 CommonJS로 통일하고 빌드를 다시 검증했다. + +### 배포 후 OAuth가 localhost로 돌아감 + +- **이슈:** 로그인 성공 후 EC2가 아닌 localhost 콜백으로 이동한다. +- **문제:** 콜백 URL이 코드에 하드코딩되어 있고 Google Console에도 로컬 URI만 등록되어 있다. +- **해결:** 콜백을 환경변수로 분리하고 코드와 Google Console에 동일한 배포 URI를 설정한다. + +## 6. 시니어 미션 계획 + +현재 systemd `restart` 방식에는 짧은 다운타임이 존재한다. 기본 미션을 완료한 후 다음 순서로 PM2 무중단 배포를 적용할 예정이다. + +1. PM2 cluster mode 설정 +2. `ecosystem.config.cjs` 작성 +3. `pm2 startup`, `pm2 save`로 부팅 자동 실행 +4. 배포 단계에서 `pm2 reload` 사용 +5. 연속 요청을 보내며 실패 응답이 발생하지 않는지 확인 + +## 7. 회고 + +로컬에서 실행되는 코드를 그대로 서버에 복사하는 것만으로는 배포가 완성되지 않는다. CI 환경에서 재현 가능한 빌드를 만들고, Secret을 안전하게 전달하며, 서버 프로세스 상태와 실제 HTTP 응답까지 확인해야 하나의 배포라고 할 수 있다. + +또한 OAuth처럼 외부 서비스가 포함된 기능은 애플리케이션 코드뿐 아니라 배포 주소와 외부 콘솔 설정이 함께 일치해야 한다는 점을 확인했다. From 1a5a6a4f3ce641e2c0782b57be06fdbe293dce95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=B0=AC=ED=98=81?= Date: Fri, 19 Jun 2026 01:44:35 +0900 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20=EB=B0=B0=ED=8F=AC=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=8B=A8?= =?UTF-8?q?=EA=B3=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mission/chapter10/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/mission/chapter10/README.md b/mission/chapter10/README.md index 0ee591d..6836adb 100644 --- a/mission/chapter10/README.md +++ b/mission/chapter10/README.md @@ -19,6 +19,7 @@ main push → TypeScript build → artifact 생성 → SSH/rsync로 EC2 전송 + → Prisma migration 적용 → systemd restart → 서비스 상태와 HTTP 응답 확인 ``` From 07a9273f2a004b2c4f46259771063b4a79835558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=B0=AC=ED=98=81?= Date: Fri, 19 Jun 2026 03:26:48 +0900 Subject: [PATCH 5/5] =?UTF-8?q?docs:=2010=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=99=84=EB=A3=8C=20=EA=B2=B0=EA=B3=BC=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mission/chapter10/README.md | 38 +++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/mission/chapter10/README.md b/mission/chapter10/README.md index 6836adb..57d98ef 100644 --- a/mission/chapter10/README.md +++ b/mission/chapter10/README.md @@ -35,11 +35,11 @@ main push - [x] 배포용 build/start 스크립트 구성 - [x] GitHub Actions build/deploy job 작성 - [x] 로컬 `npm run build` 성공 -- [ ] EC2 인스턴스와 탄력적 IP 준비 -- [ ] GitHub Actions Secrets 등록 -- [ ] `main` 병합 후 Actions 성공 -- [ ] EC2 systemd 서비스 `active` 확인 -- [ ] 외부에서 API 응답 확인 +- [x] EC2 인스턴스와 탄력적 IP 준비 +- [x] GitHub Actions Secrets 등록 +- [x] `main` 병합 후 Actions 성공 +- [x] EC2 systemd 서비스 `active` 확인 +- [x] Nginx와 HTTPS 도메인에서 API 응답 확인 ### 2-2. 배포 환경에서 Google OAuth 수정 @@ -54,15 +54,15 @@ callbackURL: 수정 내용: 1. 콜백 주소를 `PASSPORT_GOOGLE_CALLBACK_URL` 환경변수로 분리했다. -2. GitHub Secret의 배포용 `.env`에는 EC2 콜백 주소를 넣는다. +2. GitHub Secret의 배포용 `.env`에는 HTTPS 도메인 콜백 주소를 넣는다. 3. Google Cloud Console의 승인된 리디렉션 URI에도 같은 주소를 등록한다. 진행 상태: - [x] OAuth 콜백 URL 환경변수화 -- [ ] Google Cloud Console에 EC2 콜백 URI 등록 -- [ ] EC2 주소에서 Google 로그인 성공 확인 -- [ ] 발급된 Access Token으로 `/mypage` 호출 확인 +- [x] Google Cloud Console에 HTTPS 콜백 URI 등록 +- [x] 배포 도메인에서 Google 로그인 성공 확인 +- [x] 발급된 Access Token으로 `/mypage` 호출 확인 ## 3. 파이프라인 분석 @@ -104,7 +104,25 @@ GitHub-hosted runner에서 의존성 설치와 TypeScript 컴파일을 수행하 - **이슈:** 로그인 성공 후 EC2가 아닌 localhost 콜백으로 이동한다. - **문제:** 콜백 URL이 코드에 하드코딩되어 있고 Google Console에도 로컬 URI만 등록되어 있다. -- **해결:** 콜백을 환경변수로 분리하고 코드와 Google Console에 동일한 배포 URI를 설정한다. +- **해결:** 콜백을 환경변수로 분리하고 DuckDNS, Nginx, Certbot으로 HTTPS를 구성한 뒤 Google Console에 동일한 배포 URI를 설정했다. + +### GitHub Actions에서 환경변수를 읽지 못함 + +- **이슈:** Prisma generate 단계에서 `DATABASE_URL`을 찾을 수 없었다. +- **문제:** 실습 저장소의 Repository secrets가 비어 있었다. +- **해결:** 배포에 필요한 5개 Repository secrets를 올바른 저장소에 등록했다. + +### MySQL 인증 오류가 plugin 오류로 표시됨 + +- **이슈:** Prisma가 `Unknown authentication plugin sha256_password`를 출력했다. +- **문제:** 실제 원인은 배포 Secret과 MySQL 사용자의 비밀번호 불일치였다. +- **해결:** 전용 사용자 비밀번호를 재설정하고 `DATABASE_URL`, `DB_PASSWORD`를 같은 값으로 갱신했다. + +### 작은 EC2에서 migration 중 연결 실패 + +- **이슈:** MySQL이 실행 중인데도 Prisma가 `P1001`을 출력했다. +- **문제:** 약 1GiB 메모리에서 가용 메모리가 100MiB 미만이었고 swap이 없었다. +- **해결:** 기존 EBS에 1GiB swap을 추가한 후 migration과 전체 배포가 성공했다. ## 6. 시니어 미션 계획