원글: Building an Image Gallery with Next.js, Supabase, and Tailwind CSS (Lee Robinson / 2022년 3월 28일)

 

* 오역 있을 수 있습니다. 댓글이나 이메일로 말씀 부탁드립니다.

 

Supabase에 데이터 추가하기

Supabase 클라이언트는 이미 준비해뒀으니, images 테이블에 있는 모든 이미지를 select 할 수 있습니다.

const IMAGE_TABLE = 'images';

const { data } = await supabaseAdmin.from( IMAGE_TABLE ).select( '*' );

우리는 getStaticProps 내부에서 데이터를 가져오려고 합니다. getStaticProps는 데이터를 서버에서 가져올 수 있도록 하면서, 가져온 데이터를 페이지에서 default export 된 리액트 컴포넌트의 props로 반환합니다. 예제에서 default export 된 리액트 컴포넌트인 Gallery 컴포넌트에서 images를 props로서 전달 받기 위해, Image 타입도 미리 정의해줍니다.

import { useState } from 'react';

import { createClient } from '@supabase/supabase-js';

import Image from "next/image"

const IMAGE_TABLE = 'images';

type Image = {
  id: number;
  created_at: string;
  name: string;
  href: string;
  userName: string;
  imageSrc: string;
};

// Supabase client
const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL as string,
  process.env.NEXT_PUBLIC_SERVICE_KEY as string,
);

export async function getStaticProps() {
  const { data } = await supabaseAdmin.from( IMAGE_TABLE ).select( '*' );
  
  return {
    props: {
      images: data,
    },
  };
}

const addTestImage = async () => {
  try {
    await supabaseAdmin.from(IMAGE_TABLE).insert([{
      name: 'undefined-study',
      href: 'https://github.com/undefined-study',
      userName: 'ahnanne',
      imageSrc: 'https://avatars.githubusercontent.com/u/105836469?s=200&v=4',
    }]);
  
    console.log('완료!');
  } catch (err) {
    console.error(err);
  }
};

/**
 * 배열 안에 존재할 수 있는 falsy한 값들을 제거하여 배열을 믿을 수 있는 상태로 만들기 위해 사용
 * 참고: https://velog.io/@yongbum/filter-boolean
*/
function cn ( ...classes: string[] ) {
  return classes.filter(Boolean).join(' ');
}

function BlurImage() {
  const [ isLoading, setLoading ] = useState( true );

  return (
    <a href="#" className="group">
      <div className="w-full aspect-w-1 aspect-h-1 bg-gray-200 rounded-lg overflow-hidden xl:aspect-w-7 xl:aspect-h-8">
        <Image
          alt=""
          src="https://bit.ly/placeholder-img"
          layout="fill"
          objectFit="cover"
          className={cn(
            'group-hover:opacity-75 duration-700 ease-in-out',
            isLoading
              ? 'grayscale blur-2xl scale-110'
              : 'grayscale-0 blur-0 scale-100'
          )}
          onLoadingComplete={() => setLoading(false)}
        />
      </div>
      <h3 className="mt-4 text-sm text-gray-700">홍길동</h3>
      <p className="mt-1 text-lg font-medium text-gray-900">@hgd</p>
    </a>
  )
}

export default function Gallery ( { images }: { images: Image[]} ) {
  return (
    <div className="bg-slate-100 max-w-2xl mx-auto py-16 px-4 sm:py-24 sm:px-6 lg:max-w-7xl lg:px-8">
      <button className="bg-blue-100" onClick={addTestImage}>
        테스트 이미지를 추가해보시겠어요?
      </button>
      <div className="grid grid-cols-1 gap-y-10 sm:grid-cols-2 gap-x-6 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
        <BlurImage />
      </div>
    </div>
  )
}

다음으로, 임시 이미지가 있던 자리를 이젠 실제 데이터로 교체해봅시다. 실제 이미지를 받아서 보여줄 수 있도록 BlurImage 컴포넌트를 다음과 같이 수정해주세요.

function BlurImage({ image }: { image: Image }) {
  const [ isLoading, setLoading ] = useState( true );

  return (
    <a href={image.href} className="group">
      <div className="w-full aspect-w-1 aspect-h-1 bg-gray-200 rounded-lg overflow-hidden xl:aspect-w-7 xl:aspect-h-8">
        <Image
          alt=""
          src={image.imageSrc}
          layout="fill"
          objectFit="cover"
          className={cn(
            'group-hover:opacity-75 duration-700 ease-in-out',
            isLoading
              ? 'grayscale blur-2xl scale-110'
              : 'grayscale-0 blur-0 scale-100'
          )}
          onLoadingComplete={() => setLoading(false)}
        />
      </div>
      <h3 className="mt-4 text-sm text-gray-700">{image.name}</h3>
      <p className="mt-1 text-lg font-medium text-gray-900">{image.userName}</p>
    </a>
  )
}

그리고 Gallery 컴포넌트 안에서 map을 이용하여 images 배열을 매핑하면서, BlurImage 컴포넌트에 각 image 요소를 전달해주세요.

export default function Gallery ( { images }: { images: Image[]} ) {
  return (
    <div className="bg-slate-100 max-w-2xl mx-auto py-16 px-4 sm:py-24 sm:px-6 lg:max-w-7xl lg:px-8">
      <button className="bg-blue-100" onClick={addTestImage}>
        테스트 이미지를 추가해보시겠어요?
      </button>
      <div className="grid grid-cols-1 gap-y-10 sm:grid-cols-2 gap-x-6 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
        {images.map( ( image ) =>
          <BlurImage key={image.id} image={image} />
        )}
      </div>
    </div>
  )
}

일단 여기까지만 해두고 애플리케이션을 실행시켜보면 next/image Un-configured Host 에러가 뜹니다. 이를 해결하기 위해 next.config.js에 허용할 도메인(들)을 추가해주세요!

/** @type {import('next').NextConfig} */
module.exports = {
  reactStrictMode: true,
  images: {
    domains: ['pbs.twimg.com'],
  },
}
여기서 한 가지 문제는, 이미지를 가져오는 도메인이 예제와 같이 하나뿐이면 상관없지만 여러 도메인으로부터 이미지를 가져오는 경우라면 일일이 도메인을 추가해줘야 해서 번거로워진다는 것이다. 😓
이와 관련하여 domains: ['*'] 와 같은 와일드카드를 허용해달라는 PR이 약 1년 전부터 올라와있었지만, 모든 도메인을 허용할 경우에 발생할 수 있는 보안 상의 이유 등으로 아직 정식 도입은 안 된 듯 하다. 정식 도입은 아니지만 한달 전쯤에 canary 브랜치에 실험적으로 도입 되었다고 한다. domains 속성 대신 remotePatterns 속성을 통해 패턴 매칭을 사용하여 와일드카드를 설정할 수 있다고 하는데, 아직 실험 단계라 상용 서버에서는 사용하지 말라고 한다.
본인이 스스로 어떤 일을 하고 있는지 충분히 인지하고 있는 전제 하에 불가피하게 여러 도메인으로부터 이미지를 가져와야 하는 상황이라면, Next.js의 Image 컴포넌트 대신 별 수 없이 직접 이미지 컴포넌트를 만드는 방법이 아직까지는 최선이지 않을까 싶다.

 

이제 이미지가 잘 보이네요!

 

귀찮아서 두 개만 추가해둠 ㅠ_ㅠ (^^)

 

이제 Vercel로 배포해봅시다!

 

// 다음편: Vercel로 배포하기

 

 

복사했습니다!