
이제 커뮤니티에 게시글을 등록해보자. 게시글에 필요한 Title과 Description을 등록하는 post create page를 만들어보자.
✅ 파일 생성

const PostCreate = () => {
...
return (
<div>
<div>
<div>
<h1>포스트 생성하기</h1>
<form onSubmit={submitPost}>
<div>
<input
type="text"
placeholder="제목"
maxLength={20}
value={title}
onChange={e => setTitle(e.target.value)}
/>
<div> {title.trim().length}/20 </div>
</div>
<textarea
rows={4}
placeholder="설명"
value={body}
onChange={e => setBody(e.target.value)}
/>
<div>
<button> 생성하기 </button>
</div>
</form>
</div>
</div>
</div>
);
};
✅ state 생성
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
✅ 권한 없는 유저는 login페이지로 이동
1️⃣ getServerSideProps 사용
getServerSideProps: getServerSideProps요청 시 데이터를 가져와야 하는 페이지를 렌더링해야 하는 경우에만 사용해야 한다. 이는 요청의 데이터 또는 속성(예: authorization헤더 또는 지리적 위치)의 특성 때문일 수 있다. 사용하는 페이지 getServerSideProps는 요청 시 서버 측에서 렌더링되며 캐시 제어 헤더가 구성된 경우에만 캐시된다.💡 참고하자 👉 getServerSideProps
2️⃣ 쿠키가 없다면 에러를 보내기
3️⃣ 커뮤니티 존재한다면 경고글 출력
4️⃣ 백엔드에서 요청에서 던져준 쿠키를 이용해 인증 처리할 때 에러가 나면 /login 페이지로 이동
client - r/[sub]/create.tsx
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
try {
const cookie = req.headers.cookie; // 1️⃣ 번
if (!cookie) throw new Error('Missing auth token cookie'); // 2️⃣ 번
// 3️⃣ 번
await axios.get(`/auth/me`, {
headers: { cookie },
});
return { props: {} };
} catch (error) { // 4️⃣ 번
res.writeHead(307, { Location: '/login' }).end();
return { props: {} };
}
};
✅ submitPost 함수 생성 (게시글 등록)
1️⃣ router.query: 매개변수는 쿼리 매개변수로 페이지에 전송되어 subName에 저장(참고)
2️⃣ title의 값이 없거나 sub이 존재하지 않다면 return하여 종료.
title.trim(): 문자열 양 끝의 공백을 제거하고 원본 문자열을 수정하지 않고 새로운 문자열을 반환3️⃣ title, body, sub을 axios를 통해 post.
4️⃣ 해당 url로 이동
const router = useRouter();
const { sub: subName } = router.query; // 1️⃣ 번
const submitPost = async (e: FormEvent) => {
e.preventDefault();
if (title.trim() === '' || !subName) return; // 2️⃣ 번
try {
const { data: post } = await axios.post<Post>('/posts', { // 3️⃣ 번
title: title.trim(),
body,
sub: subName,
});
router.push(`/r/${subName}/${post.identifier}/${post.slug}`); // 4️⃣ 번
} catch (error) {
console.log(error);
}
};
💡
post<Post>- 타입 설정// types.tsx export interface Post { identifier: string; title: string; slug: string; body: string; subName: string; username: string; createdAt: string; updatedAt: string; sub?: Sub; url: string; userVote?: number; votesScore?: number; commentCount?: number; }
✅ 파일 생성

✅ route 경로 설정
// server.ts
...
app.use('/api/posts', postRoutes);
✅ createPost 핸들러 생성
1️⃣ client의 request으로 title, body, sub 값을 받았다.
2️⃣ title의 값이 빈값이라면 에러 반환
3️⃣ res.locals를 활용하여 user을 전역에서 사용 가능한 변수로 설정
4️⃣ post객체에 게시글 데이터 삽입 및 저장.
// routes/posts.ts
const createPost = async (req: Request, res: Response) => {
const { title, body, sub } = req.body; // 1️⃣ 번
if (title.trim() === '') { // 2️⃣ 번
return res.status(400).json({ title: '제목을 비워둘 수 없습니다.' });
}
const user = res.locals.user; // 3️⃣ 번
try {
const subRecord = await Sub.findOneByOrFail({ name: sub });
const post = new Post(); // 4️⃣ 번
post.title = title;
post.body = body;
post.user = user;
post.sub = subRecord;
await post.save();
return res.json(post);
} catch (error) {
console.log(error);
return res.status(500).json({ error: '문제가 발생했습니다.' });
}
};
const router = Router();
router.post('/', userMiddleware, authMiddleware, createPost);
export default router;
✅ getSub 핸들러의 sub 데이터에 posts 데이터 추가
sub.name(커뮤니티 이름)만 가져오고 있었는데, 이제 post한 게시글의 정보도 담길 수 있도록 sub에 데이터를 추가하자.1️⃣ 위에서 Post 테이블에 데이터를 저장했었다. 따라서, Post 테이블에서 원하는 데이터를 찾아서 posts변수에 저장.
2️⃣ sub 데이터에 posts 데이터 저장.
// routes/subs.ts
const getSub = async (req: Request, res: Response) => {
...
try {
...
const posts = await Post.find({
where: { subName: sub.name },
order: { createdAt: 'DESC' },
relations: ['comments', 'votes'], // posts와 관련된 Entity
});
sub.posts = posts;
return res.json(sub);
} catch (error) {
...
}
};
💡 이제 게시글을 생성하면, 커뮤니티(sub)에 posts로 데이터가 잘 담기는 것을 확인할 수 있다.
이제 실제로 화면에 해당 게시글이 보이도록 해보자.
✅ 파일 생성

const PostPage = () => {
...
return (
{post && (
<>
<div>
<p> Posted by
<Link href={`/u/${post.username}`}>
/u/{post.username}
</Link>
<Link href={post.url}>
{dayjs(post.createdAt).format('YYYY-MM-DD HH:mm')}
</Link>
</p>
</div>
<h1>{post.title}</h1>
<p>{post.body}</p>
<div>
<button> <span>{post.commentCount} Comments</span>
</button>
</div>
</div>
</div>
</>
);
};
게시글 데이터를 가져오자!
useSWR hook을 사용하여 key 문자열과 fetcher 함수를 받는다./posts/${identifier}/${slug}}: 게시글마다 고유한 7개의 아이디를 가진 identifier
- this.identifier = makeId(7);
// r/[sub]/[identifier]/slug.tsx
const Slug = () => {
const router = useRouter();
const { identifier, sub, slug } = router.query;
const fetcher = async (url: string) => {
try {
const res = await Axios.get(url);
return res.data;
} catch (error: any) {
throw error.response.data;
}
};
const { data: post, error } = useSWR<Post>(
identifier && slug ? `/posts/${identifier}/${slug}}` : null,
fetcher
);
return <div>[slug]</div>;
};
export default Slug;
💡 잠깐) 반복되는 fetcher
✅ fetcher는 SWR의 key를 받고 데이터를 반환하는 비동기 함수로, 중복되는 코드가 발생한다.
커뮤니티 데이터를 가져와야하는 [sub].tsx, 게시글 데이터를 가져와야하는 [slug]tsx 가 중복된 fetcher를 사용하고 있다.
const fetcher = async (url: string) => { try { const res = await axios.get(url); return res.data; } catch (error: any) { throw error.response.data } }앞으로 다른 곳에서도 데이터를 가져올 수 있는 경우를 생각하여, fetcher를 한번에 적용시켜주자.
✅ 모든 컴포넌트에 fetcher가 적용되도록 SWRConfig로 감싸주기
import axios from 'axios'; import { SWRConfig } from 'swr'; export default function App({ Component, pageProps }: AppProps) { ... const fetcher = async (url: string) => { try { const res = await axios.get(url); return res.data; } catch (error: any) { throw error.response.data; } }; return ( <SWRConfig value={{ fetcher }}> <AuthProvider> {!authRoute && <NavBar />} <div className={authRoute ? '' : 'pt-20'}> <Component {...pageProps} /> </div> </AuthProvider> </SWRConfig> ); }✅ 따라서, 데이터가 필요한 컴포넌트에서 별도의 fetcher를 생성하지 않아도 된다.
res.send(): 기본적으로 response를 보내는 역할을 한다. 기본적으로 서버에서 response 처리를 할 때 Content-Type을 지정해주어야 한다. res.send는 우리가 어떤 데이터를 보내는지 파악을 해서 이에 알맞게 Content-Type을 지정해준다.const getPost = async (req: Request, res: Response) => {
const { identifier, slug } = req.params;
try {
const post = await Post.findOneOrFail({
where: {
identifier,
slug,
},
relations: ['sub', 'votes'],
});
return res.send(post);
} catch (error) {
console.log(error);
return res.status(404).json({ error: '게시물을 찾을 수 없습니다' });
}
};
const router = Router();
router.get('/:identifier/:slug', userMiddleware, getPost);
💡 res.send() vs res.json() vs res.end() + Content-type

