Spotit 회고

jade·2025년 9월 2일
1
post-thumbnail

서로 다른 직군의 7명의 팀원이 모여 완성한 spotit 프로젝트의 MVP가 완성되었습니다. 부트캠프를 하며 개발자로만 이루어진 팀만 경험하다가, 비개발 직군과 함께한 협업은 완전히 다른 느낌이었습니다. 그만큼 미숙한 점이 많았지만 분명 성장한 점이 눈에 띄일만큼 소중한 경험이었습니다.

글을 쓰는 지금은 팀 리뷰와 파트리뷰를 모두 완료한 상태인데, 팀원들의 회고글을 읽어보며 다시 깨닫게 되는 부분이 있었습니다. 개인 리뷰를 적으며 동료리뷰중 인상깊었던 점들도 차례로 풀어보겠습니다.

기술

Zustand

상태관리 라이브러리인 Zustand를 다뤄보았습니다. Redux에 비해 보일러 플레이트가 적고 직관적으로 상태를 정의할 수 있어서 빠르게 적응할 수 있었습니다. 특히 중간에 유저 정보 유지를 위해 persist를 도입해야 했었는데 zustand에서는 해당 기능이 내장되어 있어서 추가로 라이브러리가 필요하지 않았던 점이 편리했습니다.

🔥 전역 유저 상태 관리를 위한 useUserStore

hasHydrated : zustand persist로 로컬 스토리지의 값이 클라이언트 상태에 복원되었는지 여부를 알기 위함.

  • Next.js 같은 SSR 환경에서는 서버 렌더링 시점에는 localStorage가 없어서 항상 기본값(initialUserState) 으로만 그려지고, 이후 클라이언트에서 persist 미들웨어가 동작하면서 localStorage에 저장된 유저 상태가 복원됩니다.
  • 이때 서버에서 그려진 HTML과 클라이언트에서 복원된 상태가 달라서 hydration mismatch 경고나 UI 깜빡임이 발생했습니다.

onRehydrateStorage 훅에서 복원 완료 시 _setHasHydrated(true) 실행 🔜 스토어 안의 _hasHydrated 값이 true로 바뀌며 hydration 이 완료됨을 알려줍니다.

onRehydrateStorage의 시그니처


1. 첫번째 함수 : rehydration이 시작될 때 호출됨
2. 두번째 함수 : 첫번째 함수가 리턴하는 콜백으로 rehydration이 끝났을 때 호출됨

type Store = {
  userState: UserState;
  clearUser: () => void;
  setUser: (user: User) => void;
  _hasHydrated: boolean; // client에서 hydration완료 여부 확인용
  _setHasHydrated: (v: boolean) => void;
};

export const useUserStore = create<Store>()(
  persist(
    set => ({
      userState: initialUserState,
      clearUser: () => set({ userState: initialUserState }),
      setUser: (user: User) => set({ userState: { isLoggedIn: true, user } }),
      _hasHydrated: false,
      _setHasHydrated: v => set({ _hasHydrated: v }),
    }),
    {
      name: 'user',
      storage:
        typeof window !== 'undefined'
          ? createJSONStorage(() => localStorage)
          : undefined,
      onRehydrateStorage: () => state => {
        state?._setHasHydrated(true);
      },
    }
  )
);

export function useUserHydrated() {
  return useUserStore(s => s._hasHydrated);
}

useUserHydrated로 hydration상태를 읽어서, 복원이 끝나기 전까지는 로딩만 보여줌으로써 mismatch 방지할 수 있었습니다.

const AuthGuard = dynamic(() => import('@/features/auth/lib/AuthGuard'), {
ssr: false,
loading: () => (
  <div>
    <div>
      유저정보 확인 중이에요…
    </div>
  </div>
),
});

export default function AuthRequiredLayout({
children,
}: {
children: React.ReactNode;
}) {
const hasHydrated = useUserHydrated();

if (!hasHydrated) return null;
return <AuthGuard>{children}</AuthGuard>;
}

TanStack Query

서버 상태 관리를 위한 @tanstack/react-query를 더 깊게 다뤄보았습니다. 이전에도 사용 경험이 있었지만, 이번에는 v4 → v5 업그레이드를 진행하면서 쿼리 옵션과 다양한 훅을 더 깊이 이해할 수 있었습니다. 기존에는 useQuery, useMutation 정도만 사용했는데, 이번에는 Suspense 기반 훅을 활용하며 로딩/에러 상태 처리와 에러 핸들링 방식을 보다 세밀하게 다룰 수 있게 되었습니다.

특히 v5로 업그레이드되면서 useQuery 훅 옵션으로 제공되던 onSuccess, onError(및 onSettled) 콜백 사용을 지양하는 흐름이 강해졌습니다. 따라서 데이터 패칭사이드 이펙트 처리를 분리하여, 커스텀 훅을 통해 보다 명시적으로 제어하는 방식을 적용하였습니다.

왜 deprecated(또는 지양)되었을까?

① 사이드 이펙트의 명시적 분리 부족
쿼리 옵션에 사이드 이펙트를 넣으면 데이터 fetching부수 효과가 암묵적으로 결합되어 컴포넌트의 책임이 모호해집니다.

② 리렌더링 및 의존성 관리 문제
부모 컴포넌트에서 inline 콜백을 전달하면, 렌더링마다 새로운 함수 참조가 생성되어 useEffect 의존성에 의해 반복 실행될 위험이 있습니다.

③ 테스트·예측 가능성 저해
내부적으로 자동 실행되는 콜백은 실행 타이밍을 예측하기 어려워 디버깅과 유지보수가 힘들어집니다.

😀 커스텀 훅 구현하기 (useQueryEffects)
import { useEffect, useRef } from 'react';
import type { UseSuspenseInfiniteQueryResult } from '@tanstack/react-query';

type QueryEffectsOptions<TData, TError> = {
  onSuccess?: (data: TData) => void;
  onError?: (error: TError) => void;
  onSettled?: (data: TData | undefined, error: TError | null) => void;
};

export function useQueryEffects<TData, TError>(
  query: UseSuspenseInfiniteQueryResult<TData, TError>,
  options: QueryEffectsOptions<TData, TError>
) {
  const { onSuccess, onError, onSettled } = options;

  // 이전 상태를 추적하기 위한 ref
  const prevStateRef = useRef({
    isSuccess: false,
    isError: false,
    data: undefined as TData | undefined,
    error: null as TError | null,
  });

  useEffect(() => {
    const { isSuccess, isError, data, error } = query;
    const prev = prevStateRef.current;

    // 새로운 성공 전이 시에만 실행
    if (isSuccess && onSuccess && !prev.isSuccess) {
      onSuccess(data as TData);
    }

    // 새로운 에러 전이 시에만 실행
    if (isError && onError && !prev.isError) {
      onError(error as TError);
    }

    // 성공 또는 에러로 처음 전이되었을 때만 실행
    if ((isSuccess || isError) && onSettled && !(prev.isSuccess || prev.isError)) {
      onSettled(data, error);
    }

    // 현재 상태 저장
    prevStateRef.current = { isSuccess, isError, data, error };
  }, [
    query.isSuccess,
    query.isError,
    query.data,
    query.error,
    onSuccess,
    onError,
    onSettled,
  ]);

  return query;
}
💡 실제 사용처에서 콜백 넘겨주기
const query = useSuspenseInfiniteQuery({
  queryKey: ['popup', 'list', { ...request }],
  queryFn: /* ... */,
});

useQueryEffects(query, {
  onSuccess: (data) => {
    if (process.env.NEXT_PUBLIC_ENV === 'DEVELOP') {
      console.log('[onSuccess]:', data);
    }
  },
  onError: (error) => {
    handleNetworkError(error);
    console.error('[onError]:', error);
    throw error;
  },
  onSettled: (data, error) => {
    if (process.env.NEXT_PUBLIC_ENV === 'DEVELOP') {
      console.log('[onSettled]:', data, error);
    }
  },
});

Next.js 캐싱 최적화하기

Next.js의 캐싱·ISR·태깅(revalidate, tags, revalidateTag)은 fetch에 최적화되어 있어 axios 대신 fetch 래퍼(API Builder) 를 만들어 사용하였습니다.

목표는
(1) 요청마다 캐시 정책이 드러나게 하고(cache, next:{ revalidate, tags })
(2) 서버/클라이언트 인증 분기를 안전하게 처리하며
(3) 파라미터 직렬화·타임아웃·에러 매핑을 공통화하는 것입니다

따라서 API Builder에서 설정과 체이닝을 담당하고, API.call()로 fetch를 실행하도록 구성하였습니다.

캐싱 옵션

  • setCache(cache: RequestCache) 메서드 → no-store/force-cache 등
  • next(config: NextFetchRequestConfig)메서드 → { revalidate, tags } 지정

인증 (서버/클라이언트 분리)

  • .auth()를 호출하면 내부의 authInternal()이 실행됩니다.
  • 서버(SSR/서버 컴포넌트/Route Handler): next/headers의 cookies()로 accessToken을 읽어 Authorization 헤더를 주입.
  • 클라이언트: 별도 주입 없이 withCredentials = true로 쿠키 기반 인증을 사용(브라우저가 자동 전송).

공개 api 호출시

export const getPopupDetailApi = async (
  popupId
) => {
  const response = await APIBuilder.get(
    POPUP_DETAIL_ENDPOINTS.GET_POPUP_DETAIL(popupId)
  )
    .timeout(5000)
    .setCache('force-cache')
  	.next({ revalidate: 300, tags: [`popup:${popupId}`] })  
  	.build()
    .call<PopupDetailResponseDto>();

  return response.data;
};

인증이 필요한 api 호출시

export default async function getUserApi() {
    const response = await (
      await APIBuilder.get('/auth/me')
        .timeout(5000)
        .withCredentials(true)
        .auth()
	    .setCache('no-store') 
        .buildAsync()
    ).call<UserResponse>();

    return response.data;
}

코드 품질/리뷰

프론트/백엔드 개발은 모노레포로 프로젝트를 관리하여 상호 코드리뷰를 통해 코드 품질을 지키려 노력하였습니다.모노레포를 적용하며 파트간 리뷰뿐 아니라 백 <-> 프론트간 리뷰도 진행되어 구현 사항에 관한 논의가 활발하게 진행되었던 점, 이슈트랙킹을 한눈에 볼 수 있던 점이 좋았습니다.

신경썼던 점 : 코드리뷰 문화 개선

코드 리뷰는 필연적으로 상대방의 부족한 점에 대한 지적이 포함되기 때문에, 자칫 감정적으로 상할 수 있다고 생각하였습니다.
그래서 리뷰 시에는 보완할 점뿐 아니라 인상 깊었던 부분에 대한 칭찬도 함께 전달하였고, 😊 같은 이모지를 활용하여 좀 더 부드럽고 긍정적인 대화가 이루어지도록 하였습니다.

또한 프로젝트 중반부터는 코드 리뷰 형식을 정립하여, 리뷰의 중요도를 표기할 수 있는 간단한 형식을 제작하였습니다. 이를 통해 리뷰의 우선순위를 명확히 하고, 피드백을 보다 체계적으로 전달할 수 있었습니다.

성장한 점

코드리뷰 통해 컴포넌트의 적절한 책임 단위의 기준을 배우고 이후에는 스스로 점검하는 습관이 생겼습니다.

인상 깊은 피드백

초기 리뷰 중 FSD 아키텍처 적용 시 폴더 구조를 어떻게 설계할지에 대한 논의 (pr#37)가 있었습니다.
이 과정에서 동료와 충분히 의견을 나누고, 합의점을 도출해낸 점이 이상적인 리뷰 사례로 팀 회고에서 언급되었습니다.
이를 통해 코드 리뷰가 단순한 코드 수정 지적을 넘어, 팀 전체의 아키텍처 방향성을 함께 만들어가는 과정이 될 수 있다는 점을 확인할 수 있었습니다.


협업

여러 직군의 동료와 협업하며 개선해야 할 점을 몇가지 발견하였습니다.

📍 디자이너와의 협업 과정에서의 문제와 개선 방안

프로젝트 진행 중 디자인 변경 사항이 프론트엔드 개발 과정에 원활히 반영되지 못하는 문제를 경험하였습니다.
이번 프로젝트는 ‘UI 구현 완료 → API 연결 및 기능 개발 → QA’ 의 순서로 진행되었는데, 기능 구현 단계에서 시안이 수정되었음에도 불구하고 이를 제때 공유받지 못하는 상황이 있었습니다.
그 결과 QA 단계에 이르러서야 변경 사항을 확인하는 경우가 발생하였습니다.

CSS 수정 자체는 어렵지 않았으나, 디자인 변경 사실이 개발자에게 늦게 전달된다는 점은 협업 과정의 문제라고 판단하였습니다.

💗 개선 방안

  1. 디자인 시안을 명확히 버전으로 구분하여 관리합니다.
  2. 특정 컴포넌트에 변경이 있을 경우, 해당 변경이 영향을 미치는 페이지를 정리해 전달합니다.
  3. 프론트엔드에서는 수정 가능 마감일을 설정하여 불필요한 혼선을 줄입니다.

✏️협업 툴 검토

추가적으로 디자이너와의 협업 효율을 높이기 위해 몇 가지 툴을 검토하였습니다.

  • Design Lint (플러그인)
    : 디자인 시안에서 “디자인 시스템 규칙 위반”을 자동으로 탐지합니다.
    (예: 디자인 시스템에는 #4A90E2 색상만 사용해야 하는데, 실수로 #4A90E3을 사용한 경우 자동으로 탐지)

  • Version History Notifie (플러그인, API)
    : Figma 파일의 변경 이력을 추적하고 알림으로 전달합니다.

⚡️배운 점

이 경험을 통해 디자인 변경 사항의 즉각적인 공유가 개발 효율에 직결된다는 점을 깨달았습니다.
또한 단순히 개인 간의 커뮤니케이션 문제가 아니라, 프로세스와 협업 도구 차원에서 해결해야 하는 문제임을 인식하게 되었습니다.


📍백엔드 협업 과정에서의 API 문서 관리 문제

프로젝트 진행 중 API 문서와 실제 구현 간 불일치로 인해 불필요한 디버깅 시간이 자주 발생하였습니다.
예를 들어,

  1. query 타입이 변경되었거나 응답 필드명이 바뀌었음에도 문서가 최신화되지 않은 경우
  2. 문서는 업데이트되었지만 프론트엔드 코드가 여전히 이전 버전 요청 방식을 사용한 경우
  3. 혹은 백엔드 구현이 문서 스펙과 다르게 이루어진 경우

프론트엔드에서 직접 디버깅을 통해 원인을 찾아야 했으며, 이러한 상황이 예상보다 자주 발생하면서 협업 효율에 부정적인 영향을 주었습니다.

💗 개선 하기

이를 개선하기 위해 문서 관리 방식을 GitHub Wiki에서 OpenAPI(Swagger) 기반으로 전환하자는 제안을 하였습니다.
백엔드 팀 역시 관리되지 않는 문서로 인한 문제에 공감하였으나,

  • Swagger를 사용하지 않은 이유는 “코드가 복잡해지고 더러워진다”는 우려 때문이었으며
  • 대안으로 “테스트 코드 기반 문서 생성 도구”를 검토하였으나 학습 곡선 문제로 도입이 지연되면서 자연히 문서 관리도 어려워졌습니다

결론적으로는 다음 페이즈부터 Swagger를 도입하기로 합의하였습니다.
Swagger는 기존 GitHub Wiki보다 훨씬 간략하게 스펙을 명시할 수 있어 관리 포인트를 줄이면서도 협업 효율을 높일 수 있을것 같기 때문입니다.

⚡️배운 점

이 논의를 통해 문서가 친절해질수록 관리 포인트가 늘어나고, 간략해질수록 관리 포인트는 줄지만 프론트엔드에서 일정 부분 추론이 필요하다는 점을 알게 되었습니다.
결국 프로젝트 상황과 팀 역량에 맞는 현실적인 균형점을 찾는 것이 중요하다는 교훈을 얻었습니다.


📍 비개발자 직군과의 커뮤니케이션 방식 개선하기

비개발자 동료와 함께하는 회의에서 문제 상황을 설명할 때, 저는 주로 문제의 원인(Why) 에 초점을 맞춰 설명하였습니다.
하지만 원인을 설명하다 보면 자연스럽게 구현 방식까지 들어가게 되고, 이 과정에서 개발 용어 사용 빈도가 높아졌습니다.
이로 인해 비개발 직군 입장에서는 결론이 흐려지고 소외감을 느낄 수 있다는 점을 간과하였습니다.

이러한 방식은 효율적이지 못할 뿐만 아니라, 상대방의 입장을 고려하지 못한 부적절한 커뮤니케이션 방식이라고 판단하였습니다.
따라서 ‘왜(Why)’ 보다는 ‘어떻게(How)’에 초점을 맞추는 방식으로 접근하는 것이 바람직하다고 생각하게 되었습니다.

💗 개선 방향

  1. 문제 → 영향 → 선택지 구조
  • 문제(Why) : 원인은 짧고 현상 위주로 설명합니다.
  • 영향(Effect) : 해당 문제가 프로젝트, 사용자, 일정에 어떤 영향을 미치는지 설명합니다.
  • 선택지(How) : 어떤 해결 방향(옵션)이 있는지를 중심으로 제안합니다.
  1. 소통의 레벨 조절
  • 디자이너·기획자와의 회의
    : 사용자 경험, 일정, 우선순위, 업무 흐름에 초점을 맞춥니다.
  • 개발자 간 회의
    : 구현 방식, 코드 레벨까지 상세히 논의합니다.

💡 배운 점

이 경험을 통해, 커뮤니케이션에서 중요한 것은 상대방이 이해할 수 있는 방식으로 핵심을 전달하는 것임을 배웠습니다.
특히 비개발 직군과의 소통에서는 원인 설명보다 해결 방법과 영향에 집중해야 협업 효율과 신뢰를 높일 수 있다는 점을 깨달았습니다.


🤯 정면으로 마주한 갈등 상황

상황 (Situation)

리뷰위크 전까지 MVP를 최종 완성해야 했으나, 같은 파트 팀원의 일정이 지연되면서 전날 새벽까지 트러블슈팅과 기능 구현을 병행해야 하는 상황이 발생하였습니다.
사실 리뷰위크 2주 전부터 진행된 QA 과정에서도 해당 기능이 구현되지 않아 충분한 테스트가 어려웠고, 결국 일정이 연이어 밀리면서 마감 직전까지 이어진 사례였습니다.
또한 팀원의 일정 지연으로 인해, 원래 해당 팀원이 맡은 기능 중 일부를 제가 대신 처리해야 했습니다.

과제 (Task)

이 상황에서 제 과제는 단순히 마감을 맞추는 것뿐만 아니라, 팀 내 일정 지연 문제의 원인을 파악하고 재발을 방지할 수 있는 협업 방식을 마련하는 것이었습니다.

나의 행동 (Action)

  • 마감 직전에는 일단 감정을 누르고, 우선적으로 문제가 되는 기능을 제가 추가로 구현하며 팀의 마감 일정을 맞추는 데 집중했습니다.
  • 리뷰위크 이후에는 프론트엔드 파트 회고 방식을 활용하여 사전에 문항을 준비하고, 팀원과 함께 일정 지연이 발생한 원인에 대해 논의했습니다. (준비했던 회고 문항 바로가기)
  • 그 과정에서 팀원의 일정이 다른 스케줄과 충돌하고, 업무 우선순위 산출 방식이 저와 달랐다는 점을 이해할 수 있었습니다.
  • 개선 방안으로는 QA 주간·리뷰 주간 등 마일스톤 설정, 기능 머지 마감일(T-2일) 규칙화, QA/버그 픽스 전용일(T-1일) 운영을 제안하였고, 팀에서 합의 후 실제로 적용했습니다.
  • 또한 파트 회의에서 업무 우선순위를 함께 정하는 방식을 도입하여 일정 관리의 투명성을 높였습니다.

결과 (Result)

이러한 과정을 통해 일정 지연 문제가 구조적으로 개선되었고, 팀 전체의 협업 방식이 한층 더 명확해졌습니다.
무엇보다도 갈등 상황에서는 감정적인 대응보다는 상대방의 상황을 이해하고, 제도적 개선책으로 풀어내는 접근이 효과적이라는 점을 배웠습니다.


기여한 점, 스스로 칭찬 할 점


  • 이 활동은 젝트에서 진행한 프로젝트입니다
profile
keep on pushing

0개의 댓글