상세 컨텐츠

본문 제목

[개발자 포트폴리오 > Projects] 노션 데이터베이스 조회하기 | 화면 구현하기

Project/포트폴리오(NextJS + Notion API)

by yooputer 2025. 6. 11. 16:54

본문

오늘은 /projects로 접속했을 때 노션 데이터베이스에 있는 프로젝트 목록을 조회해서

화면에 보여주도록 구현하는 과정을 정리해보려고 한다. 

 


Notion Database 프로퍼티 추가

우선 노션에 Database를 생성한 후 아래와 같이 프로퍼티들을 추가한다. 

category는 select 타입, description과 slug는 text 타입, order는 number타입, skills는 multi-select 타입이다. 

 

그리고 해당 데이터베이스의 ID를 조회하여 .env에 다음과같이 추가한다. 

NOTION_PROJECT_DATABASE_ID=DB아이디

 

데이터베이스 ID는 데이터베이스의 링크를 복사하여 조회할 수 있다. 

https://www.notion.so/<DB아이디>?v=<~~>&source=copy_link


데이터베이스 목록 조회 함수 구현

타입 정의 

우선 /types/project.ts파일을 생성한 후 아래와 같이 프로젝트 목록에 해당하는 type을 정의하였다. 

export interface MultiSelect {
    id: string;
    name: string;
    color?: string;
}

export interface ProjectItem {
    id: string;
    title: string;
    coverImage?: string;
    skills: MultiSelect[];
    slug: string;
    description?: string;
}

 

 

getProjectListByCategory 함수 구현

노션API를 사용하여 데이터베이스를 조회하고 그 결과들을 ProjectItem 타입으로 변환하여 반환하는 함수를 구현해보자. 

나는 /lib/apis/projects.ts 파일을 생성한 후 작성하였다. 

import type {PageObjectResponse} from "@notionhq/client/build/src/api-endpoints";
import {ProjectItem} from "@/types/project";
import { notion } from "@/lib/notion";

export const getProjectListByCategory = async (category: string): Promise<ProjectItem[]> => {
    const response = await notion.databases.query({
        database_id: process.env.NOTION_PROJECT_DATABASE_ID!,
        filter: {
            and: [
                {
                    property: 'category',
                    select: {
                        equals: category,
                    },
                },
            ],
        },
        sorts: [
            {
                property: 'order',
                direction: 'ascending',
            },
        ],
        page_size: 4,
    });

    const projects = response.results
        .filter((page): page is PageObjectResponse => 'properties' in page)
        .map(convertToProjectItem);

    return projects;
};

 

코드를 살펴보면 notion.databases.query 함수를 사용하여 데이터베이스를 조회하는 것을 확인할 수 있다. 

만약 getProjectListByCategory('work')를 호출하면 category 프로퍼티의 값이 work인 목록만 필터링되어 조회된다.

 

나는 카테고리별 프로젝트 개수가 4개를 넘지 않아서 page_size를 4로 설정하였는데, 자유롭게 변경하여도 된다. 

 

convertToProjectItem 함수 구현

이제 PageObjectResponse 타입인 객체를 ProjectItem 타입의 객체로 변환하는 함수를 작성해보자. 

function convertToProjectItem(page: PageObjectResponse): ProjectItem {
    const { properties } = page;

    return {
        id: page.id,
        title: getTextByPropertyName(properties, 'Name'),
        coverImage: getCoverImage(page.cover),
        skills: getMultiSelectByPropertyName(properties, 'skills'),
        description: getTextByPropertyName(properties, 'description'),
        slug: getTextByPropertyName(properties, 'slug'),
    };
}

 

convertToProjectItem 함수에서 사용된 함수들은 다음과 같다. 

export const getCoverImage = (cover: PageObjectResponse['cover']) => {
  if (!cover) return '';

  switch (cover.type) {
    case 'external':
      return cover.external.url;
    case 'file':
      return cover.file.url;
    default:
      return '';
  }
};

export function getTextByPropertyName(properties: PageObjectResponse['properties'], propertyName:string): string {
  if (!properties[propertyName]) {
    return '';
  }

  const type = properties[propertyName].type;

  if (type === 'title' && properties[propertyName].title.length > 0) {
    return properties[propertyName].title[0].plain_text ?? '';
  } else if (type === 'rich_text' && properties[propertyName].rich_text.length > 0){
    return properties[propertyName].rich_text[0].plain_text ?? '';
  }

  return '';
}

export function getSelectByPropertyName(properties: PageObjectResponse['properties'], propertyName:string): null | MultiSelect {
  if (!properties[propertyName] || properties[propertyName].type !== 'select' ) {
    return null;
  }

  return  properties[propertyName].select;
}

export function getMultiSelectByPropertyName(properties: PageObjectResponse['properties'], propertyName:string): MultiSelect[] {
  if (!properties[propertyName] || properties[propertyName].type !== 'multi_select' ) {
    return [];
  }

  const multiSelects: MultiSelect[] = properties[propertyName].multi_select.map((item: MultiSelect) => {
        return { id: item.id, name: item.name, color: item.color }
      }
  )

  return multiSelects;
}

 


화면 구현

앞서 구현한 getProjectListByCategory 함수를 사용하여 프로젝트 목록을 조회하고, 보여주는 화면을 구현해보자.

/app/projects/page.tsx 파일을 생성한 후 다음과 같이 작성한다. 

export default async function ProjectList() {
  const workProjects = await getProjectListByCategory('work');
  const toyProjects = await getProjectListByCategory('toy');

  const sections = [
    { title: 'Work', projects: workProjects },
    { title: 'Toy Projects', projects: toyProjects }
  ];

  return (
    <PageLayout>
      {sections.map((section, index) => (
        <div key={index}>
          <ProjectSection title={section.title} projects={section.projects} />
          {index < sections.length - 1 && <Separator className="my-10" />}
        </div>
      ))}
    </PageLayout>
  );
}

 

ProjectList 컴포넌트의 자식 컴포넌트는 다음과 같다. 

export default function PageLayout({ children, leftSidebar, rightSidebar }: PageLayoutProps) {
  return (
    <div className="container py-6 md:py-8 lg:py-12">
      <div className="grid grid-cols-1 gap-4 md:grid-cols-[240px_1fr_240px] md:gap-8">
        {/* 왼쪽 사이드바 */}
        <div className="hidden md:block">
          <div className="sticky top-16">
            {leftSidebar}
          </div>
        </div>

        {/* 메인 컨텐츠 */}
        <div>
          {children}
        </div>

        {/* 오른쪽 사이드바 */}
        <div className="hidden md:block">
          <div className="top-4 space-y-6">
            {rightSidebar}
          </div>
        </div>
      </div>
    </div>
  );
}

 

function ProjectSection({ title, projects }: { title: string; projects: ProjectItem[] }) {
  return (
    <div>
      <h1 className="text-3xl font-bold mb-4">{title}</h1>
      <div>
        {projects.map((project) => (
          <ProjectLink key={project.id} item={project} />
        ))}
      </div>
    </div>
  );
}

 

function ProjectLink({ item }: { item: ProjectItem }) {
  return (
    <Link
      href={`/projects/${item.slug}`}
      className="w-[100%] mx-auto mb-6 block hover:opacity-80 transition-opacity"
    >
      <div className="flex gap-6 p-4 rounded-lg border border-border">
        {/* 왼쪽 섹션 - 커버 이미지 */}
        <div className="w-[35%] relative flex-shrink-0">
          {item.coverImage ? (
            <Image
              src={item.coverImage}
              alt={item.title}
              fill
              className="object-cover rounded-md"
            />
          ) : (
            <div className="w-full h-full bg-muted rounded-md" />
          )}
        </div>

        {/* 오른쪽 섹션 - 제목, 스킬, 설명 */}
        <div className="flex-1 space-y-3">
          <h3 className="text-lg font-bold">{item.title}</h3>
          
          <div className="flex flex-wrap gap-2">
            {item.skills.map((skill) => (
              <span
                key={skill.id}
                className="px-2 py-1 text-xs bg-primary/10 text-primary rounded-full"
              >
                {skill.name}
              </span>
            ))}
          </div>

          {item.description && (
            <p className="text-muted-foreground line-clamp-2 text-sm">
              {item.description}
            </p>
          )}
        </div>
      </div>
    </Link>
  );
}

 

next.config.js 수정

다음과같이 next.config.js를 수정하여야 이미지가 정상적으로 조회된다. 

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['prod-files-secure.s3.us-west-2.amazonaws.com'],
  },
}

module.exports = nextConfig

이제 프로젝트 목록 화면 개발이 끝났다. 

다음에는 /projects/프로젝트명 경로로 접속했을 때 프로젝트의 상세 내용을 조회하는 페이지를 구현해보도록 하겠다. 

관련글 더보기