블로그 제작 프로젝트
by yuyeol3, 2025-02-11
github에서 react를 활용하여 블로그를 제작하고 싶었다. 하지만 github pages는 static 파일만 제공하기 때문에 동적으로 데이터를 주고받기에는 어려움이 있었다. 그래서 여러 가지 편법(?)을 사용하여 react를 이용한 정적 블로그 사이트를 생성해 보았다.
1. 계획
동적으로 데이터를 조작하는 것은 어려움이 있지만 정적 파일을 레포지토리에 올려놓고 조회하는 것은 가능했기에 다음과 같이 계획을 세웠다.
- 포스트 하기 전 posts의 폴더를 훑고 포스트 목록을 json 형태로 생성
- frontend에서 해당 posts를 조회하여 게시판, 게시글 목록, 검색 기능을 제공
- SEO 문제는 정적 html 파일을 생성, previews 파일에 저장하여 해결
2. React 사용해보기
블로그를 만들 때 처음으로 리액트를 시도해 보았다. 기존에는 자바스크립트를 이용해 직접 dom을 조작하는 방식으로 개발하였다. 그러나 react를 사용하면 정의된 컴포넌트의 state들에 따라 자동으로 리렌더링되고, 가상 돔을 통해 필요한 부분만 렌더링하는 장점이 있다.
1. JSX
function Main() { return ( <> <p className="main-message">Hello, World!</p> </> ); }
jsx는 자바스크립트 문법을 그대로 사용하지만 html처럼 마크업 문법도 사용할 수 있는 것이 특징이다. 위의 예시처럼 괄호 안에 마크업 문법을 사용하여 컴포넌트를 구성할 수 있다. html과 대체로 비슷하지만 class를 지정하는 경우는 className으로 지정해 줘야 하고, 엘리먼트 리스트를 만들 경우 key를 지정해 주어야 하는 등의 차이점이 있다.
2. Hooks
리액트를 시도하면서 가장 낯설었던 것은 Hook을 사용해 상태(state)들을 관리하는 것이었다. 따라서 주로 사용하는 훅 세 가지를 정리해 보았다.
- State Hooks
- 컴포넌트에 상태 정보를 내장
- state가 바뀌면 컴포넌트는 리렌더링됨
function ImageGallery() { const [index, setIndex] = useState(0); // ...
- Effect Hooks
- 컴포넌트를 외부 시스템과 연결
- 컴포넌트가 렌더링 될 때 지정된 state의 변화를 감지한다.
- 지정된 state가 변화면 정의된 효과를 일으킨다.
function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); // ...
- Ref Hooks
- 렌더링에 사용되지 않는 정보를 보유
- 상태의 변화는 감지하지만 리렌더링을 일으키지 않음
- 특정 컴포넌트 인스턴스의 참조 등을 저장할 때 유용
- 렌더링에 사용되지 않는 정보를 보유
function Form() { const inputRef = useRef(null); // ...
출처 : React Hooks
3. posts 목록 생성
function buildPostsJson() { const files = getMarkdownFiles(postsDir); // 각 파일에 대해 메타 데이터 추출 const posts = files.map(extractPostData); // 필요한 경우 폴더별 그룹화 const grouped = {}; posts.forEach(post => { // 예시로 posts/ 이후의 첫 폴더명을 카테고리로 사용 const parts = post.path.split('/'); const category = parts.length > 2 ? parts[1] : 'default'; if (!grouped[category]) { grouped[category] = []; } grouped[category].push(post); }); // 그룹별로 최근에 작성된 글 순으로 정렬 for (const category in grouped) { grouped[category].sort((a, b) => new Date(b.date) - new Date(a.date)); } // 정적 html, sitemap 생성성 renderStaticHtml(grouped); createSiteMap(grouped); // 그룹 설정 추가 grouped.__groupSetting = groupSetting; classifyCategories(grouped); // JSON으로 저장 (필요에 따라 grouped 혹은 posts 배열을 저장) fs.writeFileSync(outputFile, JSON.stringify(grouped, null, 2), 'utf-8'); console.log(`Posts JSON saved to ${outputFile}`); }
build.js를 실행하면 위 사진과 같이 json 형태로 posts 폴더 내의 마크다운 문서가 폴더 이름에 따라 카테고리에 맞게 정리된다. build.groupsetting.js 에서는 그룹별로 묶을 카테고리를 지정할 수 있다.
프론트엔드에서 이렇게 생성된 posts.html 파일에 대한 ajax 요청을 보내 게시글 목록을 동기화한다.
4. previews 파일 생성
function renderStaticHtml(posts) { const htmlTemplate = (title, content, path, description)=> ` <html lang="ko"> <head> <title>${title}</title> <link rel="canonical" href="http://${hostName}/"/> <meta charset="utf-8"/> <meta name="description" content="${description}"> <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> </head> <body style="display : none;"> ${marked.parse(content)} <script> location.href = "/post-view?href=${path}" </script> </body> `; // console.log(posts); for (const category of Object.keys(posts)) { const postList = posts[category]; for (const post of postList) { const content = fs.readFileSync(__dirname + "/" + post.pathList.join("/")) fs.writeFileSync("./previews/" + post.pathList.join(".") + ".html", htmlTemplate(post.title, content.toString('utf-8'), post.pathList.join("/"), post.preview), 'utf-8'); } } }
react를 사용하여 프론트엔드를 구성하면 페이지가 완전히 로드된 뒤의 사용자 경험은 좋지만, 검색 엔진에 대응할 때는 문제가 있다. 자바스크립트가 로드된 뒤에 페이지가 렌더링되기 때문에 검색 엔진의 봇이 단순히 페이지 원본만 스크랩한 경우 내용을 알 수 없다. 따라서 미리 렌더링된 previews 파일을 생성하여 페이지 공유나 sitemap 목록에 사용하였다.
댓글 불러오는 중...