[회고] 2차 팀 프로젝트 회고

imloopy·2022년 8월 19일
4

Today I Learned

목록 보기
44/56

개요

[ 기본 정보 ]

[ 이번 프로젝트의 개인적인 목표 ]

  • 유지 보수성이 뛰어난 코드를 만들자.
  • 컴포넌트의 책임을 분리하고, 가독성이 높은 코드를 작성하여 전반적인 코드 퀄리티를 높여보자.
  • 코드 리뷰를 통해 팀적 성장을 이끌어나가자.

[ 나의 역할 ]

📌 개발자의 개발 경험을 개선하기 위한 노력을 시도하였다.
  • 우리맵 서비스에 맞는 지도 컴포넌트를, Next.js 환경에서 사용할 수 있게 커스터마이징
    • kakao-map-sdk 패키지를 바탕으로, next.js 환경에서 사용할 수 있도록 캡슐화
      • 이 과정에서 문제가 발생했었음 - 이를 해결하기 위하여 어떤 방법을 사용했는지 서술하기
    • 다른 팀원들이 손 쉽게 서비스를 구현할 수 있도록, 검색을 위한 hook을 구현
      • hook을 만들자고 결심한 이유는, 검색 기능은 서비스 전반적으로 사용될 기능이므로 재사용 필요성이 있었다.
      • 검색 기능이 컴포넌트 내부에 존재하면 컴포넌트 하나의 책임이 너무나 증가되는 문제가 있었고, 미래에 유지 보수성이 저하된다고 생각했다.
      • 따라서 hook을 통해 기능(데이터)와 UI, 상호작용을 완전히 분리하는 headless 기반 컴포넌트 추상화 방식을 추구하여 분리할 수 있는 데이터 및 기능을 hook으로 분리하였다.
  • 로그인 및 로그아웃 기능
    • 서비스 전반적으로 사용될 인증 처리 부분 담당
    • 인증은 토큰 기반 인증을 사용하였으며, access token과 refresh token으로 나누어서 사용자 정보를 안전하게 저장하려고 시도
    • access token은 서버로부터 body로 전달받아 local storage에 저장, refresh token은 http-only cookie로 전달받아, 만료 시간이 지날 경우
    • access token을 instance 사용 시 post 요청 시마다 accessToken을 넣어주지 않도록 interceptor로 만들어 처리
    • 또한, access token 만료 시, 팀원들이 따로 처리하지 않더라도 refresh token을 통해 자동으로 refresh 할 수 있도록, response interceptor를 구성
      • refresh token 만료 시, 자동으로 로그아웃 처리 및 recoil 전역 유저 상태와 연동시키도록 hook으로 구성
  • 실시간 알림 기능
    • 실시간 알림은 Server sent events 로 구현하였고, 이 역시 useSSE라는 hook으로 구현
    • useSSE hook을 이용하는 useNotification hook을 만들고, 도메인에 연결된 로직들을 해당 hook에 구성
    • 커플로 맺어진 다른 사용자가 게시글을 생성하거나 수정할 때, 사용자에게 실시간 알림이 전달
  • git graph 관리
    • 프론트엔드 컨벤션을 만들고, git에 대한 관리를 하였으나, 대체적으로 잘 되었지만, 종종 리베이스를 깜빡하고 머지하는 경우가 발생하여, git graph가 제대로 관리되지 않는 문제가 있었음

진행사항

[ 진행 방식 ]

  • 진행 방식: 애자일
  • 협업 관리 도구: Jira, notion, slack, discord
  • 총 세 번의 스프린트 기간을 두었고, 유저 스토리 기반으로 협업을 진행

스프린트

  • 첫 번째 스프린트: 기획 단계 및 UI 구상 단계
  • 두 번째 스프린트: Basic 기능 구현
  • 세 번째 스프린트: Advanced 기능 구현 및 UI 고도화, QA

→ 스프린트 기간은 잘 지켜지지 않았는데, 자세한 내용은 아래에 서술한다.

트러블 슈팅 및 어려웠던 점, 해결 방식

[ 기술 측면 ]

Next.js 환경에서 axios interceptor를 전역(모듈 스코프에서) 선언하게 되면, 클라이언트 사이드에서 비동기 작업을 할 때 인터셉터를 통과하지 못하는 문제가 있었다.

처음 리코일을 구성하기 전, 리덕스를 사용했었다. (리코일은 next.js 에서 key duplication warning 문제가 있었다. 해당 문제에 대해서 멘토님한테 질문드렸었는데, 뜸에도 불구하고 편해서 현업에서도 그냥 쓴다고 하더라… 그래서 그냥 쓰기로 했다)

리덕스를 사용한 이유는, 리덕스의 스토어는 리액트 외부에서(물론 사용하려면 Provider로 감싸주어야 하긴 하지만)선언하여, 스토어와 인터셉터를 연결할 때 인터셉터가 리액트 스코프 내부에 존재하지 않아도 스토어에 접근할 수 있는 장점이 있기 때문이다. 따라서 인터셉터를 외부에 선언하고, 리덕스 스토어와 묶어서 처리하면, 상태와 인터셉터를 연결할 수 있을 뿐더러 코드 분리차원에서도 이득이 있다고 생각을 했으나, Next.js는 서버에서 돌아가는 프레임워크이므로, redux와 연결된다는 장점을 취할 수 없었으며, 인터셉터 또한 클라이언트 사이드에서 동작하지 않았다.

이를 해결하기 위하여, 모듈 스코프에 선언하지 않고, 함수 스코프로 선언하여 instnace 역시 함수로 묶어 호출 시 인터셉터가 호출되도록 처리를 했다.

이 과정에서 발생한 딜레마 - 코드 분리 우선 vs 개발 경험 우선?

instance와 interceptor를 함수로 선언하고, 비동기 작업 시 해당 함수를 호출하는 방식은 동작도 잘 될 뿐더러 코드 분리 차원에서 굉장한 이득이었지만 (특히, api 함수들을 컴포넌트 외부로 뺄 수 있으므로 불필요한 hook의 사용을 자제할 수 있으므로 컴포넌트 내부에 불필요한 로직들이 필요가 없다.) 로그아웃 시 전역 유저 상태에 접근하여 해당 데이터를 null로 만들어야 하는데, 이러한 방식이 불가능했다. 이미 이 시점에서는 recoil을 사용하고 있기 때문에, 다른 방식으로 접근을 하게 되었다. 사실 hook으로 만들어서 instance hook을 사용하면 해결될 것을 알고는 있었지만, hook을 만들어서 사용하면 코드 분리 차원에서 좋지 않을 것이라, 다른 방식을 우선 생각해보기로 했다.

  • recoil 데이터에 접근하는 부분을 제거한다.

사실 전역 데이터에 접근할 필요가 없다면 굳이 hook으로 인스턴스를 만들어, 인터셉터를 선언할 필요가 없었을 것이다. 다만, 이 경우에는 여러 문제가 있다.

  • 다른 개발자가 비동기 로직을 작성 시 try… catch문으로 401 status가 뜨면, 리코일 데이터를 추가해야 하는 로직을 넣어줘야 한다는 것
    • 각 api 요청에서 반복적인 작업을 하지 않도록 interceptor가 존재하는 것인데, 이렇게 되면 오히려 인터셉터의 목적에 역행하는 것이 아닌지에 대한 고민을 하였다.

이를 해결하기 위해 window.addEventListener를 통해 로그아웃 이벤트가 연결되면 로그아웃 시킬 수 있지 않을까? 하여, 이벤트리스너 컨텍스트를 만들어 감싸보았는데, 이 역시 다른 문제가 발생했었다.

  1. window.addEventListner가 이벤트가 dispatch되는 시점에 완벽히 실행되지 않는다.
    1. web api등 event loop는 event queue에 담겨있다가, 콜스택 함수들이 모두 종료되면 콜스택으로 이동하여 실행하게 되므로, 콜스택에 담겨있는 함수가 많다면, 상태 관리와 실제 서버에서 로그아웃 되는 시간 차이가 발생하게 된다.
  2. useEffect 실행 순서

리액트의 useEffect는, 가장 children 컴포넌트부터 실행된다. 이는 실제 브라우저가 렌더링 하는 순서와 흡사하다. child컴포넌트가 렌더링되고, 해당 크기를 바탕으로 부모의 크기를 계산하기 때문이다.

그래서 initial load 시 자식 컴포넌트에서 비동기 요청이 일어났을 때, Context의 useEffect는 아직 실행 전 상태이기 때문에 이벤트가 dispatch 되더라도 Context에 정의된 event listener는 동작하지 않는다.

여러 복합적인 이슈들을 따져보았을 때, 현재로서는 axios instance를 hook으로 선언하여 사용하는 것이 최선이라고 생각을 했고, 각 페이지 로직 또는 필요한 부분에서 instance hook을 호출하는 방식으로 합의를 했다.

Recoil effects, selector가 Next.js에서 동작하지 않는 문제

이 문제 역시 Next.js가 풀스택 프레임워크라는 것에 기인한다

Next.js는 서버로부터 프리렌더링을 한 뒤 클라이언트 사이드로 작업을 넘겨준다. 만약 이런 recoilState가 있다고 생각하자.

// recoil code

const LocalStorageEffect: AtomEffect<UserResponseType | null> = ({
  setSelf,
  onSet,
}) => {
  const savedValue = LocalStorage.getItem('user', null);
  setSelf(validateUser(savedValue) ? savedValue : null);

  onSet((newValue, _, isReset) =>
    isReset
      ? LocalStorage.removeItem('user')
      : LocalStorage.setItem('user', newValue),
  );
};

const userState = atom<UserResponseType | null>({
  key: 'user',
  default: null,
  effects: [LocalStorageEffect],
});

export default userState;

리코일은 initialState가 null로 초기화됨과 동시에, localstorage로부터 값을 불러와서 초기화한다. 클라이언트 컴포넌트에서 recoilState를 불러와서 사용하려고 할 경우, 다음과 같이 사용할 수 있다.

function Component() {
  const [user, setUser] = useRecoilState(userState)
}

근데, 이렇게 사용하면 Next.js 환경에서 에러가 난다. 왜냐하면 로컬스토리지는 클라이언트 사이드에서만 접근이 가능한데, 프리렌더링 단계에서 로컬스토리지에 접근할 수 없기 때문이다. 따라서 Next.js는 서버 렌더링과 클라이언트 사이드 렌더링이 일치하지 않는다고 에러를 보낸다.

생각한 해결 방법은 다음과 같았다.

  • 서버 사이드 렌더링을 통해 데이터를 불러오면 해결되긴 한다. Next.js에는 getServerSideProps라는 서버에서 데이터 fetch를 실행하고, 데이터 fetch를 실행 한 뒤 해당 데이터를 prop으로 넘겨준다.
    • 그러나 해당 방법을 통해 Authorized 된 데이터를 불러올 때, token이 담기기는 하지만, refresh token을 통해 access token을 갱신하고자 할 때, refresh token이 cookie header에 담기지 않는 문제가 있었다. 따라서 프로젝트 상황을 봤을 때 단시간에 해결하는 것은 어려웠다.
  • useEffect를 통해 값을 불러온다.
    • useEffect는 Next.js 환경에서 완벽하게 클라이언트 사이드에서 동작함이 보장되므로, useEffect를 통해 값을 설정하고, 해당 값을 불러오면 된다. 해당 동작 방식은 직접 구현한 것은 아닌, recoil github repository에서 issue를 검색하다가, 비슷한 환경의 문제에 직면한 사람들이 많았고, 그 중 다른 유저가 올린 방법이 재사용성에서도 괜찮았고, 다양한 곳에서 활용할 수 있을 것 같아 채용해 보았다.
function useComponentDidMount() {
  const [mounted, setMounted] = useState(false)
  useEffect(() => {
    setMounted(true)
  }, [])
  return mounted
}

여기서 useEffect는 항상 클라이언트에서 동작하기 때문에, 클라이언트 환경에서 mounted는 true이다. 다만, 여기서 조금 더 최적화를 하기 위해 값을 useMemo로 감싸주었다.

function useComponentDidMount() {
  const [mounted, setMounted] = useState(false)
  useEffect(() => {
    setMounted(true)
  }, [])
  return useMemo(() => mounted, [mounted])
}

그리고, mount 됬을 때 recoilValue를 불러올 수 있도록 다음의 hook을 불러왔다.

function useRecoilValueAfterMount<T>(recoilState: RecoilState, defaultValue: T) {
  const mounted = useComponentDidMount()
  const value = useRecoilValue(recoilState)
  
  // defaultValue는 recoil의 default와 동일해야 한다.
  return mounted ? value : defaultValue 
}

recoilValue를 클라이언트 사이드에서 값을 불러올 때 해당 hook을 사용할 경우, mount 된 후 클라이언트 사이드에서 값을 변경하므로 렌더링 문제가 없게 된다.(단, react보다는 null → value로 변하는 시간이 길 것이라고 예상한다.)

Map 컴포넌트 스크립트 로딩 문제

맵 컴포넌트는 응집도를 위하여 다음과 같이, 맵 스크립트를 불러오는 부분 + 맵을 표시하는 부분으로 이루어져 있었다.

function Map({ latitude, longitude }) {
  return (
    <>
      <Script src="src" onLoad={loadMap} />
      <KakaoMap />
    </>
  )
}

스크립트 로딩이 완전히 끝나기 전에 kakao api method에 접근하게 되면 에러가 발생하기 때문에, onLoad 리스너를 통해 로딩이 완전히 끝나면 loaded = true로 설정한 뒤, loaded = true일 경우에만 맵 컴포넌트가 보여지게끔 설정하였다.

그런데 다른 페이지들을 구현하고 보니, 문제가 발생했다. router를 이용하여 페이지를 이동했다가 다시 되돌아오면 맵 컴포넌트가 보여지지 않는 문제였다.

이를 확인해보니, 문제점은 다음과 같았다.

  1. Next.js의 스크립트태그는 처음 Mount될 때 생기고, 계속 남아있다.
  2. 그렇기 때문에 스크립트 태그의 load 이벤트는 오직 처음 스크립트를 불러올 때만 실행된다.
  3. onLoad 콜백은 loaded를 true바꾸는 역할을 한다.
  4. 다시 되돌아오면 Map 컴포넌트가 생성되지만, Script 태그는 다시 생성되지 않아 onLoad 콜백이 실행되지 않아, loaded가 false 인 상태로 남아있다.

이를 좀 더 리액트스럽게 해결하였다.

리액트에서 해결하는 방법은, 리액트에서 사이드 이펙트를 관리하는 useEffect hook에서 처리하는 것이다.

  1. useEffect에서 script 태그를 만든다.
  2. script태그에 onLoad 리스너에 콜백함수를 등록한다.
  3. 해당 맵 컴포넌트가 unmount될 때, script 태그를 지운다.
function Map() {
  useEffect(() => {
    const $script = document.createElement('script');
    $script.src = URL;
    $script.addEventListener('load', onLoad);
    document.head.appendChild($script);
    return () => {
      $script.remove();
    };
  return (
    isLoaded ? <Map /> : null
  )
}

해당 방식으로 테스트를 해 보았더니, 페이지 이동 후 돌아온 뒤에도 script tag가 다시 생기고, load event가 실행됨을 확인할 수 있었다.

그 외 다소 효율적이지 않은 메인 페이지 로직을 리팩토링하면서, 부수적인 버그 역시 해결하였다.

회고

👍 Keep & Learned

[ 팀 및 커뮤티케이션 측면 ]

  • 화이트보드 시스템을 운영하여 스크럼 시간을 단축
    • 스크럼이 늘어지는 것을 최대한 방지하기 위하여, 스크럼 때 공유할 만한 내용, 질문들을 화이트보드에 적어서 공유해 두었다. 백엔드, 프론트간 일정 공유가 끝나고 나면, 화이트보드의 질문들을 공유하고 해결되면 지워나가는 방식을 사용했다. 이 방식은 확실히 스크럼 시간을 줄여주면서, 이슈를 빠르게 공유하고 다 같이 해결 방안에 대해 생각할 수 있는 좋은 시스템이었다.
  • 애자일 시스템 기반 프로젝트를 운영하여, 총 두 번의 회고를 진행
    • 회고를 통해서 문제점을 발견하고, 이를 해결하는 과정을 통해 성장할 수 있다고 생각하기 때문에, 중간회고와 최종 회고를 한 것이 맘에들었다. 실제로 중간 회고때 나온 문제점 중 하나가, Jira 관리가 잘 안된다였는데, 이를 해결하기 위하여 프론트엔드와 백엔드가 따로 쓰던 유저 스토리를 통합하고, 담당자를 명확하게 할당하였다. 철저히 에픽 단위 및 스토리 위주로 태스크를 계획하고, 서브 태스크를 등록하여 작업을 진행했더니, 해당 스토리 진행도가 어느정도인지 한 눈에 파악할 수 있었다.

[ 프로젝트 측면 ]

  • PR 크기를 줄이기
    • 해당 역시 이전 1차 프로젝트에서 가장 아쉽다고 생각했던 것 중 하나이다. 그래서 프론트엔드 개발을 시작하기 전, 팀원들과 함께 몇 가지 룰 을 정했었다.
      • PR 등록 후 24시간 이내 리뷰 마치기
      • 리뷰하기 용이하도록 PR의 크기를 300줄 이내로 제한
      • 좀 더 세부적으로 서브태스크를 구현하여 조합하는 방식으로 구현하기
    • 물론 구현에 집중하느라 해당 룰이 항상 완벽하게 지켜진 것은 아니지만, 그래도 귀에 못이 박히도록 강조를 하다 보니, 대부분의 경우에서 준수하게 지켜졌다고 생각했고, 실제로 코드 리뷰의 시간은 절약되면서 퀄리티가 증가한 좋은 시도였다.
  • 단일 책임 원칙 및 headless 기반 컴포넌트 추상화 시도
    • 사실 이번 프로젝트에서 가장 고대했던 부분이기도 하다. 애초에 이번 프로젝트의 목표는 팀 적 성장 뿐만 아니라, 개인의 기술적 성장이었다.
    • 프로젝트 시작 전에 토스에서 발표한 “Headless 기반의 컴포넌트 추상화” 라는 기술 주제에 대한 유튜브를 본 적이 있다.
    • 해당 유튜브 영상을 보면서 그 동안 조금 생각 없이 컴포넌트를 짰구나 라는 생각도 했다. 이전에 작성한 컴포넌트들은 하나의 컴포넌트가 가지고 있는 책임이 너무나도 많았고, 재사용성에 대한 고려가 되어 있지 않아 일정 시간이 지나고 난 뒤에는 이 컴포넌트가 작동하기 위해서 어떠한 props들이 필요한지 알 수 있는 방법이 없었다.
    • 그래서 이번 프로젝트에서는 컴포넌트를 만들 때 조금 더 신경써서 만들어보자 다짐했었는데, 완벽하지 않지만, 그래도 신경은 썼다고 개인적으로 생각한다.
    • Dropdown 컴포넌트
      function Dropdown({ trigger, children, ...props }: IDropdown) {
        const [isOpen, setIsOpen] = useState(false);
        const [boundary, setBoundary] = useState(0);
        const dropdownMenuRef = useRef<HTMLUListElement | null>(null);
      
        const calculateWidth = useCallback(() => {
          if (!dropdownMenuRef.current) return;
          setBoundary(
            dropdownMenuRef.current.getBoundingClientRect().width +
              dropdownMenuRef.current.getBoundingClientRect().left,
          );
        }, []);
      
        const toggle = useCallback(() => {
          setIsOpen((prev) => !prev);
        }, []);
      
        const close = useCallback(() => {
          setIsOpen(false);
        }, []);
      
        const dropdownRef = useClickAway<HTMLDivElement>(close);
      
        const triggerWithProps = trigger
          ? React.cloneElement(trigger, {
              onClick: (e: React.MouseEvent<HTMLElement>) => {
                calculateWidth();
                toggle();
                trigger.props.onClick?.(e);
              },
            })
          : null;
      
        const childrenWithProps = React.Children.map(children, (child) => {
          if (!React.isValidElement(child)) return null;
          return React.cloneElement(child, {
            onClick: (e: React.MouseEvent<HTMLElement>) => {
              close();
              child.props.onClick?.(e);
            },
          });
        });
      
        return (
          <S.DropdownWrapper ref={dropdownRef}>
            <S.DropdownTrigger>{triggerWithProps}</S.DropdownTrigger>
            <S.DropdownMenu
              isOpen={isOpen}
              widthBoundary={boundary}
              ref={dropdownMenuRef}
              {...props}
            >
              {childrenWithProps}
            </S.DropdownMenu>
          </S.DropdownWrapper>
        );
      }
    1. 드롭 다운 컴포넌트를 단일 책임 원칙에 맞게 분리한다. → 사실 현재 상태는 완벽히 분리된 것이 아니지만, 내 실력상 더 쪼개는게 너무나 어려웠다. 나중에 실력이 올라가면, 해당 컴포넌트를 리팩토링 해봐야겠다.
    2. 드롭 다운에 필요한 프롭들을 제외하고, 나머지 상태 관리를 어떻게 할지 생각해본다. 우선, 가장 바깥 컴포넌트는 드롭다운의 on, off를 담당하므로, on, off와 관련된 메서드와 상태를 모두 해당 컴포넌트에 집어넣는다.
    3. react.cloneElement, react.Children.map으로 컴포넌트에 프롭으로 전달하고, 외부에서 해당 상태에 대하여 신경쓸 필요가 없게한다.
  • 새로운 기술과 스택에 도전(Next.js 및 실시간 알림 기능)
    • 새로운 기술 스택에 도전해보았다. 다만, 이는 오직 도전했다는 의의 그 이상, 그 이하도 아닌 듯 하다. 파일럿 프로젝트를 해보고 해당 스택을 적용함으로써 프로젝트에 어떤 영향을 미칠 지 고려한 것도 아니고, 사실 page 기반 라우팅과 환경 설정을 할 것들이 적다는 이유로 스택을 사용하였는데, 결과를 떠나서 좋은 시도였다고 생각한다.
  • 백엔드의 입장을 이해
    • 아래 좀 더 자세히 서술

⚠️ Problem & Laked

[ 팀 및 커뮤티케이션 측면 ]

원래 예상하던 개발 일정보다 많이 늦춰졌다. 그래서 원래 4주간 진행인 프로젝트에서, 기획으로 거의 일주일을 쓰고, 베이직 기능을 빠르게 구현해서 디테일 고도화 및 추가 기능 구현까지가 목표였는데, 추가 기능으로 기획했던 것 중 오직 실시간 알림 기능만 구현할 수 있었다. 원래 예상보다 프로젝트가 늦어진 이유는 다음과 같다.

  • 기존의 기획안을 갈아엎음
    • 전 날 기획을 확정했음에도 불구하고, 백엔드쪽에서 기획안을 엎었다. 사실 기획을 엎는 것은 언제든지 발생할 수 있는 일이다. 솔직히 좀 더 좋은 기획을 위해서는 엎어도 된다고 생각한다. 다만 엎은 이유가 다소 이해가 가지 않는게, 백엔드 멘토님이 의견 제안을 했는지, 백엔드 멘토님과 미팅을 하고 난 뒤마다 기존에 정했던 기획안에 대해 문제를 제기하였다. 결국에는 아이디어를 백지화하고 다시 정했다. 자기 주장이 아니었음에도 불구하고, 다른 사람에게 휘둘려서 아이디어를 바꾼 것이 개인적으로는 이해가 잘 가지도 않을 뿐더러, 사실 자기 주장 및 소신이 뚜렸했다면 애초에 이런 사태 또한 발생하지 않았을 것이다.
    • 기획을 바꾸고 확정한 뒤에는 이미 시간이 많이 지난 뒤였다.
  • 지라 관리의 미숙으로 인한 태스크 관리가 잘 안됨
    • 지라 관리가 미흡하여, 유저 스토리 관리나, 태스크 담당자 관리가 잘 되지 않아, 누가 현재 어떤 파트를 맡고 있고, 총 태스크가 어느정도 진행되었는지 확인이 어려웠다. 많이 진행된 부분과 미숙하게 진행된 부분에 대해 확인하고 다른 팀원이 보완하는 형식으로 했어야 하는데, 이런 부분은 많이 아쉽다.
  • 태스크 분배 문제 & 팀원간 실력 격차 문제
    • 이전 팀에서는 단순 도메인별로 태스크를 나누었음에도 불구하고 기능 구현이 거의 비슷한 시간에 마쳐서 이런 문제가 없었다. (일단 팀원간 실력 격차가 많이 나지 않았다.)
    • 그러나 이번 팀에서는 실력 격차가 많이 났다. 백엔드 4명, 프론트엔드 5명의 팀으로 이루어졌음에도 불구하고 개발 속도는 오히려 이전 팀보다 훨씬 느린건 확실히 문제라고 생각한다.
    • 팀이란게 항상 내가 좋아하고, 잘 하는 팀원들과 함께 개발할 수 는 없기 때문에, 어느 정도 감수하고 이해해야 할 부분이라고 생각한다. 그러나 내가 중요하게 생각하는 문제는 태도 문제다.
    • 베이직 기능이 끝난 뒤 상황을 보니, 베이직 기능 구현이 끝난 팀원이 나 밖에 없어서, 자칫 남은 시간 붕 뜰뻔했다. 처음에 주어진 베이직 기능 구현이 스프린트보다 늦게 끝나서 생긴 문제였다고 생각한다.
    • 그래서 다른 백엔드 팀원분과 함께 실시간 알림 기능 추가 기능을 구현하기로 하였다.

[ 프로젝트 측면 ]

  • 섣부른 기술 스택의 채택은 피(?)를 부른다.
    • Next.js라는, 프로젝트를 시작하기 전에 맛(?)도 본것이 아닌 핥아만 본 프레임워크를 파일럿 프로젝트 없이 바로 투입을 하게 된 것은 큰 실수였다고 생각한다. 실제로 서버 사이드 렌더링의 특징을 활용해보려고 했는데, 쿠키가 전달이 안되어 Authorization에 활용을 못해서 서버 사이드 렌더링의 장점을 거의 활용하지 못하고 클라이언트 사이드 렌더링으로 프로젝트를 구현하고, router 역시 HOC 형식으로 라우팅 처리를 했다.
    • 실제로 프로젝트 시작 쯤에 그냥 리액트를 쓸까 팀원들과 진지한 토의를 했었는데, 일단은 그냥 진행하기로 했다.
  • 확실하지 않은 컨벤션
    • 아무래도 만난 지 얼마 안된 팀원들과 함께 프로젝트를 구성하기 때문에 팀 적 컨벤션이 많이 부족했다. 어느정도 컨벤션을 정하고 개발을 시작하긴 했는데, 디테일하게 컨벤션을 정하지는 않았기 때문에 개발 도중에 컨벤션 논의가 적지 않게 이루어졌다. 개발 막바지 단계에서는 해당 문제는 많이 해결되긴 했다.
  • atomic design 컴포넌트 관리의 부실
    • atomic design 자체가 추상적인 방법론이기 때문에, 실제 구현 단계에서 의견 차이가 발생했다. 나는 atom이나, molecule 단계에서는 컴포넌트가 자체 상태를 가지고 있으면 안되고, 또한 도메인에 얽혀있으면 안된다고 생각한다. 그러나 이런 부분에서 충분한 협의가 이루어지지 않아, components 폴더 구조가 잘 관리가 되지 않았다. 프로젝트 끝날 때쯤엔 미래에 유지보수가 힘들어질 정도로? 엉망이되어, 추후 리팩토링이 된다면 해당 컴포넌트의 책임 및 도메인 분리부터 시작을 해 볼 예정이다.
  • 테스트 프레임워크 미도입
    • 시간 관계상 테스트 프레임워크를 도입하지 않은게 다소 후회된다. 그 동안, 프론트엔드에서 테스트 하는 것이 정말 의미가 있나? 라는 생각을 많이 했다(지금은 안함). 왜냐하면, 프론트엔드에서 주로 문제가 되는 것이 UX 측면인데, 이는 기능적으로 제대로 동작 하더라도 실제 눈으로 보기 전까지는 알 수 없는 부분이기 때문이다(물론 사이프레스처럼 e2e 테스트 툴이 존재하긴 한다).
    • 그런데 utils 폴더의, 뷰가 제외된 어떤 연산 역할을 하는 함수는 다르다. 엣지 케이스 환경에서도 정확하게 돌아가야만 유저에게 정확한 정보를 보여줄 수 있다. 실제로 테스트 코드가 없어서, 엣지 케이스를 마주했을 때 정확한 데이터가 보여지지 않는 버그가 발생했었고, 이를 해결하느라 같은 작업을 두 세 번 반복해야만 했다.
  • 부실한 코드 리뷰
    • 분명히 Keep에 퀄리티 있는 코드 리뷰라고 써놓았는데 이게 왠말이냐?? 라고 할 수 있는데, 아무래도 프로젝트 막바지로 갈 수록 구현에 집중하다보니, 코드 리뷰가 원활히 이루어지지 않았다. 따라서 코드가 브랜치에 머지된 후에 버그를 발견하여 수정하는 일이 적지 않았다.
    • 이를 해결할 좋은 방법이 있나..?? 아직은 잘 모르겠다.

🚵 Try

[ 팀 및 커뮤티케이션 측면 ]

  • 기획 단계에서 일정 산출까지 어느정도 마치는 연습을 하자.
    • 원래 테크스펙을 보면 바로 개발에 들어갈 수 있을 정도로 작성한다고 한다. 테크스펙을 작성하여 서브 태스크가 전부 산출이 되면, 좀 더 애자일하게 프로젝트를 진행할 수 있다고 생각한다.
  • 스크럼 시 개발 일정 공유 및 이슈 공유 - 이슈가 있다면 해당 이슈를 해결했는지, 해결 중인지, 아니면 다 같이 해결이 필요한지를 알면, 팀적으로 서로 도우면서 해결할 수 있을 것이다.

[ 프로젝트 측면 ]

  • 파일럿 프로젝트 실시해보기
    • 새로운 스택을 도입 결정을 하기 한 2주 전부터, 해당 기술 스택을 사용하여 파일럿 프로젝트를 만드는 것이 좋을 듯 하다. 파일럿 프로젝트를 실시할 때는, 실제 프로젝트와 비슷한 환경을 만들어(비동기 처리 등등) 하는 것이 좀 더 도움이 될 듯하다.
  • 좀 더 체계적인 비동기 관리
    • redux-saga를 실제로 사용해본적은 없지만, 많은 기업들이 이전에 리덕스를 사용하면서 비동기 상태관리를 했다고 들었다.
    • 비동기 상태를 두어 분기 처리를 하는 것이 인상 깊었다. 비동기 과정을 다음과 같이 5개로 나눌 수 있다고 한다.
    • init - loading - success - fail - finish
    • 해당 상태들을 적절히 활용하여, 좀 더 체계적인 비동기 처리를 해보고 싶다. + 상태를 연결하여 로딩중일 때 보이는 스켈레톤을 구현해보고 싶다.
  • 유틸리티 함수들에 대한 테스트 프레임워크 도입
    • 이번 프로젝트를 통해 많은 사람들과 협업을 할 때, 테스트 코드는 무조건적으로 필요하다는 것이다. 사실 귀로 들어서 테스트 코드가 있으면 좋다고 느꼈었는데, 실제로 몸으로 경험해보니, 이는 좋다 수준이 아닌 필수라고 생각한다. jest 도입을 적극적으로 고려해봐야겠다.

총평 및 느낀 점

프로덕트 측면에서 봤을 때, 많이 아쉬운 프로젝트였다. 인원수가 많았음에도 불구하고(많아서 사공이 많아 산으로 간건지는 모르겠지만) 이전 프로젝트에 비해 잡음이 꽤 존재했었다.

기획 단계에서 좀 더 확실히 사용자에게 와 닿을 수 있는 서비스를 기획했어야 하는데, 역량 부족으로 공감하기 어려운 서비스가 만들어진 것 같아 많이 아쉽다.

그러나 개인적 챌린징으로 봤을 때는 그래도 만족스러운 프로젝트였다. 새로운 환경에서 트러블 슈팅을 통해 앞으로 비슷한 문제를 마주했을 때 팁들과, 앞으로는 지양해야할 것들에 대해 확실히 알게 되었다.

또한 이전 프로젝트에 비해 코드 퀄리티에 신경을 쓰면서 코드를 작성하였고, 작성하는 과정 중에 했던 고민들이, 미래에 좋은 양분이 될 것 같아 기쁘다. 또 같은 팀원들도 확실히 코드에 신경을 쓴 티가 난다고 해주었을 때 내 노력이 그래도 헛되지 않았구나…라는 생각이 들었다.

해당 프로젝트를 끝으로 공식적인 프로그래머스 데브코스 일정을 모두 마쳤다. 한 달 가까이 함께 동고동락한 팀원들에게 너무 감사하며, 다음에 좋은 인연으로 만날 수 있으면 좋겠다.

1개의 댓글

comment-user-thumbnail
2022년 8월 21일

준혁님 글 잘봤습니다! 정말 한 달동안 고생많으셨고 정말 감사했습니다! 🤗

답글 달기