codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›중앙 관리자 플랫폼 — 여러 도메인을 한 허브에서›3단계

3단계

여러 PostgreSQL 풀 연결

0회 조회

여러 PostgreSQL 풀 연결

관리자 앱이 3 개 도메인 DB 를 직접 다루려면 풀 3 개. pg 드라이버만으로 충분합니다.

1. 풀 싱글톤

// src/shared/lib/db.ts
import { Pool } from 'pg';

function sslConfig(mode?: string): any {
  if (mode === 'require') return { rejectUnauthorized: true };
  return false;
}

function requireEnv(name: string): string {
  const v = process.env[name];
  if (!v) throw new Error(`env ${name} missing`);
  return v;
}

export const blogPool = new Pool({
  host: requireEnv('BLOG_DB_HOST'),
  port: Number(process.env.BLOG_DB_PORT ?? 5432),
  database: requireEnv('BLOG_DB_NAME'),
  user: requireEnv('BLOG_DB_USER'),
  password: requireEnv('BLOG_DB_PASSWORD'),
  ssl: sslConfig(process.env.BLOG_DB_SSL_MODE),
  max: 10,
});

export const marketPool = new Pool({
  /* MARKET_DB_* */
});
  • requireEnv 가 부재 시 즉시 throw → 오타 · 환경설정 누락을 런타임 첫 호출에서 드러남
  • max: 10 이 기본. 동시 요청이 많으면 pg_stat_activity 관찰 후 조정

2. 얇은 쿼리 헬퍼

도메인별 헬퍼를 두면 호출 측 코드가 짧아지고 grep 이 명확해집니다.

// src/shared/lib/blog-db.ts
import { blogPool } from './db';

export async function queryBlog<T>(
  sql: string,
  params: unknown[] = []
): Promise<T[]> {
  const { rows } = await blogPool.query<T>(sql, params);
  return rows;
}

export async function queryOneBlog<T>(
  sql: string,
  params: unknown[] = []
): Promise<T | null> {
  const rows = await queryBlog<T>(sql, params);
  return rows[0] ?? null;
}

호출 측:

const posts = await queryBlog<Post>(
  `SELECT id, title FROM posts WHERE published = true ORDER BY created_at DESC LIMIT $1`,
  [20]
);

3. 트랜잭션

풀에서 클라이언트를 체크아웃해 BEGIN / COMMIT.

export async function createPostWithTags(post, tags) {
  const client = await blogPool.connect();
  try {
    await client.query('BEGIN');
    const { rows } = await client.query(
      `INSERT INTO posts (title, body) VALUES ($1, $2) RETURNING id`,
      [post.title, post.body]
    );
    const postId = rows[0].id;
    for (const tag of tags) {
      await client.query(
        `INSERT INTO post_tags (post_id, tag) VALUES ($1, $2)`,
        [postId, tag]
      );
    }
    await client.query('COMMIT');
    return postId;
  } catch (e) {
    await client.query('ROLLBACK');
    throw e;
  } finally {
    client.release();
  }
}

finally { client.release() } 누락이 풀 고갈의 대표 원인. withPoolClient<T>() 래퍼를 직접 만들어도 좋습니다.

4. graceful shutdown

// src/app/api/health/route.ts 에 가까운 곳에 1 회 등록
const pools = [blogPool, marketPool];

if (typeof process !== 'undefined') {
  process.once('SIGTERM', async () => {
    await Promise.all(pools.map((p) => p.end()));
  });
}

Docker stop · Next 재시작 시 커넥션을 깨끗이 닫음.

5. 페이지 예 — 블로그 포스트 목록

// src/app/admin/blog/posts/page.tsx
import { queryBlog } from '@/shared/lib/blog-db';

export default async function Page() {
  const posts = await queryBlog<Post>(
    `SELECT id, title, published, created_at FROM posts ORDER BY created_at DESC LIMIT 50`
  );
  return <PostsView initialRows={posts} />;
}

type Post = {
  id: number;
  title: string;
  published: boolean;
  created_at: string;
};

Server Component 에서 직접 풀 호출 → PostsView 에 props 로 전달. 'use client' 는 PostsView 내부에만.

6. 자주 걸리는 자리

  • 환경변수 오타 → requireEnv 로 즉시 실패
  • max 너무 큼 → PG 쪽 max_connections 초과
  • 트랜잭션 미종결 → client.release() 누락 추적
  • 로컬 · Docker · 클라우드 간 SSL 설정 혼동

하고픈 말

풀 싱글톤 + 얇은 헬퍼 조합은 ORM 없이도 충분히 버팁니다. Drizzle · TypeORM 을 당기는 건 join 이 복잡해지거나 마이그레이션 자동화가 필요해지는 중반부 — 3 ~ 5 개 테이블 수준에서는 raw SQL 이 더 빠르고 읽기 쉽습니다.

Next

  • 04-resource-table-ssot

← 2단계

프로젝트 셋업

4단계 →

AdminResourceTable 공통 컴포넌트