🎁[속보] 인프런 내 깜짝 선물 출현 중🎁

[인프런 워밍업 스터디 클럽 3기 풀스택] 2주차 발자국

[인프런 워밍업 스터디 클럽 3기 풀스택] 2주차 발자국



학습 내용

인프런 워밍업 클럽 스터디 2주차로,

이번 주는 드롭 박스 프로젝트와 함께 Supabase의 Storage를 다뤄볼 수 있는 시간이었다.

 

Supbase Storage

1. 기본 구성 요소

  • Files: 모든 종류의 미디어 파일 저장 가능 (이미지, GIF, 비디오 등)

  • Folders: 파일을 체계적으로 구성하기 위한 디렉토리 구조

  • Buckets: 파일과 폴더를 담는 최상위 컨테이너 (접근 규칙별로 구분)

2. 접근 제어 모델

  • Private Buckets (기본값)

     

    • RLS(Row Level Security) 정책을 통한 접근 제어

    • JWT 인증 필요

    • Signed URL을 통한 임시 접근 가능

  • Public Buckets

    • 파일 조회 시 접근 제어 없음

    • URL만 있으면 누구나 접근 가능

    • 업로드/삭제 등 다른 작업은 여전히 접근 제어 적용

3. 보안 기능

  • RLS 정책 설정 가능

    • SELECT (다운로드)

    • INSERT (업로드)

    • UPDATE (수정)

    • DELETE (삭제)

  • 소유권 관리

    • owner_id 필드로 리소스 소유자 추적

    • JWT의 sub claim 기반 소유권 할당

4. 이미지 변환 기능 (Pro Plan 이상)

  • 실시간 이미지 최적화

    • 크기 조정

    • 품질 조정 (20-100)

    • WebP 자동 최적화

  • 변환 옵션

    • resize 모드: cover, contain, fill

    • width/height 지정 (1-2500px)

    • 최대 파일 크기: 25MB

    • 최대 해상도: 50MP

5. 인증 방식

  • S3 액세스 키

    • 서버 사이드 전용

    • 모든 버킷에 대한 완전한 접근 권한

  • 세션 토큰

    • 클라이언트 사이드 사용 가능

    • RLS 정책 기반 제한된 접근

6. 통합 기능

  • Next.js 이미지 로더 지원

  • AWS S3 호환성

  • PostgreSQL DB와 연동

7. 제한사항

  • 파일명은 AWS S3 명명 규칙 준수 필요

  • HTML 파일은 보안상 plain text로 반환

  • 이미지 변환 기능은 Pro Plan 이상에서만 사용 가능



미션 2 구현 내용

과제 구현 저장소

image

Dropbox 중

파일의 마지막 수정(업로드) 시간을 표시하는 기능 추가 하기

 

export interface FileObject {
  name: string
  bucket_id: string
  owner: string
  id: string
  updated_at: string
  created_at: string
  last_accessed_at: string
  metadata: Record<string, any>
  buckets: Bucket
}

=> DropboxImage 컴포넌트가 prop으로 받는 image의 타입은 FileObject로 그 중 업로드시간created_at을 의미하기에 이를 이미지에 추가하였다.(사진 참고)

 

포인트 1: 한글 파일명 es-hangul 사용

// 안전한 파일명 생성을 위한 유틸리티
export class FileNameConverter {
  // 안전한 문자 패턴 정의
  private static readonly SAFE_CHARACTERS = /^[a-zA-Z0-9!\-_.*'()]+$/;

  private static generateRandomString(length: number = 8): string {
    const chars =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    return Array.from({ length }, () =>
      chars.charAt(Math.floor(Math.random() * chars.length))
    ).join("");
  }

  // 파일명이 안전한 문자들로만 구성되었는지 확인
  private static isSafeFileName(name: string): boolean {
    return this.SAFE_CHARACTERS.test(name);
  }

  // 안전하지 않은 문자를 포함한 파일명을 안전한 형식으로 변환
  private static convertToSafeFileName(name: string): string {
    try {
      // 파일명 정규화
      const normalized = name.trim().normalize();

      // 한글이나 특수문자가 있는지 확인
      const hasKorean = /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(normalized);
      const hasSpecialChars = /[^A-Za-z0-9]/.test(normalized);

      if (!hasKorean && !hasSpecialChars) {
        return normalized;
      }

      // 한글이 있는 경우 로마자로 변환 시도
      if (hasKorean) {
        const romanized = romanize(normalized);
        if (romanized && romanized !== normalized) {
          // 로마자 변환 결과에서 안전하지 않은 문자 제거
          return romanized.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase();
        }
      }

      // 변환 실패 시 랜덤 문자열 생성
      return this.generateRandomString();
    } catch (error) {
      console.error("Conversion error:", error);
      return this.generateRandomString();
    }
  }

  // 원본 파일명을 안전한 형식으로 변환
  static encode(fileName: string): string {
    console.log("Original filename:", fileName);

    const extension = fileName.split(".").pop() || "";
    const nameWithoutExt = fileName.slice(0, fileName.lastIndexOf("."));

    const safeName = this.isSafeFileName(nameWithoutExt)
      ? nameWithoutExt
      : this.convertToSafeFileName(nameWithoutExt);

    console.log("Safe filename:", safeName);

    return `${safeName}_${Date.now()}.${extension}`;
  }

  // 파일명에서 타임스탬프 제거하여 원본 이름 추출
  static decode(fileName: string): string {
    const [name] = fileName.split("_");
    return name || fileName;
  }
}

포인트 2 : 업로드 날짜 표시

export function formatDate(timestamp: string): string {
  const date = new Date(timestamp);
  const now = new Date();
  const diff = now.getTime() - date.getTime();

  // 1일 이내
  if (diff < 24 * 60 * 60 * 1000) {
    const hours = Math.floor(diff / (60 * 60 * 1000));
    if (hours < 1) {
      const minutes = Math.floor(diff / (60 * 1000));
      return `${minutes}분 전`;
    }
    return `${hours}시간 전`;
  }

  // 30일 이내
  if (diff < 30 * 24 * 60 * 60 * 1000) {
    const days = Math.floor(diff / (24 * 60 * 60 * 1000));
    return `${days}일 전`;
  }

  // 그 외
  return date.toLocaleDateString("ko-KR", {
    year: "numeric",
    month: "long",
    day: "numeric",
  });
}


회고

파일명 변환하는데 생각보다 시간이 많이 소요됐다.

여찌저찌 구현은 헀지만, 이미지가 어떻게 encoding되고 decoding되는지 일련의 과정에 대한 공부가 필요함을 느끼는 이번주 였다.

 

 

댓글을 작성해보세요.


  • leeebug
    leeebug

    저도 한글파일명 변환하는 부분에서 애먹었네요..ㅎㅎ
    es-hangle 찾아봐야겠네요. 감사합니다!
    이번주도 고생하셨습니다..!

채널톡 아이콘