[오픈소스 기여] Supabase 대시보드 크래시 원인 분석 및 해결 (Zod safeParse 도입)

Ooleem·2026년 1월 17일

오픈소스 기여

목록 보기
1/1
post-thumbnail

오픈소스 기여모임 10기

작년 11월 K-DEVCON 2025 밋업에서 오픈소스 기여모임의 존재를 처음 알게 되었고, 10기 모집 소식을 보자마자 바로 참여하기로 결정했다. 개발자들이 보고한 다양한 문제들을 같이 고민하고 해결하는 과정이야말로 개발자로서의 문제 해결력을 기르는 것과 동시에, 오픈소스 생태계에 기여함으로써 개발자로서 세상에 흔적을 남길 수 있는 좋은 활동이라고 생각했다.

오픈소스, 이슈 선정

다음과 같은 기준으로 오픈소스와 이슈를 선정했다.

  1. 내가 사용해 봤거나, 활발히 사용할 예정인 오픈소스
    : 간단해 보이는 오픈소스도 막상 소스 코드를 보면 만만치 않다. 적어도 어떤 용도인지, 어떻게 사용하는지 정도는 알고 있어야 기여하기 수월할 것이라 생각했다.
  2. 이슈에 대해 메인테이너의 응답이 빠르고, PR merge가 활발히 이루어지는 오픈소스
    : 사실 FastAPI를 제일 먼저 찾아봤지만, FastAPI의 경우 거의 대부분 메인테이너가 올린 PR만 merge되고 있었다. 몇 가지 더 찾아본 끝에, Supabase로 결정했다.
  3. 단순 문서 편집보다는 직접적으로 코드를 수정해서 해결해야 하는 이슈
    : "오픈소스에 기여했다"라는 스펙만 얻고 싶다면 문서 수정이 제일 빠르겠지만, 첫 기여부터 문서 수정으로 남기고 싶지는 않았다. 조금이라도 코드를 수정해서 내 코드가 해당 오픈소스에 남고, 모든 사용자들이 겪고 있던 문제를 해결해주는 보람을 느끼고 싶었다.

Supabase Issue #41698 문제 상황

로컬 개발 환경에서 Supabase Studio의 Authentication 페이지에 접근하면, 다음과 같은 에러 메세지가 표시된다.

PR 생성까지의 과정

이슈 검증 (에러 재현)

예상대로 Supabase는 정말 거대한 프로젝트였다. 공식 문서에서 제공하는 아키텍처 구조도는 다음과 같다.

로컬 환경 설정하고, 무사히 돌아가게 만드는 데만 한 세월이 걸렸다. 그 와중에 동시에 돌려야 하는 도커 이미지가 하도 많다 보니 맥북이 버티질 못하고 뻗어버리기 일쑤였다..
기여 가이드를 꼼꼼히 읽어야 했다. 공식 문서를 읽는 능력이 얼마나 중요한지 새삼 깨달았다.
한참 고생한 끝에 동일한 에러를 확인했고, 로그에 어떻게 출력되는지도 확인할 수 있었다.

최대한 스스로 원인을 찾고 싶었지만, 에러 메세지가 암시하는 힌트가 너무 적었다. 에러 메세지를 긁어다가 검색해봐도 도저히 원인을 알 수 없었다. 어쩔 수 없이 이번에는 클로드의 도움을 받았다.

원인 발견

Docker 디렉터리에 있는 .env에는 PG_META_CRYPTO_KEY라는 환경변수가 있는데, app/studio 디렉터리에 있는 .env에는 해당 키가 존재하지 않았다.

studio에서 pg-meta (Postgres) 쪽으로 쿼리 요청을 보낼 때, 다음과 같이 DB 연결 정보
postgresql://${postgresUser}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE}를 암호화한다.

export function encryptString(stringToEncrypt: string): string {
  return crypto.AES.encrypt(stringToEncrypt, ENCRYPTION_KEY).toString()
}

그리고 암호화된 connectionStringEncrypted를 x-connection-encrypted 헤더로 보낸다.

const response = await fetch(`${PG_META_URL}/query`, {
  method: 'POST',
  headers: constructHeaders({
    ...headers,
    'Content-Type': 'application/json',
    'x-connection-encrypted': connectionStringEncrypted,
  }),
  body: JSON.stringify(requestBody),
})

constants.ts에 이 때 사용되는 암호화 키 ENCRYPTION_KEY = .env에 있는 PG_META_CRYPTO_KEY로 정의되고, 없을 경우 다른 기본값을 사용하도록 정의되어 있었다.

export const ENCRYPTION_KEY = process.env.PG_META_CRYPTO_KEY || '엉뚱한 값'

문제는 해당 값이 app/studio/.env에 없었고, 뒤에 '엉뚱한 값'이 docker 쪽에 있는 키 값과 달랐다.
따라서 pg-meta 서버에서 DB 연결 정보를 복호화하는 데 실패하고, {"message": "failed to get connection string"}이라는 에러 응답을 반환하게 된다.
그런데, studio가 데이터베이스 에러를 받았으니 databaseErrorSchema라는 zod 스키마를 통해 적절하게 파싱해야 하는데, 스키마에 이런 유형의 에러가 정의되지 않았다.

export const databaseErrorSchema = z.object({
  message: z.string(),
  code: z.string(),        // ← pg-meta 에러에 없음
  formattedError: z.string(), // ← pg-meta 에러에 없음
})

따라서 이 databaseErrorSchema에서 string을 기대하는데, code와 formattedError 없이 message만 딸랑 왔으니(undefined), string을 기대했는데 undefined를 받았다는 오류가 뜨게 된 것이다.

해결 방향

당연히 개발자 문서에 이와 관련된 언급이 있는지 두번 세번 체크했고, 정말로 누락된 것이 맞다는 걸 확실히 했다.
일단 studio 쪽 .env에 PG_META_CRYPTO_KEY를 docker와 동일한 값으로 넣었다.
이것만으로도 일단 당장 500 에러가 뜨는 건 막을 수 있었다.


하지만 이것만으로는 부족했다. zod 검증 실패 메시지가 studio 화면에 출력되는 건 바람직하지 않기 때문에, 적절하게 코드를 수정해야 했다.
처음 클로드가 제시한 해결책은 암호화 관련 코드가 있는 util.ts에 따로 EncryptionKeyError라는 에러를 새로 정의하고, ENCRYPTION_KEY가 비어 있거나 '엉뚱한 값'일 때 에러를 미리 내뱉도록 하는 것이었다.

그런데, 아무리 생각해 봐도 그냥 constant.ts에 있는 '엉뚱한 값'이라는 이 기본값만 docker에 있는 제대로 된 값으로 수정하면 해결될 문제인 것 같았다.
어차피 .env에 PG_META_CRYPTO_KEY 항목은 추가해 줄 것이고, 나중에 prod 환경일 때 .env에서 값을 변경하면 그 값으로 반영될 것이기 때문이다. (docker에 있는 값도 예시로 들어간 placeholder였다)

일반적으로도 그렇지만, 특히 오픈소스 코드는 영향을 받는 사람이 많기 때문에 불필요한 수정은 지양해야 한다. 그냥 기본값을 docker 쪽과 일치시켜서 해결했다.

대신 만약 실수로 키가 양쪽이 달라질 경우를 대비해서, zod 검증이 실패하면 500 에러로 서버가 터져버리는 parse() 대신 safeParse()를 쓰도록 query.ts를 변경했다.

if (!response.ok) {
      // Use safeParse to avoid throwing on schema mismatch
      const parsed = databaseErrorSchema.safeParse(result)

      if (parsed.success) {
        const { message, code, formattedError } = parsed.data
        const error = new PgMetaDatabaseError(message, code, response.status, formattedError)
        return { data: undefined, error }
      }

      // Flexibly extract error message when schema doesn't match (e.g., encryption key issues)
      const message =
        result?.message ?? result?.msg ?? result?.error ?? 'An unexpected error occurred'
      const code = result?.code ?? 'UNKNOWN_ERROR'
      const formattedError = result?.formattedError ?? message

      const error = new PgMetaDatabaseError(
        String(message),
        String(code),
        response.status,
        String(formattedError)
      )
      return { data: undefined, error }
    }

이렇게 조치하면, 키가 일치하지 않을 때 zod 에러 대신 다음과 같이 보다 분명한 에러 메세지가 출력된다.

API 응답과 같은 외부 서비스 응답을 검증할 때는 parse() 대신 safeParse()를 써야 서버가 터지지 않는다는 걸 확실히 알게 되었다.

PR 생성

PR을 생성하기 전 관련 가이드라인을 두번 세번 거듭 확인한 다음, 마침내 첫 번째 오픈소스 기여를 담은 PR을 요청했다. (pnpm format으로 linting하는 것도 잊지 않았다)

이제 기다림만 남았다.. merge되면 업데이트할 예정!

느낀 점

  • 좋은 의미로나 나쁜 의미로나, 오랜만에 핀토스를 다시 하는 기분이었다.
    문서를 아주 꼼꼼히 읽어야 하고, 별개로 이 거대한 구조가 왜 이렇게 되어 있는지, 각각이 어떤 역할을 하는지를 빠르게 파악하는 능력이 딱 핀토스 때 요구되던 것과 똑같았다.
    개인적으로는 핀토스를 굉장히 재미있게 했기 때문에, 이번에도 문제의 원인이 뭘까 고민하고 머리를 싸맸지만 즐겁게 진행했다. 이번에는 클로드의 도움을 받았지만, 다음에 또 오픈소스 기여에 도전한다면 그때는 꼭 내가 스스로 원인까지 찾아내고 싶다.
  • 문제의 근본적인 원인은 AI가 찾아내더라도, 어떤 방향으로 해결할지 고민하는 건 여전히 개발자의 몫이다. 문제에 대한 해결책이 여러 방향으로 나올 수 있고, 상황과 맥락을 종합적으로 고려하여 판단해야 한다.
    특히 요즘 오픈소스 기여라는 타이틀만 보고 AI 돌려서 검증도 안 해보고 PR을 찍어내는 행태가 심각한 문제라고 한다. 자기 자신을 위해서도, 오픈소스 생태계를 위해서도 절대 하지 말아야 할 행동이다.
profile
개발 / 성장 노트

0개의 댓글