PJH's Community Site - Post & Create

박정호·2022년 12월 2일
0

Community Project

목록 보기
9/14
post-thumbnail

⭐️ Post Create Page

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

파일 생성



✔️ Post Create UI 작성

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>
  );
};


✔️ Post Create 기능 생성

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: {} };
  }
};


👉 api 요청 (client)

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;
}


👉 api 생성 (server)

파일 생성

route 경로 설정

// server.ts
...
app.use('/api/posts', postRoutes);

createPost 핸들러 생성

1️⃣ clientrequest으로 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로 데이터가 잘 담기는 것을 확인할 수 있다.



⭐️ Post Page

이제 실제로 화면에 해당 게시글이 보이도록 해보자.

파일 생성



✔️ Post page UI 작성

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>
	      </>
  );
};



✔️ Post Page 기능 생성

👉 api 요청 (client)

게시글 데이터를 가져오자!

  • 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를 생성하지 않아도 된다.



👉 api 생성 (server)

  • 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



📷 Photos

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글

관련 채용 정보