인턴 회고록 pt.1
2022/09/06
n°9
category : Recap
☼
2022/09/06
n°9
category : Recap
회원가입 페이지
리스트 페이지
디테일 페이지
1.회원가입 페이지
핸드폰 번호로 인증번호를 요청과 입력한 인증번호 검증을 요청하는 fetch 함수
이 페이지에서는 유일하게 api와 통신하는 함수였기 때문에 기억에 남는다.
처음 react-hook-form과 함께 사용하였기 때문에 조금 더 난이도 있게 느껴졌었던 것 같다.
현직에서는 이 함수들을 libs/ 같은 폴더에 분리하여 import해서 사용했다.
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: "잘못된 번호입니다.",
});
}
};
2.리스트 페이지
리스트 페이지의 무한스크롤 구현을 위한 컴포넌트 코드이다.
SWR을 사용하면 fetch로 받은 데이터를 useEffect로 주입시켜주는 과정이 생략되는데,
그게 너무 혁명적으로 다가와서 감동받았다 (..)
(실제로도 현직에서 swr을 사용했기에 useEffect로 fetching 데이터 관리를 할 일이 잘 없었다.)
또한 처음으로 SSG같은 nextjs의 고유한 기능과 swr을 이용해 쿼리를 업데이트하는 페이지였기 때문에 더욱 특별하게 기억에 남는다.
돌아보니 내 개인 블로그를 개발할 때 큰 밑거름이 되어준 것 같다.
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>
);
}
3.디테일 페이지
NextJs의 꽃인 Pre-Rendering을 경험할 수 있어서 특별히 기억에 남는다.
개발자 도구의 네트워크 창에서 pre-render된 페이지를 보고 꽤나 뿌듯해하던 기억이 난다.
html을 열어보았을 때 <div id="root"> 대신에 (fetching된 데이터임에도) 코드가 미리 다 찍혀있는 것도 신기했다.
SSG는 맛보기라고만 생각했는데, 지나고보니 현직에서도 디테일 페이지는 SSG를 많이 썼기 때문에 요긴했다.
당시에 조금 더 fallback에 대해 살펴보면 좋았었겠지만, 여튼 때가 되니 다 알게 되었다.
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%;
}
`;