Notion html 내보내기 시, details Tag의 open attribute 지우는 방법

박진현·2024년 3월 4일
1
post-custom-banner

현재 다니고 있는 회사에서 PM/PD 분들이 Notion을 활용하여 사용자 가이드와 업데이트 노트를 관리하고 계시는데, 이 노션문서들을 html로 내보내기 후 S3에 배포하여 웹사이트에서 보여질 수 있도록 하고 있다. (내보내기한 tree구조의 파일 용량은 80mb에 달한다.)

문제는 S3에 배포된 문서의 <details> 태그가 모두 열려 있다는 것이다. 분명히 닫아놓은 상태로 문서수정을 마쳤는데 노션의 "html로 내보내기" 기능을 사용하면 모든 디테일태그가 열려 있는 것이었다.
(노션의 내보내기 기능은 <details> 태그를 모두 열어 놓는구나...PDF때문에 그런건가?)

결국 어느 날, 디테일 태그를 모두 닫아달라는 요구사항이 들어왔다.

배포할 때 마다 80mb에 달하는 tree구조의 html파일들의 <details>태그를 찾아 'open' 속성을 지우고 s3에 업로드 하는 것은 너무 비효율적인 일이었다.

먼저, <details>태그를 찾아 'open' 속성을 지우는 스크립트를 개발해서 매 배포때마다 수동으로 스크립트를 돌리고 나온 결과물을 s3에 업로드 했었다.

배포과정이 복잡해지면 휴먼에러도 발생하고, 신경쓸 것 도 많아지기도 하고, "3번 이상 반복하는 일은 자동화하자" 는 신념이 있는 만큼 이번에도 자동화를 진행했다.

  1. 노션에서 특정 path의 파일을 html로 내보내기 한다.
  2. 내보내기한 Zip파일을 현재 버전의 이름으로 바꾼다. (Ex. 3_5_5.zip)
  3. 특정 S3 버킷의 지정된 경로에 ZIP 파일을 업로드한다.
  4. 자동으로 Lambda 함수가 호출된다. 업로드된 ZIP 파일의 압축을 /tmp 폴더에 해제하고, tmp 폴더에 있는 모든 파일과 하위 폴더를 순회하면서 HTML 파일을 찾아 <details> 태그의 'open' 속성을 제거한다. 그 후, 수정된 파일들을 원본 ZIP 파일의 경로 구조와 동일하게 S3에 업로드한다.
  5. 업로드 했던 원본 ZIP 파일을 S3에서 제거한다.

위 과정을 거치도록 만들었다.

아래처럼 특정 경로에 zip파일이 업로드되면 lambda 함수가 돌도록 트리거를 건다.

트리거되면 아래 lambda 함수의 코드가 돌게된다.

import pkg from 'aws-sdk';
import * as fs from 'fs/promises';
import * as path from 'path';
import extractZip from 'extract-zip';
import cheerio from 'cheerio';
import { pipeline } from 'stream/promises';
import { createWriteStream } from 'fs';

const { S3 } = pkg;
const s3 = new S3();

export const handler = async event => {
  const record = event.Records[0].s3;
  const bucketName = record.bucket.name;
  const objectKey = decodeURIComponent(record.object.key.replace(/\+/g, ' '));
  const zipFileName = objectKey.split('/').pop();
  // zipFilePath를 여기에서 정의하여 함수 전체에서 접근 가능하게 합니다.
  const zipFilePath = path.join('/tmp', zipFileName);
  const tmpFolderPath = path.join('/tmp', zipFileName.replace('.zip', ''));

  const basePath = objectKey.substring(0, objectKey.lastIndexOf('/') + 1); // 예: "static/Test/"

  // ZIP 파일 이름(확장자 제외)을 추가하여 압축 해제된 파일의 기본 경로를 생성합니다.
  const zipBaseName = path.basename(objectKey, '.zip'); // "3_5_4"
  const fullBasePath = `${basePath}${zipBaseName}/`; // "static/Test/3_5_4/"

  try {
    const { Body } = await s3.getObject({ Bucket: bucketName, Key: objectKey }).promise();

    // 스트림을 파일로 쓰기
    if (Body instanceof Buffer) {
      await fs.writeFile(zipFilePath, Body);
    } else {
      await pipeline(Body, createWriteStream(zipFilePath));
    }

    await extractZip(zipFilePath, { dir: tmpFolderPath });
    await processDirectory(tmpFolderPath, bucketName, tmpFolderPath, fullBasePath);

    console.log('All HTML files processed and uploaded successfully.');
  } catch (error) {
    console.error(`Error processing files: ${error}`);
    throw error;
  } finally {
    // 임시 파일 및 폴더 정리
    await fs.rm(tmpFolderPath, { recursive: true, force: true }).catch(console.error);
    await fs.rm(zipFilePath, { force: true }).catch(console.error);
    // 원본 ZIP 파일을 S3에서 삭제
    await s3
      .deleteObject({
        Bucket: bucketName,
        Key: objectKey,
      })
      .promise()
      .then(() => {
        console.log(`Deleted original zip: ${objectKey}`);
      })
      .catch(console.error);
  }
};

async function processDirectory(directory, bucketName, baseFolderPath, fullBasePath) {
  const entries = await fs.readdir(directory, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(directory, entry.name);
    if (entry.isDirectory()) {
      await processDirectory(fullPath, bucketName, baseFolderPath, fullBasePath);
    } else {
      // 모든 파일을 처리하도록 processFile 함수 호출
      await processFile(fullPath, bucketName, baseFolderPath, fullBasePath);
    }
  }
}

async function processFile(filePath, bucketName, baseFolderPath, fullBasePath) {
  let fileContent = await fs.readFile(filePath);

  // 파일이 HTML인 경우 추가 처리
  if (filePath.endsWith('.html')) {
    const $ = cheerio.load(fileContent.toString());
    $('details').removeAttr('open');
    fileContent = Buffer.from($.html());
  }

  // 파일의 상대 경로를 계산합니다.
  const relativePath = path.relative(baseFolderPath, filePath);
  // S3에 업로드할 때 사용할 Key 값을 설정합니다.
  const s3Key = `${fullBasePath}${relativePath}`;

  await s3
    .putObject({
      Bucket: bucketName,
      Key: s3Key,
      Body: fileContent,
      ContentType: getContentTypeByFile(filePath),
    })
    .promise();

  console.log(`Uploaded: ${s3Key}`);
}

// 파일 확장자에 따라 적절한 Content-Type을 반환하는 함수
function getContentTypeByFile(fileName) {
  const extension = path.extname(fileName).toLowerCase();

  switch (extension) {
    case '.html':
      return 'text/html';
    case '.css':
      return 'text/css';
    case '.js':
      return 'application/javascript';
    case '.png':
      return 'image/png';
    case '.jpg':
    case '.jpeg':
      return 'image/jpeg';
    case '.gif':
      return 'image/gif';
    // 추가적인 파일 유형에 대한 처리를 여기에 포함
    default:
      return 'application/octet-stream';
  }
}

이로써 귀찮은 일이 하나 더 줄었다!

profile
👨🏻‍💻 호기심이 많고 에러를 좋아하는 프론트엔드 개발자 박진현 입니다.
post-custom-banner

0개의 댓글