도메인이 자동 갱신되는 걸 깜빡했어요. 그래서 사이드 프로젝트용 대시보드를 만들었어요.

발행: (2026년 5월 3일 AM 07:54 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

위에 제공된 텍스트를 한국어로 번역해 주세요. 번역할 내용이 포함된 전체 텍스트를 알려주시면, 요청하신 대로 소스 링크는 그대로 두고 마크다운 형식과 코드 블록, URL은 그대로 유지하면서 번역해 드리겠습니다.

Source:

나는 여러 가지 작은 일을 부업으로 하고 있다

친구 논문용 웹사이트를 만들었고, 몇 건의 풀스택 커미션 작업을 맡았으며, 실제로 수익을 내기 위해 노력 중인 부업도 하나 있다. 이 모든 것이 같은 스택을 사용하지 않는다. 나는 해당 작업에 가장 관대한 무료 티어를 제공하는 서비스를 계속 골라 사용한다.

  • 논문 사이트는 Firebase에 있다.
  • 커미션 작업은 다른 호스트에 있다.
  • 부업은 세 개의 제공업체에 걸쳐 나뉘어 있다.

무료 플랜마다, 대시보드마다, 이메일마다 다르다. 지난달에 완전히 잊고 있던 프로젝트의 도메인이 자동 갱신되는 것을 발견했다. 그때 나는 머릿속에 이 모든 정보를 기억하려 애쓰거나, 절대 업데이트하지 않을 Notion에 보관하려는 시도를 포기했다.

그래서 StackMemo를 만들었다 – 각 프로젝트마다 하나의 보드를 두고, 제공업체 API와 연결해 비용과 KPI 수치를 자동으로 업데이트한다.

이 글은 실제로 중요한 몇 가지 설계 결정을 다룬다. 비슷한 것을 만들고자 하는 사람이나, 많은 움직이는 조각들 속에서 작은 Next.js + Postgres 앱이 어떻게 맞물리는지 궁금한 사람을 위해 썼다.

스택

  • Next.js 16 (App Router) + TypeScript + Tailwind v4
  • Postgres on Neon, raw pg (ORM 사용 안 함 – SQL을 직접 보고 싶다)
  • NextAuth v5 (credentials, GitHub, Google)
  • Stripe 청구 시스템 (유료 티어)
  • Next.js instrumentation.ts를 이용한 인‑프로세스 크론

1. The connector registry

Every provider (GitHub, Stripe, Neon, Cloudflare, Koyeb today) implements the same interface and gets registered once. Adding a new provider is one new file plus one line in the registry.

// lib/connectors/types.ts
export type ConnectorImpl = {
  provider: ConnectorProvider;
  displayName: string;
  iconPath?: string;
  authType: "api_key" | "token" | "oauth";
  credentialsSchema: CredentialsSchema;
  setupGuide?: SetupGuide;
  kpiCatalog: KpiDefinition[];
  listResources(creds: ConnectorCredentials): Promise;
  sync(args: SyncArgs): Promise;
};
// lib/connectors/index.ts
const registry: Partial<ConnectorImpl> = {
  github,
  stripe,
  neon,
  cloudflare,
  koyeb,
};

export function getAllProviderMetadata(): ProviderMetadata[] {
  /* … */
}

kpiCatalog이 핵심이다. 각 커넥터는 가져올 수 있는 메트릭(github.stars, stripe.mrr, cloudflare.requests_24h, …)을 선언하고, 사용자는 프로젝트별로 활성화할 메트릭을 선택한다. Sync는 활성화된 KPI만 가져오므로 API 예산을 효율적으로 사용할 수 있다.

인터페이스는 의도적으로

type SyncResult = { kpis: Record<string, any>; warnings: string[] };

를 반환하도록 설계되었으며, 부분적인 실패 시 예외를 발생시키지 않는다. 실제 서비스 API는 플랜 제한 엔드포인트에서 403을 자주 반환하는데, 이를 치명적인 실패로 처리하면 하나의 제한된 KPI 때문에 전체 동기화가 중단된다.

마케팅 랜딩 페이지도 이 동일한 레지스트리를 읽어들이기 때문에, 새로운 커넥터를 추가하면 칩이 홈페이지 마키에 자동으로 표시된다 – 별도의 목록을 동기화할 필요가 없다.

2. Edge‑safe auth split (Next.js 16 + NextAuth v5)

NextAuth v5는 미들웨어가 사용할 수 있도록 인증 구성을 Edge 런타임에 두도록 요구합니다. 하지만 전체 구성에는 Postgres 어댑터와 자격 증명 로그인을 위한 bcrypt가 필요하지만, 이 둘은 Edge에서 동작하지 않습니다.

해결 방법: 구성을 두 파일로 분리합니다.

// lib/auth.config.ts — Edge‑safe (no pg, no bcrypt)
export default {
  providers: [GitHub({ /* … */ }), Google({ /* … */ })],
  pages: { signIn: "/login" },
  callbacks: { authorized: ({ auth }) => !!auth?.user },
} satisfies NextAuthConfig;
// lib/auth.ts — Node runtime (full version)
export const { handlers, auth, signIn, signOut } = NextAuth({
  ...authConfig,
  adapter: PostgresAdapter(pool),
  providers: [...authConfig.providers, Credentials({ /* … */ })],
});

미들웨어는 Edge‑safe 구성을 가져오고, 나머지는 전체 구성을 가져옵니다. 이 함정은 뒤돌아보면 명확하지만, 이를 모르면 반나절을 잡아먹습니다.

3. 마케팅용과 앱 크롬용 라우트 그룹

첫 번째 버전에서는 사용자 헤더가 포함된 단일 app/layout.tsx만 있었습니다. 마케팅 랜딩 페이지를 추가했을 때, 그 헤더가 페이지 상단에 표시되어 분위기가 맞지 않았습니다(랜딩 페이지는 자체 네비게이션을 가짐).

해결책: Next.js 라우트 그룹.

app/
  layout.tsx                ← html/body/fonts only
  (marketing)/
    layout.tsx              ← marketing nav + footer
    page.tsx                ← /
    pricing/page.tsx        ← /pricing
  (app)/
    layout.tsx              ← app header with auth state
    dashboard/page.tsx      ← /dashboard
    projects/[id]/...

URL은 그대로이며 라우트 그룹은 라우터에 보이지 않습니다. 각 그룹은 자체 크롬을 가집니다. 마케팅 페이지는 인증 코드를 전혀 import하지 않으므로, 인증되지 않은 방문자는 랜딩 페이지를 로드하기 위해 데이터베이스 쿼리를 수행하지 않습니다.

또한 미들웨어 매처를 업데이트하여 실제 앱 라우트만 보호하도록 하고, /, /login, /signup 및 공개 공유 URL( /p/[slug])은 인증 없이 접근할 수 있게 했습니다.

4. 휴식 중 암호화된 자격 증명

Connectors는 API 키를 저장합니다. 평문으로 저장하는 것은 절대 안 되며, KMS는 사이드 프로젝트에 비해 과도합니다. 저는 pgcrypto와 환경 변수에 저장된 키를 사용했습니다.

// lib/crypto.ts
export function encryptCred(plaintext: string): Buffer {
  // pgp_sym_encrypt(plaintext, CONNECTOR_CRED_KEY)
}
export function decryptCred(ciphertext: Buffer): string {
  // pgp_sym_decrypt(ciphertext, CONNECTOR_CRED_KEY)
}

키는 AUTH_SECRET과 의도적으로 분리되어 있습니다; 하나를 회전시켜도 다른 것이 무효화되지 않아야 합니다. openssl rand -base64 32로 한 번 생성했습니다.

5. In‑process cron, no separate worker

시간별 KPI 동기화는 실험적인 instrumentation.ts 훅을 이용해 Next.js 서버 내부에서 실행됩니다. 이 훅은 setInterval‑형식 작업을 예약합니다:

  1. KPI가 활성화된 모든 프로젝트를 조회합니다.
  2. 각 커넥터의 sync 메서드를 호출합니다.
  3. 결과(및 경고)를 Postgres에 저장합니다.

작업이 동일 프로세스 내에서 실행되기 때문에 별도의 인프라를 관리할 필요가 없습니다. 유일한 주의점은 동기화가 앱 인스턴스가 최소 하나라도 실행 중일 때만 동작한다는 점인데, 이는 취미 수준 서비스에는 충분히 괜찮습니다.

// instrumentation.ts (Next.js calls this once per worker on startup)
export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { startCron } = await import("@/lib/cron");
    startCron();
  }
}

핵심 요점

DecisionWhy it mattered
Connector registryUI와 동기화 로직 모두에 대한 단일 진실 소스이며, 새로운 제공자를 쉽게 추가할 수 있습니다.
Edge‑safe auth split미들웨어를 Edge에서 실행하면서도 전체 스택 인증 기능을 사용할 수 있게 합니다.
Route groups마케팅 페이지와 인증된 앱 UI를 URL 변경 없이 깔끔하게 분리합니다.
Encrypted credentials전체 KMS 솔루션의 복잡성 없이 API 키를 안전하게 보관합니다.
In‑process cron별도의 워커 서비스 없이도 저빈도 백그라운드 작업에 충분합니다.

여러 서드파티 API와 연동되는 개인 대시보드를 구축한다면, 이러한 패턴이 코드베이스를 관리하기 쉽게 만들고 런타임 비용을 낮춰줍니다. 즐거운 해킹 되세요!

내가 추가하고 싶은 것

오늘은 다섯 개의 커넥터가 있습니다 (GitHub, Stripe, Neon, Cloudflare, Koyeb). 다음 목록:

  • Vercel
  • Plausible
  • Supabase
  • Resend
  • Netlify

사이드 프로젝트 구독을 놓친 적이 있다면, 어떤 서비스에 가장 커넥터가 있으면 좋겠나요? 지금은 가입보다는 제안을 듣고 싶습니다.

Live demo + free tier: StackMemo – 이 글이 유용했다면 반응을 남겨 주세요!

0 조회
Back to Blog

관련 글

더 보기 »