모임을 쉽고 간편하게 MIMO
모임 생성 부터 회비 관리와 실시간 채팅까지,
쉽고 간편한 모임 관리서비스를 제공합니다.
디자인

프로젝트 개요
- 기간 : 2025.01 - 2025.02, 7주
- 역할 : Front-end 개발 50%, UX/UI 디자인 100%
- 구현 정도 : 웹 배포 후 실 사용 테스트 및 최적화
기술 스택
- React, TypeScript, Vite, npm
- 스타일링 : Tailwind CSS
- 서버 상태/캐싱 : React Query
- 컴포넌트 개발 및 검증 : StoryBook
StoryBook
StoryBook를 이용해 시나리오별 상태를 시각적으로 검증
const meta: Meta<typeof Input> = {
title: 'Components/Atoms/Input',
component: Input,
tags: ['autodocs'],
argTypes: {
type: {
control: 'select',
options: ['text', 'email', 'number', 'password'],
},
value: { control: 'text' },
defaultValue: { control: 'text' },
placeholder: { control: 'text' },
disabled: { control: 'boolean' },
readOnly: { control: 'boolean' },
multiline: { control: 'boolean' },
rows: { control: 'number' },
},
};
export default meta;
type Story = StoryObj<typeof Input>;
export const Default: Story = {
args: {
id: 'default-input',
placeholder: '텍스트 입력...',
type: 'text',
multiline: false,
},
};
export const Disabled: Story = {
args: {
id: 'disabled-input',
placeholder: '비활성화됨',
disabled: true,
multiline: false,
},
};
...

Storybook
컴포넌트 단위 개발 환경을 구축하여, 실제 서비스와 독립적으로 컴포넌트를 렌더링하고 확인할 수 있었습니다.
Storybook을 통해 props를 실시간으로 조작하면서 다양한 UI 상태를 검증했습니다.
이를 통해 예외 케이스와 UI를 조기에 확인할 수 있었고, QA 단계 이전에 오류를 줄일 수 있었습니다.
성과
이전에는 특정 props 조합을 수동으로 넣어보지 않으면 UI가 어떻게 깨지는지 확인하기 어려웠습니다. 조합에 따라 발생하는 UI 예외를 놓치거나 QA 단계에서 뒤늦게 발견하는 문제가 있었습니다.
하지만, Storybook을 이용해 컴포넌트별 다양한 상태를 사전에 테스트할 수 있었고, 프론트엔드 개발 단계에서 오류를 빠르게 수정할 수 있었습니다. 그 결과 개발 효율성과 품질이 모두 개선되었습니다.
단점
컴포넌트 변경 시 스토리 파일도 함께 수정해야 하므로 유지보수 리소스가 체감할 수 있을 정도로 증가하였음
Atomic Design
Atomic Design를 이용해 컴포넌트를 단위별로 개발하였습니다.
Atom 예시
Molecule 예시

Organism 예시



Atomic Design 사용 이유
UI를 작은 단위부터 구성하여 재사용성을 극대화하고, 컴포넌트 설계를 체계적으로 관리하기 위해 도입했습니다.
디자인 시스템과 코드의 일관성을 유지하고, 팀 단위 개발 시 협업 효율성을 높이기 위해 Atomic Design을 적용했습니다. (협업시 컴포넌트 구조를 이해하기 쉽다고 생각됨)
성과
컴포넌트 단위 재사용이 용이해져, 새로운 페이지나 기능 추가 시 개발 속도가 단축되었습니다.
유지보수 과정에서 같은 UI 요소를 여러 곳에서 반복해서 쓰더라도 중앙에서 한 번만 수정하면 전체 반영되어 유지보수 속도가 빨라졌습니다.
디자인과 레이아웃이 통일되어 UX 일관성이 확보되었습니다.
StoryBook과 함께 사용하여, Props 조합 테스트가 용이했고,작은 단위 컴포넌트를 합쳐 통합 테스트하는 과정도 효율적으로 수행할 수 있었습니다.
단점
컴포넌트를 원자 단위로 쪼개고 구조를 체계화하는 초기 설계 단계와 Markup에서 많은 시간이 소요되었습니다.
중앙에서 한 번만 수정하면 전체 반영되는 방식이 긍정적으로 사용될 때는 간편했지만, 많은 연결 요소에서 수정이 필요할 때는 번거로웠습니다.
초기 폴더 구조 설계를 잘못해, Component 아래에 atoms, molecules, organisms 폴더를 생성하여 모든 컴포넌트를 넣다 보니, 컴포넌트가 증가할수록 관리가 복잡해졌습니다.
중첩 레이아웃을 이용한 레이아웃 구조화
중첩 레이아웃을 이용하여 일관성있는 UI 구현, 로직 관리를 했습니다.
<Route path="/team">
<Route index element={<Navigate to="/" replace />} />
<Route path="create" element={<TeamCreate />} />
<Route path=":teamId" element={<TeamLayout />}>
<Route index element={<TeamDetail />} />
<Route path="edit" element={<TeamEdit />} />
<Route path="review" element={<Review />} />
...
</Route>
</Route>
중첩 레이아웃 활용
DefaultLayout → TeamLayout과 같은 중첩 레이아웃을 사용하여, 페이지 구조를 명확하게 분리했습니다.
상위 레이아웃은 한 번만 렌더링되고, 하위 <Outlet />만 교체되어 불필요한 전체 페이지 리렌더링을 최소화했습니다.
Layout 단계에서 권한 검증 로직을 구현하여, 인증되지 않은 사용자의 접근을 사전 차단했습니다.
//bodyLayout24
<div className="flex h-fit w-full flex-col items-center gap-16 px-4 py-4 pl-8">
{children}
</div>
//bodyLayout64
<div className="flex h-fit w-full flex-col items-center gap-6 py-4 pt-4 pr-3 pl-8">
{children}
</div>
//baseLayout
<section className="flex h-full flex-col gap-2">{children}</section>
상위 Layout을 활용한 UI/UX 일관성
중첩 라우팅 기반의 중첩 레이아웃뿐 아니라, 상위 Layout에서 children을 받아 재사용하는 구조를 구현했습니다. 이를 통해 페이지별 UI/UX 일관성을 유지하고, 반복되는 스타일 및 구조를 통합 관리할 수 있었습니다.
개선점 - 불분명한 에러 처리
에러 처리에 미흡한 점이 있었습니다.
export const getTeamInfo = async (
teamId: string,
): Promise<TeamInfoResponse> => {
if (!teamId) {
throw new Error('팀 아이디가 없습니다.');
}
try {
const response = await customFetch('/team', {
method: 'GET',
params: { teamId }, // params는 객체여야 함
});
return response.json();
} catch (error) {
console.error('Error fetching category teams:', error);
throw error;
}
};
const { data: teamData, error: teamError } = useQuery({
queryKey: ['teamInfo', teamId],
queryFn: () => getTeamInfo(teamId),
});
useEffect(() => {
if (teamError) {
setIsErrorModalOpen(true);
}
}, [teamError]);
현재 코드에서는 fetch 과정에서 error를 한번에 throw함
try중 error catch는 브라우저 네트워크 에러만 처리함
따라서 response를 바로 return하는 것이 아닌, response.status를 통해 error를 throw해야함
export const TEAM_ERROR_MESSAGES = {
UNAUTHORIZED: 'UNAUTHORIZED_TEAM',
FORBIDDEN: 'FORBIDDEN_TEAM',
NOT_FOUND: 'NOT_FOUND_TEAM',
}
export const getTeamInfo = async (
teamId: string,
): Promise<TeamInfoResponse> => {
if (!teamId) {
throw new Error('팀 아이디가 없습니다.');
}
const response = await customFetch('/team', {
method: 'GET',
params: { teamId }, // params는 객체여야 함
});
if (!response.ok) {
if (response.status === 401) {
throw new Error(TEAM_ERROR_MESSAGES.UNAUTHORIZED);
}
if (response.status === 403) {
throw new Error(TEAM_ERROR_MESSAGES.FORBIDDEN);
}
if (response.status === 404) {
throw new Error(TEAM_ERROR_MESSAGES.NOT_FOUND);
}
}
return response.json();
};
const { data: teamData, error: teamError } = useQuery({
queryKey: ['teamInfo', teamId],
queryFn: () => getTeamInfo(teamId),
});
useEffect(() => {
if (teamError) {
if (teamError.message === TEAM_ERROR_MESSAGES.UNAUTHORIZED) {
setErrorMessage('로그인이 필요합니다.');
}else if (teamError.message === TEAM_ERROR_MESSAGES.FORBIDDEN) {
setErrorMessage('접근 권한이 없습니다.');
}
else if (teamError.message === TEAM_ERROR_MESSAGES.NOT_FOUND) {
setErrorMessage('존재하지 않는 팀입니다.');
}else{
setErrorMessage('오류가 발생했습니다.');
}
setIsErrorModalOpen(true);
}
}, [teamError]);
Error를 const로 관리해, 각 섹션별 Error 메시지를 관리함으로 일관성을 높이고 휴먼 에러를 줄일 수 있음.
Error에 따른 에러 포맷을 리턴하여, 사용자경험을 향상시키고, 개발자도 디버깅에 용이해짐