현재 AWS 프리티어 기간이 얼마 남지 않아 티스토리로 이전을 준비 중이지만, 직접 블로그를 만들어 보고 싶어서 Next.js와 Notion API를 활용해 AWS EC2에 배포한 개인 블로그를 구현한 경험을 정리했습니다.
📌 프로젝트 깃허브, 배포 링크
현재 운영 중입니다. 한 번 구경해 보세요!
- 개인 블로그 주소 : https://minsunblog.com/
- 프로젝트 깃허브 : https://github.com/pminsun/MinsunBlog
Q. 프로젝트 소개
이 프로젝트는 기술 블로그를 운영하고 싶다는 마음에서 시작했습니다. 고민 끝에 Notion API를 알게 되었고, 이를 Next.js와 AWS EC2를 활용해 블로그로 구현하게 되었습니다.
구현 과정에서 여러 챌린지와 이슈가 있었습니다:
1. Notion API CORS 에러
2. 100개 초과 포스트 처리 문제
3. 글 유형 및 스타일 적용
4. 코드 블록 하이라이트
5. 이미지 스켈레톤 처리
6. AWS EC2 배포
Q. Notion API 활용 및 개발 이슈 사항
1. Notion API CORS 에러
Notion API는 CORS를 지원하지 않아 브라우저에서 직접 호출할 수 없었습니다. 이를 해결하기 위해, 서버 측에서 API 요청을 처리한 후 클라이언트로 전달하는 방식을 채택했습니다. Next.js 13의 서버 사이드 렌더링(SSR)을 활용해 해결했습니다.
관련 글 : https://minsun309.tistory.com/entry/CSR-SSR-CORS-%EC%97%90%EB%9F%AC
2. 100개 초과 포스트 처리
Notion API는 한 번에 100개 이하의 포스트만 반환합니다. 100개를 넘는 포스트를 처리하려면 next_cursor 값을 사용해 추가 포스트를 가져와야 합니다.
포스트 맨에서 리스트 api를 자세히 보니 100개 이하일 경우 에는 has_more : false, next_cursor: null이지만 100개를 초과하면 has_more: true, next_cursor: "{고유 id}"에 값이 부여되어 있었습니다.
next_cursor를 참조해서 start_cursor를 입력시키면, 기존 0 ~ 100 번째 결과는 그대로 두고 101번째부터 그 이후의 결과를 가져올 수 있습니다.
관련 글 : https://minsun309.tistory.com/entry/Notion-api-100%EA%B0%9C-%EC%B4%88%EA%B3%BC%EC%9D%BC-%EB%95%8C
3. 글 유형 및 스타일 적용
Notion에서 만약 글 크기, 굵기등 스타일을 줄 경우 포스팅 상세 페이지에서 어떻게 적용할 수 있는지 고민이 되었습니다.
응답 객체에서 텍스트 스타일 정보를 추출해, bold, italic 등 다양한 스타일을 적용했습니다. 스타일을 적용하기 위해 switch 문을 사용해 각 블록 유형을 처리했고, 각 스타일을 적절히 렌더링 했습니다.
응답 코드 일부
"paragraph": {
"rich_text": [
{
"type": "text",
"text": {
"content": "전송되는 데이터",
"link": null
},
"annotations": {
"bold": true,
"italic": false,
"strikethrough": false,
"underline": false,
"code": false,
"color": "default"
},
"plain_text": "전송되는 데이터",
"href": null
}
//... 다른 항목들
],
"color": "default"
}
paragraph 기준으로 보면 plain_text가 작성 된 문장이 들어가 있고 해당 문장에 대한 스타일은 annotations 객체 안에 boolean 또는 string으로 값이 들어있습니다. 그래서 위와 같이 "전송되는 데이터" 부분에는 annotations의 bold 값이 true이니 적절한 굴기를 적용하면 됩니다. 해당 부분을 위해 아래와 같이 코드를 작성했습니다.
- switch 문을 사용해 blockContent.type 이 'paragraph'일 경우 richTextContent 배열을 p 태그로 감싸서 렌더링합니다.
- textContent(=rich_text)라는 배열을 map 함수로 순회하면서 각 텍스트 조각(txtPiece)을 처리합니다.
- 각 txtPiece에서 content와 annotations (텍스트의 스타일 정보를 담고 있음)을 추출합니다.
- textAnnotations에서 추출한 정보로 스타일을 적용합니다. 각각의 스타일은 getStyle 함수와 colorStyle 함수를 사용해 결정됩니다.
- filter(Boolean)은 스타일 리스트에서 유효하지 않은 값(예: undefined 또는 null)을 제거하는 역할을 합니다.
- 텍스트가 링크일 경우 Link 컴포넌트를 사용하고, 일반 텍스트는 span으로 렌더링 됩니다.
export const stylesMap: notionText = {
bold: 'fontbold-style',
italic: 'italic',
strikethrough: 'line-through',
underline: 'underline underline-offset-4',
code: 'bg-[#f6f4ef] dark:bg-[#122c42] text-[#eb5757] px-1 rounded',
}
export default function PostDetailContent({ blockContent }: BlockContentType) {
// 스타일 매핑
const getStyle = (styleName: string, value: string | boolean) =>value ? [stylesMap[styleName]] : []
const colorStyle = (color: string) => (color !== 'default' ? [notionTxtColor(color)] : [])
const textContent = blockContent[blockContent.type]?.rich_text || null
const richTextContent = textContent?.map((txtPiece: RichText, index: number) => {
const { content } = txtPiece?.text || {};
const textAnnotations = txtPiece?.annotations || {};
const textLink = txtPiece?.text?.link;
const textStyles = [
...getStyle('bold', textAnnotations.bold),
...getStyle('italic', textAnnotations.italic),
...getStyle('strikethrough', textAnnotations.strikethrough),
...getStyle('underline', textAnnotations.underline),
...getStyle('code', textAnnotations.code),
...colorStyle(textAnnotations.color),
]
.filter(Boolean)
.join(' ');
return textLink ? (
<Link key={index} href={content} target="_blank" className="link-style">
{content}
</Link>
) : (
<span
key={index}
className={cls(
'detail-paragraph',
textStyles,
content === '참고' ? 'link-none-style' : ''
)}
>
{content}
</span>
);
});
// 각 블록 유형에 따라 내용을 렌더링
switch (blockContent.type) {
case 'paragraph':
return (
<>
{richTextContent.length > 0 ? (
<p key={blockContent.id} className={cls('paragraph-style', ...paragraphColor)}>
{richTextContent}
</p>
) : (
<p className="py-3" />
)}
</>
);
// 다른 블록 유형 처리...
default:
return null;
}
}
4. 코드 블록 하이라이트
blockContent 타입 (=객체 안 type 값)이 'code' 일 경우 타 블로그 플랫폼처럼 코드문 스타일을 적용하고 싶었습니다.
처음에는 highlight.js를 사용했으나 React 환경에서는 적절하지 않아, react-highlight 라이브러리를 사용했습니다. 이 라이브러리는 highlight.js를 기반으로 하지만 React에 더 자연스럽게 통합됩니다.
react-highlight
내부적으로 highlight.js를 사용하지만, React 환경에서 쉽게 통합하고 사용하기 위한 래퍼(wrapper)입니다. React 컴포넌트 방식으로 사용됩니다. JSX 내부에서 코드 블록을 쉽게 하이라이트 할 수 있게 해 주며, React의 상태 관리나 컴포넌트 구조와 자연스럽게 어울립니다.
관련 글 : https://minsun309.tistory.com/entry/react-highlight
적용 코드
case 'code':
const codeTxt = blockContent.code?.rich_text[0]?.text?.content;
const codeLag = blockContent.code?.language;
return (
<pre key={blockContent.id} className="code-style">
{codeLag === 'javascript' ? (
<Highlight className="javascript">{codeTxt}</Highlight>
) : codeLag === 'css' ? (
<Highlight className="css">{codeTxt}</Highlight>
) : (
<Highlight className="html">{codeTxt}</Highlight>
)}
</pre>
);
5. 이미지 스켈레톤 처리
포스팅 리스트 페이지에서 이미지가 텍스트에 비해 다소 느린 경향이 있어 이미지 영역 스켈레톤을 적용해 보았습니다.
Next.js <Image /> 태그의 blurDataUrl 속성을 사용하면 스켈레톤처리가 가능합니다.
관련 글 : https://minsun309.tistory.com/entry/Nextjs-Image-blurDataURL
6. AWS EC2 배포
이 프로젝트에서 기술 블로그 구현 및 작성도 목표였지만 AWS EC2 배포 또한 주요 목적 중 하나였습니다.
배포 과정에서 발생한 문제는 주로 이미지 용량과 관련이 있었습니다. 노션의 이미지 데이터를 모두 불러오는 방식이었으나, 용량이 너무 커 배포가 어려웠습니다. 이를 해결하기 위해 이미지 주소를 content 일부로 처리해 AWS EC2 배포에 성공했습니다.
관련 글
- https://minsun309.tistory.com/entry/Nextjs-Amazon-EC2-%EB%B0%B0%ED%8F%AC1
- https://minsun309.tistory.com/entry/Nextjs-Amazon-EC2-%EB%B0%B0%ED%8F%AC2
Q. 마지막 후기
이번 프로젝트를 통해 블로그라는 서비스에는 다양한 세부 기능이 필요하다는 점을 체감했습니다. Notion API와의 통합 및 AWS EC2 배포 과정은 특히 도전적이었지만, 이를 해결하면서 많은 것을 배울 수 있었습니다. 지금은 티스토리로 이전 중이지만, 이 프로젝트는 저에게 의미 있는 경험이었습니다.
'Project' 카테고리의 다른 글
또랑또랑, AI 발표 준비 도우미 :: 407 포텐데이X클로바 스튜디오 참여 후기 (0) | 2024.08.27 |
---|