풀스택 백엔드 2026.06.12

syyu21b·2026년 6월 12일

풀스택 - 백엔드

목록 보기
29/49
post-thumbnail

With You - AI 빌더를 이용한 모바일 청첩장 서비스 만들기

  • 프로젝트 개요

With You는 사용자가 직접 아름다운 모바일 청첩장을 만들고 공유할 수 있는 웹 서비스입니다. 결혼식 정보, 웨딩 사진, 혼주 연락처, RSVP 기능을 모두 담아 하객에게 특별한 초대 경험을 제공합니다.

핵심 기능

  • 5가지 색감 테마 (로맨틱 핑크, 내추럴 베이지, 모던 화이트, 엘레강스 골드, 포레스트 그린)
  • 웨딩 사진 갤러리 (최대 10장, S3 저장, 드래그앤드롭 순서 변경)
  • 혼주 연락처 관리 (신랑·신부측 부모님 정보, 고인 표시)
  • RSVP 참석 확인 (참석 여부, 식사 선택, 축하 메시지)
  • 모바일 최적화 (스마트폰 중심 반응형 디자인)
  • 간편한 공유 (고유 URL 링크로 카카오톡, 문자 공유)

기술 스택

프론트엔드

  • React 19 + TypeScript - 최신 React 기능 활용
  • Tailwind CSS 4 - 유틸리티 기반 스타일링
  • shadcn/ui - 접근성 높은 UI 컴포넌트
  • Wouter - 경량 라우팅 라이브러리
  • Sonner - 토스트 알림

백엔드

  • Express.js - 경량 웹 프레임워크
  • tRPC 11 - 타입 안전한 API 통신
  • Drizzle ORM - 타입 안전한 데이터베이스 접근
  • MySQL/TiDB - 데이터베이스

인프라

  • Manus OAuth - 소셜 로그인 통합
  • AWS S3 - 이미지 저장소
  • Vite - 고속 번들러
  • Vitest - 유닛 테스트

데이터베이스 설계

핵심 테이블 구조

-- 청첩장 정보
CREATE TABLE invitations (
  id INT PRIMARY KEY AUTO_INCREMENT,
  userId INT NOT NULL,
  slug VARCHAR(255) UNIQUE NOT NULL,
  groomName VARCHAR(100),
  brideName VARCHAR(100),
  weddingDate DATE,
  weddingTime TIME,
  venueName VARCHAR(255),
  venueAddress VARCHAR(255),
  venueDetail TEXT,
  greetingMessage TEXT,
  theme ENUM('romantic-pink', 'natural-beige', 'modern-white', 'elegance-gold', 'forest-green'),
  galleryLayout ENUM('grid', 'swipe', 'list'),
  downloadProtection BOOLEAN DEFAULT true,
  isPublished BOOLEAN DEFAULT false,
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 혼주 정보
CREATE TABLE parents (
  id INT PRIMARY KEY AUTO_INCREMENT,
  invitationId INT NOT NULL UNIQUE,
  groomFatherName VARCHAR(100),
  groomFatherPhone VARCHAR(20),
  groomFatherDeceased BOOLEAN DEFAULT false,
  groomMotherName VARCHAR(100),
  groomMotherPhone VARCHAR(20),
  groomMotherDeceased BOOLEAN DEFAULT false,
  brideFatherName VARCHAR(100),
  brideFatherPhone VARCHAR(20),
  brideFatherDeceased BOOLEAN DEFAULT false,
  brideMotherName VARCHAR(100),
  brideMotherPhone VARCHAR(20),
  brideMotherDeceased BOOLEAN DEFAULT false,
  FOREIGN KEY (invitationId) REFERENCES invitations(id) ON DELETE CASCADE
);

-- 갤러리 이미지
CREATE TABLE gallery_images (
  id INT PRIMARY KEY AUTO_INCREMENT,
  invitationId INT NOT NULL,
  storageKey VARCHAR(255) NOT NULL,
  storageUrl VARCHAR(500) NOT NULL,
  sortOrder INT NOT NULL,
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (invitationId) REFERENCES invitations(id) ON DELETE CASCADE
);

-- RSVP 응답
CREATE TABLE rsvp_responses (
  id INT PRIMARY KEY AUTO_INCREMENT,
  invitationId INT NOT NULL,
  guestName VARCHAR(100) NOT NULL,
  guestPhone VARCHAR(20),
  attendance ENUM('attending', 'not-attending') NOT NULL,
  guestCount INT DEFAULT 1,
  mealChoice ENUM('korean', 'western', 'none') DEFAULT 'none',
  message TEXT,
  createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (invitationId) REFERENCES invitations(id) ON DELETE CASCADE
);

디자인 시스템

색감 팔레트

프로젝트에서 제공된 우아한 웨딩 청첩장 디자인을 기반으로 CSS 변수로 정의했습니다:

:root {
  /* Primary Colors */
  --ivory: #FBF5EF;           /* 배경 */
  --paper: #F6EEE5;           /* 보조 배경 */
  --wine: #8B3A42;            /* 주요 강조색 */
  --wine-deep: #6E2C33;       /* 진한 강조색 */
  
  /* Accent Colors */
  --blush: #F3DCD4;           /* 밝은 강조색 */
  --sage: #9CAF93;            /* 자연스러운 녹색 */
  --sage-deep: #74876B;       /* 진한 녹색 */
  --gold: #C8A464;            /* 고급스러운 금색 */
  
  /* Text Colors */
  --ink: #3D332E;             /* 기본 텍스트 */
  --ink-soft: #7A6F68;        /* 보조 텍스트 */
  --line: rgba(139, 58, 66, 0.16); /* 구분선 */
}

타이포그래피

  • Gowun Batang (세리프) - 제목, 강조 텍스트
  • Cormorant Garamond (디스플레이 세리프) - 아이브로우, 이탤릭 강조
  • Noto Sans KR (산스) - 본문, UI 텍스트

테마 시스템

5가지 테마를 CSS 클래스로 구현:

const THEME_COLORS = {
  'romantic-pink': {
    bg: '#FBF5EF',
    accent: '#8B3A42',
    text: '#3D332E',
  },
  'natural-beige': {
    bg: '#F6EEE5',
    accent: '#9CAF93',
    text: '#3D332E',
  },
  'modern-white': {
    bg: '#fafafa',
    accent: '#3D332E',
    text: '#3D332E',
  },
  'elegance-gold': {
    bg: '#1a1410',
    accent: '#C8A464',
    text: '#f5e8d0',
  },
  'forest-green': {
    bg: '#f3f6f2',
    accent: '#74876B',
    text: '#1a2820',
  },
};

핵심 구현 사항

1. tRPC를 활용한 타입 안전한 API

// server/routers.ts
export const appRouter = router({
  invitations: router({
    create: protectedProcedure
      .input(z.object({
        groomName: z.string(),
        brideName: z.string(),
      }))
      .mutation(async ({ ctx, input }) => {
        const slug = nanoid(8);
        return await db.insertInvitation({
          userId: ctx.user.id,
          slug,
          ...input,
        });
      }),
    
    getBySlug: publicProcedure
      .input(z.object({ slug: z.string(), preview: z.boolean().optional() }))
      .query(async ({ input }) => {
        const inv = await db.getBySlug(input.slug);
        if (!inv || (!inv.isPublished && !input.preview)) {
          throw new TRPCError({ code: 'NOT_FOUND' });
        }
        return inv;
      }),
  }),
});

장점:

  • 클라이언트와 서버 간 자동 타입 동기화
  • 런타임 에러 감소
  • 개발자 경험 향상 (자동완성, 타입 체크)

2. S3 기반 이미지 저장

// server/storage.ts
import { storagePut } from "./server/storage";

const { url: imageUrl } = await storagePut(
  `${userId}-gallery/${fileName}.jpg`,
  fileBuffer,
  "image/jpeg"
);
// 반환된 URL: /manus-storage/{key}

이점:

  • 로컬 저장소 용량 절약
  • 빠른 이미지 로딩 (CDN)
  • 영구적 파일 관리

3. 갤러리 레이아웃 선택

세 가지 갤러리 레이아웃을 React 컴포넌트로 구현:

// Grid Layout - 바둑판형
function GridGallery({ images }: { images: GalleryImage[] }) {
  return (
    <div className="grid grid-cols-3 gap-1">
      {images.map((img) => (
        <img key={img.id} src={img.storageUrl} alt="" />
      ))}
    </div>
  );
}

// Swipe Layout - 슬라이드형
function SwipeGallery({ images }: { images: GalleryImage[] }) {
  const [current, setCurrent] = useState(0);
  return (
    <div className="overflow-hidden">
      <div
        className="flex transition-transform duration-300"
        style={{ transform: `translateX(-${current * 100}%)` }}
      >
        {images.map((img) => (
          <img key={img.id} src={img.storageUrl} alt="" className="flex-shrink-0 w-full" />
        ))}
      </div>
    </div>
  );
}

// List Layout - 펼쳐보기형
function ListGallery({ images }: { images: GalleryImage[] }) {
  return (
    <div className="space-y-2">
      {images.map((img) => (
        <img key={img.id} src={img.storageUrl} alt="" className="w-full rounded" />
      ))}
    </div>
  );
}

4. RSVP 시스템

// 하객 응답 제출
const submitRSVP = trpc.rsvp.submit.useMutation({
  onSuccess: () => {
    toast.success("참석 여부가 등록되었습니다!");
    setShowThanks(true);
  },
});

// 청첩장 주인 알림
const notifyOwner = async (invitationId: number) => {
  await notifyOwner({
    title: "새로운 RSVP 응답이 있습니다",
    content: `${guestName}님이 참석 여부를 제출했습니다.`,
  });
};

5. 다운로드 방지 기능

// 우클릭 방지
<div onContextMenu={protect ? (e) => e.preventDefault() : undefined}>
  <img
    src={imageUrl}
    className={protect ? "no-download" : ""}
    draggable={false}
  />
</div>

// CSS
.no-download {
  -webkit-user-select: none;
  user-select: none;
  pointer-events: none;
}

.no-download img {
  pointer-events: none;
  -webkit-user-drag: none;
}

테스트 커버리지

Vitest를 활용한 유닛 테스트 작성:

// server/invitations.test.ts
describe("invitations", () => {
  it("creates a new invitation", async () => {
    const caller = appRouter.createCaller(mockContext);
    const result = await caller.invitations.create({
      groomName: "이도현",
      brideName: "박서연",
    });
    expect(result.slug).toBeDefined();
    expect(result.userId).toBe(mockContext.user.id);
  });

  it("rejects non-public invitations without preview flag", async () => {
    const caller = appRouter.createCaller(publicContext);
    await expect(
      caller.invitations.getBySlug({
        slug: "private-slug",
        preview: false,
      })
    ).rejects.toThrow("NOT_FOUND");
  });
});

테스트 결과: 13/13 통과 ✅


배포 및 성능

성능 최적화

  1. 이미지 최적화

    • S3 저장소 활용으로 로컬 부하 감소
    • 자동 리사이징 및 CDN 배포
  2. 번들 최적화

    • Vite로 빠른 개발 서버 (HMR)
    • 프로덕션 빌드 최소화
  3. 렌더링 최적화

    • React 19의 자동 배치 처리
    • 불필요한 리렌더링 방지 (useMemo, useCallback)

배포 환경

  • 프론트엔드: Vite + React
  • 백엔드: Express.js + Node.js
  • 데이터베이스: MySQL/TiDB
  • 호스팅: Manus (자동 배포)
  • 도메인: weddingapp-ksn7nv6g.manus.space

🔗 프로젝트 링크

0개의 댓글