Skip to content

화면 구성 및 주요 기능 설명

Lee Min Wook edited this page Feb 28, 2022 · 17 revisions

👨‍💻 화면 구성 목차

사이트 방문하기

  1. 메인 페이지
  2. 게시글 상세 페이지
  3. 게시글 등록 페이지
  4. 검색 페이지

1. 메인 페이지

velogClone main

무한스크롤

  • 벨로그의 무한스크롤 기능은 Intersection Observer API를 이용하였습니다. Intersection Observer가 관찰하고 있는 요소에 사용자가 접근했다면 옵저버에 구독된 이벤트를 발생시켜 서버에서 추가로 데이터를 가져오게 됩니다.
const observer = node => {
    if (node === null || lastPageCheck.current === true) return;
    if (observerRef.current) observerRef.current.disconnect();

    // 타겟이된 노드에 접근을 했다면 옵저버가 구독된 이벤트를 실행시켜준다.
    observerRef.current = new IntersectionObserver(([entry], observer) => {
      // 타겟이 접근을 했는지 감지한다.
      if (entry.isIntersecting) {
        // 서버에게 자주 호출하는 걸 방지 하기 위해 디바운스
        if (timer) {
          return clearTimeout(timer);
        }

        // ... 생략

        timer = setTimeout(
          call(async () => {
            // 요소에 접근했다면 서버에서 데이트를 추가로 받아온다.
            const postsResponse = await posts.get(
              currentLimit,
              currentPage + 1
            );
            const { results, page, totalPages, limit } = postsResponse.data;
            const newPosts = await comments.getComments(results);
            return {
              posts: [...newPosts],
              state: { page, totalPages, limit },
            };
          }),
          REQUEST_DELAY
        );
      }
    });
    observerRef.current.observe(node);
  };
  • 짫은 시간에 서버에 데이터를 여러번 요청하는걸 방지하기위해 디바운스를 통해 제어하였습니다. 요소에 접근하여 1초 동안 새로운 접근이 없어야 서버에서 데이터를 가져옵니다.
let timer;
// 1초 동안 새로운 접근이없어야 요청을한다.
const REQUEST_DELAY = 1000;

  // ... 생략

timer = setTimeout(
          call(async () => {
            // 요소에 접근했다면 서버에서 데이트를 추가로 받아온다.
            const postsResponse = await posts.get(
              currentLimit,
              currentPage + 1
            );
            // ... 생략
          }),
          REQUEST_DELAY
        );
  • 사용자에게 게시글을 추가로 가져온다는 경험을 주기 위해 loading 이미지를 사용하였습니다. loading 컴포넌트는 재사용이 가능하도록 HOC로 만들어서 다른 컴포넌트도 이용할 수 있도록 했습니다.
import React from 'react';
import Loading from '@/Components/common/Loading';

const withLoading = Component => {
  return function component({ loading, ...rest }) {
    return (
      <>
        <Loading {...{ loading }} />
        <Component {...rest} />
      </>
    );
  };
};

export default withLoading;

// hoc 사용
export default withLayout(MainPage);

2. 게시글 상세 페이지

velogClone detail

수정 기능

velogClone update

삭제 기능

velogClone delete

댓글 작성 기능

velogClone comment

댓글 수정 기능

velogClone comment update

댓글 삭제 기능

velogClone comment delete

3. 게시글 작성 페이지

velogClone insert

임시저장

  • 임시저장 기능을 구현하기 위해 리덕스를 이용하였습니다. 리덕스 스토어에 저장된 글이 있다면 임시 저장된 글을 가져와 보여줍니다.
  useEffect(() => {
    if (transientStorageState.data) {
      const { data } = transientStorageState;
      setForm(prev => ({
        ...prev,
        ...data,
        tags: new Set([...data.tags]),
      }));
    }
  }, [transientStorageState]);

글 작성하기

  • 게시글 작성을 위해 CK에디터를 이용하였습니다.
    <Body>
      <CKEditor editor={ClassicEditor} config={editorConfiguration} {...rest} />
    </Body>

섬네일 업로드

  • 게시글의 섬네일 이미지는 S3에 저장을 하며 프론트에서 바로 올릴 수 있도록 AWS SDK를 이용하여 구현하였습니다.
const s3Upload = file => {
  AWS.config.update({
    region: 'ap-northeast-2', // 버킷이 존재하는 리전을 문자열로 입력합니다. (Ex. "ap-northeast-2")
    credentials: new AWS.CognitoIdentityCredentials({
      // API_KEY는 웹팩에서 설정
      IdentityPoolId: API_KEY, // cognito 인증 풀에서 받아온 키를 문자열로 입력합니다. (Ex. "ap-northeast-2...")
    }),
  });

  const upload = new AWS.S3.ManagedUpload({
    params: {
      Bucket: BUCKET,
      Key: `file/${file.name}`,
      Body: file,
    },
  });
  return upload;
};

4. 검색 페이지

velogClone search

실시간 검색

  • 검색은 제목, 태그, 내용 3가지 필터를 통해서 검색할 수 있습니다. 사용자가 글을 작성하면 자동으로 서버로 요청해서 실시간으로 검색 결과를 보여줍니다.
  • 짧은 시간 서버로 요청을 제어하기 위해 디 바운스를 이용하였습니다.
    timer = setTimeout(() => {
        call(async () => {
          setList(null);
          const { value } = target;

          let response;
          if (keyWord === 'body') {
            response = await postsByKeyWord(
              keyWord,
              encodeURIComponent(`&lt;p>${value}&lt;/p>`)
            );
          } else {
            response = await postsByKeyWord(keyWord, encodeURIComponent(value));
          }

          const { results, limit, page, totalPages, totalResults } =
            response.data;
          const newPosts = await getComments(results);
          return {
            keyWord,
            value,
            posts: [...newPosts],
            state: { limit, page, totalPages, totalResults },
          };
        });
      }, 1000);