2023년 9월 회고 (괴발개발 프로젝트 회고)

dlwl98·2023년 10월 3일
4
post-thumbnail

9월 회고

9월 1일부터 27일까지 팀 프로젝트 기간이었다.
따라서 9월 회고는 팀 프로젝트 회고라고 봐도 되겠다. 괴발개발 깃허브
원래 5명이서 하게 되는 프로젝트이지만 개인 사정으로 2명이 데브코스를 그만두면서 3명이서 팀 프로젝트를 진행했다.
처음으로 마무리를 지어본 팀 프로젝트이기도 하고, 처음으로 팀장을 해본 프로젝트여서 기억에 많이 남을 것 같다.


기획

주제 정하기

9월 1일 오프라인으로 만나서 프로젝트 주제에 대해서 회의를 했다.
여러가지 참신하고 재미있는 주제들이 많았지만,
api 명세와 프로젝트 기간을 고려해 '개발자를 위한 커뮤니티 사이트'로 확정지었다.
서비스 명은 '괴발개발'로 '고양이의 발과 개의 발' 이라는 의미도 가지고,
'글씨를 함부로 갈겨 써 놓은 모양'이라는 의미도 가진다. 무엇보다 '개발'이라는 단어로 끝나는 점이 좋았다.

디자인

프로젝트 주제를 정하고 피그마를 이용해 대략적으로 디자인을 했다.
프로젝트 기간과 줄어든 인원을 고려해 디자인에 시간을 들이기 어렵다고 판단했고, 하루만에 3명이서 후딱 마무리지었다.
(프로젝트를 진행하면서 디자이너의 중요성을 느꼈다. 개발자 셋이서 디자인을 하니 뭔가 엉성하고 부족한 느낌이 들더라)


협업

일일 스크럼을 오전 10시에 진행하고, 노션에 스크럼 일지를 간단하게 작성했다.
스크럼은 어제 못한 일, 오늘 할 일을 공유한다.
작성된 스크럼 일지를 바탕으로 깃허브 이슈를 생성하고, 각자 featrue#이슈번호 브랜치로 분기해 작업했다.
깃허브 칸반보드를 통해 진행도를 한 눈에 볼 수 있게 했다.
오후 5시에는 각자의 진행상황을 공유하고, 문제 혹은 잘 안되는 것이 있다면 같이 얘기하는 시간을 가졌다.

스프린트

1주를 스프린트 단위로 잡아 총 3개의 스프린트를 계획했다.
각 스프린트마다 구현할 기능들을 적어두고, 1주 안에 구현하기위해 노력했다.
하지만 일정 산정은 역시 너무 어려운 일이었고, 제대로 지켜지지 못했다.
경험을 늘려 자신의 맨데이를 명확히하는 것이 중요하다는 것을 깨달았다.

아쉬웠던 점

일을 배분하거나 일정 관리에 어려움을 느꼈다.
잘못된 일 배분이 프로젝트의 일정에까지 영향을 미치는 것을 보았다.

프로젝트를 계획하는 단계에서 테스크의 순서와 종속성에 대해서 소홀했던 것이 크다.
종속성을 명확히 하지 못하니 테스크의 순서가 꼬이게되고, 다른 사람의 PR이 머지될 때까지 손까락만 빨고 있거나 머지해달라고 부탁하는 상황이 자주 나왔다.
또한 비슷한 Query 훅을 작성하는 경우도 발생했다.

이번 프로젝트를 계기로 테스크의 종속성을 판단하고 테스크 순서를 정하는 일이 얼마나 중요한지 깨달았다.


개발

내가 맡은 부분

  • 채널 페이지
  • 게시글 페이지
  • 검색 페이지
  • 검색 모달 및 Modal atom
  • 알림 드롭다운 및 Dropdown atom
  • 카드 컴포넌트 및 그와 관련된 atoms

아토믹 디자인 패턴 도입

프로젝트에 UI라이브러리를 쓸 생각이 없었기 때문에 아토믹 디자인 패턴을 도입해보았다.
아토믹 디자인 패턴의 단점으로 각 계층간의 경계가 모호다는 점이 있었다.
그래서 프로젝트 시작 전, 계층간의 경계를 컨벤션으로 확실하게 정해보았다. (pages 는 따로 폴더로 분리)

  • atom 서비스 로직과 분리된 컴포넌트
  • molecules atom들이 합쳐져 만들어진 컴포넌트 or 서비스 로직이 들어간 컴포넌트
  • organisms atom들과 하나 이상의 molecule이 합쳐져 만들어진 컴포넌트
  • templates 컴포넌트들의 위치를 잡아주기 위한 컴포넌트

기술 스택 정하기

  • React 가장 커뮤니티가 큰 SPA 라이브러리
  • Typescript 오류를 줄이고, 좋은 개발자 경험을 위해
  • ESLint Prettier 코드 일관성 유지, 오류 방지를 위해
  • Vite 빠른 개발환경 초기설정 및 빌드를 위해
  • Storybook 컴포넌트의 사용 방법과 예시를 한 눈에 보기 위해
  • Emotion 팀원 모두 사용해 보았으며 구현속도가 가장 빠를 것으로 예상되어 선택
  • TanStack Query 페이지 탐색이 많은 커뮤니티 서비스 특성 상 캐시가 필요했으며, 글 작성, 수정, 좋아요 등의 활동 시 캐시 무효화가 필요했기 때문에 선택
  • Zustand 다크모드, 토큰, 뷰포트 크기를 저장하기 위한 전역 상태가 필요해 선택

Storybook 사용 경험

아토믹 디자인 패턴을 도입 후, 컴포넌트들을 만들어 나가면서 Storybook도 같이 채워나갔다.
하지만 molecules, organisms 등의 컴포넌트들도 스토리를 만들어야 하는지가 모호했다.
프로젝트를 진행하면서 atom 컨포넌트들만 Storybook으로 만드는 것이 적합하다는 느낌을 받았다.
다음 프로젝트에서는 디자인 시스템이 없다면 굳이 Storybook을 사용하지는 않을 것 같다.

TanStack Query 사용 경험

리액트에서 쉽게 비동기 데이터를 불러오고 캐시할 수 있어 편했다.
캐시 시간을 쿼리마다 지정할 수 있는 점, 캐시 무효화를 통해 서버 상태와 동기시킬 수 있는 점이 좋았다.
SuspenseErrorBoundary를 통해 컴포넌트 내에서 데이터 fetch의 성공을 보장할 수 있는 점이 좋았다.
Suspense를 통해 선언적으로 로딩처리(스켈레톤 UI)를 한 점이 좋았다.
hooks/api 폴더에 QueryMutation 훅 파일들을 넣었는데 프로젝트를 진행하면서 폴더가 너무 비대해졌다. 관심사별로 분리하지 못한 점이 아쉽다.

Zustand 사용 경험

persist 옵션을 통해 localStorage와의 동기화가 정말 좋았다.
전역 상태 관리를 위해 필요한 코드의 양이 적어 구현하기도 편하고 사용하기도 쉬웠다.
TanStack Query를 같이 쓰다보니 Zustand가 필요한 전역상태가 별로 없어 아쉬웠다.

컨벤션 정하기

괴발개발 컨벤션
깃허브 커밋 컨벤션, 브랜치 룰, PR, ISSUE 템플릿을 만들고,
폴더명, 파일명, 폴더구조 등의 컨벤션을 정했다.
huskylint-staged를 통해 코드에 오류가 없을 때에만 커밋이 가능하도록 했다.
코드 작성 시 일관성이 지켜지지 않을 만한 부분들도 컨벤션으로 정하고(완벽히 정하지는 못했다),
타입스크립트 사용 시 언제 타입별칭과 interface 를 사용할지 정했다.

추가적으로 나의 의견이 많이 들어간 컨벤션이 하나 있다.
Emotion 사용 시 styled는 사용하지 말자는 것이다.
컴포넌트가 스타일만 입혀진 컴포넌트인지 아닌지 확인하려면 해당 컴포넌트 파일까지 들여다봐야 하는 단점이 있다고 생각했기 때문이다.
그래서 css prop을 이용하고 별도의 파일로 스타일을 분리하는 방식을 사용해보았다.

Emotion(css prop) 사용 경험

semantic 태그 활용, 스타일과 서비스 로직의 분리가 좋았지만,
동적으로 스타일이 바뀌어야 하는 경우 함수의 형태로 분리할 수 밖에 없었고,
이 부분에서 변수 -> 함수로의 코드 수정, 타입 정의의 오버헤드가 귀찮게 느껴졌다.


트러블 슈팅

드롭다운 컴포넌트 구현과정

UI 라이브러리를 사용하지 않았기 때문에 드롭다운을 직접 구현했다.
특정 요소에 달라붙는다는 드롭다운의 특징을 살려 position: absolute css 속성을 이용했다.
하지만 html 요소상 나중에 나오는 요소에 position: relative 속성이 있을 경우,
그 요소의 아래로 그려지는 문제가 발생했다.

이를 통해 html 문서상 요소의 순서와 css 속성의 관계에 대해 공부해볼 수 있었고,

드롭다운 컴포넌트를 React Portal로 변경하고, 위치값을 props로 받게해 문제를 해결하였다.

사이드바 토글

작은 너비의 뷰포트에 대응하기 위해 사이드바를 반응형으로 구현했고,
너비가 좁을 시 사이드바를 토글해 나타날 수 있게 했다.
하지만 사이드바가 몇몇 요소 뒤로 가려지는 문제가 발생했다.
원인은 드롭다운 컴포넌트의 문제와 동일했다.
다음과 같이 사이드바 컴포넌트를 html 문서상 가장 뒤로 보내 해결하였다.

PageTemplate.tsx

  return (
    <>
      <Flex css={pageTemplateWrapperStyle}>
        <div css={pageInnerWrapperStyle}>
          <Outlet />
        </div>
        <SidebarProvider>
          <SidebarWrapper />
        </SidebarProvider>
        <FloatingButtons scrollPosition={scrollPosition} />
      </Flex>
    </>
  );

useInfiniteQuery가 무효화되지 않는 문제

채널 페이지에서 useInfiniteQuery를 이용해 무한스크롤을 구현하였고,
몇몇 Mutation에는 이 쿼리를 무효화하기 위해 invalidateQueries 메서드를 사용했다.
하지만 쿼리가 무효화되지 않는 문제를 겪었다.
이와 관련해 TanStack Query 공식 깃허브에 같은 문제를 겪는 사람들이 있었고,
resetQueries라는 해결책을 찾을 수 있었다.

ErrorBoundary가 특정 에러를 catch하지 못하는 문제

프로젝트에서 ErrorBoundary를 이용해 에러 처리를 하기로 계획했고,
다음과 같이 커스텀 ErrorBoundary를 구현해 에러 처리를 수행하였다.

AuthErrorBoundary.tsx

import { Component } from "react";

import AuthErrorFallback from "@components/_errorFallbacks/AuthErrorFallback";

import { AuthError } from "@utils/AuthError";

interface Props {
  children?: React.ReactNode;
}

interface State {
  error?: Error;
  hasError: boolean;
}

class AuthErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false
  };

  public static getDerivedStateFromError(error: Error): State {
    return { error, hasError: true };
  }

  public render() {
    if (!this.state.hasError) {
      return this.props.children;
    }
    if (this.state.error instanceof AuthError) {
      return (
        <AuthErrorFallback
          error={this.state.error}
          onMounted={() => this.setState({ hasError: false })}
        />
      );
    }
    throw this.state.error;
  }
}

export default AuthErrorBoundary;

기능 구현을 하는 도중, 분명히 ErrorBoundary의 자식 컴포넌트에서 발생한 에러인데도 catch하지 못하는 문제가 발생했다.
원인은 ErrorBoundary가 모든 에러를 catch하지는 못한다는 것이 문제였다.

다음은 ErrorBoundarycatch하지 못하는 에러이다.

  • 이벤트 핸들러
  • 비동기 코드(예: setTimeout 또는 requestAnimationFrame 콜백)
  • 서버 측 렌더링

위 상황에서 발생한 에러를 ErrorBoundary에 전달하려면 렌더링과정에서 Error가 발생해야했고,
setState를 이용해 렌더링를 촉발시켜 에러를 던지는 훅을 부득이하게 만들었다. (최선의 해결책이 아닐 수 있다)

useError.ts

import { useCallback, useState } from "react";

export const useError = () => {
  const [, set] = useState();

  const dispatchError = useCallback((error: Error) => {
    set(() => {
      throw error;
    });
  }, []);

  return { dispatchError };
};

이벤트 핸들러나 비동기 코드 내부에서 dispatchError 함수를 사용해 ErrorBoundary까지 에러를 전달 할 수 있었다.

1개의 댓글

comment-user-thumbnail
2023년 10월 4일

회고 잘 봤읍니다~
수고많으셨어요 !!

답글 달기