๐Ÿ’ก 08. ์‹ค์ „ ๊ฐ€์ด๋“œ โ€” ๋ฐฐ์šด ๊ฒƒ๋“ค์„ ๋ชจ๋‘ ์—ฎ์–ด '์ง„์งœ' API ๋งŒ๋“ค๊ธฐ

2026๋…„ 2์›” 26์ผ ์ˆ˜์ •๋จ

๐Ÿ“‹ ๊ฐœ์š”

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋ชจ๋“ˆ, ์ปจํŠธ๋กค๋Ÿฌ, ์„œ๋น„์Šค, Drizzle, Zod๋ฅผ ์ด๋™์›ํ•˜์—ฌ ์‹ค์ œ ์ž‘๋™ํ•˜๋Š” '์ฃผ์‹ ํฌํŠธํด๋ฆฌ์˜ค ๊ด€๋ฆฌ API'๋ฅผ ๋ฐ‘๋ฐ”๋‹ฅ๋ถ€ํ„ฐ ์™„์„ฑํ•ด ๋ด…๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 25๋ถ„ (์ง์ ‘ ์ฝ”๋”ฉํ•˜๋ฉฐ ๋”ฐ๋ผ์˜ค๋ฉด 1์‹œ๊ฐ„)

๐Ÿงณ ์ „์ œ ์ง€์‹ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • 02์žฅ์˜ ์ปจํŠธ๋กค๋Ÿฌ, 03์žฅ์˜ ์„œ๋น„์Šค ๊ธฐ๋ณธ ํ˜•ํƒœ๋ฅผ ๊ธฐ์–ตํ•œ๋‹ค.
  • 04์žฅ์˜ DTO ๊ฐœ๋…๊ณผ 05์žฅ์˜ Drizzle ORM ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ๋ฌธ๋ฒ•์„ ํ›‘์–ด๋ดค๋‹ค.
  • 06์žฅ์˜ ๊ฐ€๋“œ(JwtAuthGuard)๊ฐ€ ์–ด๋–ค ์—ญํ• ์„ ํ•˜๋Š”์ง€ ์•ˆ๋‹ค.

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
๋ชฉํ‘œ ๊ธฐ๋Šฅ ์ •์˜ โ†’ DB ํ…Œ์ด๋ธ” ์„ค๊ณ„ โ†’ DTO ์ฐ์–ด๋‚ด๊ธฐ โ†’ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง(์„œ๋น„์Šค) ์ž‘์„ฑ โ†’ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ๊ธธ ๋šซ๊ธฐ(์ตœ์ข… ์กฐ๋ฆฝ)

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(SSOT) ํŒจํ„ด์ด ์‹ค๋ฌด์—์„œ ์–ผ๋งˆ๋‚˜ ์ฝ”๋“œ๋ฅผ ์ค„์—ฌ์ฃผ๋Š”์ง€ ์ฒด๊ฐํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋‚ด ํฌํŠธํด๋ฆฌ์˜ค(๊ฐœ์ธ ์ž์‚ฐ)๋ฅผ ๋ณดํ˜ธํ•˜๋Š” ๋ผ์šฐํŒ… ๊ธฐ๋ฒ•์„ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ํ˜ผ์ž์„œ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ(์˜ˆ: ๋Œ“๊ธ€ ๊ธฐ๋Šฅ, ์ข‹์•„์š” ๊ธฐ๋Šฅ)์„ ์„ค๊ณ„๋ถ€ํ„ฐ API ๊ตฌํ˜„๊นŒ์ง€ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ํ˜ผ์ž ํ•ด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

์ง€๊ธˆ๊นŒ์ง€ ์šฐ๋ฆฌ๋Š” ๋ ˆ๊ณ  ๋ธ”๋ก์˜ '๋ถ€ํ’ˆ'๋“ค์ด ๊ฐ๊ฐ ์–ด๋–ค ์—ญํ• ์„ ํ•˜๋Š”์ง€ ๋”ฐ๋กœ๋”ฐ๋กœ ๋–ผ์–ด๋†“๊ณ  ๋ฐฐ์› ์–ด.
์—”์ง„(์„œ๋น„์Šค), ๋ฐ”ํ€ด(DB), ์šด์ „๋Œ€(์ปจํŠธ๋กค๋Ÿฌ) ์ž‘๋™ ์›๋ฆฌ๋ฅผ ์•„๋Š” ๊ฒƒ๊ณผ, ๋ง‰์ƒ ๋ถ€ํ’ˆ์„ ์‹น ๋‹ค ๋ชจ์•„๋†“๊ณ  "์ž, ์ด์ œ ๊ตด๋Ÿฌ๊ฐ€๋Š” ๋น„ํ–‰๊ธฐ๋ฅผ ์ง์ ‘ ์กฐ๋ฆฝํ•ด ๋ด!" ๋ผ๊ณ  ํ•˜๋Š” ๊ฑด ์™„์ „ํžˆ ๋‹ค๋ฅธ ์˜์—ญ์ด์•ผ.
์ด ์‹ค์ „ ๊ฐ€์ด๋“œ๋ฅผ ๋”ฐ๋ผ๊ฐ€๋ฉด์„œ ํŒŒํŽธํ™”๋œ ๋จธ๋ฆฟ์† ์ง€์‹๋“ค์„ ํ•˜๋‚˜์˜ ๋งค๋„๋Ÿฌ์šด ์—…๋ฌด ํ๋ฆ„(Workflow) ์œผ๋กœ ์••์ถ•(Compile) ์ง€์ผœ์•ผ ๋น„๋กœ์†Œ ๋‚ด ๊ฒƒ์ด ๋ผ.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

์ž๋™์ฐจ ๊ณต์žฅ์—์„œ ๋ฉ‹์ง„ ์ž๋™์ฐจ๋ฅผ ์กฐ๋ฆฝํ•˜๋Š” ๊ณผ์ •์„ ์ƒ์ƒํ•ด ๋ณผ๊นŒ?

  1. ๋ผˆ๋Œ€ ๋งŒ๋“ค๊ธฐ (์Šคํ‚ค๋งˆ): ์ž๋™์ฐจ์˜ ์ฒ ์ œ ํ”„๋ ˆ์ž„ ๋””์ž์ธ์„ ๊ทธ๋ฆฐ๋‹ค.
  2. ์‚ฌ์šฉ ์„ค๋ช…์„œ ์ฐ์–ด๋‚ด๊ธฐ (SSOT DTO): ํ”„๋ ˆ์ž„ ๋””์ž์ธ๋งŒ ๋ณด๊ณ  ์ปดํ“จํ„ฐ(Zod)๊ฐ€ ์•Œ์•„์„œ "์ด ์ฐจ์˜ ์ตœ๋Œ€ ์ •์›์€ 4๋ช…" ์ด๋ผ๋Š” ์‚ฌ์šฉ ์„ค๋ช…์„œ๋ฅผ ์ž๋™์œผ๋กœ ์ฐ์–ด๋‚ธ๋‹ค.
  3. ์—”์ง„ ๋‹ฌ๊ธฐ (์„œ๋น„์Šค): ํ•ธ๋“ค์„ ๊บพ์œผ๋ฉด ๋ฐ”ํ€ด๊ฐ€ ๋Œ์•„๊ฐ€๊ฒŒ ์ฒ ์‚ฌ๋ฅผ ์—ฐ๊ฒฐํ•œ๋‹ค.
  4. ๋ฒ„ํŠผ ๋šซ๊ธฐ (์ปจํŠธ๋กค๋Ÿฌ): ์šด์ „์ž๊ฐ€ ๋ฐ–์—์„œ ๋ˆ„๋ฅผ ์ˆ˜ ์žˆ๊ฒŒ ํ•ธ๋“ค๊ณผ ํŽ˜๋‹ฌ์„ ์˜ˆ์˜๊ฒŒ ๋ฐฐ์น˜ํ•˜๊ณ , ์šด์ „๋ฉดํ—ˆ(๊ฐ€๋“œ)๊ฐ€ ์žˆ๋Š” ์‚ฌ๋žŒ๋งŒ ํƒˆ ์ˆ˜ ์žˆ๊ฒŒ ์—ด์‡  ๊ตฌ๋ฉ์„ ํŒ๋‹ค.

๐Ÿงฉ 1๋‹จ๊ณ„: ๋ผˆ๋Œ€ ์„ค๊ณ„ (DB ์Šคํ‚ค๋งˆ) ๐ŸŸก

๐ŸŽฏ **์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:**Drizzle ์Šคํ‚ค๋งˆ๋ฅผ ์„ ์–ธํ•˜๊ณ  ํƒ€์ž…์„ ๋นผ๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ๊ฑด "์ฃผ์‹ ํฌํŠธํด๋ฆฌ์˜ค ๊ด€๋ฆฌ" ๊ธฐ๋Šฅ์ด์•ผ.

  • ๋ชฉํ‘œ ๊ธฐ๋Šฅ: ์ด๋ฆ„๊ณผ ์„ค๋ช…์œผ๋กœ ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ๋งŒ๋“ค๊ณ , ๋‚ด ๋ชฉ๋ก์„ ๋ณด๊ณ , ์ˆ˜์ •/์‚ญ์ œํ•œ๋‹ค.
// packages/schema/src/schema.ts
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
 
// 1. ํ…Œ์ด๋ธ” ์ •์˜ (๋ฌผ๋ฆฌ์  DB ๊ตฌ์กฐ)
export const portfolios = pgTable('portfolios', {
  id: serial('id').primaryKey(),
  userId: text('user_id').notNull(),      // ์†Œ์œ ์ž ID
  name: text('name').notNull(),           // ์˜ˆ: "๋‚ด ๋…ธํ›„ ๋“ ๋“  ์กฐํ•ฉ"
  description: text('description'),       // ์„ค๋ช… (Optional)
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow(),
});
 
// 2. ํƒ€์ž… ์ถ”๋ก  (์—ฌ๊ธฐ๊ฐ€ Drizzle์˜ ๊ฟ€์žผ ํฌ์ธํŠธ)
export type Portfolio = typeof portfolios.$inferSelect;   // SELECT ํ•  ๋•Œ ๋‚˜์˜ค๋Š” ๊ฐ์ฒด์˜ ํƒ€์ž…
export type NewPortfolio = typeof portfolios.$inferInsert; // INSERT ํ•  ๋•Œ ๋„ฃ์–ด์•ผ ํ•˜๋Š” ๊ฐ์ฒด์˜ ํƒ€์ž…

์ด์ œ ํ„ฐ๋ฏธ๋„์„ ์—ด๊ณ  NestJS CLI๋กœ ๋นˆ ๊นกํ†ต ๋ชจ๋“ˆ์„ ์ƒ์„ฑํ•ด์ค˜. (์—†์œผ๋ฉด ์ง์ ‘ ํด๋”/ํŒŒ์ผ ๋งŒ๋“ค์–ด๋„ ๋ผ)

nest g resource portfolios
# ๋ฌผ์–ด๋ณด๋ฉด REST API๋ฅผ ์„ ํƒํ•ด!

๐Ÿงฉ 2๋‹จ๊ณ„: ์™„๋ฒฝํ•œ DTO ์ƒ์„ฑ (SSOT) ๐ŸŸข

๋ฐฑ์—”๋“œ ํŒŒํŠธ๋„ˆ Zod ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ DTO๋ฅผ ๋ฝ‘์•„๋‚ผ ๊ฑฐ์•ผ. nestjs-zod ์™€ drizzle-zod๋ฅผ ์“ด๋‹ค!

๋ฐฉ๊ธˆ 1๋‹จ๊ณ„์—์„œ ์ง  portfolios DB ์Šคํ‚ค๋งˆ ๋‹จ ํ•˜๋‚˜๋งŒ ์ˆ˜์ •ํ•˜๋ฉด, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์—๋Ÿฌ ๋กœ์ง(DTO)๊นŒ์ง€ ์•Œ์•„์„œ ์—ฐ๋™๋˜์–ด ์‹น ๋‹ค ๋ฐ”๋€Œ๋Š” ๊ถ๊ทน์˜ ๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(SSOT) ๊ตฌ์กฐ์•ผ.

// src/portfolios/dto/create-portfolio.dto.ts
import { createZodDto } from 'nestjs-zod';
import { createInsertSchema } from 'drizzle-zod';
import { portfolios } from '@repo/schema'; // ๋ฐฉ๊ธˆ ์ง  ์Šคํ‚ค๋งˆ ๊ฐ€์ ธ์˜ค๊ธฐ
 
// 1. Zod ์Šคํ‚ค๋งˆ๋กœ ์ž๋™ ๋ฒˆ์—ญ!
const CreateSchema = createInsertSchema(portfolios, {
  // DB ๋ชจ๋ธ์—๋Š” ์—†๋Š” Zod๋งŒ์˜ ๊น๊นํ•œ ๊ทœ์น™์„ ์ถ”๊ฐ€!
  name: (s) => s.min(2, 'ํฌํŠธํด๋ฆฌ์˜ค ์ด๋ฆ„์€ 2๊ธ€์ž ์ด์ƒ ์ ์–ด์ฃผ์„ธ์š”!').max(50),
  description: (s) => s.max(200).optional(),
}).pick({
  name: true,        // ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ž…๋ ฅํ•ด์•ผ ํ•  ๊ฑด ์ด๋ฆ„๊ณผ ์„ค๋ช…๋ฟ์ด์•ผ.
  description: true, // userId ๊ฐ™์€ ๊ฑด ํ—ค๋”์˜ ํ† ํฐ์—์„œ ๋นผ๋‚ผ ๊ฑฐ๋‹ˆ๊นŒ DTO์—์„  ์ง€์›€!
});
 
// 2. ์™„๋ฒฝํ•œ DTO ํด๋ž˜์Šค ํƒ„์ƒ
export class CreatePortfolioDto extends createZodDto(CreateSchema) {}

์ˆ˜์ •์šฉ DTO๋„ 1์ดˆ ์ปท์ด์•ผ.

// src/portfolios/dto/update-portfolio.dto.ts
import { createZodDto } from 'nestjs-zod';
import { CreatePortfolioDto } from './create-portfolio.dto';
 
// Create DTO์˜ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋ชจ์กฐ๋ฆฌ Optional(?)๋กœ ๋ฐ”๊พผ๋‹ค.
export class UpdatePortfolioDto extends createZodDto(
  CreatePortfolioDto.schema.partial()
) {}

๐Ÿงช ๋”ฐ๋ผํ•ด๋ณด๊ธฐ: ์ปจํŠธ๋กค๋Ÿฌ์™€ ์„œ๋น„์Šค ์—ฎ์–ด์„œ CRUD ์™„์„ฑํ•˜๊ธฐ

๋ถ€ํ’ˆ์ด ๋‹ค ๋ชจ์˜€์–ด. ์ด์ œ ์—”์ง„(์„œ๋น„์Šค)์„ ์กฐ๋ฆฝํ•˜๊ณ , ํ•ธ๋“ค(์ปจํŠธ๋กค๋Ÿฌ)๊ณผ ์—ฐ๊ฒฐ๋งŒ ํ•˜๋ฉด ์™„์„ฑ๋ผ!

Step 1. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง(์„œ๋น„์Šค) ์ž‘์„ฑ

// src/portfolios/portfolios.service.ts
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { DATABASE_TOKEN, Database } from '../database/database.module';
import { portfolios, NewPortfolio } from '@repo/schema';
import { eq, and } from 'drizzle-orm';
import { CreatePortfolioDto } from './dto/create-portfolio.dto';
import { UpdatePortfolioDto } from './dto/update-portfolio.dto';
 
@Injectable()
export class PortfoliosService {
  // DB ์ „์šฉ ํ•ซ๋ผ์ธ ์ฃผ์ž…!
  constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
 
  // 1. ์ƒ์„ฑ ๋กœ์ง
  async create(userId: string, dto: CreatePortfolioDto) {
    // DTO(์ด๋ฆ„, ์„ค๋ช…)์— ์‹œ์Šคํ…œ ๋ณ€์ˆ˜(์œ ์ €ID)๋ฅผ ๊ฒฐํ•ฉ
    const newPortfolio: NewPortfolio = { ...dto, userId };
 
    const [created] = await this.db
      .insert(portfolios)
      .values(newPortfolio)
      .returning(); // Postgres ๋งˆ๋ฒ•: INSERT ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๊บผ๋‚ด์คŒ
 
    return created;
  }
 
  // 2. ๋‹ค๊ฑด ์กฐํšŒ ๋กœ์ง (๋ฐ˜๋“œ์‹œ ๋‚ด ๊ฒƒ๋งŒ ๋‚˜์™€์•ผ ํ•จ!!)
  async findAll(userId: string) {
    return this.db.query.portfolios.findMany({
      where: eq(portfolios.userId, userId), 
      orderBy: (p, { desc }) => [desc(p.createdAt)], // ์ตœ์‹ ์ˆœ
    });
  }
 
  // 3. ๋‹จ์ผ ์กฐํšŒ (๋ณด์•ˆ ์ฒดํฌ ํฌํ•จ)
  async findOne(userId: string, id: number) {
    const portfolio = await this.db.query.portfolios.findFirst({
      // id๋„ ๋งž๊ณ , userId๋„ ๋งž์•„์•ผ ํ•จ (๋‚จ์˜ ํฌํŠธํด๋ฆฌ์˜ค๋Š” ๋ชป ๋ณด๊ฒŒ ๋ฐฉ์–ด)
      where: and(eq(portfolios.id, id), eq(portfolios.userId, userId)),
    });
 
    if (!portfolio) {
      throw new NotFoundException(`ํฌํŠธํด๋ฆฌ์˜ค #${id}๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋‹น.`);
    }
    return portfolio;
  }
}

Step 2. API ์—”๋“œํฌ์ธํŠธ(์ปจํŠธ๋กค๋Ÿฌ) ๋šซ๊ธฐ

์ด์ œ 100% ๋šซ์–ด์ฃผ๋ฉด ๋ผ. ๋‹จ, ๋ชจ๋“  API๋Š” ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ฒฝ๋น„์›(Guard)์„ ์„ธ์šฐ์ž.

// src/portfolios/portfolios.controller.ts
import { Controller, Get, Post, Body, Param, UseGuards, ParseIntPipe } from '@nestjs/common';
import { PortfoliosService } from './portfolios.service';
import { CreatePortfolioDto } from './dto/create-portfolio.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { User } from '../auth/types'; // (๋‚ด๊ฐ€ ๋งŒ๋“  ์œ ์ € ์ธํ„ฐํŽ˜์ด์Šค)
 
// 1. ๋ผ์šฐํ„ฐ ์ ‘๋‘์‚ฌ์™€ ์ „์—ญ ๊ฒฝ๋น„์› ๋ฐฐ์น˜
@Controller('portfolios') 
@UseGuards(JwtAuthGuard)  
export class PortfoliosController {
  constructor(private readonly portfoliosService: PortfoliosService) {}
 
  // 2. ์ƒ์„ฑ API (POST /portfolios)
  @Post()
  create(
    @CurrentUser() user: User,       // ๐Ÿ‘ˆ ๋ˆ„๊ฐ€ ์š”์ฒญํ–ˆ๋Š”์ง€ ์‚ฌ์›์ฆ์—์„œ ๋ฝ‘๊ธฐ
    @Body() dto: CreatePortfolioDto  // ๐Ÿ‘ˆ ์ž…๊ตฌ์—์„œ Zod๊ฐ€ ๋ฐ์ดํ„ฐ ๊ฒ€์‚ฌ ๋‹ค ํ•ด์คŒ
  ) {
    // ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๊ฒ๋‚˜ ์ฟจํ•˜๊ฒŒ ํŒŒ๋ผ๋ฏธํ„ฐ๋งŒ ๋„˜๊ฒจ์ฃผ๊ณ  ํ‡ด๊ทผ!
    return this.portfoliosService.create(user.id, dto);
  }
 
  // 3. ๋‹ค๊ฑด ์กฐํšŒ API (GET /portfolios)
  @Get()
  findAll(@CurrentUser() user: User) {
    return this.portfoliosService.findAll(user.id);
  }
 
  // 4. ๋‹จ์ผ ์กฐํšŒ API (GET /portfolios/:id)
  @Get(':id')
  findOne(
    @CurrentUser() user: User,
    @Param('id', ParseIntPipe) id: number // ๐Ÿ‘ˆ ๋ฌธ์ž๋ฅผ ์ˆซ์ž๋กœ ๊ฐ•์ œ ๋ณ€ํ™˜
  ) {
    return this.portfoliosService.findOne(user.id, id);
  }
}

๐Ÿ’ผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค์™€ ์‹ค๋ฌด ํŒ ๐ŸŸก

1. ๋ณด์•ˆ์˜ ํ•ต์‹ฌ: ์†Œ์œ ๊ถŒ ์ฆ๋ช… (Ownership Check)

findAll ์ด๋‚˜ findOne์„ ์งค ๋•Œ ์Šต๊ด€์ ์œผ๋กœ where: eq(portfolios.id, id) ๋กœ๋งŒ ๋๋‚ด๋ฉด ์ตœ์•…์˜ ๋ณด์•ˆ ์‚ฌ๊ณ (IDOR ์ทจ์•ฝ์ ) ๊ฐ€ ํ„ฐ์งˆ ์ˆ˜ ์žˆ์–ด.
์•…์˜์ ์ธ ํ•ด์ปค๊ฐ€ GET /portfolios/999 ๋ผ๊ณ  ์•„๋ฌด ์•„์ด๋””๋‚˜ ์ฐ์–ด ๋„ฃ์œผ๋ฉด, ์ƒํŒ ๋‚จ์˜ ๊ณ„์ขŒ ์ •๋ณด๊ฐ€ ๋‹ค ๋– ๋ฒ„๋ฆฌ๊ฑฐ๋“ .
์ฝ”๋“œ์—์„œ ๋ณธ ๊ฒƒ์ฒ˜๋Ÿผ ๋ฐ˜๋“œ์‹œ and(eq(id), eq(userId)) ์กฐ๊ฑด์„ ๊ฑธ์–ด์„œ ๋ฌด์กฐ๊ฑด '๋‚ด ์ž์‚ฐ'์ธ์ง€ ๊ต์ฐจ ๊ฒ€์ฆ ํ•ด์•ผ ํ•ด!

2. Drizzle $infer... ํƒ€์ž…์˜ ์žฌ์‚ฌ์šฉ

์‹ค๋ฌด์—์„  User๋‚˜ Portfolio ๊ฐ™์€ ํƒ€์ž…์„ interfaces ํด๋”์— ์ผ์ผ์ด ์น˜์ง€ ๋งˆ.
Drizzle ์Šคํ‚ค๋งˆ ํŒŒ์ผ์—์„œ typeof ํ…Œ์ด๋ธ”๋ช….$inferSelect ๋กœ ๋‚ด๋ณด๋‚ธ(export) ํƒ€์ž…์„ ์•ฑ ์–ด๋””์„œ๋“  importํ•ด์„œ ์žฌํ™œ์šฉํ•ด. ์ด๋ž˜์•ผ ๋‚˜์ค‘์— DB ์ปฌ๋Ÿผ์ด ์ถ”๊ฐ€๋ผ๋„ TypeScript ์—๋Ÿฌ ๋ ˆ์ด๋”๋ง์— ๋‹ค ๊ฑธ๋ ค์„œ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ๋งค์šฐ ์‰ฌ์›Œ์ ธ.


๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด Ctrl+F๋กœ ๊ฒ€์ƒ‰ํ•ด๋ด.

โŒ TypeError: Cannot read properties of undefined (reading 'insert')

**ํ˜„์ƒ:**POST ์š”์ฒญ์„ ๋‚ ๋ ธ๋”๋‹ˆ 500 ์—๋Ÿฌ๋ฅผ ๋ฑ‰์œผ๋ฉฐ ํ„ฐ๋ฏธ๋„์— ์ €๋Ÿฐ ๋กœ๊ทธ๊ฐ€ ๋œธ.
์›์ธ: PortfoliosService ํด๋ž˜์Šค ์ƒ๋‹จ์˜ constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {} ๋ถ€๋ถ„์— @Inject(...) ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ๋นผ๋จน์€ ๊ฑฐ์•ผ. NestJS๋Š” DB ๊ฐ์ฒด๋ฅผ ๊ฝ‚์•„์ฃผ์ง€ ์•Š์•˜์œผ๋‹ˆ this.db๊ฐ€ undefined ์ธ ์ƒํƒœ๋กœ insert๋ฅผ ๋‚ ๋ฆฐ ๊ฑฐ์ง€!
ํ•ด๊ฒฐ์ฑ…: @Inject๋ฅผ ์ œ๋Œ€๋กœ ๋‹ฌ์•˜๋Š”์ง€ ํ™•์ธํ•ด.

โŒ 400 Bad Request: Expected string, received object

์›์ธ: ํฌํŠธํด๋ฆฌ์˜ค๋ฅผ ๋งŒ๋“ค ๋•Œ, nestjs-zod ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ์ž…๊ตฌ์ปท ๋‹นํ•˜๋Š” ์ค‘. Postman์ด๋‚˜ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‚ผ ๋•Œ ๊ฐ์ฒด ๋งคํ•‘์ด ์–ด๊ธ‹๋‚œ ๊ฑฐ์•ผ.
ํ•ด๊ฒฐ์ฑ…: ์„œ๋ฒ„ ์ฝ”๋“œ ์ž˜๋ชป์ด ์•„๋ƒ! CreatePortfolioDto์— ์ ํžŒ ์ŠคํŽ™ ๊ทธ๋Œ€๋กœ { "name": "๋‚ด ํˆฌ์ž์กฐํ•ฉ" } ๊ฐ™์€ ์˜ฌ๋ฐ”๋ฅธ JSON ํŽ˜์ด๋กœ๋“œ๋ฅผ ๋‚ ๋ ธ๋Š”์ง€ ์š”์ฒญ ์ชฝ์„ ์ ๊ฒ€ํ•ด ๋ณผ ๊ฒƒ.


๐Ÿ“‹๏ธ ์น˜ํŠธ์‹œํŠธ โ€” ์‹ค๋ฌด ์š”์•ฝ ์นด๋“œ

[๊ฐœ๋ฐœ ํ๋ฆ„(Workflow) ํ•œ๋ˆˆ์— ๋ณด๊ธฐ]

  1. DB ๋ ˆ๋ฒจ: schema.ts ์— pgTable ๋ผˆ๋Œ€ ์„ ์–ธ
  2. ์ฝ”๋“œ ๋ ˆ๋ฒจ: nest g resource ๋กœ ๋ชจ๋“ˆ/์ปจํŠธ๋กค๋Ÿฌ/์„œ๋น„์Šค ์ƒ์„ฑ
  3. DTO ํ†ต์ œ: drizzle-zod ๋กœ ์Šคํ‚ค๋งˆ์—์„œ DTO ๋™๊ธฐํ™”
  4. ์„œ๋น„์Šค ๋‘๋‡Œ: constructor(@Inject(DATABASE_TOKEN)) ๋กœ ๋ถˆ๋Ÿฌ์„œ ๋กœ์ง ์ž‘์„ฑ
  5. ๋ผ์šฐํ„ฐ ์ž…๊ตฌ: ์ปจํŠธ๋กค๋Ÿฌ์— ๊ฐ€๋“œ ์น˜๊ณ (@UseGuards), ๋ผ์šฐํ„ฐ ์—ด๊ธฐ(@Get)

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. NestJS ํ”„๋ ˆ์ž„์›Œํฌ์™€ Zod ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—ฐ๋™ ์‹œ, '๋‹จ์ผ ์ง„์‹ค ๊ณต๊ธ‰์›(SSOT)' ์ด๋ผ๋Š” ์šฉ์–ด์˜ ์žฅ์ ์„ ๊ฐ€์žฅ ์ž˜ ์„ค๋ช…ํ•œ ๊ฒƒ์€ ๋ฌด์—‡์ธ๊ฐ€?

  • A) ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ํ•œ ๊ตฐ๋ฐ์—๋งŒ ์„œ๋ฒ„๋ฅผ ๋‘์–ด ๋ณด์•ˆ์ด ์™„๋ฒฝํ•ด์ง„๋‹ค.
  • B) DB ์Šคํ‚ค๋งˆ ๊ตฌ์กฐ ์ฝ”๋“œ๋ฅผ ํ•œ ๋ฒˆ๋งŒ ๋ฐ”๊พธ๋ฉด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์กฐ๋Š” ๋ฌผ๋ก  ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋ง‰์•„์ฃผ๋Š” ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ(DTO) ๊ทœ์น™๊นŒ์ง€ ์•Œ์•„์„œ ํ•จ๊ป˜ ๋ณ€ํ•ด์„œ ์‹ค์ˆ˜๋ฅผ ์—†์• ์ค€๋‹ค.
  • C) ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ(Controller, Service)๋ฅผ ํ•˜๋‚˜์˜ ๋ชจ๋“ˆ ์•ˆ์—์„œ ์ „๋ถ€ ๊ด€๋ฆฌํ•˜๋Š” ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์„ ๋งํ•œ๋‹ค.

โœ… ์ •๋‹ต: B

๐Ÿ’ก **์„ค๋ช…:**DB ์Šคํ‚ค๋งˆ ๋”ฐ๋กœ, ์ธํ„ฐํŽ˜์ด์Šค(Interface) ํƒ€์ž… ๋”ฐ๋กœ, ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํด๋ž˜์Šค ๋”ฐ๋กœ ๋งŒ๋“ค๋‹ค๊ฐ€ ์ด๋ฆ„(name) ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ 50์ž์—์„œ 100์ž๋กœ ๋Š˜๋ฆฌ๋ ค๋ฉด ์„ธ ๊ตฐ๋ฐ ์ฝ”๋“œ๋ฅผ ๋‹ค ๊ณ ์ณ์•ผ ํ•ด. SSOT ํŒจํ„ด์€ ์ œ์ผ ๊ทผ์›(Source)์ธ DB ์Šคํ‚ค๋งˆ ํ•˜๋‚˜๋งŒ ๊ณ ์น˜๋ฉด ์ „๋ถ€ ์—ฐ๋™๋ผ์„œ ๋ฐ”๋€Œ๋Š” ๋งˆ๋ฒ•์ด์•ผ!

Q2. ๋‹ค์Œ ์ค‘ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ @CurrentUser() ๊ฐ€ ์•„๋ฌด๋ฆฌ ์ฝ์–ด๋„ ๋ฌด์กฐ๊ฑด ๊ฐ’์„ ์ฑ„์›Œ ์˜ฌ ์ˆ˜ ์—†๋„๋ก ์‹คํŒจํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ์›์ธ์€?

  • A) ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค ์ƒ๋‹จ์— @UseGuards(JwtAuthGuard)๋ฅผ ๋ถ™์ด์ง€ ์•Š์•˜๋‹ค.
  • B) ์œ ์ €๊ฐ€ ํ—ค๋”์— ์˜ฌ๋ฐ”๋ฅธ Authorization: Bearer <token> ๊ฐ’์„ ๋„ฃ์–ด์„œ ๋ณด๋ƒˆ๋‹ค.
  • C) DTO์— @IsString() ์†์„ฑ์„ ๋นผ๋จน์—ˆ๋‹ค.

โœ… ์ •๋‹ต: A

๐Ÿ’ก ์„ค๋ช…: ์ž…๊ตฌ์— ๊ฒฝ๋น„์›(๊ฐ€๋“œ)๋ฅผ ์„ธ์šฐ์ง€ ์•Š์œผ๋ฉด, ๋‹น์—ฐํžˆ ์‹ ๋ถ„์ฆ(ํ† ํฐ) ๋„ ๊ฒ€์‚ฌํ•˜์ง€ ์•Š๊ณ , ๋”ฐ๋ผ์„œ ์š”์ฒญ(request) ๊ฐ์ฒด ์•ˆ์— ์œ ์ € ์ •๋ณด๋„ ๊ฝ‚ํžˆ์ง€ ์•Š์•„!

Q3. findAll (ํšŒ์› ํฌํŠธํด๋ฆฌ์˜ค ๋ชฉ๋ก ์ „์ฒด ์กฐํšŒ) ๊ธฐ๋Šฅ์„ ์งค ๋•Œ, ๋ณด์•ˆ์„ ์œ„ํ•ด where: eq(portfolios.userId, userId) ์ฝ”๋“œ๋ฅผ ๋ฐ˜๋“œ์‹œ ๋„ฃ์–ด์•ผ ํ•˜๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์„ ๋ง‰๊ธฐ ์œ„ํ•จ์ธ๊ฐ€?

โœ… ์ •๋‹ต: ๊ถŒํ•œ ์—†๋Š” ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ๋‚จ์˜ ํฌํŠธํด๋ฆฌ์˜ค ์ •๋ณด๋ฅผ ๋ฌด๋‹จ์œผ๋กœ ์—ด๋žŒ(IDOR ๋ณด์•ˆ ์ทจ์•ฝ์ )ํ•˜๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•จ!
์„ค๋ช…: ๋ฌด์กฐ๊ฑด "DB ์•ˆ์— ์žˆ๋Š” ๋ชจ๋“  ํฌํŠธํด๋ฆฌ์˜ค ๋‹ค ๋‚ด๋†”!" ๊ฐ€ ์•„๋‹ˆ๋ผ, "๋กœ๊ทธ์ธํ•œ ๋‹น์‹ (userId)์˜ ์†Œ์œ ๊ถŒ์ด ํ™•์ธ๋œ ํฌํŠธํด๋ฆฌ์˜ค๋งŒ ๋‚ด๋†”!" ๋กœ ํ•„ํ„ฐ๋ฅผ ๊ฑธ์–ด์•ผ ํ•ด.


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ