6 min read

Next.js ๋ธ”๋กœ๊ทธ์— Notion ์—ฐ๋™ํ•˜๊ธฐ

Table of Contents

Next.js ๋ธ”๋กœ๊ทธ์— Notion ์—ฐ๋™ํ•˜๊ธฐ

๋„์ ๋„์  ๊ธ€์„ ์ž‘์„ฑํ•˜๊ณ  ๋ธ”๋กœ๊ทธ๋กœ ๊ด€๋ฆฌํ•˜๋‹ค ๋ณด๋ฉด ๊ธ€ ์ž‘์„ฑ์„ ๋” ํŽธํ•˜๊ฒŒ ํ•˜๊ณ  ์‹ถ์€ ๋งˆ์Œ์ด ์ƒ๊ธฐ๊ณค ํ•œ๋‹ค.

๊ธฐ์กด์—๋Š” ์˜ต์‹œ๋””์–ธ์„ ์ด์šฉํ•ด์„œ ๋งˆํฌ๋‹ค์šด ํŒŒ์ผ์„ ์ž‘์„ฑํ•˜๊ณ  Git์— ์ปค๋ฐ‹ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ด€๋ฆฌํ–ˆ๋‹ค. ์˜ต์‹œ๋””์–ธ์€ ๋ธ”๋กœ๊ทธ๋ฅผ ์ฒ˜์Œ ๋งŒ๋“ค ๋‹น์‹œ๋งŒ ํ•˜๋”๋ผ๋„ ์˜ต์‹œ๋””์–ธ์€ ๊ฐœ๋ฐœ์ž ์‚ฌ์ด์—์„œ ๊ฝค ์ž…์†Œ๋ฌธ์„ ํƒ€๋˜ ์•„์นด์ด๋น™ ๋„๊ตฌ์˜€๋‹ค.

์ด์— ํŽธ์Šนํ•ด ๋‚˜๋„ ์˜ต์‹œ๋””์–ธ์„ ๋ฉ”์ธ ์•„์นด์ด๋น™ ๋„๊ตฌ๋กœ ์‚ฌ์šฉํ•˜์˜€์œผ๋‚˜, ์˜ต์‹œ๋””์–ธ์„ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋งˆํฌ๋‹ค์šด ํ˜•์‹์— ์ต์ˆ™ํ•ด์•ผํ•˜๊ณ  ์ž์œ ๋„๊ฐ€ ๋†’์€ ๋„๊ตฌ์ด๋‹ค ๋ณด๋‹ˆ ๋‚˜์˜ ํ•„์š”๋ฅผ ์„ธ์„ธํ•˜๊ธฐ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ†ตํ•ด์„œ ๋งž์ถฐ๊ฐ”์–ด์•ผ ํ–ˆ๋‹ค.

๋‚จ๋“ค์ด ์ข‹๊ณ , ํŽธํ•˜๋‹ค๋Š” ๋ง์— ์–ต์ง€๋กœ ๋งž์ง€๋„ ์•Š๋Š” ์˜ท์„ ์ž…๊ณ  ์ข‹๋‹ค๊ณ , ํŽธํ•˜๋‹ค๊ณ  ์–ต์ง€๋กœ ์›ƒ์Œ ์ง“๊ณ  ์žˆ์—ˆ๋‹ค. ๐Ÿ˜“ (๋ฌผ๋ก , ๋‚ด๊ฐ€ ์ž˜ ํ™œ์šฉํ•˜์ง€ ๋ชปํ–ˆ๊ธด ํ•˜๋‹ค.)

์™œ Notion์„ ์„ ํƒํ–ˆ๋‚˜

๊ธฐ์กด ๋ธ”๋กœ๊ทธ๋Š” MDX ํŒŒ์ผ์„ ๋ณ„๋„์˜ ํด๋”๋กœ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ–ˆ๋‹ค. ๊ทธ๋ ‡์ง€๋งŒ ์•„๋ž˜์™€ ๊ฐ™์€ ๋‹จ์ ๋“ค์ด ์žˆ์—ˆ๋‹ค.

  • ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ๋งŒ ๊ธ€์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Œ
  • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ๊ฐ€ ๋ฒˆ๊ฑฐ๋กœ์›€
  • ์—๋””ํ„ฐ ๊ธฐ๋Šฅ์ด ์ œํ•œ์ 
  • ์—ฌ๋Ÿฌ ๊ธฐ๊ธฐ์—์„œ ์ž‘์—…ํ•˜๊ธฐ ์–ด๋ ค์›€

์œ„ ๋‹จ์ ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์‹œ Notion์œผ๋กœ ๋Œ์•„์™”๋‹ค. ๋” ๋‚˜์•„๊ฐ€ ๋‚˜๋Š” ๊ธฐ์กด๊ณผ ๊ฐ™์ด MDย ํŒŒ์ผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ๋ณด๋‹ค๋Š” ์—๋””ํ„ฐ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ์•„์นด์ด๋น™ ๋œ ์ž๋ฃŒ๋“ค์ด ๋ธ”๋กœ๊ทธ์˜ ์ž๋™์œผ๋กœ ์˜ฌ๋ผ๊ฐ€๊ธธ ์›ํ–ˆ๋‹ค.

ํ•„์š” ๊ธฐ๋Šฅ

Notion ์—ฐ๋™์„ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ–ˆ๋‹ค.

  1. Notion โ†’ MDX ๋™๊ธฐํ™”: Notion์—์„œ ์ž‘์„ฑํ•œ ๊ธ€์„ MDX ํŒŒ์ผ๋กœ ๋ณ€ํ™˜
  2. MDX โ†’ Notion ์—…๋กœ๋“œ: ๊ธฐ์กด MDX ํŒŒ์ผ์„ Notion์œผ๋กœ ์—…๋กœ๋“œ
  3. ์ž๋™ ๋นŒ๋“œ ํ†ตํ•ฉ: ๋นŒ๋“œ ์‹œ ์ž๋™์œผ๋กœ ์ตœ์‹  ๊ธ€ ๋™๊ธฐํ™”

์œ„ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์šฐ์„ ์ ์œผ๋กœ ๊ธฐ๋ณธ์ ์ธ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค.

Notion API ์„ค์ •

๋จผ์ € Notion Integration์„ ์ƒ์„ฑํ•˜๊ณ  ๋ธ”๋กœ๊ทธ ๊ธ€์„ ์•„์นด์ด๋น™ํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์„ค์ •ํ–ˆ๋‹ค.

Integration ์ƒ์„ฑ

  1. Notion Integrations ํŽ˜์ด์ง€์—์„œ ์ƒˆ Integration ์ƒ์„ฑ
  2. Internal Integration Token ๋ณต์‚ฌ

image.png

  1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— Integration ์—ฐ๊ฒฐ

image.png

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์†์„ฑ ๊ตฌ์„ฑ

๋ธ”๋กœ๊ทธ ํฌ์ŠคํŠธ์— ํ•„์š”ํ•œ ์†์„ฑ๋“ค์„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ถ”๊ฐ€ํ–ˆ๋‹ค

  • Title (Title): ํฌ์ŠคํŠธ ์ œ๋ชฉ
  • Date (Date): ๋ฐœํ–‰์ผ
  • Tags (Multi-select): ํƒœ๊ทธ ๋ชฉ๋ก
  • Summary (Text): ์š”์•ฝ
  • Draft (Checkbox): ์ดˆ์•ˆ ์—ฌ๋ถ€
  • Authors (Multi-select): ์ž‘์„ฑ์ž

์ƒ๊ฐ๋ณด๋‹ค ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ธฐ๋ณธ์ ์ธ ์„ค์ •์„ ๋งˆ์ณค๋‹ค.

3๋‹จ๊ณ„ ์Šคํฌ๋ฆฝํŠธ ๊ตฌ์„ฑ

1. Notion โ‡’ MDX ๋™๊ธฐํ™” (sync-notion.mjs)

1๏ธโƒฃ ํ™˜๊ฒฝ ์„ค์ • ๋ฐ Notion ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™”

const NOTION_TOKEN = process.env.NOTION_TOKEN;
const NOTION_DATABASE_ID = process.env.NOTION_DATABASE_ID;

// Notion API ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
const notion = new Client({ auth: NOTION_TOKEN });
const n2m = new NotionToMarkdown({ notionClient: notion });

2๏ธโƒฃ Notion ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๊ฒŒ์‹œ๊ธ€ ์ฟผ๋ฆฌ

// Published = true์ธ ๊ธ€๋งŒ ๊ฐ€์ ธ์˜ค๊ณ , Date ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ
const queryOptions = {
  database_id: NOTION_DATABASE_ID,
  filter: {
    property: 'Published',
    checkbox: { equals: true },
  },
  sorts: [
    {
      property: 'Date',
      direction: 'descending',
    },
  ],
};

const response = await fetch(
  `https://api.notion.com/v1/databases/${NOTION_DATABASE_ID}/query`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${NOTION_TOKEN}`,
      'Notion-Version': '2022-06-28',
    },
    body: JSON.stringify(queryOptions),
  }
);

3๏ธโƒฃ Notion ์†์„ฑ๊ฐ’ ์ถ”์ถœ

function getPropertyValue(page, propertyName) {
  const property = page.properties[propertyName];

  switch (property.type) {
    case 'title':
      return property.title.map((t) => t.plain_text).join('');
    case 'multi_select':
      return property.multi_select.map((s) => s.name);
    case 'date':
      return property.date?.start || null;
    case 'checkbox':
      return property.checkbox;
    case 'files':
      return property.files.map((f) => f.file?.url || f.external?.url);
    default:
      return null;
  }
}

4๏ธโƒฃ Notion ํŽ˜์ด์ง€๋ฅผ ๋งˆํฌ๋‹ค์šด์œผ๋กœ ๋ณ€ํ™˜

const mdBlocks = await n2m.pageToMarkdown(page.id);
const mdString = n2m.toMarkdownString(mdBlocks);
const markdownContent = mdString.parent || mdString || '';

5๏ธโƒฃ ์ด๋ฏธ์ง€ ์ž๋™ ๋‹ค์šด๋กœ๋“œ ๋ฐ ๊ฒฝ๋กœ ์น˜ํ™˜

async function downloadAndReplaceImages(content, slug, imagesDir) {
  // Notion S3 URL ์ฐพ๊ธฐ
  const imageUrls = extractAllImageUrls(content);
  const notionImages = imageUrls.filter((img) => isNotionS3Url(img.url));

  // ๊ฐ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ
  for (let i = 0; i < notionImages.length; i++) {
    const fileName = generateImageFileName(url, i); // MD5 ๊ธฐ๋ฐ˜ ํ•ด์‹œ
    const localPath = join(postImageDir, fileName);
    const publicPath = `/images/blog/${slug}/${fileName}`;

    // ์›๋ณธ URL โ†’ ๋กœ์ปฌ ๊ฒฝ๋กœ๋กœ ์น˜ํ™˜
    // ![image](https://prod-files-secure.s3...) โ†’ ![image](/images/blog/slug/image-1-abc123.png)
    updatedContent = updatedContent.replace(
      fullMatch,
      `![${alt}](${publicPath})`
    );
  }
}

6๏ธโƒฃ MDX ํ”„๋ก ํŠธ๋งคํ„ฐ ์ƒ์„ฑ ๋ฐ ํŒŒ์ผ ์ €์žฅ

const frontmatterLines = [
  `title: '${title}'`,
  `date: '${formatDate(date)}'`,
  `tags: ${JSON.stringify(tags)}`,
  `draft: ${draft}`,
  `summary: '${summary}'`,
  `images: ${JSON.stringify([downloadedImage])}`,
  `authors: ${JSON.stringify(authors)}`,
];

const mdxContent = `---\n${frontmatterLines.join('\n')}\n---\n\n${processedContent}`;
writeFileSync(`/src/content/blog/${slug}.mdx`, mdxContent);

  1. MDX โ†’ Notion ์—…๋กœ๋“œ (upload-to-notion.mjs)

1๏ธโƒฃ ๊ธฐ์กด Notion ํŽ˜์ด์ง€ ๋ชฉ๋ก ์กฐํšŒ

const existingPagesResponse = await fetch(
  `https://api.notion.com/v1/databases/${NOTION_DATABASE_ID}/query`,
  {
    method: 'POST',
    body: JSON.stringify({ page_size: 100 }),
  }
);

const existingPages = await existingPagesResponse.json();
const existingTitles = new Set();

// ๊ธฐ์กด ์ œ๋ชฉ์„ Set์— ์ €์žฅ (์ค‘๋ณต ์ฒดํฌ์šฉ)
for (const page of existingPages.results) {
  const pageDetails = await notion.pages.retrieve({ page_id: page.id });
  const title =
    pageDetails.properties?.Title?.title?.[0]?.plain_text || '';
  existingTitles.add(title.toLowerCase().trim());
}

2๏ธโƒฃ MDX ํŒŒ์ผ ์ฝ๊ธฐ ๋ฐ ํ”„๋ก ํŠธ๋งคํ„ฐ ํŒŒ์‹ฑ

import matter from 'gray-matter';

const fileContent = readFileSync(file.path, 'utf-8');
const { data: frontmatter, content } = matter(fileContent);

// ์ถ”์ถœ๋˜๋Š” ๋ฐ์ดํ„ฐ:
// frontmatter = { title, date, tags, summary, draft, authors, ... }
// content = ์‹ค์ œ ๋งˆํฌ๋‹ค์šด ๋ณธ๋ฌธ

3๏ธโƒฃ ๋งˆํฌ๋‹ค์šด์„ Notion ๋ธ”๋ก์œผ๋กœ ๋ณ€ํ™˜

function markdownToNotionBlocks(markdown) {
  const blocks = [];
  const lines = markdown.split('\n');
  let inCodeBlock = false;
  let codeLanguage = 'plain text';
  let codeContent = [];

  for (const line of lines) {
    if (line.startsWith('```')) {
      // ์ฝ”๋“œ๋ธ”๋ก ์‹œ์ž‘/์ข…๋ฃŒ
      if (inCodeBlock) {
        blocks.push({
          object: 'block',
          type: 'code',
          code: {
            rich_text: [
              { type: 'text', text: { content: codeContent.join('\n') } },
            ],
            language: normalizeLanguage(codeLanguage),
          },
        });
        inCodeBlock = false;
        codeContent = [];
      } else {
        codeLanguage = line.substring(3).trim();
        inCodeBlock = true;
      }
    } else if (inCodeBlock) {
      codeContent.push(line);
    } else if (line.startsWith('# ')) {
      // H1 โ†’ heading_1
      blocks.push({
        object: 'block',
        type: 'heading_1',
        heading_1: {
          rich_text: [
            { type: 'text', text: { content: line.substring(2) } },
          ],
        },
      });
    } else if (line.startsWith('- ') || line.startsWith('* ')) {
      // ๋ฆฌ์ŠคํŠธ โ†’ bulleted_list_item
      blocks.push({
        object: 'block',
        type: 'bulleted_list_item',
        bulleted_list_item: {
          rich_text: [
            { type: 'text', text: { content: line.substring(2) } },
          ],
        },
      });
    } else {
      // ์ผ๋ฐ˜ ํ…์ŠคํŠธ โ†’ paragraph
      blocks.push({
        object: 'block',
        type: 'paragraph',
        paragraph: {
          rich_text: [{ type: 'text', text: { content: line } }],
        },
      });
    }
  }

  return blocks;
}

4๏ธโƒฃ Notion ํŽ˜์ด์ง€ ์†์„ฑ ๋งคํ•‘

function createPropertyValue(type, value) {
  switch (type) {
    case 'title':
      return { title: [{ text: { content: value } }] };
    case 'date':
      return { date: { start: value } };
    case 'multi_select':
      return { multi_select: value.map((item) => ({ name: item })) };
    case 'checkbox':
      return { checkbox: value || false };
    case 'rich_text':
      return { rich_text: [{ text: { content: value || '' } }] };
  }
}

// ํ”„๋ก ํŠธ๋งคํ„ฐ ๋ฐ์ดํ„ฐ โ†’ Notion ์†์„ฑ์œผ๋กœ ๋ณ€ํ™˜
const properties = {};
properties.Title = createPropertyValue('title', frontmatter.title);
properties.Date = createPropertyValue('date', frontmatter.date);
properties.Tags = createPropertyValue('multi_select', frontmatter.tags);
properties.Draft = createPropertyValue('checkbox', frontmatter.draft);
properties.Authors = createPropertyValue('multi_select', frontmatter.authors);

5๏ธโƒฃ Notion ํŽ˜์ด์ง€ ์ƒ์„ฑ (๋ธ”๋ก์€ 100๊ฐœ์”ฉ ๋ถ„ํ• )

// 100๊ฐœ์”ฉ ์ฒญํฌ๋กœ ๋‚˜๋ˆ„๊ธฐ
const maxBlocksPerRequest = 100;
const blockChunks = [];
for (let i = 0; i < allBlocks.length; i += maxBlocksPerRequest) {
  blockChunks.push(allBlocks.slice(i, i + maxBlocksPerRequest));
}

// ์ฒซ๋ฒˆ์งธ ์ฒญํฌ๋กœ ํŽ˜์ด์ง€ ์ƒ์„ฑ
const response = await fetch('https://api.notion.com/v1/pages', {
  method: 'POST',
  headers: { Authorization: `Bearer ${NOTION_TOKEN}` },
  body: JSON.stringify({
    parent: { database_id: NOTION_DATABASE_ID },
    properties: properties,
    children: blockChunks[0], // ์ฒซ 100๊ฐœ ๋ธ”๋ก
  }),
});

const page = await response.json();
const pageId = page.id;

// ๋‚˜๋จธ์ง€ ์ฒญํฌ๋ฅผ ํŽ˜์ด์ง€์— ์ถ”๊ฐ€
for (let i = 1; i < blockChunks.length; i++) {
  await fetch(`https://api.notion.com/v1/blocks/${pageId}/children`, {
    method: 'PATCH',
    body: JSON.stringify({ children: blockChunks[i] }),
  });
}

3. ์ž๋™ ๋นŒ๋“œ ํ†ตํ•ฉ

๋นŒ๋“œํ•  ๋•Œ ์ž๋™์œผ๋กœ Notion ๋™๊ธฐํ™”๋ฅผ ๋จผ์ € ์‹คํ–‰ํ•˜๋ฏ€๋กœ, ํ•ญ์ƒ ์ตœ์‹  ๊ธ€๋กœ ๋ฐฐํฌ๋ฉ๋‹ˆ๋‹ค.

package.json์˜ build ์Šคํฌ๋ฆฝํŠธ

{
  "scripts": {
    "dev": "astro dev",
    "sync-notion": "node ./scripts/sync-notion.mjs",
    "upload-to-notion": "node ./scripts/upload-to-notion.mjs",
    "build": "npm run sync-notion && astro check && astro build"
  }
}

ํ™˜๊ฒฝ ๋ณ€์ˆ˜

# .env.local ํŒŒ์ผ ์ƒ์„ฑ
NOTION_TOKEN=ntn_your_token_here
NOTION_DATABASE_ID=your_database_id

Vercel
ํ”„๋กœ์ ํŠธ ์„ค์ • โ†’ Environment Variables์—์„œ NOTION_TOKEN๊ณผ NOTION_DATABASE_ID๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.


์ฒซ ๋ฒˆ์งธ ์ด์Šˆ: ์ฝ”๋“œ ๋ธ”๋ก ์–ธ์–ด ํ˜•์‹

MDX ํŒŒ์ผ์˜ ์ฝ”๋“œ ๋ธ”๋ก์„ Notion์œผ๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์–ธ์–ด ์ฝ”๋“œ๊ฐ€ Notion์ด ์ง€์›ํ•˜๋Š” ํ˜•์‹๊ณผ ๋งž์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด:

  • js โ†’ javascript๋กœ ๋ณ€ํ™˜ ํ•„์š”
  • ts:file.ts โ†’ ํŒŒ์ผ๋ช… ์ œ๊ฑฐํ•˜๊ณ  typescript๋กœ ๋ณ€ํ™˜
  • tex โ†’ latex๋กœ ๋ณ€ํ™˜
  • ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์–ธ์–ด โ†’ plain text๋กœ ํด๋ฐฑ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

์–ธ์–ด ์ฝ”๋“œ๋ฅผ ์ •๊ทœํ™”ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค:

function normalizeLanguage(lang) {
  if (!lang) return 'plain text'
  
  // ํŒŒ์ผ๋ช… ํ˜•์‹ ์ œ๊ฑฐ
  lang = lang.split(':')[0].split('.')[0].toLowerCase().trim()
  
  // ์–ธ์–ด ๋งคํ•‘
  const languageMap = {
    'js': 'javascript',
    'ts': 'typescript',
    'py': 'python',
    'tex': 'latex',
    // ... ๋” ๋งŽ์€ ๋งคํ•‘
  }
  
  const normalized = languageMap[lang] || lang
  
  // Notion์ด ์ง€์›ํ•˜๋Š” ์–ธ์–ด์ธ์ง€ ํ™•์ธ
  const supportedLanguages = ['javascript', 'typescript', 'python', ...]
  
  return supportedLanguages.includes(normalized) ? normalized : 'plain text'
}

๋‘ ๋ฒˆ์งธ ์ด์Šˆ: ๋ธ”๋ก ๊ฐœ์ˆ˜ ์ œํ•œ

Notion API๋Š” ํ•œ ๋ฒˆ์— ์ตœ๋Œ€ 100๊ฐœ์˜ ๋ธ”๋ก๋งŒ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ธด ๊ธ€์˜ ๊ฒฝ์šฐ ์ด ์ œํ•œ์„ ์ดˆ๊ณผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ธ”๋ก์„ 100๊ฐœ์”ฉ ๋‚˜๋ˆ ์„œ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค:

const maxBlocksPerRequest = 100
const blockChunks = []
for (let i = 0; i < allBlocks.length; i += maxBlocksPerRequest) {
  blockChunks.push(allBlocks.slice(i, i + maxBlocksPerRequest))
}

// ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ๋Š” ํŽ˜์ด์ง€ ์ƒ์„ฑ ์‹œ
const response = await fetch(`https://api.notion.com/v1/pages`, {
  // ... ์ฒซ ๋ฒˆ์งธ ์ฒญํฌ ํฌํ•จ
})

// ๋‚˜๋จธ์ง€ ์ฒญํฌ๋Š” append_block_children API๋กœ ์ถ”๊ฐ€
for (let i = 1; i < blockChunks.length; i++) {
  await fetch(`https://api.notion.com/v1/blocks/${pageId}/children`, {
    method: 'PATCH',
    // ... ๋‚˜๋จธ์ง€ ์ฒญํฌ ์ถ”๊ฐ€
  })
}

์„ธ ๋ฒˆ์งธ ์ด์Šˆ: ์ค‘๋ณต ์—…๋กœ๋“œ ๋ฐฉ์ง€

๊ธฐ์กด MDX ํŒŒ์ผ์„ Notion์œผ๋กœ ์—…๋กœ๋“œํ•  ๋•Œ, ๊ฐ™์€ ์ œ๋ชฉ์˜ ํฌ์ŠคํŠธ๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•˜๋ฉด ์ค‘๋ณต์œผ๋กœ ์ƒ์„ฑ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

์—…๋กœ๋“œ ์ „์— ๊ธฐ์กด ํฌ์ŠคํŠธ์˜ ์ œ๋ชฉ์„ ๋ฏธ๋ฆฌ ์กฐํšŒํ•ด์„œ ์ค‘๋ณต์„ ์ฒดํฌํ–ˆ์Šต๋‹ˆ๋‹ค:

// ๊ธฐ์กด ํฌ์ŠคํŠธ ์ œ๋ชฉ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
const existingPagesResponse = await fetch(/* ... */)
const existingTitles = new Set()

for (const page of existingPages.results) {
  const title = page.properties?.Title?.title?.[0]?.plain_text || ''
  if (title) {
    existingTitles.add(title.toLowerCase().trim())
  }
}

// ์—…๋กœ๋“œ ์ „ ์ค‘๋ณต ์ฒดํฌ
if (existingTitles.has(titleKey)) {
  console.log(`โญ๏ธ  "${title}" ์ œ๋ชฉ์ด ์ด๋ฏธ ์กด์žฌํ•˜์—ฌ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.`)
  continue
}