처음 개발자에 입문하는 사람이 프론트를 고를지 백엔드를 고를지 고민을 하는데 조금이나마 도움이 되고자 만들게 되었고
심리테스트만 있으면 스코프가 너무 작아 간단한 로드맵과 정보, 게시판을 추가하기로 하였다.
팀원은 4명으로 프론트 2명 백엔드 2명이다
프로젝트 구조이다
프론트에서 쓰인 라이브러리이다
"axios": "^0.27.2",
"cross-env": "^7.0.3",
"dompurify": "^2.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
"react-intersection-observer": "^9.4.0",
"react-query": "^3.39.2",
"react-redux": "^8.0.2",
"react-responsive": "^9.0.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"sockjs-client": "^1.6.1",
"styled-components": "^5.3.5",
css는 항상 styled-component를 사용하는데 tailwind도 사용해봤지만 가독성이 너무 떨어져 팀원과 같이 할때는 유지 보수가 너무 불편하여 항상 styledComponent를 애용하고 있다.
axios도 api를 한꺼번에 관리할수 있게 instance를 만들고 intercepter로 request에 토큰 헤더에 담아서 보낼수 있어 편리해서 항상 사용하고 있다.
리액트 쿼리의 경우 초반에는 리덕스툴킷을 사용했는데 리액트쿼리를 공부하고 사용하다 보니 너무나도 코드량이 줄어들고 재렌더링 시키기 쉽고 여러 기능이 많아 리덕스 보다는 리액트 쿼리를 사용하는 편이다.
무한스크롤구현에 있어 infiniteQuery와 intersection-observer를 사용하니 구현하기 간편하였다.
리덕스는 리액트 쿼리를 잘 사용하지못하는 팀원이 사용했지만 담당하는 파트가 코드체험과 퀴즈부분이여서 리액트 쿼리를 써서 하는것보다 리덕스를 사용것이 좋아 보았다. 따로 데이터 받아올 일이 크게 없고 데이터 받아와서 가공하는 편이 많아 보여 리액트쿼리를 썼다면 할수는 있겠지만 리덕스도 괜찮아 보였다.
react-responsive는 나중에 다른조 코드를 리뷰하다가 보게되었는데
화면 사이즈를 정해놓고 모바일뷰일때랑 웹뷰일때를 나눠서 보여줄수 있어서 현재 웹에서만 보여주고 모바일같이 화면이 작아질때 반응형 웹이 아니다 보니 화면이 작아지면 아예 웹크기를 키워달라고 뷰를 보여주었다.
일단 내가 맡은 파트는 로그인, 메인페이지, 로드맵페이지, 게시판페이지,상세페이지, 마이페이지, 사이드바, 글작성-수정페이지, 댓글 기능 이다
로그인은 소셜로그인으로 카카오와 구글로그인을 진행을 했다.
처음 소셜로그인을 진행하다 보니 공부를 하고 시작을 했는데 서버사이드와 클라이언트 사이드가 있었는데 인가코드만 받아서 서버에 넘겨주고 대부분의 처리를 서버에서 하는 서버사이드 형식으로 진행을 하는데 백쪽에서
Bad Response가 나와서 이틀동안 해결을 못하고 있었다.
계속 에러가 나오다보니 백에서도 자기쪽 잘못이 아닌거 같다. 라고해서 같이 수정해볼려고 코드를 비교해보니
`https://accounts.google.com/o/oauth2/v2/auth? client_id=${GoogleClientId}&redirect_uri= ${process.env.REACT_APP_REDIRECT_GOOGLE} &response_type=code&scope=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email openid`;
이것이 구글로 로그인 하는 URL인데 구글콘솔에서도 요구하는 스코프가 유저 정보랑 이메일 그리고 openid인데 백에서 인가코드 받아서 토큰을 받아올려고 할때 스코프를 잘못지정하여 유저 정보만 가져와서 BadResponse가 나왔던것이였다.
로그인을 해결하고 메인페이지를 완성을 하게 되었다
그리고 사이드 바이다
로그인전
로그인 후
사이드바에서는 현재 페이지를 알수있게 카테고리가 굵게 표시가 된다.
<NavLink end to="/" activeclassname="active">홈</NavLink>
NavLink를 사용한것인데 /일때 원래 exact 없이 어떻게 홈으로 보내나 싶었는데 구글링해보니 end 를 사용하면 같은 기능을 수행할 수있었다.
테스트결과페이지는 특별한거 없이 테스트 결과를 받아와서 api요청 보내 response로 받아온데이터를 보여주기만 하면 되었다.
로드맵페이지
간단한 로드맵이 제공이 되고 해당하는 카테고리를 클릭하면 해당하는 게시글을 무한스크롤로 볼수 있다.
로드맵 컨텐츠를 가져오는 api는
getContent: (data, pageParam, getSort) =>
api.get(
`/api/roadmap/${data.title}/${data.id}?page=${pageParam}&size=7&sortBy=${getSort}`
),
이런형식으로 스텍과 카테고리의 아이디값 그리고 pageParam와 정렬값이 있어야한다.
const [choseCategory, setChoseCategory] = useState({
id: 1,
title: "html",
});
기본값으로 html과 id 1을 가지고 시작한다. 이후 카테고리를 클릭하면 변경되는 식이다.
무한스크롤의 infiniteQuery
const getContent = async (data, pageParam, getSort) => { const res = await RoadmapAPI.getContent(data, pageParam, getSort); return { result: res.data.data, nextPage: pageParam + 1, isLast: res.data.data[0].contentList.length === 7 ? false : true, }; }; const infiniteQuery = useInfiniteQuery( ["contentList", choseCategory, getSort], ({ pageParam = 1 }) => getContent(choseCategory, pageParam, getSort), { getNextPageParam: (lastPage, pages) => { //hasNextPage 대용 if (!lastPage.isLast) { return lastPage.nextPage; } else { return undefined; } }, refetchInterval: 1000, } );
원래 정석이라고 생각하는 부분은 서버 쪽에서 데이터에 isLast같은 경우도 데이터로 담겨서 해당 page의 데이터를 가져오면 그 이후에 데이터가 없으면 true값을 넘겨줘야한다고 생각을 했다 .
하지만 백에서 그렇게 보내기 힘들다고 하니 알아서 구현을 해보았다.
limit가 7이니 마지막 페이지는 7개가 딱떨어지거나 그 이하로 가져오게 되니 그때 isLast를 true로 받았다 이때 딱 7개일경우는 어쩔 수 없이 한번 더 요청을 하게된다.
nextPage의 경우도 마찬가지로 백에서 데이터 넘길때 설정해줘야한다고 생각하는데 못해주기 때문에 +1씩해주는것이로 대체하였다.
또 hasNextPage의 경우에도 없기때문에 isLast로 대체하였다.
refetchInterval은 1초마다 데이터를 새로 받아오기는 하지만 리액트 쿼리가 데이터를 최신화 할려고 쓰는만큼 요청이 많다고 할수 있지만 그만큼 데이터가 최신화 된다는것이다.
그리고 이렇게 데이터를 최신화 할려면 리덕스를 택해서 구현할려고 하면 Websoket이나 SSE를 사용하여 실시간으로 데이터 변화를 받아 올 수 있겠지만 코드도 길어지고 보일러플레이트도 생각보다 많아져 리액트쿼리를 쓴다면 Websoket과 SSE 없이 사용하면 된다고 생각이든다.
저번에도 무한스크롤을 구현했지만 미완성이라고 생각했던부분이 데이터 삭제 수정후 재렌더링 문제때문이였다.
일반적으로 useQuery를 사용하여 무한스크롤을 구현하게 되면 page별로 데이터를 받아오기때문에 무한스크롤 보다는 페이지네이션에 가깝게 된다고 생각이 들어서 useMutation으로 바로 재렌더링 시킬 수 있게 쿼리키값도 생각보다 설정하기 쉬운 infiniteQuery를 사용하니 pageParam의 따른 데이터가 따로 분류된게 아니라 page별로 분류가 되어 한 쿼리키에 담기니 수정해여 재렌더링 시키기가 쉬웠다.
intersection-observer 부분
useEffect(() => { if (inView) { infiniteQuery.fetchNextPage(); } }, [inView]);
{infiniteQuery.data?.pages.map((x, idx) => { return ( <React.Fragment key={idx}> {x?.result[0]?.contentList.map((y, keys) => {if (keys % 7 === 6) {return ( <RoadmapContent ref={ref} key={y.id} querykey={choseCategory} data={y} />); } else { return ( <RoadmapContent key={y.id} querykey={choseCategory} data={y}/>);} })} </React.Fragment> ); })}
inView를 사용하여 7번째마다 ref를 넘겨서 7번째 게시물이 보일때 마다 다음페이지를 가져오도록 설정하였다.