메인 콘텐츠로 건너뛰기
API 메서드 내에서 Sonamu.getContext()를 호출하여 현재 요청의 Context를 가져올 수 있습니다.

기본 사용법

Context 가져오기

import { BaseModelClass, api, Sonamu } from "sonamu";

class UserModel extends BaseModelClass {
  modelName = "User";
  
  @api({ httpMethod: "GET" })
  async getCurrentUser(): Promise<User> {
    // Context 가져오기
    const context = Sonamu.getContext();
    
    // user 정보 접근
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    const rdb = this.getPuri("r");
    
    const user = await rdb
      .table("users")
      .where("id", context.user.id)
      .first();
    
    if (!user) {
      throw new Error("User not found");
    }
    
    return user;
  }
}
Sonamu.getContext()는 현재 실행 중인 API 요청의 Context를 반환합니다. 비동기 작업 중에도 올바른 Context를 반환합니다.

언제 사용하나?

1. 인증된 사용자 확인

class PostModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async create(params: PostSaveParams): Promise<{ postId: number }> {
    const context = Sonamu.getContext();
    
    // 로그인 확인
    if (!context.user) {
      throw new Error("Login required");
    }
    
    // 작성자로 현재 사용자 설정
    const wdb = this.getPuri("w");
    const [post] = await wdb
      .table("posts")
      .insert({
        ...params,
        user_id: context.user.id,
        author: context.user.username,
      })
      .returning({ id: "id" });
    
    return { postId: post.id };
  }
}

2. 권한 확인

class AdminModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  async deleteUser(userId: number): Promise<void> {
    const context = Sonamu.getContext();
    
    // 인증 확인
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    // 권한 확인
    if (context.user.role !== "admin") {
      throw new Error("Admin permission required");
    }
    
    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", userId).delete();
  }
}

3. 소유권 확인

class PostModel extends BaseModelClass {
  @api({ httpMethod: "PUT" })
  async update(
    postId: number,
    params: PostSaveParams
  ): Promise<void> {
    const context = Sonamu.getContext();
    
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    const rdb = this.getPuri("r");
    
    // 게시글 조회
    const post = await rdb
      .table("posts")
      .where("id", postId)
      .first();
    
    if (!post) {
      throw new Error("Post not found");
    }
    
    // 소유권 확인 (작성자이거나 관리자만)
    const isOwner = post.user_id === context.user.id;
    const isAdmin = context.user.role === "admin";
    
    if (!isOwner && !isAdmin) {
      throw new Error("Permission denied");
    }
    
    // 수정
    const wdb = this.getPuri("w");
    await wdb.table("posts").where("id", postId).update(params);
  }
}

4. Request 정보 접근

class AnalyticsModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async trackEvent(params: EventParams): Promise<void> {
    const context = Sonamu.getContext();
    
    const wdb = this.getPuri("w");
    
    // Request 정보 기록
    await wdb.table("analytics").insert({
      ...params,
      user_id: context.user?.id,
      ip_address: context.request.ip,
      user_agent: context.request.headers["user-agent"],
      referer: context.request.headers.referer,
      timestamp: new Date(),
    });
  }
}

5. Response 헤더 설정

class FileModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async download(fileId: number): Promise<Buffer> {
    const context = Sonamu.getContext();
    const rdb = this.getPuri("r");
    
    const file = await rdb
      .table("files")
      .where("id", fileId)
      .first();
    
    if (!file) {
      throw new Error("File not found");
    }
    
    // 다운로드 헤더 설정
    context.reply.header("Content-Type", file.mime_type);
    context.reply.header(
      "Content-Disposition",
      `attachment; filename="${file.filename}"`
    );
    
    return Buffer.from(file.content);
  }
}

헬퍼 함수 패턴

재사용 가능한 인증 체크

class BaseModelClass {
  // 인증 확인 헬퍼
  protected requireAuth(): ContextUser {
    const context = Sonamu.getContext();
    
    if (!context.user) {
      context.reply.status(401);
      throw new Error("Authentication required");
    }
    
    return context.user;
  }
  
  // 권한 확인 헬퍼
  protected requireRole(role: UserRole): ContextUser {
    const user = this.requireAuth();
    
    if (user.role !== role) {
      const context = Sonamu.getContext();
      context.reply.status(403);
      throw new Error(`${role} permission required`);
    }
    
    return user;
  }
  
  // 관리자 확인 헬퍼
  protected requireAdmin(): ContextUser {
    return this.requireRole("admin");
  }
}

// 사용
class UserModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  async remove(userId: number): Promise<void> {
    // 간단하게 권한 확인
    this.requireAdmin();
    
    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", userId).delete();
  }
  
  @api({ httpMethod: "GET" })
  async getMyProfile(): Promise<User> {
    // 로그인만 확인
    const user = this.requireAuth();
    
    const rdb = this.getPuri("r");
    return rdb.table("users").where("id", user.id).first();
  }
}

소유권 확인 헬퍼

class BaseModelClass {
  protected async requireOwnership(
    tableName: string,
    itemId: number,
    userIdField: string = "user_id"
  ): Promise<void> {
    const user = this.requireAuth();
    const context = Sonamu.getContext();
    const rdb = this.getPuri("r");
    
    const item = await rdb
      .table(tableName)
      .where("id", itemId)
      .first();
    
    if (!item) {
      context.reply.status(404);
      throw new Error("Item not found");
    }
    
    // 소유자이거나 관리자만 허용
    const isOwner = item[userIdField] === user.id;
    const isAdmin = user.role === "admin";
    
    if (!isOwner && !isAdmin) {
      context.reply.status(403);
      throw new Error("Permission denied");
    }
  }
}

// 사용
class PostModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  async remove(postId: number): Promise<void> {
    // 소유권 자동 확인
    await this.requireOwnership("posts", postId);
    
    const wdb = this.getPuri("w");
    await wdb.table("posts").where("id", postId).delete();
  }
}

여러 위치에서 사용

Model 메서드에서

class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async getCurrentUser(): Promise<User> {
    const context = Sonamu.getContext();
    
    if (!context.user) {
      throw new Error("Authentication required");
    }
    
    return this.getUserById(context.user.id);
  }
  
  // private 메서드에서도 사용 가능
  private async getUserById(userId: number): Promise<User> {
    const context = Sonamu.getContext();
    
    // 요청 로깅
    console.log("getUserById called from IP:", context.request.ip);
    
    const rdb = this.getPuri("r");
    const user = await rdb.table("users").where("id", userId).first();
    
    if (!user) {
      throw new Error("User not found");
    }
    
    return user;
  }
}

Frame에서

class AnalyticsFrame extends BaseFrameClass {
  frameName = "Analytics";
  
  @api({ httpMethod: "POST" })
  async track(params: TrackParams): Promise<void> {
    const context = Sonamu.getContext();
    
    const wdb = this.getPuri("w");
    
    await wdb.table("analytics").insert({
      event_type: params.eventType,
      event_data: JSON.stringify(params.data),
      user_id: context.user?.id,
      ip_address: context.request.ip,
      user_agent: context.request.headers["user-agent"],
      timestamp: new Date(),
    });
  }
}

비동기 작업에서

class NotificationModel extends BaseModelClass {
  @api({ httpMethod: "POST" })
  async sendBulk(params: {
    userIds: number[];
    message: string;
  }): Promise<void> {
    const context = Sonamu.getContext();
    
    // 비동기 작업에서도 Context 접근 가능
    const promises = params.userIds.map(async (userId) => {
      const innerContext = Sonamu.getContext();
      
      // 동일한 Context
      console.log(innerContext.user?.id === context.user?.id); // true
      
      return this.sendNotification(userId, params.message);
    });
    
    await Promise.all(promises);
  }
  
  private async sendNotification(
    userId: number,
    message: string
  ): Promise<void> {
    const context = Sonamu.getContext();
    
    // 여기서도 동일한 Context
    const wdb = this.getPuri("w");
    
    await wdb.table("notifications").insert({
      user_id: userId,
      message,
      sender_id: context.user?.id,
      created_at: new Date(),
    });
  }
}

실전 패턴

감사 로그 (Audit Log)

class BaseModelClass {
  protected async logAudit(action: string, details: any): Promise<void> {
    const context = Sonamu.getContext();
    const wdb = this.getPuri("w");
    
    await wdb.table("audit_logs").insert({
      user_id: context.user?.id,
      action,
      details: JSON.stringify(details),
      ip_address: context.request.ip,
      user_agent: context.request.headers["user-agent"],
      timestamp: new Date(),
    });
  }
}

class UserModel extends BaseModelClass {
  @api({ httpMethod: "DELETE" })
  async remove(userId: number): Promise<void> {
    this.requireAdmin();
    
    const rdb = this.getPuri("r");
    const user = await rdb.table("users").where("id", userId).first();
    
    // 감사 로그 기록
    await this.logAudit("USER_DELETE", {
      targetUserId: userId,
      targetUsername: user.username,
    });
    
    const wdb = this.getPuri("w");
    await wdb.table("users").where("id", userId).delete();
  }
}

요청 속도 제한 체크

class ApiModel extends BaseModelClass {
  private async checkRateLimit(): Promise<void> {
    const context = Sonamu.getContext();
    const rdb = this.getPuri("r");
    
    const key = context.user
      ? `user:${context.user.id}`
      : `ip:${context.request.ip}`;
    
    const oneMinuteAgo = new Date(Date.now() - 60000);
    
    const [{ count }] = await rdb
      .table("request_logs")
      .where("key", key)
      .where("timestamp", ">=", oneMinuteAgo)
      .count({ count: "*" });
    
    if (count >= 100) {
      context.reply.status(429);
      throw new Error("Too many requests");
    }
    
    // 요청 기록
    const wdb = this.getPuri("w");
    await wdb.table("request_logs").insert({
      key,
      timestamp: new Date(),
    });
  }
  
  @api({ httpMethod: "GET" })
  async list(): Promise<any[]> {
    await this.checkRateLimit();
    
    const rdb = this.getPuri("r");
    return rdb.table("items").select("*");
  }
}

다국어 지원

class I18nModel extends BaseModelClass {
  private getLanguage(): string {
    const context = Sonamu.getContext();
    
    // Accept-Language 헤더에서 언어 추출
    const acceptLanguage = context.request.headers["accept-language"];
    
    if (acceptLanguage) {
      const lang = acceptLanguage.split(",")[0].split("-")[0];
      return lang;
    }
    
    return "en"; // 기본값
  }
  
  @api({ httpMethod: "GET" })
  async getMessages(): Promise<Record<string, string>> {
    const language = this.getLanguage();
    const rdb = this.getPuri("r");
    
    const messages = await rdb
      .table("translations")
      .where("language", language)
      .select("*");
    
    return messages.reduce((acc, msg) => {
      acc[msg.key] = msg.value;
      return acc;
    }, {} as Record<string, string>);
  }
}

주의사항

Context 사용 시 주의사항:
  1. API 메서드 내에서만 호출 가능
  2. 생성자나 클래스 필드에서 호출 불가
  3. 비동기 경계를 넘어도 같은 Context 유지
  4. 여러 번 호출해도 같은 객체 반환
  5. Context는 읽기 전용으로 사용 권장

흔한 실수

// ❌ 잘못됨: 생성자에서 호출
class UserModel extends BaseModelClass {
  private currentUser?: ContextUser;
  
  constructor() {
    super();
    // 생성자에서는 Context 없음
    const context = Sonamu.getContext(); // ← 에러
    this.currentUser = context.user;
  }
}

// ❌ 잘못됨: 클래스 필드에서 호출
class UserModel extends BaseModelClass {
  // 클래스 필드 초기화 시점에는 Context 없음
  private context = Sonamu.getContext(); // ← 에러
}

// ❌ 잘못됨: API 메서드 밖에서 호출
class UserModel extends BaseModelClass {
  private getContext() {
    // private 메서드는 가능하지만,
    // API 요청 컨텍스트 밖에서 호출하면 에러
    return Sonamu.getContext();
  }
}

// ✅ 올바름: API 메서드 내에서 호출
class UserModel extends BaseModelClass {
  @api({ httpMethod: "GET" })
  async getCurrentUser(): Promise<User> {
    const context = Sonamu.getContext(); // ← 정상
    // ...
  }
  
  private async helperMethod(): Promise<void> {
    // API 메서드에서 호출된 경우 정상
    const context = Sonamu.getContext(); // ← 정상
    // ...
  }
}

다음 단계