uniQ 개발 노트
Dev note는 정식 회고록이 아닌 draft 입니다.
📆 25-05-20
기획
내용 보기
📌 Opened Issues
📌 프로젝트 기획
티스토리 -> 네이버 -> velog 로 유목민 생활을 해본 결과, 각자 하나씩은 아쉬움이 있어 그냥 자체 블로그 프레임워크를 만들기로 했다.
FE 지식이 많진 않은데 당장 목표하는 기본 기능만 구상해서 맨땅에 헤딩하려 한다.
우선 다음의 원칙은 지켜야 한다.
기능 측면
- 글 작성이 빠르고 쉬우면서 결과물이 이쁘게 나올 것
- 보호, 비공개 글 기능이 있을 것
관리 측면
- 기본 언어는 영어 (나중에 i18n으로 한국어 넣음)
- 주석 포함 모든 문서화는 영어로 작성되어야 함
따라서 JAMstack 기반의 정적 페이지는 사실상 불가능하고, 애초에 정적 페이지로 블로그 운영할거였으면 기존에 널린거 주워다 썼을 것이다.
보호/비공개 기능 때문에 글 원본은 접근이 제한되는 영역에 있어야 하고, 이는 self-host 또는 private repo 형식으로 관리되는 방식이 될 것 같다.
📌 기술 스택
- [FE] React.js
- [BE] Node.js / Express
- [DB] MongoDB
검색은 algolia로 고민중이다. ES까진 오버엔지니어링이라고 생각.
나중에 알았는데 이걸 MERN 스택이라고 하더라
📆 25-05-21
MDX renderer 구현
내용 보기
📌 프로젝트 세팅
Node.js를 오랫동안 업데이트하지 않았었는데 디펜던시 warning이 뜨길래 최신 LTS로 바꿔줬다. 16 -> 22로 올렸으니 진짜 징하게 안바꾸긴 함.
프로젝트는 CRA로 init 했다.
📌 padding이 width, height를 건드리는 문제
EditorSideBar에 padding 넣는데 넣은 만큼 width, height가 늘어나는 문제가 있었다.
스택오버플로를 참고해서 고쳤다.
📌 MDX 로드하기
쌩 CRA로는 MDX 로딩이 안되고, CRA의 Webpack 설정을 건드려야 한다고 한다.
하지만 Webpack 설정이 기본적으로 숨겨져있기 때문에 Eject 하거나 craco를 써야 했고, 나는 craco 방식을 선택했다.
Webpack이 하는 일
모든 FE 리소스(JS, CSS, 이미지, 폰트 등)를 하나의 JS 번들로 변환하는 빌드 도구이다.
MDX같이 브라우저가 이해할 수 없는 파일을 JS 코드로 변환해준다.
MDX -> JSX 변환 필수
브라우저는 MDX가 뭔지 모른다.
따라서 브라우저가 이해하는 JSX 코드로 변환해주어야 하는데, 이걸 해주는 게
Webpack + @mdx-js/loader 이다.
📌 MDX 로딩을 위한 세팅
- 필요한 패키지 설치
npm install @craco/craco @mdx-js/react @mdx-js/loader
package.json
수정
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test"
}
craco.config.js
생성 및 설정
module.exports = {
webpack: {
configure: (webpackConfig) => {
// 1. remove mdx from the rule
webpackConfig.module.rules = webpackConfig.module.rules.map((rule) => {
if (rule.oneOf) {
rule.oneOf = rule.oneOf.filter(
(r) => !(r.test && r.test.toString().includes('mdx'))
);
}
return rule;
});
// 2. add mdx loader
const mdxRule = {
test: /\.mdx?$/,
use: [
{
loader: require.resolve('babel-loader'),
},
{
loader: require.resolve('@mdx-js/loader'),
options: {
providerImportSource: "@mdx-js/react",
},
},
],
};
const oneOfRule = webpackConfig.module.rules.find((rule) => Array.isArray(rule.oneOf));
if (oneOfRule) {
oneOfRule.oneOf.unshift(mdxRule);
}
return webpackConfig;
},
},
};
babel-loader는 이미 CRA에 포함되어 있다.
📌 컴포넌트에서 MDX 렌더링하기
const mdxContext = require.context('../post', false, /\.mdx$/);
이런식으로 Webpack의 require.context
를 이용해서 동적 로드한 다음 (이 방식 아니면 import 문 직접 써야 하는데 내가 원하는 방식이 아님)
<div className="content">
{MdxComponent && (
<MDXProvider>
<MdxComponent />
</MDXProvider>
)}
</div>
대충 요런식으로 변환된 내용을 불러올 수 있다.
🐞 craco config 설정시 주의점
기존에 gpt가 알려준 이 설정은 틀렸다.
module.exports = {
webpack: {
configure: (webpackConfig) => {
webpackConfig.module.rules.push({
test: /\.mdx?$/,
use: [
{
loader: require.resolve('babel-loader'),
},
{
loader: require.resolve('@mdx-js/loader'),
options: {
providerImportSource: "@mdx-js/react",
},
},
],
});
return webpackConfig;
},
},
};
왜냐하면 단순히 @mdx-js/loader
의 설정을 push만 했기 때문이다. 이건 rule을 뒤에다 붙인 것이다.
CRA Webpack의 기본 설정은 mdx를 알 수 없는 파일로 간주하여 정적 파일로 처리하기 때문에
@mdx-js/loader
가 기존 로더보다 먼저 실행되지 않으면 무시된다 (!)
따라서 최종 config에선 filter로 기존 로더를 제거하고 unshift로 새 로더를 맨 앞에 붙여서 처리 우선순위를 확보했다.
🌌 렌더링 결과
요기까지 완성하고 내일의 나에게 맡긴다.
📆 25-05-26
frontmatter 파싱, FE 1.0.0-beta.1
릴리즈
내용 보기
📌 Closed Issues
📌 frontmatter 파싱
MDX가 잘 렌더링되는 것 같지만 frontmatter는 사실 안그랬다.
-----
를 기점으로 안의 내용들이 한 뭉탱이로 다 h2 처리되더라.
admonition도 별도로 처리해야하는 것 같지만 frontmatter는 메타데이터라 중요해서, 먼저 처리하기로 했다.
목표는 이러했다.
title
: 글 최상단에 h1으로 렌더링 & 사이드바에 렌더링created_date
: 사이드바에 렌더링updated_date
: 사이드바에 렌더링
그리고 하단의 방식으로 해결했다.
- 필요한 패키지 설치
npm install remark-frontmatter remark-mdx-frontmatter
- craco.config.js 수정
상단에 요거 추가하고
module.exports = async (env) => {
const { default: remarkFrontmatter } = await import('remark-frontmatter');
const remarkMdxFrontmatter = (await import('remark-mdx-frontmatter')).default;
...
mdxRule의 options에 frontmatter 플러그인을 추가했다.
options: {
providerImportSource: "@mdx-js/react",
remarkPlugins: [
remarkFrontmatter,
[remarkMdxFrontmatter, { name: 'frontmatter' }],
],
},
...
import
구문 쓰는데 애 좀 먹었어서 default에 대해 알아봐야겠다.
그나저나 모듈마다 CJS/ESM 호환 갈리는거 진심 탈모 요소 중 하나인듯
- EditorPage.js, EditorSideBar.js 수정
📌 다음 릴리즈 계획
서버 컴이 와서 놀고있기 때문에 좀 더 열심히 개발해야겠다.
다음 버전에 선 publish한 mdx를 서버쪽으로 보내고, 서버에선 이를 쏴주는 api를 만들어야 한다.
그리고 private gh repo에 push가 되어야하기 때문에... 이리저리 고민한 결과
백엔드 API를 통해서 처리하는 것이 제일 정석적인 flow라고 생각한다.
왜냐하면,
- 카테고리 정보 받으려면 결국 백엔드 통신이 필요함
- Electron으로 렌더링 부분만 데스크탑 앱으로 빼면 프로젝트 복잡해짐
- FE단에 뷰어와 private repo 접근 기능 모두를 넣으면 보안상 안좋음.
- CORS 잘~ 설정하면 로컬 -> 리모트 통신 가능
그래서 내일은 express 작업을 할 것 같다.
📆 25-05-27
MDX publish API 구현
내용 보기
📌 Opened Issues
📌 express 기본 세팅
백엔드 단 프로젝트 명을 uniq-cms
로 정하고 express 서버로 세팅했다.
UI는 uniq
CRA 프로젝트에서 다 맡고 있으니 uniq-cms
는 headless CMS인 격이다.
npm install express
npm install --save-dev nodemon
디펜던시를 상단과 같이 설치하고 index.js와 post.js를 생성했다.
const express = require('express');
const app = express();
const port = 6229;
// parse JSON body
app.use(express.json());
// set /api prefix for all endpoints
const postRoutes = require('./routes/post');
app.use('/api/post', postRoutes);
app.listen(port, () => {
console.log(`🚀 uniq-cms running at http://localhost:${port}`);
});
const express = require('express');
const router = express.Router();
router.get('/:id', (req, res) => {
const postId = req.params.id;
res.send(`Post content ${postId} :3`);
});
router.post('/', (req, res) => {
res.send('Post published.');
});
module.exports = router;
📌 MDX Publish API 구현 (1/2)
Publish 요청이 들어오면 해당 MDX 파일에 대해 다음의 두 가지를 처리해야 한다.
- 서버의
/post
경로에 저장 - GH private repo에 push
그 중 1번부터 작업했다.
mdx 파일 저장하기
중복 파일명 문제에 대해선 MVP 단계에서 생각할 부분이 아닌 것 같아 나중에 처리하기로 했다.
npm install multer
// temporary upload
const upload = multer({
dest: 'temp_uploads/',
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
});
/* Publish MDX file */
router.post('/', upload.single('file'), (req, res) => {
const file = req.file;
if (!file) {
return res.status(400).send('No mdx file uploaded.');
}
// Check if the file is mdx
if (path.extname(file.originalname) !== '.mdx') {
fs.unlinkSync(file.path); // delete file if not mdx
return res.status(400).send('Only mdx files are allowed.');
}
// set mdx save directory
const postDir = path.join(__dirname, '../../post');
// if not exist then mkdir
if (!fs.existsSync(postDir)) {
fs.mkdirSync(postDir, { recursive: true });
}
// final save path for the mdx file
const targetPath = path.join(postDir, file.originalname);
// move mdx file from temporary upload path
fs.rename(file.path, targetPath, (err) => {
if (err) {
return res.status(500).send('Failed to save file.');
}
res.send('Post published.');
});
});
json 파싱하기
mdx 뿐만 아니라 json 데이터도 같이 필요해질 확률이 99.99%라서 json 파싱 로직도 추가했다.
// parse json
let jsonData = null;
if (req.body.json) {
try {
jsonData = JSON.parse(req.body.json);
} catch (err) {
return res.status(400).send('Invalid json payload.');
}
}
[DEBUG] Received json: { category: 'dev-note', title: 'uniQ 개발 노트' }
📌 MDX Publish API 구현 (2/2)
npm install simple-git
npm install dotenv
서버 최상단에 env를 불러오도록 설정한다.
require('dotenv').config();
그리고 repo 권한 추가한 GitHub PAT를 발급하여 env에 넣는다.
그럼 push할때 sign in 창이 안뜨고 아묻따 push가 가능해진다.
올바른 git 참조하기
simple-git
으로 push util을 만들어서 모듈화하고, 이 모듈을 post.js에서 불러와 처리하고자 했다.
그런데 /post
에서 git init하면 동기화를 못하기 때문에, 프로젝트 루트 경로의 git을 참조해야 한다.
const gitPath = path.join(__dirname, '../../');
const git = simpleGit(gitPath);
따라서 simpleGit에 이런식으로 .git이 있는 루트 path를 넣어준다.
암튼 이렇게 해서 pushToGithub.js를 작성했고
publish api에 GH push flow를 추가했다. (5e00690)
git 작업 시 참고사항
push util로 main에 checkout 해서 push하려니까 현재 feature 브랜치에 있어서 stash 경고가 떴다.
-
➡️ stash하고 main으로 checkout 했는데 stash때문에 util 작성한게 다 과거로 돌아감 ㅋ
- ➡️ stash pop을 했는데 merge conflict가 떠서 keep theirs로 stash 버전을 살리고 main에서 util 테스트를 진행했다.
프로덕션에선 브랜칭할 일이 없을테니 상관없지만 개발하는 repo에선 이거 좀 불편하다. 😐
그리고 publish 관련 커밋을 다이렉트로 main에 꽂아버리기 때문에 사용자 입장에서는 fork를 통한 CMS 관리가 어렵다. 업데이트를 위해 pull 땡길 시 충돌나기 때문.
어떻게 하면 api 버전업이 용이할지는 다음의 고민 사항이다.
아직 DB 연결은 안되어있음!
push util 상의 설정 정보들은 (ex. remote url, username 등) 사용자가 수정할 수 있어야 한다.
그래서 DB에서 퍼오는걸로 점진적 수정을 거쳐야 하는데
우선 조회 api 먼저 구현해서 #1 이슈를 끝내고 #2에서 몽고DB 작업을 할 예정이다.
📆 25-05-28
MDX query API 구현, MongoDB 연결, publish API DB 연결
내용 보기
📌 Closed Issues
📌 Opened Issues
📌 MDX query API 구현
DB 연결이 안된 상태라 mock으로 구색만 맞춰놓고 1번 이슈를 끝냈다.
router.get('/:id', (req, res) => {
const postId = req.params.id;
if (postId === '1') {
const filePath = path.join(__dirname, '../../post/test.mdx');
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error('[Error] Failed to read MDX:', err);
return res.status(500).send('Failed to read post file.');
}
res.type('text/markdown').send(data);
});
} else {
res.status(404).send('Cannot find requested post.');
}
});
📌 MongoDB 연결
뭣모르고 썼는데 Express 4.16.0 이상부터 body-parser
가 내장되어있다고 한다.
app.use(express.json());
그래서 index.js에 이렇게 설정해주면 all set이었던 거였음!
📌 MongoDB 연결
npm install mongoose
const mongoose = require('mongoose');
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/uniq-cms')
.then(() => console.log('✅ Successfully connected to MongoDB'))
.catch(err => console.error('❌ Failed to connect to MongoDB:', err));
📌 Post Collection 정의
다음과 같이 Collection 스키마를 정의할 수 있다.
별도의 설정을 넣지 않는다면 자동 생성되는 Collection은 소문자 & 복수형으로 네이밍된다. (ex. Post -> posts)
visibility
는 포스트 접근권한으로, enum으로 관리하기로 했다.
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
category: { type: String, required: true },
filePath: { type: String, required: true },
visibility: {
type: String,
enum: ['public', 'protected', 'private'],
default: 'public',
required: true
}
}, {
timestamps: true, // automatically set createdAt and updatedAt
});
module.exports = mongoose.model('Post', postSchema);
Post Document 저장
JPA의 repository마냥 require
로 Post 스키마를 불러와 서 필요한 document를 저장하면 된다.
절대경로인 targetPath
는 프로젝트 경로까지 포함하기 때문에 프로젝트 파일이 이동되면 관리하기 힘들어진다.
따라서 post 경로부터 시작하는 상대경로로 변환하여 저장했다.
이러면 post 경로가 바뀌어도 document에는 영향이 없다.
const Post = require('../models/Post');
const projectRoot = process.cwd(); // project root path
const relativePath = path.relative(projectRoot, targetPath);
await Post.create({
title: jsonData.title,
category: jsonData.category,
filePath: relativePath,
visibility: jsonData.visibility
});
{
"title": "uniQ 개발 노트",
"category": "dev-note",
"filePath": "post\\test.mdx",
"visibility": "protected",
"createdAt": {
"$date": "2025-05-28T14:13:13.519Z"
},
"updatedAt": {
"$date": "2025-05-28T14:13:13.519Z"
},
"__v": 0
}
📆 25-05-29
MDX query API DB 연결, slug 필드 추가
내용 보기
📌 Closed Issues
📌 query API 수정하기
기존에 mock으로 하드코딩했던 부분을 MongoDB와 연결했다.
하지만 테스트해보니 요런 에러가 터졌다.
[Error] Failed to get post: CastError: Cast to ObjectId failed for value "1" (type string) at path "_id" for model "Post"
이 말인 즉슨 MongoDB에 보낸 1이라는 쿼리 값이 ObjectId가 아니라는 뜻이다.
mongoose의 findById
는 내부적으로 _id가 MongoDB의 ObjectId 타입이라고 가정하는데 내가 무지성으로 MySQL 마냥 정수형 id 값을 날린게 원인이다.
Auto increment처럼 id 필드를 따로 만들어주는 방법이 있긴 했는데, 찾아보니 ObjectId를 사용하는 것이 일반적이고 성능도 가장 최적화되어있다고 하여 해당 방식을 그대로 따르기로 했다.
http://localhost:6229/api/post/683860a3561f6209b13787fb
그리고 ObjectId로 다시 호출하니 잘 조회되었다.
📌 하지만 주소창에 683860a3561f6209b13787fb 를 쓸 순 없자너
그렇다. 그래서 UX와 SEO-friendly함을 고려하여 slug라는 것이 존재하는 것이었다.
slug란?
slug는 웹 페이지를 쉽게 읽을 수 있는 형태로 식별하는 URL의 일부이다.
당연히 unique 해야 한다.
http://localhost:3000/post/uniq-dev-note
그렇다면 FE에서 slug 기반 URL로 route 할 경우
http://localhost:6229/api/post/683860a3561f6209b13787fb
FE가 BE에 ObjectId로 조회 요청을 날리는 flow가 되는데, 이는 아주 일반적인 방법이라고 한다.
개인적으로 정수형 id를 더 선호해왔어서 slug 방식이 SEO 이득을 보는지 몰랐다 😂
아무튼 스키마와 API 둘 다 slug 필드를 추가해주었고,
slugify라는 npm 패키지로 자동 생성도 가능하다는데 MVP 단계니까 있다는 것만 적어두고 패스한다.
slug: { type: String, required: true, unique: true }
...그나저나 개발 일지 쓰면서 갑자기 보였는데 slug 필드에 unique 빼먹었다.
내일 fix하자 ㅋㅋㅋㅋㅋㅋㅋㅋ
😙 내일의 계획!
내일은 리트코드 POTD 말고도 프로그래머스 문제 하나를 더 풀고 싶기 때문에 가능할지는 모르겠으나
- slug field fix
- MDX list query API impl
- 무시무시한(?) CORS setting
이 3가지가 일단 목표이고, 토요일이 5월의 마지막 날이니 이 날 뷰 작업이 얼추 되었으면 좋겠다고 생각한다.
6월부터는 DOKI 양도 봐드려야 하고 정처기 실기도 준비해야 되기 때문에~
📆 25-05-30
slug 필드 패치, MDX list query API 구현
내용 보기
📌 Opened Issues
📌 slug 필드의 누락된 제약 조건 패치
📌 MDX list query API 구현
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
+ description: { type: String, default: '' },
slug: { type: String, required: true, unique: true },
category: { type: String, required: true },
filePath: { type: String, required: true },
visibility: {
type: String,
enum: ['public', 'protected', 'private'],
default: 'public',
required: true
}
}, {
timestamps: true, // automatically set createdAt and updatedAt
});
slug fix에 이어 목록 조회시 필요할 description 필드도 Post.js에 추가했다.
Post list query API
Timezone 지정하기
timestamp가 UTC 기준으로 찍히길래 query에 대한 timezone 변환도 필요하더라.
MongoDB config가 따로 없나 싶었는데 시간대 변환은 어플리케이션 레벨에서 처리하는 것이 일반적이라고 한다.
npm install dayjs
const dayjs = require('dayjs');
const utc = require('dayjs/plugin/utc');
const timezone = require('dayjs/plugin/timezone');
dayjs.extend(utc);
dayjs.extend(timezone);
상단과 같이 dayjs 패키지를 이용하여 UTC -> GMT+9로 변환한다.
createdAt: dayjs(post.createdAt).tz('Asia/Seoul').format('YYYY-MM-DD HH:mm:ss')
{
"createdAt": "2025-05-29T13:26:59.764Z"
}
{
"createdAt": "2025-05-29 22:26:59"
}
📆 25-06-09
CORS 설정, 포스트 목록 UI 구현, slug 기반 MDX query API 구현
내용 보기
📌 Closed Issues
https://github.com/Queue-ri/uniq-cms/issues/5
https://github.com/Queue-ri/uniq-cms/issues/7
📌 Opened Issues
https://github.com/Queue-ri/uniq-cms/issues/7
https://github.com/Queue-ri/uniq/issues/4
https://github.com/Queue-ri/uniq-cms/issues/9
📌 CORS FE origin 허용하기
BE에 cors 패키지를 설치하고 허용할 origin을 명시해주면 된다.
왜이렇게 쉽게 해결됐지? 이게 아닌데? 싶지만 생각해보니 웹 공부 3년째다. 아직도 이해 못했으면 심각한 것이다.
CORS 설정 도중에 카카오 맵 API에서 허용 IP 주소를 설정했던 것이 떠올라서
CORS origin도 동적으로 관리할 수 있는지 알아보았는데, 된다고 한다.
로그인 기능이 추가되면, 추후 관리자 페이지에서 설정 가능하면 좋을 것 같다.
npm install cors
// allowed CORS origins
let allowedOrigins = [
'http://localhost:3000',
];
// CORS middleware setting
app.use(cors({
origin: function (origin, callback) {
if (allowedOrigins.includes(origin)) {
callback(null, true);
}
else {
callback(new Error('Not allowed by CORS: ' + origin));
}
}
}));
📌 MainPage와 PostList 컴포넌트 구현
MainPage에서 fetching 관련 useEffect를 두고 PostList는 컴포넌트로써 렌더링만 담당하도록 분리했다.
data fetching은 페이지 단위에서 처리하는 게 일반적이라고 한다.
- 유지보수 측면에서 데이터와 UI를 분리하는 것이 좋고
- 다른 페이지와 데이터 공유가 용이해지며
- route 전환이나 refresh 될 때 한번씩만 실행되어야 하기 때문이다.
📌 formatDate
유틸 함수 구현
locale 기반 datetime 포맷팅이 자주 쓰일 것 같아 util로 모듈화하여 구현했다.
export function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
}
💥 사실 slug로 조회 가능했어야 함 💥
😠.................😡.....
현재 MainPage에서 PostList를 통해 포스트 목록을 보여주고,
여기서 item 하나를 클릭하면 PostDetail로 라우팅해서 넘어가려고 했는데
이렇게 넘어가려면 navigate
해야 하지만 URL 상에 id를 쿼리로 주지 않고는 컴포넌트에 넘기는게 안된다고 한다.
하지만 URL에 ObjectId가 노출되면 안된다. slug를 내가 왜 추가했는데 ㅜㅜㅋㅋ
navigate
에 state를 줄 순 있지만 이는 새로고침시 bye 하는거라 refresh하면 포스트 내용이 증발하는 대참사가 일어나고
사실 redux-persist같은 상태관리 패키지 쓰면 안될것이야 없긴 한데,, 뇌절이다.
결국 미디엄, 노션 다 slug 기반 조회 API를 두길래, 보편성을 고려해서 BE에 API를 추가하기로 결정했다.
Origin 명시해줘요 ^ㅅ^
Error: Not allowed by CORS: undefined
at origin (C:\Users\Hexagoner\Desktop\uniq-cms\api\index.js:22:16)
at C:\Users\Hexagoner\Desktop\uniq-cms\node_modules\cors\lib\index.js:219:13
이젠 BE에 CORS 정책을 설정해놨기 때문에 포스트맨 헤더에 Origin을 명시해줘야 한다.
📆 25-06-10
라우팅, 포스트 상세 UI 구현, CSS Module, 포스트 접근제어, BE 1.0.0-beta.1
릴리즈
내용 보기
📌 Closed Issues
https://github.com/Queue-ri/uniq-cms/issues/9
https://github.com/Queue-ri/uniq/issues/4
https://github.com/Queue-ri/uniq-cms/issues/11
📌 Opened Issues
https://github.com/Queue-ri/uniq/issues/6
https://github.com/Queue-ri/uniq-cms/issues/11
📌 라우터 설정 및 PostViewPage 연결
npm install react-router-dom
<Router>
<Routes>
<Route path="/" element={<MainPage />} />
<Route path="/post/:slug" element={<PostViewPage />} />
</Routes>
</Router>
📌 응답으로 받은 MDX 렌더링하기
이미 EditorPage에서 렌더링 로직을 구현했으나,
PostViewPage에서 PostDetail로 건네주는 것은 MDX 파일 자체가 아니라 내용이 적힌 문자열이다.
따라서 MDX 문자열을 컴포넌트로 변환해주는 패키지와, frontmatter 파싱용 gray-matter가 필요.........
npm install @mdx-js/runtime
npm install gray-matter
.....할 줄 알았으나?
Compiled with problems:
ERROR in ./src/component/post/PostDetail.js 7:0-45
Module not found: Error: Can't resolve '@mdx-js/runtime' in 'C:\Users\Hexagoner\Desktop\uniq\src\component\post'
필요하지 않았음 ^^
패키지 사이트에 가보니 @mdx-js/runtime은 deprecated 되었고
거기 공지에 @mdx-js/mdx
를 쓰라고 해서 하라는대로 했다.
이렇게 되면 frontmatter는 기존에 쓰던 remark 플러그인을 사용하면 된다.
useEffect(() => {
const compileMdx = async () => {
try {
const compiled = await evaluate(mdxData, {
...runtime,
useDynamicImport: false,
format: 'mdx',
remarkPlugins: [
remarkFrontmatter,
[remarkMdxFrontmatter, { name: 'frontmatter' }],
],
});
setContent(() => compiled.default);
if (compiled.frontmatter) {
setFrontmatter(compiled.frontmatter);
}
} catch (error) {
console.error('MDX compile error:', error);
}
};
compileMdx();
}, [mdxData]);
📌 CSS 충돌과 모듈화를 통한 해결
MainPage의 CSS와 PostViewPage의 CSS가 충돌나는듯 했다. 같은 wrapper 클래스를 가지고 있었는데
자꾸 MainPage의 wrapper가 PostViewPage의 wrapper 사이즈로 지정되고, 타이틀 폰트도 꼬였다.
알아보니 foo.css 이런식으로 import하면 해당 CSS는 전역 스코프라고 한다.
이 경우 가장 마지막으로 로딩된 스타일을 적용한다고 했으니, MainPage에 PostViewPage 스타일이 적용되어버린 것이다.
따라서 로컬 스코프인 CSS Module 방식으로 변경했는데...
사실 이거 쓰면 해싱된 네이밍 때문에 가독성이 떨어져서 일부러 안하고 있었는데, 그냥 처음부터 쓸 걸 그랬나보다.
📌 기존의 query API 수정
이제 protected와 private MDX는 direct access되면 안되기에
- ObjectId 기반 query API는 보안상 제거 (=주석처리)
- slug 기반 query API를 대표 query API로 지정 ->
/slug
를 삭제하여 endpoint 간소화 - query API에서 MDX visibility만 조회하는 metaOnly 옵션 추가
- query API에서 protected면 password verify하기
- query API에서 private면 403 FORBIDDEN 던지기
- list query API에서 private 포스트는 필터링하기
요런 API상의 많은 수정들이 필요하다. 관련 이슈는 11번이므로 참고.
🤔 API를 분리하는 것이 좋을까?에 대한 고민과 그 결과
최종적으로는 slug 기반 query API 하나로 통합하고 여기서 접근제어를 다 처리하기로 했다.
왜냐하면 API를 여러 개 분리해서 구현할 경우,
이럴땐 여기다 호출하고 저럴땐 저기다 호출하고... 이렇게 되면
- FE: 여기선 엔드포인트 뭐였더라 ㅇㅁㅇ? (이전 코드나 API 문서 찾아보는 비효율성)
- BE: 헐 다른쪽 쿼리 API 유효성 검사 빼먹고 머지했다 (추가 이슈 처리하는 비효율성)
- 눈: 살려...ㅈ... (반복되는 fetch 코드로 인한 쓸데없는 라인 수 증가 및 시력 저하)
같은 상황이 발생하기 때문이다.
따라서 엔드포인트는 하나로 두고,
- 포스트의 visibility check 모드 여부를 확인하는
metaOnly
query와 - private 접근 제한
- protected 비밀번호 유효성 검증
이 모든걸 한 곳에서 처리하도록 설계했다.
📌 패스워드에 bcrypt 적용하기
평문으로 저장하는 것은 매우 안좋은 인상을 남기므로 11번 이슈와 함께 처리한다.
- MDX publish API
- MDX query API
해싱은 두 가지 모두에 적용해야 한다.
일단 https 통신이기만 하면 FE -> BE 평문 전송은 괜찮다고 한다.
JWT같이 어디 저장할때가 문제인거고, 이건 그냥 타이핑해서 바로 보내는거니까.
npm install bcrypt
여기까지 BE 작업을 마무리하고 1.0.0-beta.1
을 릴리즈했다. 잔디에 반영하고 싶어서