도메인이 자동 갱신되는 걸 깜빡했어요. 그래서 사이드 프로젝트용 대시보드를 만들었어요.
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‑형식 작업을 예약합니다:
- KPI가 활성화된 모든 프로젝트를 조회합니다.
- 각 커넥터의
sync메서드를 호출합니다. - 결과(및 경고)를 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();
}
}
핵심 요점
| Decision | Why it mattered |
|---|---|
| Connector registry | UI와 동기화 로직 모두에 대한 단일 진실 소스이며, 새로운 제공자를 쉽게 추가할 수 있습니다. |
| 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 – 이 글이 유용했다면 반응을 남겨 주세요!