티스토리 뷰
1월 6일부터 20일까지 약 2주간 진행했던 프롱이들의 첫 팀 프로젝트가 끝났다.
프로젝트가 끝나고 바로 쓰려고 했지만, 구현하고 싶었던 좋아요 기능 리팩터링과 설 연휴 때문에 이제야 쓰게 되었다.
목차
HIT이 뭔가요?
기술 스택
와이어프레임
기능 구현
마치며
HIT이 뭔가요?
How about IT의 줄임말로 IT기기 리뷰 서비스다.
처음엔 "리뷰잇"으로 시작해서 "이거어때?" => "How about IT" => "HIT" 으로 이름이 진화했다.
(3depth 이상으로 깊이 생각하는 우리 나영팀..⭐)
데브코스의 프로젝트 요구사항이 소셜 네트워크 서비스를 만드는 것이였고, 개발자 지망생인 우리들도 "이 서비스는 사용하겠다." 싶은걸로 주제를 선정했다.
기술 스택
HIT의 기술 스택은 다음과 같다.
TypeScript & React
데브코스 강의에서 타입스크립트를 배우긴 했지만 리액트와 함께 프로젝트에 적용 해본적은 없어서 잘 할수 있을까 걱정이 됐었다. 하지만 요즘 많은 회사들이 타입스크립트를 선호하기도 하고, 항상 React.js만 써왔어서 React.ts는 어떨지 궁금했다.
정적 타입의 컴파일 언어라서 코드 작성 단계에 미리 타입을 체크할 수 있기 때문에 자바스크립트로 개발했을 때보다 확실히 에러는 거의 안보였던 것 같다. 다만, 그만큼 타입 선언을 하는 코드량이 많아지고 매번 타입을 결정해야해서 살짝 번거로웠던 것 같다. (빨간줄 싫어)
Vite
이번 프로젝트를 하며 써본 기술들 중에 가장 신세계였던건 바로 Vite지 않나 싶다.
기존의 빌드 속도보다 대략 100배나 빠른 도구인 esbuild로 만들어졌고 실제로 사용해보니 웹팩만 사용했던 기존 프로젝트보다 몇배는 빨라진걸 체감할 수 있었다. HIT프로젝트가 끝나고 개인 프로젝트 리팩터링을 잠깐 했었는데 진짜 "원래 이렇게 느렸었나??" 싶을 정도로 비교가 많이 됐다.
Tailwind CSS
우리팀은 더 나은 UI를 위해 DaisyUI 라이브러리를 사용했다.
DaisyUI는 Use Tailwind CSS but write fewer class names 이라는 문구처럼 실제로 정말 짧은 클래스명으로 쉽게 디자인이 가능했다.
하지만 그것만으로 모든걸 커버할 순 없어서 Tailwind를 사용하여 추가적인 CSS 작업을 했는데 Tailwind에 지정된 클래스 명을 찾는게 시간이 더 걸렸다..😅 물론 이것도 처음 써보는거라 어쩔 수 없었지만 아마 앞으로 쓰진 않을 것 같다..
지금 생각해보면 DaisyUI를 제외한 다른 CSS작업은 Emotion을 써서 재사용할 수 있게 만드는 쪽이 더 나았을 것 같다.
와이어 프레임
디자인은 Figma로 작업 했다. Figma를 조금이나마 쓸 줄 아는 건오님과 별님이 디자인을 맡아서 해주셨고 나머지 인원은 CS스터디와 겹쳐서 스터디를 하고 올 동안 디자인을 해주셨다.
초반 프로젝트를 기획할 땐 사람들이 편리하게 사용하기 위해 모바일 뷰로 작업하기로 했는데, 이 프로젝트를 포트폴리오로 사용하면 대부분의 사람들이 데스크톱 환경에서 보시지 않을까 하는 생각에 데스크톱, 모바일 둘 다 사용할 수 있는 반응형으로 만들기로 결정했다.
데스크톱 디자인은 따로 디자인하지 않고 인스타그램의 레이아웃을 참고하면서 바로 개발했다.
라우팅 세팅을 내가 맡아서 했는데 라우팅 테스트를 하는 김에 네비게이션까지 내가 구현하게 되었다.
Tailwind에서 미디어 쿼리를 사용하는 방법은 저런 키워드로 간단하게 사이즈를 지정해 줄 수 있고 이 부분은 엄청 간편해서 꽤나 마음에 들었다.
레이아웃 관련된 클래스 네임만 간추려보자면 아래와 같다.
// BottomNavigation
<div className="fixed z-[100] md:hidden bottom-0 max-w-3xl w-full">
...
</div>
// SideNavigation
<div className="fixed z-[100] w-60 left-0 h-full max-md:hidden max-xl:w-16">
...
</div>
BottomNavigation과 SideNavigation을 분리해서 작업했고, BottomNavigation은 width가 768px이상이 되면
display: hidden되는 식으로 구현했다.
글을 쓰다보니 위 태그들은 nav 태그로 감싸면 더 좋은 코드가 될 것 같다는 생각이 든다.
시맨틱 태그를 습관화 하자!
기능 구현
기능 구현은 크게 페이지 단위로 나누었고 필요한 기능, 컴포넌트가 있으면 추가적으로 개발하는 식으로 진행했다.
내가 이 프로젝트에서 구현한 기능들은 다음과 같다.
모달창 컴포넌트
리뷰 게시글 상세페이지
게시글 삭제, 수정
게시글 좋아요
게시글 댓글
모달창 컴포넌트
우리는 DaisyUI 라이브러리에 있는 Modal 컴포넌트를 사용했다.
창이 부드럽게 열리고 무엇보다 모바일 환경에서 bottom-up 형식으로 열리는 모달창이 마음에 들었다.
기본적으로 Modal이나 Tooltip같은 컴포넌트들은 최상위로 "튀어나오는" UI를 갖고 있다. 그리고 이런 요소들은 z-index, overflow: hidden 같은 부모컴포넌트의 CSS에 영향을 받을 수 있다.
때문에 DOM트리에서 부모 컴포넌트의 영향을 받지 않도록 최상위 계층으로 옮기는 작업이 필요한데, React Portal을 사용하면 쉽게 구현할 수 있다.
/* index.html */
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HIT</title>
<script type="module" src="/src/main.tsx"></script>
</head>
<body>
<div id="root"></div>
<div id="modal-root"></div>
<div id="toast-root"></div>
</body>
</html>
리액트의 가장 최상위 계층인 "root" 옆에 형제 요소로 id="modal-root"라는 엘리먼트를 만들어주고,
// components/Modal/ModalPortal.ts
import { ReactNode } from 'react';
import ReactDom from 'react-dom';
type ModalProps = {
children: ReactNode;
};
const ModalPortal = ({ children }: ModalProps) => {
const portalId = "modal-root";
const element = document.getElementById(portalId) as HTMLElement;
if(element) {
return ReactDom.createPortal(children, element);
} else {
const portalContainer = document.createElement('div');
portalContainer.id = portalId;
portalContainer.appendChild(element);
return ReactDom.createPortal(children, element);
}
};
export default ModalPortal;
ReactDom.createPortal 메서드에 첫번째 인자로 화면에 띄울 모달컴포넌트, 두번째 인자로(모달을 띄울 위치) 엘리먼트 이렇게 각각 넣어주었다. 엘리먼트를 못찾을 경우를 대비하여 예외처리 코드도 넣어주었다.
또 우리 팀의 코딩 컨벤션으로 2depth 이상 내려줘야하는 prop, 부모 or 형제 컴포넌트 사이의 prop 전달들은 prop-drilling을 막기 위해 전역상태로 관리하기로 했다. 모달창은 모달창을 띄우거나 닫을때 부모 컴포넌트에 true, false를 전달해줘야하기 때문에 전역상태로 관리하는게 더 효율적이라 생각했고, Recoil로 전역상태관리를 했다.
리뷰 게시글 상세페이지
팀원들이 디자인을 이쁘고 깔끔하게 해준 덕분에 화면 그리는건 금방 끝낼 수 있었다.
대신 컴포넌트 구조에 대해 고민을 하면서 작업을 했다.
상세페이지 안에 생각보다 많은 기능들이 있고, 그만큼 api를 호출해야했다. 먼저 구조를 잡지 않고 개발을 하면 하나의 컴포넌트에 많은 로직이 들어갈 것 같아서 컴포넌트 구조를 먼저 잡고 개발을 진행했다.
리뷰 게시글 삭제, 수정
리뷰 게시글의 삭제와 수정은 본인이 작성한 게시글에서만 가능하고, ContentHandler라는 컴포넌트로 분리를 했다.
유저 프로필과 글 수정을 클릭 시 페이지 이동을 해야하고, 삭제 버튼을 누르면 정말 삭제할건지 다시 물어보는 모달창을 띄워줘야했다
각각 따로 함수를 만들면 중복 로직이 발생할 것 같아서 클릭한 요소가 어떤건지 문자열을 매개변수로 받고 switch-case문으로 처리를 해줬다.
// ContentHandler.tsx
const onMovePage = async (page: 'userPage' | 'reviewUpdate' | 'reviewDelete') => {
const updateBodyProp = {
postId: id,
title,
image,
channel: channel,
};
switch (page) {
case 'userPage':
navigate(USER_PAGE, { state: { id: author._id } });
return;
case 'reviewUpdate':
navigate(UPDATE_REVIEW_PAGE, { state: updateBodyProp });
return;
case 'reviewDelete':
setOpen(true);
return;
}
};
그리고 글 수정과 삭제는 Button이라는 컴포넌트로 따로 분리를 해서 공통 컴포넌트로 묶어줬다.
// ContentHandler.tsx
return(
...
<div className="flex items-center gap-2 text-TEXT_BASE_BLACK">
<span>{formatDate(createdAt)}</span>
{userId === author._id && (
<>
<Button
childrenIcon={<FaRegEdit className="text-xl" />}
tooltipText="글 수정"
page="reviewUpdate"
onMovePage={onMovePage}
/>
<Button
childrenIcon={<BsTrash className="text-xl" />}
tooltipText="글 삭제"
page="reviewDelete"
onMovePage={onMovePage}
/>
</>
)}
</div>
...
);
// ReviewDetail/ReviewContent/Button.tsx
import React from 'react';
type ContentButtonType = {
childrenIcon: React.ReactNode;
tooltipText: string;
page: 'userPage' | 'reviewUpdate' | 'reviewDelete';
onMovePage: (page: 'userPage' | 'reviewUpdate' | 'reviewDelete') => void;
};
const Button = ({ childrenIcon, tooltipText, page, onMovePage }: ContentButtonType) => {
return (
<span
onClick={() => onMovePage(page)}
className="hover:cursor-pointer tooltip tooltip-bottom"
data-tip={`${tooltipText}`}
>
{childrenIcon}
</span>
);
};
export default Button;
글 쓰면서 틈틈히 아쉬운 부분들을 회고 해보자면
- onMovePage 함수
- async는 빼줘도 된다.
- 네이밍이 아쉽다. - reviewDelete는 페이지 이동이 아니기 때문에
- Button 컴포넌트
- 컴포넌트 네이밍이 아쉽다. - 자칫 다른 Button 컴포넌트와 혼동을 줄 수도 있을 것 같다.
(ContentHandlerButtons 이게 나으려나... 이름 짓는건 항상 어렵다. 😓)
- 컴포넌트 네이밍이 아쉽다. - 자칫 다른 Button 컴포넌트와 혼동을 줄 수도 있을 것 같다.
게시글 좋아요
해당 게시글을 좋아요, 좋아요 취소 할 수 있는 기능을 구현했다.
데브코스에서 제공해주는 API로 프로젝트를 진행하는데 API문서가 살짝... 불친절해서.. 시간이 꽤 걸린 작업이었다.
DELETE 요청을 할 때 id를 같이 보내주는데 이게 post_id인지 user_id인지 해당 좋아요의 id인지 안적혀 있어서 시간을 좀 허비했다. 😂 (당연히 post_id인줄 알았던 나)
그리고 좋아요 버튼을 디바운싱으로 구현해보고 싶었는데 이 좋아요 관련 내용은 좀 길어서 따로 포스팅 해뒀다.
게시글 댓글
ReviewCommentInput 컴포넌트에서 댓글을 작성하면 ReviewCommentList에 바로바로 적용이 되게 만들고 싶었다.
이 둘은 서로 형제 컴포넌트라서 Recoil로 상태관리를 해줬고, ReviewCommentList에서 값이 들어올 때마다 렌더링을 해줘야하기 때문에 useEffect의 의존성 배열로 commentState value를 넣어줬다.
// ReviewCommentInput.tsx
const setCreatedComment = useSetRecoilState(commentState);
...
const onCreateComment = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = await callCreateCommentAPI(callCreateCommentAPIBody);
setCreatedComment(data);
if (data.user !== author._id) {
const createAlarmAPIBody = {
notificationType: COMMENT,
notificationTypeId: data._id,
userId: author._id,
postId: data.post,
};
await callCreateAlarmAPI(createAlarmAPIBody);
}
setComment('');
};
setCreateComment함수에 POST요청한 응답 데이터를 넣어주고, 부모 컴포넌트인 ReviewDetail에서 commentState에 값이 들어오면 리렌더링을 해준다.
// ReviewDetail.tsx
// 댓글 관련 state
const [commentList, setCommentList] = useState();
const createdComment = useRecoilValue(commentState);
...
useEffect(() => {
const getReviewDetail = async () => {
try {
const data = await callGetReviewDetailAPI(id);
const user = await getUserId();
setReviewContent(data);
setCommentList(data.comments);
setReviewContentHandler({
userId: user._id,
author: data.author,
title: data.title,
image: data.image,
createdAt: data.createdAt,
channel: data.channel,
});
setLikeState({ likes: data.likes, id });
} catch (error) {
console.log(error);
}
};
getReviewDetail();
}, [createdComment]);
마치며
이번 팀 프로젝트가 나의 첫 협업 프로젝트였다.
평소에 코드 저장소 용도로만 썼던 Git을 이번 팀 프로젝트를 하며 제대로 쓴 것 같은 경험을 했고 팀원들에게 피해가 가지 않게 잘 모르겠거나 애매하다 싶으면 물어보고 또 물어보면서 했다. 그때마다 친절하게 알려준 팀원들에게 너무 고맙고 덕분에 협업에서 사용하는 Git에 대해 많이 알게 되었다.
타입스크립트 지식이 많이 부족한걸 느꼈다.
매번 타입을 지정해줘야 하는 번거로움이 있지만 이번 프로젝트보다 훨씬 규모가 큰 프로젝트에서는 타입스크립트를 꼭 사용해야 "미래의 내가 고생하지 않겠구나" 라는 생각이 들었다. 타스 공부하자!
개인 과제 코드리뷰보다 이번 코드리뷰를 더 꼼꼼하게 했던 것 같다.
아무래도 개인 플젝이 아니라 팀 프로젝트기 때문에 더 나은 코드를 위해 눈에 불을 켜고 리뷰 했던 것 같다. 이런 과정을 거쳐서 그런지 예전보다 어떻게 코드를 짜는게 좀 더 클린한 코드인지 알게 되었고 많이 배울 수 있었다.
내일 최종 프로젝트를 위한 마지막 팀 변경이 이루어지는데 오늘이 공식적인 나영팀 마지막 날이다. 😥
새로운 팀 배정이 되면 내가 이번 팀원들에게 도움을 받았던 만큼 다음 팀에선 도움을 많이 주는 역할이 되었으면 한다! 🤩
'프로그래머스 데브코스 > 프로젝트' 카테고리의 다른 글
좋아요 버튼 디바운싱 구현 (3) | 2023.01.27 |
---|---|
VanillaJS로 Notion 클로닝 개인 프로젝트 회고 (0) | 2022.11.22 |
- Total
- Today
- Yesterday
- 토이 프로젝트
- propTypes
- 프로그래머스
- React.Memo
- 알고리즘
- 네트워크
- JavaScript
- 프로그래머스 데브코스 FE
- 노션 클로닝 프로젝트
- 배열의 메서드
- 스코프
- kdt
- CORS
- 번들러
- 힙
- 라이프사이클
- 프로그래머스 데브코스
- Recoil
- 회고
- 교착상태
- useMemo
- 호이스팅
- jwt
- 리액트
- 프로세스 동기화
- 코딩테스트
- 원티드 프리온보딩 챌린지
- 프로젝트 회고
- 웹 브라우저 객체
- 무한스크롤
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |