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

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

2주차에 배운 내용을 정리해본다.

깃허브 링크

 

1. 배운 내용

1. Next.js에서 메타데이터 정의하기

<meta> 태그를 이용해 사이트 정보를 정의하려면,

  • 서버 컴포넌트 파일에서

  • Metadata를 정의해줘야 함

// page.tsx
import { Metadata } from "next";
import Ui from "./Ui";

// 페이지의 메타데이터를 정의
// use client에서는 사용 불가 - 클라이언트 코드는 Ui.tsx에서 정의하는 이유
export const metadata: Metadata = {
    title: "Dropbox Clone",
    description: "A minimalist Dropbox Clone",
};

export default function Home() {
    return <Ui />;
}

2. 파일 드랍 존 만들기

  • <input /> 태그를 이용, type="file"

3. supabase

1. storage bucket 만들기

  • 업로드 가능한 파일 종류 설정 가능

    • 만들 때 Allowed MIME types 옵션에서 image/* 등의 조건을 추가하면 됨

2. policy 생성

  • 사이드바 policy 메뉴에서 생성 가능

  • 이름, 가능한 액션 종류 선택, 누구에게 가능하게 할지 선택 가능

4. 파일 드래그앤드롭 - react-dropzone 라이브러리 사용

 

2. 이슈 사항

1. storage의 get url 형태 변경: getImageUrl 함수 커스텀 어려움

  • 이미지의 만료일을 지정할 수 있게 변경됨

  • 그러면서 token이라는 서치 파라미터가 필수값으로 추가된 듯

    토큰을 누락한 형태로 확인 시 에러가 발생하며 이미지 로드 실패

    {"statusCode":"400","error":"Error","message":"querystring must have required property 'token'"}
    
  1. bucket을 public으로 전환하고, supabase에서 제공하는 getPublicUrl 메서드를 사용

    1. storage에서 bucket 이름 옆 드롭다운 메뉴 → edit bucket → public으로 설정

       

    2. getImageUrl 함수 내부를 아래와 같이 수정

      const { data } = supabase.storage
          .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET!)
          .getPublicUrl(path);
       /**
         * A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset.
         * This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset.
         *
         * @param path The path and name of the file to generate the public URL for. For example `folder/image.png`.
         * @param options.download Triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.
         * @param options.transform Transform the asset before serving it to the client.
         */
        getPublicUrl(
          path: string,
          options?: { download?: string | boolean; transform?: TransformOptions }
        ): { data: { publicUrl: string } } {
          const _path = this._getFinalPath(path)
          const _queryString = []
      
          const downloadQueryParam = options?.download
            ? `download=${options.download === true ? '' : options.download}`
            : ''
      
          if (downloadQueryParam !== '') {
            _queryString.push(downloadQueryParam)
          }
      
          const wantsTransformation = typeof options?.transform !== 'undefined'
          const renderPath = wantsTransformation ? 'render/image' : 'object'
          const transformationQuery = this.transformOptsToQueryString(options?.transform || {})
      
          if (transformationQuery !== '') {
            _queryString.push(transformationQuery)
          }
      
          let queryString = _queryString.join('&')
          if (queryString !== '') {
            queryString = `?${queryString}`
          }
      
          return {
            data: { publicUrl: encodeURI(`${this.url}/${renderPath}/public/${_path}${queryString}`) },
          }
        }
      
  2. StorageFileApi.ts를 참고하면 다양한 메서드가 있음 - createSingedUrl를 이용하면 expiresIn을 직접 지정 가능. 이걸 이용하면 bucket이 public이 아니어도 가능할듯

     /**
       * Creates a signed URL. Use a signed URL to share a file for a fixed amount of time.
       *
       * @param path The file path, including the current file name. For example `folder/image.png`.
       * @param expiresIn The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.
       * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.
       * @param options.transform Transform the asset before serving it to the client.
       */
      async createSignedUrl(
        path: string,
        expiresIn: number,
        options?: { download?: string | boolean; transform?: TransformOptions }
      ): Promise<
        | {
            data: { signedUrl: string }
            error: null
          }
        | {
            data: null
            error: StorageError
          }
      > {
        try {
          let _path = this._getFinalPath(path)
    
          let data = await post(
            this.fetch,
            `${this.url}/object/sign/${_path}`,
            { expiresIn, ...(options?.transform ? { transform: options.transform } : {}) },
            { headers: this.headers }
          )
          const downloadQueryParam = options?.download
            ? `&download=${options.download === true ? '' : options.download}`
            : ''
          const signedUrl = encodeURI(`${this.url}${data.signedURL}${downloadQueryParam}`)
          data = { signedUrl }
          return { data, error: null }
        } catch (error) {
          if (isStorageError(error)) {
            return { data: null, error }
          }
    
          throw error
        }
      }
    
      /**
       * Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time.
       *
       * @param paths The file paths to be downloaded, including the current file names. For example `['folder/image.png', 'folder2/image2.png']`.
       * @param expiresIn The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.
       * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.
       */
      async createSignedUrls(
        paths: string[],
        expiresIn: number,
        options?: { download: string | boolean }
      ): Promise<
        | {
            data: { error: string | null; path: string | null; signedUrl: string }[]
            error: null
          }
        | {
            data: null
            error: StorageError
          }
      > {
        try {
          const data = await post(
            this.fetch,
            `${this.url}/object/sign/${this.bucketId}`,
            { expiresIn, paths },
            { headers: this.headers }
          )
    
          const downloadQueryParam = options?.download
            ? `&download=${options.download === true ? '' : options.download}`
            : ''
          return {
            data: data.map((datum: { signedURL: string }) => ({
              ...datum,
              signedUrl: datum.signedURL
                ? encodeURI(`${this.url}${datum.signedURL}${downloadQueryParam}`)
                : null,
            })),
            error: null,
          }
        } catch (error) {
          if (isStorageError(error)) {
            return { data: null, error }
          }
    
          throw error
        }
      }
    
    

2. server action 파일에서 console.log는 개발자 도구가 아닌 터미널에 찍힌다는 사실..

  • storageActions.ts에 정의한 uploadFile 함수를 수정하고 제대로 데이터가 들어가는지 확인하려고 console.log를 사용

  • 아무리 해도 useQuery까지는 잘 로그가 찍히는데, uploadFile에 작성한 로그가 브라우저 개발자도구에 전혀 찍히지 않아 뭐가 문제인지 한참 헤맴

  • 코드 에디터 터미널에 찍히고 있었음.. ㅠㅠ

3. 파일명에 한글이 포함될 경우 업로드 안되는 오류

  • 디스코드에 다른 러너분들이 공유해준 정보에 따르면 supabase storage는 AWS의 S3 스토리지와 호횐되어, 파일명도 AWS S3의 Safe charaters - 영문, 숫자, 일부 특수기호 만 허용한다고 함

  1. 어떻게 저장할까?

    • base64 인코딩을 통해 S3-safe한 이름으로 변경해 저장하면 업로드 가능

    ⇒ 저장 및 다운로드, 이름 표시하는 코드에서 인코딩/디코딩 함수를 사용하게 변경 완료

  2. 어떻게 검색할까?

    • 인코딩/디코딩된 값으로 검색 호환이 안됨

    ⇒ db에 저장해야 함

  3. db에 저장하도록 변경했는데, 한글 초성만 검색됨..

    • todo-list 검색 때와의 차이가 뭐길래 안되는거지?

 

3. 미션 - 파일의 마지막 수정(업로드) 시간 표시하기

  • storage를 확인하면, Added on, Last modified 정보가 있음

  • file이 어떤 형태인지 로그 찍어 확인

  • file.updated_at 키에 저장된 값임을 확인, 이 값을 사용

  • 추가로 file.metadata에 파일 타입과 사이즈 등의 정보도 있어 그 정보들도 적절히 표시하도록 함 (ui는 supabase storage를 참고함)

댓글을 작성해보세요.


채널톡 아이콘