링북 Project 회고
링북 프로젝트 회고
약 한달 간 데브코스 최종 프로젝트를 진행하느라 블로그 글을 적지 못했다. 😥 생각 정리할 시간 조차 없게 바쁘게 한달을 보냈다.
따라서 블로그 관리를 다시 시작하는 의미에서 회고록을 작성한다.
그리고 오늘도 어김없이 KPT 회고 방법을 사용해 작성해보려 한다.
링북
우선 링북은 ‘북마크 소셜 공유 서비스’이다.
북마크를 하나의 폴더로 묶어 관리할 수 있고, 이를 다른 사용자들에게 공유할 수 있다.
다른 사용자의 폴더에는 댓글과 좋아요를 달 수 있으며, 원하는 폴더의 경우 스크랩을 할 수 있다.
기존 북마크 기능을 가진 여러 플랫폼들은 폴더의 기능이 없거나, 소셜 서비스가 지원되지 않았다.
따라서 이 둘을 합친 북마크 소셜 서비스 링북을 기획하고 개발하게 되었다.
나는 아래와 같은 부분을 맡아 구현하게 되었다.
1. Figma를 통한 디자인 시스템 구축을 위한 노력
저번 프로젝트와 동일하게 이번에도 디자인을 맡아 진행했는데, 배운점이 또 많았다.
우선적으로 신경 쓴 부분은 이러하다.
일관성있는 UI 디자인과 사용자 경험을 위해 프로젝트에서 사용될 색상과 사용할 폰트, 두께, 크기까지 변수화시켜 사용자가 최대한 일관성있는 경험을 가질 수 있도록 했다.
또한 이를 토대로 style
변수를 만들어 디자인 그대로 웹을 구현할 수 있었다.
import { Theme } from "@emotion/react";
const theme: Theme = {
colors: {
black: ["#000000", "#222222"],
white: ["#FFFFFF"],
main: ["#4285F4"],
mainLight: ["#A0C4FF", "#F9FBFF"],
gray: ["#333333", "#4F4F4F", "#828282", "#BDBDBD", "#E0E0E0", "#F2F2F2"],
},
fontSize: {
t: ["36px"],
h: ["24px", "21px", "15px"],
b: ["18px", "14px", "13px"],
c: ["11px", "12px"],
l: ["45px", "36px", "32px"],
},
};
export default theme;
또한 프로토타입을 만들어 비즈니스 로직을 검증하고, 사용자 플로우를 따라가며 웹의 부족한 부분들을 미리 개선할 수 있었다.
나는 디자인 역량이 전문가 수준이 아니다. 다만 디자인 시스템 구축을 위한 노력을 했다.
내가 구현한 디자인 시스템은 일반적으로 말하는 디자인 시스템보다는 부족한 점이 많았다. 컴포넌트를 새로 만들어야 할때, 기존에 존재하는 패턴으로만 디자인할 수 있어야 하지만 우리의 프로젝트에서는 충분히 명확한 디자인 가이드를 만들지는 못했다고 생각한다. 😅
하지만 Figma
를 통한 와이어프레임, 프로토타입 설계 및 폰트, 색상 변수 관리는 추후 디자이너분들과의 협업에서 굉장히 도움이 될 것이라고 생각한다.
2. 로그인/회원가입/회원정보 수정 모달 + Navigation Bar
로그인, 회원가입, 회원정보 수정 모두 모달에서 작업이 일어난다.
최대한 페이지 분리를 없게 하기 위해 모달을 채택했고, 해당 부분을 구현하게 되었다.
모달의 데이터 부분에서 많은 고민을 했다. 해당 부분은 하단에서 다룰 예정이다.
3. 게시글 디테일 페이지
folder detail Page
의 구현을 맡았다.
folder detail Page
는 내용이 보여지는 contentSection
과 댓글이 보여지는 commentSection
으로 나누어 작업했다.
(API가 분리되어 있어 컴포넌트 또한 분리해서 진행했다.)
ContentSection
CommentSection
Keep
1. NextJS
넥제를 처음 도입했다.
링북 사이트는 초기에 불러와야 하는 이미지가 굉장히 많다는 특징이 있다.
중간 프로젝트였던 ‘가봤슈’와 비슷한 레이아웃을 가지고 있다.
- 가봤슈
- 링북
가봤슈 웹 페이지를 개발하며 느꼈던 점은, 메인 페이지의 리스트 부분 이미지가 렌더링 되는 데에 너무나도 긴 시간이 걸려 불편하다는 것이었다.
이러한 문제점을 해결하고자, NextJS
를 사용한 SSR
을 사용하자는 결론이 나왔다.
SSR
(Server Side Rendering)은 서버에서 모든 컨텐츠를 포함해 만든 HTML을 클라이언트 사이드로 보내준다. 따라서 클라이언트는 만들어진 HTML을 보여주기만 하면 된다.
따라서 초기 렌더링 속도가 빠르다.
실제로도 프로젝트를 개발하며 이미지 파일이 많아도 CSR 방식인 가봤슈보다 초기 렌더링 속도가 빠른 것이 체감되었다.
2. ISSUE/ Branch 전략
처음으로 Git
의 이슈를 활용했다.
기존 프로젝트에서는 Notion
을 통한 작업 단위 관리와 분배가 이루어졌다.
하지만 이번에는 Git
을 적극적으로 사용해보자는 팀원들의 의견이 나왔고, Issue
를 통한 작업 단위 관리와 분배가 이루어졌다.
또한 이러한 작업 단위 Issue
에 따라 Branch
및 PR
을 생성하고 한 명 이상의 Approve
가 있을 때에만 머지가 이루어지도록 했다.
Issue
와 PR은 뱃지를 사용하여 작업 우선순위와 내용을 한 눈에 파악할 수 있게 했다.
또한 Git Project
칸반 보드를 사용하여 작업 진행상황을 파악할 수 있었다.
작업이 활발한 상황을 캡쳐하고 싶었지만, 프로젝트가 어느정도 마무리된 후 회고록을 적는지라, 전부 다 Done 상태로 전환되어있다.
각자의 작업량에 밀려 코드 리뷰가 소홀해질 수 있었지만, 팀원 모두가 코드 리뷰에 적극적으로 임해 작업 내용의 피드백이 이루어질 수 있었다. 😎
이 방식을 도입하고 좋았던 점을 정리해보았다.
- 특정 팀원이 어떤 작업을 하고 있는지 파악하기 쉬웠다.
- 작업 단위별
Branch
가 생성되어 있기 때문에 코드 리뷰 시 해당 브랜치에서 작업 진행상황을 확인하기 좋았다. - 한 명 이상의
Approve
를 받아야 작업을 머지할 수 있다. 따라서 팀원 모두가 자신의 작업 외에 책임감을 가지고 코드 리뷰에 응했던 것 같다. 덕분에 활발한 피드백이 이루어졌다. Git project
와Issue
,PR
을 연동할 수 있어 전체 작업 상황을 한 눈에 파악하기가 좋았다.
Problem
1. 디렉터리 구조
우리 팀원들은 NextJS
를 모두 처음 사용해봤다.
따라서 Pages 디렉터리 내부의 파일로 자동으로 라우팅이 된다는 것은 알고 있었지만, component
폴더 조차 라우팅이 될 줄은 몰랐다.
이렇게 접근해보는 사용자는 물론 별로 없을 걸 안다. 하지만 우리에겐 큰 문제였다. 😥
이미 개발이 상당히 진행된 후에 이 문제를 발견했기 때문이다.
2. 좋아요 버튼 / 댓글 Section
우리는 대댓글 기능까지 가능한 댓글 부분을 기획했다.
따라서 사용자의 이름과 댓글 작성일자(또는 한 줄 소개)를 보여주는 Profile Component
와 댓글을 작성할 수 있는 CommentInput Component
를 적절히 조합해 하나의 Comment Component
를 만들었다.
단순히 컴포넌트를 구현하고 API
를 연동시켜 댓글의 CRUD
기능을 구현할 수 있었다.
하지만 낙관적 업데이트를 간과했다.
실시간으로 수정하고 작성되는 댓글과 좋아요 버튼을 보여주기 위해서 계속해서 API 호출을 하는 것은 정말 비효율적이다. 작업이 이루어졌을 때 마다 페이지의 API
를 호출하면 화면이 반짝여 사용자에게 좋지 않은 UX를 제공한다. 어떻게 해결하면 좋을지 많은 고민을 했다.
3. 회원가입/로그인/회원정보 수정 모달
모달은 정말 많은 컴포넌트에서 호출되며 다양한 컴포넌트의 비즈니스 로직을 전달받아 대신 수행한다.
3-1. 모달의 show 상태
기존에 구현한 나의 모달 구현 방식은, 모달이 필요한 각 컴포넌트 내부에서 모달의 show 상태를 관리하는 방식이었다. 하지만 이는 모달이 사용되는 각 컴포넌트 내부 상태의 복잡도를 증가시키고 비효율적인 렌더링이 발생한다.
예를 들어 A 컴포넌트와 B 컴포넌트에서 모달을 사용하고, A와 B 컴포넌트가 동시에 사용되는 경우가 있다고 가정합니다. 이는 두 가지 문제를 발생시킨다. 첫 번째 문제는 모달을 열어야 하는 비즈니스 로직이 겹칠 경우에 모달이 중쳡되어 보여지는 문제가 발생한다. 두 번째로는 모달을 겹치지 않게 열기 위해서는 A 컴포넌트에서 모달을 열고, B 컴포넌트에서는 모달의 show
값을 false
로 설정해줘야 하는 불필요한 작업이 이뤄진다.
이러한 비효율적인 패턴을 개선하는 방법에 대해 많이 고민했다.
3-2. 각 모달 내부의 데이터
내가 디자인한 모달은 여러 페이지가 존재한다. 페이지가 넘어가 다른 내용을 보여주는 부분은 각 모달 컴포넌트 내부 page state
에 따른 조건부 렌더링으로 해결할 수 있었다.
밑의 사진은 FirstLogin
모달의 디자인이다.
이 데이터들을 유지하면서 모달의 4번째 페이지에서 회원정보 수정 API를 한번 전송해야 한다.
하지만 내가 구현한 모달의 페이지는 조건부 렌더링을 사용해 각 컴포넌트가 분리되어 있어 데이터 공유가 쉽지 않았다.
어떻게하면 상태 관리 복잡도를 낮출 수 있을까 고민했다.
Try
1. 디렉터리 구조
페이지 디렉터리 내부 각 페이지 폴더에서 component
들을 따로 관리하던 방식에서, 페이지 컴포넌트들을 따로 분리했다.
base Component
들은 components
폴더에 따로 관리했다.
해당 구조를 채택하니 NextJS
라우팅 문제도 해결할 수 있었고, base Conponent
와 각 Page에서만 사용되는 컴포넌트들 또한 구분할 수 있었다.
하지만 컴포넌트를 조합해 Page 컴포넌트들을 만드는데, import depth
가 너무 들어간다.
따라서 다음 프로젝트를 진행할 땐, Page 컴포넌트들 또한 components
폴더에 넣어도 좋을 것 같다는 생각을 했다.
components
- base
- mainPage
- listPage
- detailPage
- errorPage
이러한 방식으로 말이다.
정답은 없다. 또한 회사에 입사하게 되면, 해당 회사의 디렉토리 구조를 따라야 한다. 하지만 계속해서 더 좋은 방법을 고민하고 시도해보는 마음가짐이 중요하다고 생각한다. 😊
2. 좋아요 버튼 / 댓글 Section
낙관적 업데이트를 적용했다.
2-1. 좋아요 버튼
먼저 좋아요 버튼의 낙관적 업데이트 부분이다. 서버에 요청하는 로직 외에, isLikedValue
와 isLikedNum
상태를 따로 두어 사용자에게 좋아요가 눌러진 효과를 보여줄 수 있었다. isLikedValue
는 사용자가 좋아요를 누른 상태를 나타내는 boolen
값이며, isLikedNum
은 폴더의 좋아요 개수를 나타내는 number
값이다. 좋아요 취소 또한 같은 방식으로 진행할 수 있었다.
const handleClickAddLike = async () => {
if (!user) {
alert("로그인 후 가능합니다.");
setShowModalStatus(showLoginModal);
return;
} // 로그인을 하지 않은 사용자 예외처리
try {
await createLike(folderId, user.id, token);
setIsLikedValue(true);
setLikesNum(likesNum + 1);
} catch (error) {
alert("문제가 발생했습니다.");
console.log(error);
}
};
2-2. 댓글 Section
commentSection Page Component
에서 comments
상태를 만들고 초기 상태를 getFolderComment API
로 받아오는 서버 데이터를 넣었다.
// commentSection.tsx 일부
useEffect(() => {
if (!id) return;
(async () => {
try {
const commentRes = await getFolderComment(id);
setData(commentRes);
setComments(commentRes.comments); // 쨔잔
} catch (error) {
console.log(error);
}
})();
}, [id]);
이후로 추가되거나 삭제되는 comments
들에 대해서는 API 호출과 동시에, 해당 comment
상태 또한 업데이트 해줬다.
const handleCreateComment = (
id: number,
parentId: number,
content: string,
user: User
) => {
const newComment = {
id,
children: [] as Comments[],
content,
user,
createdAt: getDate(),
};
if (parentId) {
setComments(
comments.map((comment: any) =>
comment.id === parentId
? { ...comment, children: [...comment.children, newComment] }
: comment
)
);
inputRef.current.value = "";
return;
}
setComments([...comments, newComment]);
inputRef.current.value = "";
};
간단한 방법으로 해결했지만, 해당
comments
배열을props
로 계속 내려줘야 하는 문제가 있었다. 추후에contextAPI
로 관리할 수 있지 않을까 싶다.
3. 회원가입/ 로그인 모달
3-1. 모달의 show 상태 전역 관리
위에서도 서술했듯, 모달은 정말 다양한 컴포넌트에서 사용된다. 따라서 모달이 열리고 닫히는 show
상태를 전역으로 관리하는 방법을 채택했다.
Recoil
을 사용하여 전역으로 모달의 show
값을 관리했다. 프로젝트에서 사용되는 Login, SignUp, FirstLogin, User
모달 컴포넌트의 show
값을 객체로 묶어 recoil
에서 관리했다.
Login
모달이 열리면 SignUp
모달은 닫혀야한다 즉 필수적으로 한번에 하나의 모달만 열어야 하기 때문에 객체 형태로 관리하게 되었다.
// recoil/showModal.tsx
import { atom } from "recoil";
export const showModalStatus = atom({
key: "showModalStatus",
default: {
Login: false,
SignUp: false,
FirstLogin: false,
User: false,
},
});
모달을 열고 닫는 로직은 모달이 필요한 각 컴포넌트에서 수행한다. 이 과정에서 객체 형태의 전역 값을 전부 적어주기에는 가독성이 좋지 않아, 모달이 열리고 닫히는 상태값은 상수로 관리했다.
따라서 각 컴포넌트에서 useRecoilState
를 사용하여 간단하게 모달을 조작할 수 있었으며 모달의 중첩 렌더링 문제도 해결할 수 있었다.
// 모달 상수
// constants/modal.constants.ts
export const closeModal = {
Login: false,
SignUp: false,
FirstLogin: false,
User: false,
};
export const showLoginModal = {
Login: true,
SignUp: false,
FirstLogin: false,
User: false,
};
// 이후 값은 생략합니다
// 사용 예시
// 1. MyFolderAreaLogOut 컴포넌트
// ...
<S.SignUpButton
type="submit"
onClick={
() => setShowModal(showSignUpModal)
}
>
회원가입
</S.SignUpButton>
// 2. NavigationBar 컴포넌트
// ...
<Button
type="button"
version="navBar"
onClick={
() => setShowModalStatus(showLoginModal)
}
>
로그인
</Button>
3-2. 모달 종류마다의 내부 데이터 지역 상태 관리
모달 종류마다 각각 사용되는 상태는 ContextAPI
를 사용해 데이터를 관리하게 되었다.
// FirstLogin 모달 일부
return <UserInfoProvider>{switchPage(page)}</UserInfoProvider>;
// SignUp 모달 일부
return <UserProvider>{switchPage(page)}</UserProvider>;
개인적으로 관련된 부분끼리 같은 디렉터리 내부에 묶여있어야 한다고 생각해 각 모달 디렉터리 내부에 contexts 디렉터리를 따로 생성했다.
다음은 FirstLogin
모달 컴포넌트 Page01
컴포넌트의 일부이다.
import { useUserInfo } from "../contexts/UserInfoProvider";
// ...
const { userInfo, setUserName } = useUserInfo();
const handleClickStoreName: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
const nameValue = nameRef.current.value;
const res = setUserName(nameValue); // userName ContextAPI 사용하여 저장
if (typeof res === "string") {
setErrorText(res);
return;
}
handleNextPage(e);
};
ContextAPI
내부에 작성된 setUserName
을 사용하여 이름을 저장하고, 조건에 맞지 않을 시 에러 메세지를 반환한다. 데이터가 올바르며 저장이 되었으면 true
를 반환하여 다음 페이지로 넘어가는 작업이 수행된다. 다음 페이지에서 입력이 요구되는 유저의 한 줄 소개, 관심사, 사진도 똑같은 방식으로 저장된다.
따라서 각 페이지에서 입력되는 데이터를 각 모달 내부에서 관리할 수 있었고, 한번의 API
호출 만으로 유저가 입력한 여러 정보를 전달할 수 있었다.
이 방법이 좋은지는 모르겠다. recoil의 selector를 사용하면 보다 쉽게 상태를 관리할 수 있을 것 같다. 조금 더 깊게 공부한 후 리팩토링 과정을 거쳐야 할 것 같다.
소감
링북 프로젝트는 한달이라는 짧은 기간 동안 진행되었다.
너무 좋았던 점은, 지금까지 해보지 않았던 부분에 대해 많이 도전해봤다는 것이었다. 로그인/회원가입 로직과 동시에 ContextAPI
적극 활용까지 저번 프로젝트보다는 확실히 조금 더 많은 분야를 구현할 수 있었다.
프로젝트 기간이 끝나고 가장 먼저 들었던 생각은 ‘아쉽다’ 였다. 생각보다 많은 기능을 구현했지만, 기능의 완성도는 낮았다. 자잘한 이슈들을 캐치하지 못했고 기능의 우선적인 구현만 바라보며 한 달 동안 달렸던 것 같다.
또한 NextJS
의 기능을 제대로 활용하지 못해서 너무 아쉬웠다. getServerSideProps
를 활용하는 부분은 사용자의 자동 로그인 여부 확인 로직밖에 없다. NextJS
를 활용해서 얻은 이점이 빠른 초기 렌더링 속도밖에 없는 것 같다. 리팩터링 과정에서 조금 더 적극적으로 NextJS
를 도입해볼 수 있지 않을까.
프로젝트로 한층 더 성장했지만, 내가 모르는 부분들이 더 많구나를 자각할 수 있었다. 내가 모른다는 것을 모르는 사람보다, 모른다는 것을 알고 더 공부하려고 노력하는 사람이 되어야겠다. 앞으로도 이런 프로젝트 기간처럼 꾸준히 어딘가에 부딫히고 성장하는 시간을 가지지 않을까 싶다.
댓글남기기