첫번째 인턴쉽 과제 회고

이택우·2022년 5월 31일
0

프로젝트 회고

목록 보기
3/4

Internship - 과제 1

코인고스트 회원가입, 리스트, 디테일

프로젝트 깃허브 링크

기능 구현 상세

회원가입

  • 가입하기 클릭 시 input tag 에 빈값이나 알맞지 않는 형식일 경우 에러 문구 띄어주는 기능 구현
  • 인증번호 받기 API 이용해서 인증번호 받아서 인증번호 인증까지 기능 구현
    • 휴대폰 번호는 82-1012341234 으로 입력
    • 에러핸들링
    • 인증완료시 휴대폰 인증하기 버튼 disabled 처리

리스트

  • data fetch 는 SWR 라이브러리 사용해서 리스트 및 상세 api 호출
  • 전체글 , 인기글 필터링 기능
  • 리스트 페이지의 경우 swr infinte scroll 사용

디테일

  • 상세 페이지는 getStaticProps , getStaticPath 이용해서 pre-rendering

Challenging Points :

  • TypeScript, Next.js 모두 처음 접해보았기 때문에 이 언어, 프레임워크를 이해하고 적응하는 것이 가장 큰 과제였습니다.
  • getStaticProps, getStaticPaths, SWR을 익히는 과정이 어려웠지만 성취감 또한 컸습니다.

코드

회원가입

회원가입 페이지 링크

핸드폰 번호로 인증번호를 요청과 입력한 인증번호 검증을 요청하는 fetch 함수

const fetchRegister = async () => {
    const phone = watch("id");
    try {
      const response = await fetch(`../api/register`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ phone }),
      });

      if (response.ok) {
        const result = await response.json();
        setRegister(true);
        alert(`인증번호는 ${result?.data?.message}  입니다.`);
        trigger("id");
      } else {
        throw await response.json();
      }
    } catch (e) {
      const error = e as Error;
      alert(error.data?.message);
      setError("id", {
        type: "wrong number",
        message: "잘못된 번호입니다.",
      });
    }
  };

const fetchAuth = async () => {
    const auth = watch("auth");
    try {
      const response = await fetch(`../api/auth`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ auth }),
      });

      if (response.ok) {
        setAuth(true);
        alert(`인증되었습니다.`);
        trigger("auth");
      } else {
        throw await response.json();
      }
    } catch (e) {
      const error = e as Error;
      alert(error.data.message);
      setError("auth", {
        type: "wrong number",
        message: "잘못된 번호입니다.",
      });
    }
  };

리스트

리스트 컨테이너 컴포넌트 링크

function ListContainer({ likes }: { likes: boolean }) {
// useSwrInfinite에 들어갈 fetch 함수
  const getKey = (
    pageIndex: number,
    prevPageData: { data: { data: object } }
  ) => {
    let order = "";
	// 더이상보낼 페이지가 없으면 return
    if (prevPageData && !prevPageData.data) {
      return null;
    }
// 좋아요 필터링 적용시 다른 URI로 요청
    if (likes) {
      order = "orderBy=likes&";
    }
    return `https://api.dev.coinghost.com/blogs?${order}page=${
      pageIndex + 1
    }&limit=10`;
  };

  const scrollLoading = (): void => {
// 무한 스크롤 구현 함수. 
// fetch로 보내온 페이지 데이터가 totalPage(전체 페이지)보다 크다면 return
    if (
      !error &&
      data &&
      data[data.length - 1].data?.meta.totalPage <=
        data[data.length - 1].data?.meta.page
    ) {
      return;
    }
    setSize(size + 1);
  };

  const { data, setSize, size, error } = useSWRInfinite<
    fetchDataInterface,
    object
  >(getKey, fetcher);

// intersection-observer ref 객체와 옵션에 함수 적용
  const { ref } = useInView({
    onChange: () => {
      scrollLoading();
    },
    threshold: 0,
  });

  // 컴포넌트 출력 함수
  const Dataprint = data && data.map((el) => el.data).map((el) => el.data);
  return (
    <ListWrapper>
      <div className="position">
        {!Dataprint
          ? ""
          : Dataprint.flat().map((el) => {
              return (
                <ListContent
                  id={el.id}
                  key={el.id}
                  title={el.title}
                  creator={el.creator}
                  createdAt={el.createdAt}
                  defaultThumbnail={el.defaultThumbnail}
                  likes={el.likes}
                  comments={el.comments}
                />
              );
            })}
      </div>
      {data &&
      data[data.length - 1].data?.meta.page !==
        data[data.length - 1].data?.meta.totalPage ? (
        <InView>
	// 로딩 시 스피너가 나오도록 함
          <LoaderStyle ref={ref}>
            <FadeLoader color={theme.colors.sign} />
          </LoaderStyle>
        </InView>
      ) : (
        <ListTip />
      )}
    </ListWrapper>
  );
}

디테일

디테일 페이지 부모 컴포넌트 링크

const fetcher = (url: RequestInfo) => fetch(url).then((res) => res.json());

const detail: NextPage = ({
  post,
  blogs,
}: InferGetStaticPropsType<typeof getStaticProps>) => {
  const router = useRouter();
  const result: DataType = post?.data?.data;
  const blogsData = blogs?.data?.data;
  const currentIndex = blogsData?.findIndex(
    (i: { id: number }) => i.id === Number(router.query.id)
  );

	//  이전 글, 다음 글을 위해 인덱스 가져오기
  const next =
    blogsData &&
    blogsData.find((el: object, i: number) => i === currentIndex + 1);

  const prev =
    blogsData &&
    blogsData.find((el: object, i: number) => i === currentIndex - 1);

  return (
    <Layout width="750px">
      <DetailHeader />
      <ContentWrapper>
        <Title title={result?.title} size="33px" margin="45px 0" />
        <Profile
          nickName={result?.creator?.nickName}
          createdAt={result?.createdAt}
          view={result?.views}
          url={result?.defaultThumbnail?.url}
        />
        <BreakLine />
        <Content content={result?.contents} />
        <NavButton />
        <DetailBanner url={result?.thumbnail?.url} />
        <Comment likes={result?.likes} comments={result?.comments} />
        <ListNav prev={prev} next={next} />
      </ContentWrapper>
      <Footer />
    </Layout>
  );
};

// 게시글 id에 따라 동적으로 url 생성
export const getStaticPaths: GetStaticPaths = async () => {
  const res = await fetch(`https://api.dev.coinghost.com/blogs`);
  const data = await res.json();
  const posts = data?.data.data;
  const paths = posts.map((post: { id: number }) => ({
    params: { id: post.id.toString() },
  }));
  return { paths, fallback: true };
};

// pre-rendering을 위한 getStaticProps
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const post = await fetcher(
    `https://api.dev.coinghost.com/blogs/${params?.id}`
  );
  const blogs = await fetcher("https://api.dev.coinghost.com/blogs");
  return { props: { post, blogs } };
};

디테일 페이지 자식 컴포넌트 링크

html-react-parser 이용, JSON 데이터의 블로그 내용 string을 html 코드로 변환

import styled from "styled-components";

function Content({ content }: { content: string }) {
  const parse = require("html-react-parser");
  return <ContentStyle>{parse(content)}</ContentStyle>;
}
const ContentStyle = styled.div`
  width: 100%;
  margin-bottom: 45px;
  font-size: 26px;
  font-weight: normal;
  font-stretch: normal;
  font-style: normal;
  line-height: 1.54;
  text-align: left;
  word-break: break-word;
  color: ${(props) => props.theme.colors.black};
  img {
    width: 100%;
  }
`;

0개의 댓글