React TypeScript 로 기술 스택을 정하고, 각자 어떤 초기 작업을 담당할지 결정하였다. 프로젝트 초기 생성, 라우팅 설정, 코드 리뷰 세팅 등 각자 담당을 정하고 나는 디자인 시스템 관리를 맡게 되었다.
Vite 환경으로 설치를 완료했으나, 계속해서 tailwind.config.js 파일에서 오류가 발생하였다. 지금 찾아보니 PostCSS 와 Autoprefixer 를 설치해주어 빌드까지 안전하게 진행하는 과정이 있다고 하지만, 당시에는 찾지 못해 일반 css 파일에 정의를 완료하였다.
💡 디자인 파트에서 정해준 color theme 을 기준으로 아래와 같이 정의하였다. (일부 생략)
@theme {
/* Gray Scale */
--color-gray-default: #2d2d2d;
--color-gray-100: #ffffff;
--color-gray-200: #f3f3f3;
...
/* Error Scale */
--color-error-400: #d44242;
/* Onboarding Background Color */
--color-gray-onboard: #f9fafb;
/* Primary Color */
--color-primary-blue: #132650;
--color-primary-variant-blue: #2e4475;
--color-calendar-blue: #4474e2;
}
또한 폰트의 경우에도 assets 폴더에 폰트를 다운받고, 폰트명 설정을 한 번에 해주었다. 피그마에 title/regular 이런 식으로 표현되어 있으면 해당 폰트의 크기, 무게, 자간 등을 한 번에 설정하여 클라이언트 개발자들이 바로 알아차릴 수 있도록 초기 설정을 하였다.
@font-face {
font-family: 'Spoqa Han Sans Neo';
src: url('./assets/fonts/SpoqaHanSansNeo-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Spoqa Han Sans Neo';
src: url('./assets/fonts/SpoqaHanSansNeo-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@layer utilities {
// 일부 생략
/* Title (24px, Regular) */
.font-title-r {
font-family: 'Spoqa Han Sans Neo', Arial, sans-serif;
font-size: 2.4rem;
font-weight: 400;
letter-spacing: -0.02em;
line-height: 150%;
}
/* Title/Sub (20px, Bold) */
.font-title-sub-b {
font-family: 'Spoqa Han Sans Neo', Arial, sans-serif;
font-size: 2rem;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 140%;
}
}
html {
font-size: 62.5%; /* 1rem = 10px */
}
매번 디자인의 위 값들을 일일히 입력하거나 이미 주어진 색상표를 사용해서 100% 마음에 들지 않는 경우가 많았다. 하지만 위처럼 색상표, 특히 Gray Scale 을 지정하고 폰트까지 한번에 처리하면 번거로운 경우를 줄일 수 있다.
특히 마지막 font-size 설정은 웹의 접근성을 위해 px 가 아닌 rem 단위로 프로젝트를 구성하자고 얘기가 나왔고, 기본 폰트 크기가 16px(브라우저의 기본 폰트 크기) * 62.5% = 10px 로 되도록 수정하였다.
🧩 현재 상황
React Tanstack Query의 useInfiniteQuery를 사용해 목표 리스트(Goal List)를 페이지 단위로 불러오고 그룹으로 다시 나누어 UI 에 나타내는 과정. 테스트를 위해 초기 데이터도 3개, 무한스크롤을 통해 정한 page size 도 3으로 설정하여 스크롤을 내리며 3개씩 아이템이 발생하는 과정을 기대했다. 그런데 왜 처음에만 3개가 보이고 내리면 나머지가 다 보일까...?
위 과정과 같이 그룹으로 다시 나누는 과정에서 무한스크롤이 정상적으로 작동하지 않는 현상 발생.
처음에는 useInfiniteQuery에서 아래와 같이 select로 데이터 구조 평탄화 진행
select: (data) => ({
pages: data.pages.flatMap((page) => page.result?.data ?? []), // GoalFilter[]
pageParams: data.pageParams,
}),
❗이는 모든 페이지의 데이터를 한 배열로 합쳐서 보여주게 되어, 무한스크롤이 아닌 나머지 전체 목록이 한 번에 보이는 문제점이 발생❗
→ 쿼리 내 select 제거 + 컴포넌트에서 직접 flatMap 처리
const goalGroups = data?.pages.flatMap((page) => page.result?.data ?? []) ?? [];
이를 통해 GoalFilter[] 형태의 그룹 리스트를 얻고, 목록 렌더링에 활용하였다.
const allGroups: GroupedGoal[] = goalGroups.map((g) => ({
key: g.filterName,
items: g.goals,
}));
const grouped = mergeGroups(allGroups); // 사용자 지정 merge
const sortedGrouped = getSortedGrouped(filter, grouped); // 정렬 로직
💡 체크박스 및 담당자 관리 로직에서의 처리의 경우 개별 리스트가 필요하여 flatMap으로 모든 목표를 추출하는 방법을 재활용하였다.
const allGoalsFlat = goalGroups.flatMap((g) => g.goals);
✅ 추후 문제 해결
현재 위처럼 요소 선택 후, 삭제 기능을 useMutation 으로 사용중이다. 하지만 더 극적인 모습을 위해 optimistic update 를 적용하려고 했지만 → 캐시 키 불일치로 인해 화면에 반영되지 않았고, 해당 과정 참고하여 추후 수정할 예정.
이전에도 기획, 디자인, 서버와의 협업으로 프로젝트를 진행한 적이 있었지만 지금처럼 대규모로 진행한 적은 처음이며, 그만큼 전체 맡은 분량도, 각자 담당하는 분야도 다양하다.
예를 들어 내가 맡은 목록 화면에서는 각 분야별로 다른 서버 담당자가 api 를 만들어주어, 초기에는 변수명, 구조 등 일치하는 것이 거의 없을 정도였다. 지속적인 소통과 이전 서버와의 협업 경험을 살려 클라이언트 측에서 UI 구현을 위해 구성한 type 과 먼저 완성도 있게 마무리된 api 구조를 토대로 변경 요청을 반복하였다.
하지만 서버 측에서 간단하게 변수명이나 구조를 바꾸는 과정의 복잡도가 어떤지 전혀 몰라 시간이 걸리더라도 계속 기다리거나 계속 변경을 요청하는 입장이 죄송하다고 느껴졌다. 그래서 이번 프로젝트를 마무리하고, 서버 측 공부를 시작할 예정이다.
데모데이 이후 우리 서비스를 발전해나갈 때도 비슷한 문제가 나타나지 않기를 희망한다.
💡 쿼리 내의 필터 활용을 위해 아래와 같이 case 를 나눠 페이지 코드들에 작성하였다. 현재 상황으로는 페이지 별 필요한 필터 요소가 달라 비효율적으로 우선 모두 작성하였는데, 추후 상수화를 하거나 컴포넌트 또는 utils 로 분리하여 재사용성을 높이는 방향으로 진행해야겠다.
const filterToQuery = (filter: ItemFilter) => {
switch (filter) {
case '상태':
return 'STATE';
case '우선순위':
return 'PRIORITY';
case '담당자':
return 'ASSIGNEE';
case '목표':
return 'GOAL';
case '외부':
return 'EXT_TYPE';
default:
return '';
}
};
💡 또한 2️⃣번에서 해결하였던 것들 토대로 더 빠른 사용자 접근성을 위해 optimistic update 를 반영할 예정이다.