- Published on
블로그 개발일지 2
- Authors
- Name
- JaeHyeok CHOI
- none
블로그 템플릿 선정
우선 블로그의 템플릿은 NextJS 공식 사이트에서 제공해주는 tailwind-nextjs-starter-blog
로 하기로 정했습니다.
무엇보다 가장 최신인 NextJS 15를 사용하고 있었고, SEO 등의 블로그에 필요한 일부 기능들을 모두 제공하며 가장 깔끔한 디자인이라 선택하게 되었습니다.
메타 데이터 수정
우선 나만의 블로그로 만들기 위해서 블로그의 메타 데이터들을 수정해야 했습니다.
수정해야 할 목록
- Personalize siteMetadata.js (site related information)
- Modify the content security policy in next.config.js if you want to use other analytics provider or a commenting solution other than giscus.
- Personalize authors/default.md (main author)
- Modify projectsData.ts
- Modify headerNavLinks.ts to customize navigation links
README.md 에서 제공하는 수정할 목록들입니다. 이 목록에 해당하는 내용들을 제 이메일과 깃허브 주소 등으로 수정해줬습니다.
기능 구현
본격적으로 기능 구현에 들어갔습니다.
우선 첫 번째로, 블로그 안에서 직접 포스팅 기능을 구현하려고 합니다.
우선 블로그에 포스팅할 내용은 마크다운으로 작성하여 배포할 예정입니다.
이 프로젝트의 데이터를 보면 data/blog
폴더에 mdx
파일로 블로그의 포스팅이 저장되어 올라갑니다. 따라서 저는 제 블로그 사이트에서 직접 수정하고 바로 배포할 수 있도록 할 것입니다.
1. NextJS App Router
페이지를 새로 만들어야 하기 때문에 app/posting
에 page.tsx 파일을 만들어 줬습니다.
이 페이지에서 블로그 포스트를 작성하고 완성된 포스트를 Next서버에 저장할 것 입니다.
2. 마크다운 에디터
- MDX Editor
- Toast UI Editor
- react-markdown-editor
이 정도를 많이 사용하는 것으로 보입니다. react-md-editor와 Toast UI 에디터를 고민하다가, Toast UI 에디터가 NHN에서 만든 툴 이라기에 Toast UI Editor를 사용하기로 했습니다. 그래서인지 이를 다룬 블로그들이 정말 많았고, 덕분에 쉽게 이 툴을 사용할 수 있었습니다.
위 블로그를 참고하고 만들어주었습니다. 제니 님의 블로그 내용처럼 사용할 에디터는 컴포넌트화 시켜서 따로 만들어 주며 커스텀 훅을 만들어 이미지 삽입할 때 커스텀 훅을 먼저 전송하여 서버측의 사진 주소를 가져오도록 했습니다.
3. 이미지 저장하기
우선 클라이언트 측에서 이미지를 전송할 때에, formData를 사용해서 이미지에 대한 정보를 전송해야합니다. 또한, 이미지를 각 포스트 별로 관리해주기 위해서 해당 API를 요청할 때에 블로그에 대한 제목을 전송하도록 해야했습니다.
// 클라이언트 측 코드
editorRef.current?.getInstance().addHook('addImageBlobHook', async (blob, callback) => {
const formData = new FormData()
formData.append('file', blob)
formData.append('title', title)
try {
const response = await fetch('api/upload', {
method: 'POST',
// headers: { 'Content-Type': 'multipart/form-data' },
body: formData,
})
if (!response.ok) {
throw new Error('Failed to upload image')
}
const data = await response.json()
callback(data.imageUrl)
// return data.imageUrl
} catch (error) {
console.error('Image upload failed:', error)
throw error
}
})
editorRef는 Toast UI 컴포넌트에 대한 레퍼런스 훅 이고, formData를 만들어 줄 때 file 뿐 아니라 title에 대한 정보도 같이 주었습니다.
그리고 서버에서는 이 API 요청을 받아서 처리하도록 GPT의 도움을 받아서 작성했습니다.
// app/api/upload/route.ts
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const imageUrl = await saveFile(formData)
return NextResponse.json({ success: true, imageUrl }, { status: 200 })
} catch (error) {
console.error(error)
return NextResponse.json({ success: false, error: 'Failed to upload file' }, { status: 500 })
}
}
//app/controllers/upload.controller.ts
// app/api/upload/route.ts
import { writeFile, mkdir } from 'fs/promises'
import path from 'path'
import { existsSync } from 'fs'
// 파일 저장 경로 생성 함수
export async function getUniqueFilename(folderPath: string, originalName: string): Promise<string> {
let filename = originalName
let counter = 1
while (existsSync(path.join(folderPath, filename))) {
const ext = path.extname(originalName)
const nameWithoutExt = path.basename(originalName, ext)
filename = `${nameWithoutExt}-${counter}${ext}`
counter += 1
}
return filename
}
export async function saveFile(formData: FormData) {
try {
const file = formData.get('file') as File
const title = formData.get('title') as string
if (!file || !title) {
throw new Error('File and title are required')
}
// 기본 저장 경로 설정
const directory = path.join(process.cwd(), process.env.INTERNAL_DIRECTORY || '', title)
// 디렉토리가 없으면 생성
if (!existsSync(directory)) {
await mkdir(directory, { recursive: true })
}
// 고유한 파일명 생성
const filename = await getUniqueFilename(directory, file.name)
// 파일을 버퍼로 변환
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
// 파일 저장
const filePath = path.join(directory, filename)
await writeFile(filePath, buffer)
// URL 생성 및 반환
return `/static/images/${title}/${filename}`
} catch (error) {
console.error('Error saving file:', error)
throw error
}
}
getUniqueFilename
은 요청들어온 파일이 중복된 이름으로 들어올 경우, 숫자를 붙여서 중복을 피하도록 하였습니다. (작성하면서 느끼지만 완전히 똑같은 파일을 두 번 올릴 경우에는 요청을 무시하도록 만드는게 좋겠습니다.)
서버에 요청이 들어오게 되면 파일과 포스팅 제목을 검사한 후 고유한 파일과 디렉토리를 만들어주고 파일을 저장하고 해당 이미지의 주소를 반환해 줍니다.
이로써 파일을 자동으로 서버에 저장하는 기능을 만들었습니다. 추후에 포스팅을 수정하는 기능과 Toast UI 에서 Ctrl+z 사용, 자동 저장 기능을 추가해야 겠습니다... 배포하기 전에 우선 로컬에서 동작 시키고 있었는데, 갑자기 파일을 수정하는 바람에 작성했던 글이 모두 사라져버렸습니다...ㅠ 그리고 글을 작성하면서 이미지를 첨부해보았는데, 이미지가 보이지 않는 오류가 있더라구요. 아무래도 포스팅의 제목대로 디렉터리를 만들어주면서 생기는 공백 때문에 그 경로를 url로 받아오는 과정에서 생기는 문제 같습니다.
해야할 일
- 이미지 저장 경로 문제
언젠가 할 일
- 블로그 수정 기능
- Ctrl+Z
- 자동 저장 기능...