이제 커뮤니티에 게시글을 등록해보자. 게시글에 필요한 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