회원가입
리스트
디테일
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%;
}
`;