개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← YouTube Shorts 자동화

OAuth 안정화 — 배포 환경 인증 흐름

2026년 7월 19일
authOAuthPrismaDockerweb

전날 채널에 userId를 붙였다. 배포하니까 이제 진짜 문제들이 터지기 시작했다.

shared 패키지 — subpath export 못 찾는 webpack

@shorts/shared에서 prisma, s3, logger 같은 서브모듈을 나눠서 export하고 있었는데, package.json의 exports 필드에 루트 하나만 정의돼 있었다.

// packages/shared/package.json
"exports": {
  ".": "./dist/index.js",
  "./prisma.js": "./dist/prisma.js",
  "./s3.js": "./dist/s3.js",
  "./logger.js": "./dist/logger.js",
  "./env.js": "./dist/env.js",
  "./types.js": "./dist/types.js"
}

web에서 @shorts/shared/prisma.js를 import하는 순간 webpack이 resolve를 못 했다. 서브패스마다 exports map에 명시해야 하는 걸 몰랐다.

Docker 빌드 — Prisma 클라이언트 빌드 누락

exports는 고쳤는데 이번엔 Docker 이미지에서 dist/prisma.js가 아예 없다고 났다. Dockerfile에서 shared 패키지를 build만 하고 prisma generate를 안 돌리고 있었다.

COPY . .
RUN pnpm --filter @shorts/shared prisma:generate && pnpm --filter @shorts/shared build

base 이미지 — Bookworm에 없는 libssl.so.1.1

Prisma 엔진 바이너리가 OpenSSL 1.1 계열로 빌드돼 있었는데, node:20-slim이 Debian Bookworm 기반이라 OpenSSL 3만 들어있다. bullseye-slim으로 내렸다.

-FROM node:20-slim AS base
+FROM node:20-bullseye-slim AS base

auth 페이지들도 빌드 타임 프리렌더에서 AUTH_SECRET 없이 auth()를 호출해 에러가 났다. force-dynamic으로 프리렌더 자체를 껐다.

export const dynamic = 'force-dynamic';

SessionProvider — useSession()이 undefined

Sidebar에서 useSession()으로 session.user.id를 읽게 바꿨는데, providers.tsx에 SessionProvider를 안 감싸놨다. TypeError.

return (
  <SessionProvider>
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  </SessionProvider>
);

JWT 콜백 — 기존 세션엔 userId가 안 채워짐

jwt 콜백이 신규 로그인(user 파라미터가 있을 때)에만 userId를 채우고 있었다. userId 붙이기 전에 이미 로그인해 있던 세션은 영영 채워지지 않는다는 뜻이다. 토큰에 email로 재조회하는 경로를 추가했다.

async jwt({ token, user }) {
  const email = (user?.email ?? token.email as string | undefined)?.toLowerCase();
  if (email && !token.userId) {
    const found = await prisma.user.findUnique({ where: { email }, select: { id: true } });
    if (found) token.userId = found.id;
  }
  ...
}

Prisma 엔진 바이너리 — debian-openssl-1.1.x Access Denied

bullseye로 내렸지만 binaryTargets에 debian-openssl-1.1.x가 없어서 standalone 빌드에 엔진 바이너리가 안 실렸다. 세 군데를 같이 고쳐야 했다.

binaryTargets = ["native", "debian-openssl-1.1.x", "rhel-openssl-3.0.x"]

next.config.ts에 serverExternalPackages와 outputFileTracingRoot를 추가하고, Dockerfile에서 엔진 파일을 찾아 runner 스테이지로 명시 복사했다.

RUN find /app -name "libquery_engine-debian-openssl-1.1.x.so.node" | head -1 | xargs -I{} cp {} /tmp/prisma-engine-debian.so.node
...
COPY --from=builder /tmp/prisma-engine-debian.so.node ./apps/web/.prisma/client/libquery_engine-debian-openssl-1.1.x.so.node

OAuth 콜백 — login_hint 해프닝과 최종 프록시

로그인 계정을 자동 선택시키려고 login_hint 파라미터를 넣었다가, authuser와 얽혀 redirect_uri 검증이 깨지는 걸 확인하고 바로 되돌렸다. 대신 콜백 자체를 우리 도메인(shortsautomation.com)을 경유하는 프록시로 만들었다.

export async function GET(request: NextRequest) {
  const apiBase = process.env.NEXT_PUBLIC_API_URL ?? '';
  return NextResponse.redirect(`${apiBase}/auth/youtube/callback${request.nextUrl.search}`, { status: 302 });
}

콜백에서 code가 없이 들어오는 케이스도 있어서 optional로 바꾸고 try-catch로 감쌌다. 실패하면 에러 메시지를 붙여서 /close로 리다이렉트한다.

try {
  const channel = await this.authService.handleCallback(code, state);
  res.code(302).redirect(`${webOrigin}/close?channelId=${channel.id}`);
} catch (err) {
  const msg = err instanceof Error ? err.message : 'OAuth 처리 실패';
  res.code(302).redirect(`${webOrigin}/close?auth_error=${encodeURIComponent(msg)}`);
}

하루 만에 인증 관련 버그를 여섯 개 넘게 잡았다. 배포 환경은 로컬이랑 다르다는 걸 다시 확인한 날.

← 이전 글소유권 구조 — 채널별 멀티유저 지원
다음 글 →썸네일 프록시 — 안정화와 일일 생성 한도