로그인이 되어 있다고 모든 작업을 같은 수준으로 믿어도 될까요? 저희는 B/L(Bill of Lading, 선하증권)처럼 민감한 업무를 실행하기 직전에 이메일 OTP로 한 번 더 확인하는 흐름이 필요했습니다.
새 로그인 체계를 만드는 대신, 기존 로그인과 권한 검사는 그대로 두고 Auth0의 Custom Token Exchange(CTE) 와 MFA API를 이어 붙였습니다. 이 글은 그 과정에서 내린 결정과, 두 번 크게 헤맨 지점을 정리한 기록입니다.
세 줄 요약
- 이미 가진 accessToken을 CTE에 넘겨 사용자를 확인하고, Post Login Action에서 MFA를 요구해 이메일 OTP를 보냅니다.
- 토큰을 직접 해석하지 않고 신뢰하는 /userinfo 서버에 물어보는 방식으로 검증을 단순하게 만들었습니다.
- OTP 검증 뒤에는 Auth0 토큰 대신 30분짜리 별도 업무 세션(Redis + HttpOnly 쿠키) 으로 보호된 요청을 통과시킵니다.
코드는 설명에 필요한 부분만 발췌했고, 도메인과 사용자 ID는 예시 값입니다.

먼저 알아둘 용어
이 글에 자주 나오는 단어들입니다. 처음 보는 용어가 있다면 여기만 훑고 넘어가도 충분합니다.
용어한 줄 설명
| Step-up 인증 | 로그인은 유지한 채, 민감한 작업 직전에만 추가 확인을 요구하는 방식 |
| CTE (Custom Token Exchange) | 외부에서 받은 토큰(subject_token)을 Auth0 Action에서 검증하고, 그 결과로 Auth0 사용자를 지정하는 토큰 교환 기능 |
| subject_token | CTE에 넘기는 "내가 누구인지 증명하는" 토큰. 이번에는 기존 로그인 accessToken |
| Subject Token Type | 어떤 종류의 토큰 교환인지 구분하는 식별자. 여기서는 urn:example:step-up-email |
| mfa_token / oob_code | MFA 진행 중임을 나타내는 임시 토큰 / 이메일 challenge를 식별하는 코드. OTP 검증 때 함께 보냄 |
| JWS / JWE | 서명만 된 JWT(내용이 보임) / 암호화된 JWT(내용이 안 보임) |
| Opaque 토큰 | 내부 구조를 해석하지 않고 발급 서버에 물어봐야 하는 토큰 |
| 테넌트 | Auth0에서 사용자·앱·설정이 들어 있는 독립된 공간. 테넌트가 다르면 완전히 다른 세계 |
1. 왜 로그인 뒤에 추가 인증이 필요했나
목표는 로그인을 새로 만드는 게 아니라, 민감한 B/L 작업을 실행하는 순간에 사용자를 한 번 더 확인하는 것이었습니다. 로그인은 그대로 살아 있어도 보호된 작업을 누르면 OTP를 요구하고, 추가 인증이 만료되면 다시 OTP를 받습니다.
핵심은 "로그인했다", "권한이 있다", "OTP를 통과했다"가 서로 다른 질문이라는 점입니다.
확인 항목답하려는 질문누가 담당하나
| 기본 인증 | 지금 요청한 사람은 누구인가? | 기존 로그인 경로 |
| 업무 권한 | 이 사람이 B/L 업무를 할 수 있는가? | BL API의 기존 권한 조회 |
| 추가 인증 | 이 사람이 방금 OTP를 통과했는가? | Auth0 MFA + BL step-up 세션 |
순서도 중요합니다. 권한 없는 사람에게 OTP 메일부터 보내면 안 되므로, BL API는 권한 검사를 통과한 요청에만 Auth0 연동을 시작합니다.
이번 작업으로 검증하려던 가설과 확인 기준은 이렇습니다.
- 가설: 기존 로그인 경로를 건드리지 않고 특정 업무에만 추가 인증을 걸 수 있다.
- 확인 기준
- 권한이 거부되면 OTP 요청이 발생하지 않는다.
- OTP 검증에 성공해야만 추가 인증 세션이 생긴다.
- 세션이 만료되면 보호된 작업이 거부된다.
2. 설계: 역할을 네 조각으로 나눴다
사용자는 이미 로그인한 상태입니다. 비밀번호를 다시 받는 대신 이미 가진 accessToken으로 사용자를 확인하고, 곧바로 Auth0의 MFA 절차로 넘어가고 싶었습니다. 그래서 전체 흐름을 네 조각으로 나눴습니다.
조각하는 일하지 않는 일
| CTE Action | 기존 accessToken으로 사용자를 확인하고 setUserById 호출 | 업무 권한 판단 |
| Post Login Action | BL용 CTE 요청에만 MFA 요구 | 일반 로그인에 MFA 강제 |
| MFA API (/mfa/*) | 이메일 challenge 발송과 OTP 검증 | 업무 세션 관리 |
| BL API + Redis | 권한 확인, step-up 세션 생성·검사 | Auth0 토큰 재사용 |
왜 CTE였나
CTE는 전달받은 subject_token을 커스텀 Action에서 확인하고, 그 결과로 Auth0 사용자를 지정할 수 있습니다(Auth0 CTE 문서). 이번 요구사항은 두 가지였습니다.
- 입력은 이미 가진 로그인 토큰이어야 한다.
- OTP 입력은 우리 애플리케이션 UI에서 처리한다.
CTE + MFA API 조합이 이 두 조건에 잘 맞았습니다. 물론 유일한 정답은 아닙니다. 요구사항이 다르면 다른 방식이 더 나을 수 있습니다.
한 가지 전제도 있습니다. CTE는 사용자를 새로 만들지 않으므로, 토큰으로 확인된 사용자가 CTE를 실행하는 테넌트에 이미 존재해야 합니다. 이 전제가 뒤에서 문제를 일으킵니다.
3. 삽질 1 — JWT인 줄 알았는데 JWE였다
받은 accessToken을 열어 보니 익숙한 xxx.yyy.zzz 세 구간이 아니라 점으로 나뉜 다섯 구간이었습니다. 헤더는 이랬습니다.
{
"alg": "dir",
"enc": "A256GCM"
}
서명된 JWT(JWS)가 아니라 암호화된 JWT(JWE) 였던 겁니다. "JWT니까 공개키로 서명 검증하면 되겠지"라는 생각이 여기서 막힙니다.
구분JWS (서명 JWT)JWE (암호화 JWT)
| 구간 수 | 3개 | 5개 |
| 목적 | 위·변조 방지 | 내용 숨기기 |
| payload | 누구나 디코딩해서 읽을 수 있음 | 키 없이는 읽을 수 없음 |
| 검증 방법 | 공개키(JWKS)로 서명 확인 | 복호화 키가 먼저 필요 |
처음에는 Client Secret이 복호화 키가 아닐까 싶어 이것저것 시도했습니다. 하지만 Client Secret이 있다는 사실만으로 그게 JWE 키라고 단정할 수는 없었습니다. 실제 발급 코드와 키 처리 방식을 모르는 상태에서 키를 변환해 보는 건, 성공하든 실패하든 원인을 설명해 주지 못했습니다.
그래서 질문 자체를 바꿨습니다.
이 토큰을 꼭 내가 직접 복호화해야 할까? 발급한 서버에 "이 토큰 주인 누구야?"라고 물어보면 되지 않을까?
브라우저 Network 탭에서 토큰 발급 요청을 확인한 뒤, 이 accessToken으로 신뢰하는 Auth0의 /userinfo를 호출해 봤습니다. 정상적인 사용자 정보가 돌아왔습니다.
참고로 Auth0는 요청한 audience에 따라 accessToken의 용도와 형식이 달라집니다. /userinfo용 opaque 토큰과 자체 API용 JWT는 같은 방식으로 다루면 안 됩니다(Auth0 accessToken 문서).
4. 해결 — 토큰 해석은 신뢰하는 /userinfo에 맡긴다
CTE Action은 받은 토큰을 해석하지 않고, 그대로 /userinfo에 보냅니다.
GET https://login.example.com/userinfo
Authorization: Bearer
200 OK이고 응답에 유효한 sub가 있으면, 그 ID를 그대로 Auth0 사용자로 지정합니다.
// 실제 Action의 일부를 발췌한 코드입니다.
const response = await fetch(new URL('userinfo', trustedIssuer), {
headers: {
Authorization: `Bearer ${subjectToken}`,
Accept: 'application/json',
},
redirect: 'error', // 리다이렉트 거부
signal: AbortSignal.timeout(5000), // 5초 타임아웃
});
// 상태 코드, JSON 형식, sub 형식 확인을 통과한 뒤에만 실행
api.authentication.setUserById(userInfo.sub);
안전장치
"다른 서버에 물어본다"는 방식은 간단한 대신, 어디에 어떻게 묻느냐가 중요합니다.
안전장치이유
| 호출 목적지를 Secret(EXPECTED_ISSUER)으로 고정 | 토큰 claim이나 요청 body의 URL로 목적지가 바뀌지 않게 |
| 리다이렉트 거부 (redirect: 'error') | 엉뚱한 서버로 토큰이 흘러가지 않게 |
| 5초 타임아웃 | 외부 서버 지연이 로그인 흐름 전체를 붙잡지 않게 |
| 401·403 → 토큰 거부 | 유효하지 않은 토큰을 명확히 차단 |
| 타임아웃·5xx·이상한 응답 → 사용자 미지정 후 실패 | 애매하면 통과시키지 않음(fail closed) |
| 토큰·응답 전문은 로그에 남기지 않음 | 민감 정보 노출 방지 |
주의: EXPECTED_ISSUER라는 이름은 이전 구현에서 그대로 가져온 것입니다. JWT의 iss를 검증하는 값이 아니라 "어느 /userinfo 서버를 믿을지"를 정하는 설정입니다.
이 검사가 보장하는 것과 보장하지 않는 것
보장함보장하지 않음
| 이 토큰으로 신뢰한 서버에서 사용자를 조회할 수 있다 | BL API용 audience인지 여부 |
| 그 사용자의 sub가 무엇인지 | B/L 업무 권한 (→ 기존 BL 로직이 따로 판단) |
덕분에 Action에 필요한 Secret도 두 개로 줄었습니다. JWE 복호화 키나 로컬 audience 검증값은 필요 없습니다.
Secret의미
| EXPECTED_ISSUER | 신뢰할 Auth0 /userinfo 서버의 HTTPS 주소 |
| ALLOWED_CLIENT_ID | CTE 호출을 허용할 애플리케이션 ID |
단, BL이 Auth0에 CTE 요청을 보낼 때 필요한 애플리케이션 인증 설정(Client Secret 등)은 이와 별개입니다.
5. 삽질 2 — 테스트는 성공, 실제 호출은 User not found
Action을 단독으로 테스트하면 기대한 명령이 정확히 나왔습니다.
{
"type": "SetUserById",
"user_id": "auth0|example-user"
}
그런데 실제 CTE를 호출하면 User not found.가 돌아왔습니다.
여기서 배운 건 "Action Test가 명령을 만들어냈다" ≠ "실제로 사용자를 찾았다" 라는 점입니다. 테스트는 Action이 어떤 명령을 생성하는지만 보여줍니다. 실제 토큰 교환은 그 사용자가 CTE를 실행하는 테넌트에 존재하는지까지 확인합니다.
원인은 테넌트 불일치였습니다.
기존 accessToken → 테넌트 A의 /userinfo → 사용자 확인 성공
BL의 CTE 요청 → 테넌트 B → 같은 sub를 찾지 못함
사용자명이나 이메일이 같아도 Auth0 사용자 ID까지 같다고 가정할 수는 없습니다. 그래서 ID를 다른 계정 것으로 바꿔 끼우는 대신, 사용자가 실제로 있는 테넌트에서 CTE가 실행되도록 설정을 맞췄습니다.
테넌트를 맞춘다 = Domain 하나 바꾸기가 아니다
아래 항목이 전부 같은 테넌트를 가리켜야 합니다.
- 호출용 애플리케이션
- Client ID와 토큰 엔드포인트 인증 설정
- Connection
- CTE 프로필 (Subject Token Type)
- 배포된 CTE Action / Post Login Action
- 실행 중인 BL의 Auth0 Domain 설정
실제로 새 테넌트에 프로필을 만들어 놓고, 실행 중인 BL은 여전히 이전 테넌트로 요청해서 Invalid subject_token_type이 난 적도 있었습니다. 이때는 Auth0 로그의 tenant_name, hostname, client_id, subject_token_type 네 값을 보고 이전 설정이 쓰이고 있다는 걸 찾아냈습니다.
6. MFA를 요구하고 이메일을 보내기까지
CTE Action에서 사용자 확인이 끝나도 메일이 바로 나가지는 않습니다. 메일이 나가기까지는 세 단계가 더 있습니다.
① Post Login Action — BL 요청에만 MFA 요구
const SUBJECT_TOKEN_TYPE = 'urn:example:step-up-email';
exports.onExecutePostLogin = async (event, api) => {
// BL용 CTE 요청이 아니면 아무것도 하지 않음
if (
event.transaction?.protocol !== 'oauth2-token-exchange' ||
event.transaction?.subject_token_type !== SUBJECT_TOKEN_TYPE
) {
return;
}
// 허용된 앱에서 온 요청인지 확인
const allowedClientId = event.secrets?.ALLOWED_CLIENT_ID;
if (!allowedClientId || event.client?.client_id !== allowedClientId) {
api.access.deny('BL CTE client is not allowed');
return;
}
api.multifactor.enable('any', {
allowRememberBrowser: false,
});
};
프로토콜과 Subject Token Type을 먼저 검사하는 이유는, 일반 로그인 전체에 MFA가 걸리지 않게 하기 위해서입니다.
기존 Post Login Action 중 폼을 렌더링하거나 challengeWith()를 쓰는 코드는 CTE에서 지원되지 않습니다(CTE의 MFA 지원과 제한). 그래서 CTE 요청이면 건너뛰도록 분기했습니다. 다만 "모든 CTE를 건너뛴다" 조건은 BL 외 다른 CTE 사용처에도 영향을 주므로, 테넌트에서 CTE를 어디에 쓰는지 함께 확인해야 합니다.
② CTE 응답에서 MFA 단계로 전환
MFA가 필요하면 Auth0는 CTE 요청에 mfa_required와 mfa_token을 돌려줄 수 있습니다. BL 클라이언트는 403 응답에 이 두 값이 있으면 실패가 아니라 다음 단계로 처리합니다.
③ 이메일 authenticator 조회 → challenge
- /mfa/authenticators로 사용자의 authenticator 목록을 조회합니다.
- 활성 상태인 이메일 authenticator를 고릅니다. 없으면 중단합니다(자동 등록은 하지 않음).
- /mfa/challenge를 호출합니다. Auth0가 OTP 메일을 보내는 시점이 바로 여기입니다. (이메일 authenticator와 challenge)
헷갈리기 쉬운 이메일 설정
이름이 비슷하지만 모두 다른 설정입니다. 메일이 안 온다면 이 표부터 확인하세요.
설정역할비고
| Email MFA factor | 이메일을 MFA 수단으로 쓸 수 있게 함 | 테넌트 단위 설정 |
| 앱의 MFA Grant Type | 해당 앱이 MFA API를 쓸 수 있게 허용 | 앱 단위 설정, factor 활성화와 별개 |
| Email Provider | 실제 메일 전송 서비스와 발신자 | |
| Verification Code for Email MFA 템플릿 | OTP 메일의 제목·본문 | |
| Verification Email (Link) 템플릿 | 이메일 주소 확인 링크 메일 | MFA OTP와 무관 |
7. 전체 흐름 한눈에 보기
지금까지의 조각을 이어 붙이면 크게 세 구간입니다. 아래는 현재 코드가 구성하는 목표 흐름이며, 실제 전 구간 성공 여부는 10장에서 따로 정리했습니다.
구간시작끝결과물
| ① OTP 발송 | 사용자가 민감한 B/L 업무 시작 | Auth0가 OTP 메일 발송 | mfaToken, oobCode |
| ② OTP 검증 | 사용자가 OTP 입력 | Redis에 세션 저장 | STEPUP_SESSION 쿠키 (30분) |
| ③ 보호된 요청 | 쿠키를 실은 업무 요청 | Guard 검사 통과 | 업무 처리 |
sequenceDiagram
actor U as 사용자
participant FE as 프런트
participant GW as API Gateway
participant BL as BL API
participant A as Auth0
participant R as Redis
Note over U,R: ① OTP 발송
U->>FE: 민감한 B/L 업무 시작
FE->>GW: send-otp
GW->>BL: accessToken과 사용자 metadata 전달
BL->>BL: 기존 업무 권한 확인
BL->>A: CTE 요청 (subject_token)
A->>A: CTE Action — 신뢰한 userinfo 호출
A->>A: 응답 sub로 사용자 지정
A->>A: Post Login Action — MFA 요구
A-->>BL: mfa_required + mfa_token
BL->>A: 활성 이메일 authenticator 조회
BL->>A: 이메일 MFA challenge 요청
A-->>U: OTP 이메일
A-->>BL: oob_code
BL-->>GW: mfaToken, oobCode
GW-->>FE: OTP 입력 화면에 필요한 값
Note over U,R: ② OTP 검증
U->>FE: OTP 입력
FE->>GW: verify-otp
GW->>BL: code + mfaToken + oobCode
BL->>A: MFA OOB 검증
A-->>BL: 검증 성공
BL->>R: 사용자별 step-up 세션 저장
BL-->>GW: stepUpToken, expiresAt
GW-->>FE: HttpOnly 쿠키 설정 + expiresAt
Note over U,R: ③ 보호된 요청
FE->>GW: 업무 요청 + 쿠키
GW->>BL: x-stepup-token (gRPC metadata)
BL->>R: 세션 유효성 + 로그인 사용자 일치 확인
R-->>BL: 확인 결과
BL-->>FE: 통과 시 업무 처리
8. OTP를 통과한 뒤에는 별도의 업무 세션을 쓴다
매 API 호출마다 OTP를 받을 수는 없습니다. 그래서 검증에 성공하면 BL API가 step-up 전용 세션을 따로 만듭니다. Auth0 토큰을 "추가 인증 완료" 쿠키로 재활용하지 않습니다.
세션 토큰 만들기
const stepUpToken = randomBytes(32).toString('base64url'); // 브라우저로 전달
const digest = createHash('sha256').update(stepUpToken).digest('hex');
const redisKey = `stepup:mfa:${digest}`; // Redis에는 해시만
항목값
| 토큰 | 32바이트 난수 (base64url) |
| Redis 키 | stepup:mfa: + SHA-256 해시 |
| Redis 값 | 사용자 ID, MFA 확인 여부, 확인 시각, 만료 시각 |
| TTL | 1,800초 (30분) |
브라우저에는 원본 난수를, Redis 키에는 해시를 씁니다. 다만 해시를 쓴다고 해서 브라우저가 가진 원본 토큰을 보호할 필요가 없어지는 건 아닙니다. 원본은 여전히 bearer token입니다.
쿠키 설정
BL API는 gRPC로 요청을 받기 때문에, 쿠키를 읽고 Set-Cookie를 내려주는 일은 HTTP 경계인 Gateway가 맡습니다.
Set-Cookie: STEPUP_SESSION=<random-token>; HttpOnly; Secure; SameSite=Lax; Path=/api; Max-Age=1800
속성의미
| HttpOnly | JavaScript에서 읽을 수 없음 (XSS로 탈취 어렵게) |
| Secure | HTTPS에서만 전송 |
| SameSite=Lax | 다른 사이트에서 시작된 요청에는 대부분 붙지 않음 |
| Path=/api | API 경로에만 전송 |
| Max-Age=1800 | Redis TTL과 같은 30분 |
이후 요청에서는 Gateway가 쿠키를 x-stepup-token gRPC metadata로 바꿔 넘기고, BL의 Guard가 Redis 세션과 현재 로그인 사용자가 같은 사람인지 확인합니다. Guard가 붙은 RPC만 검사하므로, 보호 대상 RPC를 명확히 지정해야 합니다.
놓치기 쉬운 두 가지
- 세션 연장은 양쪽 다: 연장 API에서 Redis TTL만 늘리면 브라우저 쿠키가 먼저 만료됩니다. 쿠키의 Max-Age도 같이 갱신해야 합니다.
- cross-origin이지만 same-site일 수 있다: 프런트와 Gateway가 같은 상위 도메인 아래 다른 서브도메인이면, origin은 다르지만 site는 같습니다. 프런트의 credentials 설정, Gateway의 허용 Origin·credentials, 쿠키 속성을 함께 확인해야 합니다.
9. 단계별 로그가 원인을 찾아줬다
사용자에게는 단순한 오류를 보여주더라도, 내부에서는 어느 단계에서 실패했는지 구분해야 합니다. OTP could not be sent 한 줄로는 권한 거부인지, CTE 실패인지, challenge 실패인지 알 수 없었습니다.
순서로그 키의미
| 1 | otp_permission_allowed | BL 업무 권한 확인 통과 |
| 2 | auth0_cte_failed | CTE 요청 거부 또는 교환 실패 |
| 2 | auth0_cte_mfa_required | 기대한 MFA 필요 응답 수신 |
| 3 | auth0_email_mfa_not_enrolled | 활성 이메일 authenticator 없음 |
| 3 | auth0_email_mfa_challenge_created | 이메일 challenge 생성 성공 |
| 4 | auth0_email_mfa_verified | OTP 검증 요청 성공 |
| 5 | stepup_session_created | Redis 업무 세션 생성 성공 |
CTE가 실패하면 Auth0의 fecte 로그에서 description과 테넌트·앱·Subject Token Type을 같이 봤습니다. HTTP 상태와 오류 코드가 같아도 상세 원인은 User not found와 Invalid subject_token_type처럼 달랐습니다.
성공도 같은 방식으로 쪼개서 봐야 합니다.
- challenge 생성 성공 ≠ 메일 실제 수신
- Action Test의 RequireMultifactorAuth 출력 ≠ OTP 검증 성공
10. 지금까지 확인한 것과 남은 것
솔직하게 말하면, 아직 메일 수신부터 쿠키 재전송까지 전 구간이 성공했다고 말할 단계는 아닙니다. 마지막 통합 테스트에서 BL이 이전 테넌트 설정으로 요청하는 문제를 발견했기 때문입니다.
항목상태근거
| 기존 토큰으로 /userinfo 사용자 조회 | 확인됨 | 실제 호출 성공 |
| CTE Action의 SetUserById 명령 생성 | 확인됨 | Action Test |
| Post Login의 RequireMultifactorAuth 명령 생성 | 확인됨 | Action Test |
| CTE Action 예외 처리 (잘못된 토큰·설정, 타임아웃, 비정상 JSON, sub 누락) | 확인됨 | HTTP 모킹한 로컬 테스트 |
| 실제 이메일 수신 ~ OTP 검증 ~ 쿠키 재전송 | 미확인 | 테넌트 설정 정리 후 재테스트 필요 |
로컬 테스트는 실제 테넌트 설정이나 메일 발송 성공을 대신하지 못합니다. 다음 통합 테스트에서 확인할 항목입니다.
- 실행 중인 BL이 사용자·프로필이 있는 테넌트와 앱으로 요청하는가
- 앱의 토큰 엔드포인트 인증 방식과 BL의 Client Secret 전송 방식이 맞는가
- 유효한 CTE 요청이 MFA 필요 응답으로 이어지는가
- 활성 이메일 authenticator 주소로 메일이 오고, 잘못된 OTP는 거부되는가
- OTP 절차의 사용자와 현재 로그인 사용자의 연결이 끝까지 유지되는가
- 쿠키 저장·전송, 세션 만료·연장이 기대대로 동작하는가
11. 배운 점
돌아보면 가장 큰 혼란은 토큰의 형식, 토큰의 용도, 사용자가 속한 환경을 한 덩어리로 생각한 데서 시작됐습니다.
착각실제
| JWT니까 공개키로 검증하면 된다 | 암호화된 JWE였고, 해석보다 발급 서버에 묻는 게 맞았다 |
| 사용자 조회에 성공했으니 사용자 지정도 될 것이다 | 조회한 테넌트와 CTE 테넌트가 달랐다 |
| Action Test 통과 = 동작함 | 테스트는 명령 생성만 보여줄 뿐, 실제 조회·발송과는 다르다 |
| Domain만 바꾸면 테넌트 전환 끝 | 앱·Client·Connection·프로필·Action·실행 중인 서비스 설정까지 맞춰야 한다 |
역할을 CTE = 사용자 확인, Post Login = MFA 요구, MFA API = challenge와 검증, BL + Redis = 업무 세션으로 나누고 나서야 실패 지점을 설명하기 쉬워졌습니다.
다음 테스트에서도 "전체가 된다/안 된다" 한 마디로 판단하지 않고, 각 경계가 무엇을 확인했는지를 로그와 응답으로 하나씩 확인하려고 합니다.
'Tech Notes' 카테고리의 다른 글
| tmux와 launchd로 원격 개발 환경 상시화하기 (0) | 2026.07.15 |
|---|---|
| [앱인토스] `LOGIN_CODE_NOT_EXIST` 에러 처리하기 (0) | 2026.06.22 |
| 무료 금융 API 제대로 쓰는 법 — Alpha Vantage (0) | 2026.06.15 |
| 다 같은 개념인 줄 알았다 — on-demand · serverless · Edge Function · 콜드 스타트 (0) | 2026.06.01 |
| 화면은 누가 그리나 — SSR · CSR, 그리고 그 사이의 것들 (0) | 2026.06.01 |