Untitled


링북 프로젝트 회고

약 한달 간 데브코스 최종 프로젝트를 진행하느라 블로그 글을 적지 못했다. 😥 생각 정리할 시간 조차 없게 바쁘게 한달을 보냈다.

따라서 블로그 관리를 다시 시작하는 의미에서 회고록을 작성한다.

그리고 오늘도 어김없이 KPT 회고 방법을 사용해 작성해보려 한다.




링북

우선 링북은 ‘북마크 소셜 공유 서비스’이다.

Untitled

🐳 링북 배포 사이트

북마크를 하나의 폴더로 묶어 관리할 수 있고, 이를 다른 사용자들에게 공유할 수 있다.

다른 사용자의 폴더에는 댓글과 좋아요를 달 수 있으며, 원하는 폴더의 경우 스크랩을 할 수 있다.

기존 북마크 기능을 가진 여러 플랫폼들은 폴더의 기능이 없거나, 소셜 서비스가 지원되지 않았다.

따라서 이 둘을 합친 북마크 소셜 서비스 링북을 기획하고 개발하게 되었다.


나는 아래와 같은 부분을 맡아 구현하게 되었다.



1. Figma를 통한 디자인 시스템 구축을 위한 노력

저번 프로젝트와 동일하게 이번에도 디자인을 맡아 진행했는데, 배운점이 또 많았다.

Untitled


우선적으로 신경 쓴 부분은 이러하다.

일관성있는 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;


또한 프로토타입을 만들어 비즈니스 로직을 검증하고, 사용자 플로우를 따라가며 웹의 부족한 부분들을 미리 개선할 수 있었다.

Untitled


나는 디자인 역량이 전문가 수준이 아니다. 다만 디자인 시스템 구축을 위한 노력을 했다.

내가 구현한 디자인 시스템은 일반적으로 말하는 디자인 시스템보다는 부족한 점이 많았다. 컴포넌트를 새로 만들어야 할때, 기존에 존재하는 패턴으로만 디자인할 수 있어야 하지만 우리의 프로젝트에서는 충분히 명확한 디자인 가이드를 만들지는 못했다고 생각한다. 😅

하지만 Figma를 통한 와이어프레임, 프로토타입 설계 및 폰트, 색상 변수 관리는 추후 디자이너분들과의 협업에서 굉장히 도움이 될 것이라고 생각한다.



2. 로그인/회원가입/회원정보 수정 모달 + Navigation Bar

로그인, 회원가입, 회원정보 수정 모두 모달에서 작업이 일어난다.

최대한 페이지 분리를 없게 하기 위해 모달을 채택했고, 해당 부분을 구현하게 되었다.

Untitled

Untitled

모달의 데이터 부분에서 많은 고민을 했다. 해당 부분은 하단에서 다룰 예정이다.



3. 게시글 디테일 페이지

folder detail Page의 구현을 맡았다.

folder detail Page는 내용이 보여지는 contentSection과 댓글이 보여지는 commentSection으로 나누어 작업했다.
(API가 분리되어 있어 컴포넌트 또한 분리해서 진행했다.)


ContentSection

Untitled

Untitled

Untitled


CommentSection

Untitled




Keep

1. NextJS

넥제를 처음 도입했다.

링북 사이트는 초기에 불러와야 하는 이미지가 굉장히 많다는 특징이 있다.

중간 프로젝트였던 ‘가봤슈’와 비슷한 레이아웃을 가지고 있다.


  • 가봤슈

Untitled


  • 링북

Untitled


가봤슈 웹 페이지를 개발하며 느꼈던 점은, 메인 페이지의 리스트 부분 이미지가 렌더링 되는 데에 너무나도 긴 시간이 걸려 불편하다는 것이었다.

이러한 문제점을 해결하고자, NextJS를 사용한 SSR을 사용하자는 결론이 나왔다.


SSR(Server Side Rendering)은 서버에서 모든 컨텐츠를 포함해 만든 HTML을 클라이언트 사이드로 보내준다. 따라서 클라이언트는 만들어진 HTML을 보여주기만 하면 된다.

따라서 초기 렌더링 속도가 빠르다.

실제로도 프로젝트를 개발하며 이미지 파일이 많아도 CSR 방식인 가봤슈보다 초기 렌더링 속도가 빠른 것이 체감되었다.



2. ISSUE/ Branch 전략

처음으로 Git의 이슈를 활용했다.

기존 프로젝트에서는 Notion을 통한 작업 단위 관리와 분배가 이루어졌다.

하지만 이번에는 Git을 적극적으로 사용해보자는 팀원들의 의견이 나왔고, Issue를 통한 작업 단위 관리와 분배가 이루어졌다.

Untitled

Untitled


또한 이러한 작업 단위 Issue에 따라 BranchPR을 생성하고 한 명 이상의 Approve가 있을 때에만 머지가 이루어지도록 했다.

Untitled

Issue와 PR은 뱃지를 사용하여 작업 우선순위와 내용을 한 눈에 파악할 수 있게 했다.


또한 Git Project 칸반 보드를 사용하여 작업 진행상황을 파악할 수 있었다.

Untitled

작업이 활발한 상황을 캡쳐하고 싶었지만, 프로젝트가 어느정도 마무리된 후 회고록을 적는지라, 전부 다 Done 상태로 전환되어있다.

각자의 작업량에 밀려 코드 리뷰가 소홀해질 수 있었지만, 팀원 모두가 코드 리뷰에 적극적으로 임해 작업 내용의 피드백이 이루어질 수 있었다. 😎


이 방식을 도입하고 좋았던 점을 정리해보았다.

  • 특정 팀원이 어떤 작업을 하고 있는지 파악하기 쉬웠다.
  • 작업 단위별 Branch가 생성되어 있기 때문에 코드 리뷰 시 해당 브랜치에서 작업 진행상황을 확인하기 좋았다.
  • 한 명 이상의 Approve를 받아야 작업을 머지할 수 있다. 따라서 팀원 모두가 자신의 작업 외에 책임감을 가지고 코드 리뷰에 응했던 것 같다. 덕분에 활발한 피드백이 이루어졌다.
  • Git projectIssue, PR을 연동할 수 있어 전체 작업 상황을 한 눈에 파악하기가 좋았다.




Problem

1. 디렉터리 구조

우리 팀원들은 NextJS를 모두 처음 사용해봤다.

따라서 Pages 디렉터리 내부의 파일로 자동으로 라우팅이 된다는 것은 알고 있었지만, component 폴더 조차 라우팅이 될 줄은 몰랐다.

Untitled

이렇게 접근해보는 사용자는 물론 별로 없을 걸 안다. 하지만 우리에겐 큰 문제였다. 😥

이미 개발이 상당히 진행된 후에 이 문제를 발견했기 때문이다.



2. 좋아요 버튼 / 댓글 Section

우리는 대댓글 기능까지 가능한 댓글 부분을 기획했다.

따라서 사용자의 이름과 댓글 작성일자(또는 한 줄 소개)를 보여주는 Profile Component와 댓글을 작성할 수 있는 CommentInput Component를 적절히 조합해 하나의 Comment Component를 만들었다.

Untitled

단순히 컴포넌트를 구현하고 API를 연동시켜 댓글의 CRUD 기능을 구현할 수 있었다.

하지만 낙관적 업데이트를 간과했다.

Untitled

실시간으로 수정하고 작성되는 댓글과 좋아요 버튼을 보여주기 위해서 계속해서 API 호출을 하는 것은 정말 비효율적이다. 작업이 이루어졌을 때 마다 페이지의 API를 호출하면 화면이 반짝여 사용자에게 좋지 않은 UX를 제공한다. 어떻게 해결하면 좋을지 많은 고민을 했다.



3. 회원가입/로그인/회원정보 수정 모달

모달은 정말 많은 컴포넌트에서 호출되며 다양한 컴포넌트의 비즈니스 로직을 전달받아 대신 수행한다.


3-1. 모달의 show 상태

기존에 구현한 나의 모달 구현 방식은, 모달이 필요한 각 컴포넌트 내부에서 모달의 show 상태를 관리하는 방식이었다. 하지만 이는 모달이 사용되는 각 컴포넌트 내부 상태의 복잡도를 증가시키고 비효율적인 렌더링이 발생한다.

예를 들어 A 컴포넌트B 컴포넌트에서 모달을 사용하고, A와 B 컴포넌트가 동시에 사용되는 경우가 있다고 가정합니다. 이는 두 가지 문제를 발생시킨다. 첫 번째 문제는 모달을 열어야 하는 비즈니스 로직이 겹칠 경우에 모달이 중쳡되어 보여지는 문제가 발생한다. 두 번째로는 모달을 겹치지 않게 열기 위해서는 A 컴포넌트에서 모달을 열고, B 컴포넌트에서는 모달의 show 값을 false로 설정해줘야 하는 불필요한 작업이 이뤄진다.

이러한 비효율적인 패턴을 개선하는 방법에 대해 많이 고민했다.


3-2. 각 모달 내부의 데이터

내가 디자인한 모달은 여러 페이지가 존재한다. 페이지가 넘어가 다른 내용을 보여주는 부분은 각 모달 컴포넌트 내부 page state에 따른 조건부 렌더링으로 해결할 수 있었다.

Untitled


밑의 사진은 FirstLogin 모달의 디자인이다.

Untitled

이 데이터들을 유지하면서 모달의 4번째 페이지에서 회원정보 수정 API를 한번 전송해야 한다.

하지만 내가 구현한 모달의 페이지는 조건부 렌더링을 사용해 각 컴포넌트가 분리되어 있어 데이터 공유가 쉽지 않았다.

어떻게하면 상태 관리 복잡도를 낮출 수 있을까 고민했다.

Untitled




Try

1. 디렉터리 구조

페이지 디렉터리 내부 각 페이지 폴더에서 component들을 따로 관리하던 방식에서, 페이지 컴포넌트들을 따로 분리했다.

Untitled


base Component들은 components 폴더에 따로 관리했다.

Untitled

해당 구조를 채택하니 NextJS 라우팅 문제도 해결할 수 있었고, base Conponent와 각 Page에서만 사용되는 컴포넌트들 또한 구분할 수 있었다.


하지만 컴포넌트를 조합해 Page 컴포넌트들을 만드는데, import depth가 너무 들어간다.

Untitled

따라서 다음 프로젝트를 진행할 땐, Page 컴포넌트들 또한 components 폴더에 넣어도 좋을 것 같다는 생각을 했다.

components
 - base
 - mainPage
 - listPage
 - detailPage
 - errorPage

이러한 방식으로 말이다.

정답은 없다. 또한 회사에 입사하게 되면, 해당 회사의 디렉토리 구조를 따라야 한다. 하지만 계속해서 더 좋은 방법을 고민하고 시도해보는 마음가짐이 중요하다고 생각한다. 😊



2. 좋아요 버튼 / 댓글 Section

낙관적 업데이트를 적용했다.


2-1. 좋아요 버튼

먼저 좋아요 버튼의 낙관적 업데이트 부분이다. 서버에 요청하는 로직 외에, isLikedValueisLikedNum 상태를 따로 두어 사용자에게 좋아요가 눌러진 효과를 보여줄 수 있었다. 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);
  }
};

Untitled


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 = "";
};

Untitled

간단한 방법으로 해결했지만, 해당 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>;

Untitled

개인적으로 관련된 부분끼리 같은 디렉터리 내부에 묶여있어야 한다고 생각해 각 모달 디렉터리 내부에 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를 도입해볼 수 있지 않을까.

프로젝트로 한층 더 성장했지만, 내가 모르는 부분들이 더 많구나를 자각할 수 있었다. 내가 모른다는 것을 모르는 사람보다, 모른다는 것을 알고 더 공부하려고 노력하는 사람이 되어야겠다. 앞으로도 이런 프로젝트 기간처럼 꾸준히 어딘가에 부딫히고 성장하는 시간을 가지지 않을까 싶다.



댓글남기기