7일만에 바이브 코딩으로 만든 어플 "먼지 치우기" 회고

LEE KAYOUNG / KATIE·2025년 8월 13일
0

AI

목록 보기
5/5
post-thumbnail

Cursor IDE와 GPT를 활용해서 2주일 만에 MVP를 다 만들고 앱스토어에 어플리케이션을 출시했습니다. 그 과정에서 제가 느낀 점을 회고하고 리팩토링 개선한 점은 써보겠습니다.

커서 룰 세팅

참고자료
https://github.com/PatrickJS/awesome-cursorrules
세 가지 룰셋(Clean Code Guidelines, React Best Practices, Tailwind CSS Best Practices)을 적용했습니다.

깨끗하고 유지보수하기 좋은 프론트엔드 코드를 위한 3가지 원칙

프론트엔드 개발은 단순히 “동작하는 코드”를 작성하는 것이 아니라, 읽기 쉽고, 유지보수 가능하며, 팀 전체가 일관성 있게 사용할 수 있는 코드를 작성하는 것이 목표입니다.
이번 글에서는 Clean Code, React, Tailwind CSS의 세 가지 영역에서 지켜야 할 핵심 베스트 프랙티스를 정리했습니다.


1. Clean Code – 가독성과 유지보수성을 위한 기본기

1) 매직 넘버 대신 상수 사용

코드 중간에 갑자기 7이나 0.8 같은 값이 튀어나오면, 그 의미를 이해하기 어렵습니다.
이럴 땐 의도를 설명하는 상수를 만들어 사용하세요.

const MAX_RETRY_COUNT = 3;

상수는 파일 상단이나 constants.ts 같이 한 곳에 모아두면 관리가 쉬워집니다.


2) 의미 있는 이름

변수나 함수 이름만 보고도 무엇을 하는지, 왜 존재하는지를 알 수 있어야 합니다.

  • temp 대신 selectedUser
  • data 대신 userList

3) 똑똑한 주석처리

주석은 코드가 무엇을 하는지가 아니라, 왜 그렇게 작성했는지를 설명해야 합니다.
API, 복잡한 알고리즘, 특이한 비즈니스 로직은 꼭 이유를 남겨두세요.


4) 하나의 함수, 하나의 책임

하나의 함수는 하나의 역할만 수행하도록 만들고, 너무 길어지면 분리하세요.

“함수가 길어서 주석이 필요하다면, 나눌 때라는 신호”입니다.


5) 중복 제거(DRY)

같은 코드를 여러 번 쓰기보다, 재사용 가능한 함수나 컴포넌트로 추출하세요.

중복은 버그 발생 시 수정 범위를 넓히고, 유지보수 난이도를 높입니다.


6) 깔끔한 구조

관련 있는 코드는 가까이, 관련 없는 코드는 분리하세요.
폴더/파일 네이밍 컨벤션을 일관성 있게 유지하는 것도 중요합니다.


7) 캡슐화

구현 세부사항은 숨기고, 명확한 인터페이스만 외부에 제공합니다.
중첩 조건문은 함수로 추출해 이름만 봐도 의미가 드러나게 하세요.


8) 테스트 & 버전 관리

  • 버그를 고치기 전에 테스트를 먼저 작성
  • 커밋 메시지는 명확하게
  • 커밋은 작고 집중되게

2. React Best Practices – 유지보수 가능한 컴포넌트 구조

1) 컴포넌트 구조

  • 함수형 컴포넌트를 기본으로 사용
  • 하나의 컴포넌트는 하나의 역할만
  • 공통 로직은 커스텀 훅으로 추출
  • 큰 컴포넌트는 작은 단위로 쪼개기

2) 훅(Hooks) 활용

  • Hooks 규칙을 반드시 지키기
  • useEffect의 의존성 배열은 정확하게
  • 필요하면 cleanup 함수 구현
  • 재사용 가능한 로직은 커스텀 훅으로

3) 상태 관리

  • 상태는 사용되는 범위와 가장 가까운 곳에 두기
  • 복잡한 상태는 useReducer 활용
  • Context는 꼭 필요할 때만 사용 (prop drilling 방지)
  • 글로벌 상태 관리 라이브러리는 신중하게 도입

4) 성능 최적화

  • 불필요한 리렌더 방지 (React.memo, useMemo, useCallback)
  • key props는 고유값 사용
  • 지연 로딩(lazy loading) 적극 활용
  • 성능 프로파일링으로 병목 구간 확인

5) 폼 처리

  • 컨트롤드 컴포넌트 사용
  • 유효성 검증과 에러 상태 처리
  • 로딩/에러 상태 UI 제공
  • 복잡한 폼은 react-hook-form 같은 라이브러리 고려

6) 접근성 & 에러 처리

  • 시멘틱 태그, 올바른 ARIA 속성
  • 키보드 네비게이션 지원
  • 에러 바운더리로 예외 처리
  • 사용자 친화적인 에러 메시지 제공

3. Tailwind CSS Best Practices – 유지보수 가능한 스타일 작성

1) 프로젝트 설정

  • tailwind.config.js에서 테마 확장, 색상, 브레이크포인트 정의
  • Purge 설정으로 불필요한 CSS 제거
  • 플러그인 활용

2) 컴포넌트 스타일링

  • 가능하면 유틸리티 클래스 사용
  • 반복되는 스타일은 @apply로 그룹화
  • 상태 변형(hover:, focus:)은 일관성 있게 적용
  • 다크 모드 지원 고려

3) 레이아웃

  • Flex/Grid 유틸리티 적극 활용
  • 반응형 브레이크포인트 통일
  • 간격(spacing) 시스템 통일
  • 정렬, 패딩, 마진 클래스는 일관성 유지

4) 타이포그래피 & 색상

  • 폰트 크기, 줄간격, 굵기 유틸리티 사용
  • 명확한 색상 대비 유지
  • 의미 있는 색상 네이밍
  • 호버/활성 상태 스타일 지정

5) 성능 최적화

  • 커스텀 CSS 최소화
  • 빌드 시 CSS 압축
  • 번들 사이즈 모니터링

마무리

이 세 가지 규칙은 단순한 “코드 예쁘게 쓰기”를 넘어서, 협업 효율프로젝트 수명을 좌우합니다.
프로젝트를 시작할 때부터 팀원들과 이 규칙을 합의하고, 리뷰 시 꾸준히 적용하면 코드 품질이 꾸준히 유지됩니다.

좋은 코드는 동작할 뿐 아니라, 읽는 사람을 배려합니다.

개발 후 첫 번째 문제 인식

Apple 로그인 → WebView 토큰 전달 → 서버 통신 → 타이머 로딩까지의 과정에서 지연이 발생함. 사용자 입장에서 웹뷰가 로드되는 동안 빈 화면을 마주함

문제 해결

저는 두가지 방식으로 접근했습니다.

1차 접근 - RN에서 데이터 사전 로딩

처음엔 RN에서 첫 페이지 데이터 미리 준비해서 데이터와 함께 주입해서 같이 그릴 수 있는 방식으로 생각했습니다.
위 방식은 리팩토링하기엔 모노레포가 아니였기에 중복 로직 관리 부담이 됐고,(원래 WebView(웹) 쪽에서 타이머를 불러오는 API 로직이 있었는데, RN에서도 동일한 API를 호출하게 되면 중복 관리가 필요하겠죠) 또한, API 변경이나 파라미터 수정 시 RN + 웹 양쪽을 다 고쳐야 한다는 단점이 있었습니다.

또한 저는 처음 설계를 다음과 같이 하였기 때문에 책임 영역이 혼동될 거 같아 1차 접근 방식은 사용하지 않았습니다.

"누가 뭘 관리하지?" 혼란을 야기할 수 있고 & 버그 발생시 어디를 먼저 확인할지 모호

React Native = 컨테이너

  • 로그인, 네이티브 기능, 웹뷰 관리가 주 역할
  • 비즈니스 로직까지 담당하는 건 역할 벗어남

Web App = 비즈니스 로직

  • 이미 완벽한 타이머 로직 존재
  • 무한스크롤, 캐싱, 에러 처리 모두 완성됨

2차 접근

Apple 로그인 → WebView 토큰 전달 → 서버 통신 → 타이머 로딩 과정에서 발생하는 지연 문제에 대해, 미리 RN에서 데이터를 받아 WebView로 함께 넘기는 방식을 고려했습니다.
하지만 이 접근은 다음과 같은 단점이 있었습니다.

  • 코드 복잡성 증가: RN과 WebView 양쪽에서 동일한 데이터 처리 로직을 관리해야 함
  • 동기화 문제: RN에서 받은 데이터와 WebView 내 데이터가 불일치할 가능성 존재
  • 실시간 갱신 불편: 타이머처럼 주기적으로 갱신이 필요한 데이터는 어차피 WebView에서 다시 요청해야 함

따라서 초기 로딩은 빨라질 수 있지만, 실시간성을 유지하는 데 비효율적이라 판단했습니다.
최종적으로 WebView 로딩 상태를 onLoadStart / onLoadEnd로 감지하여,
로딩 오버레이로 사용자가 "즉시 반응하는 느낌"을 받을 수 있게 하고
UX 최적화를 위해 기존의 isLoading 기반 로딩 처리에서 스켈레톤 UI를 추가 도입했습니다.

이로써 데이터 동기화 문제를 피하면서도, 사용자는 부드럽고 안정적인 로딩 경험을 할 수 있게 되었습니다.

구분평균 표시 시간(ms)표준편차(ms)
개선 전 (빈 화면)2800ms±350ms
개선 후 (스켈레톤 UI)300ms*±40ms

두 번째 문제 인식

두 번째로는, 바이브 코딩의 속도는 빠르지만 그 구현이 반드시 개발자 친화적인 구조를 보장하지 않는다는 점이었습니다.
빠르게 기능을 완성하는 과정에서, 공통 컴포넌트나 재사용 가능한 모듈을 설계 단계에서 충분히 반영하지 못한 채 넘어가는 경우가 많았습니다.

그 결과 비슷한 기능의 코드가 여러 곳에서 중복되었고, 폴더 구조와 책임 분리가 모호해져 리팩토링 비용이 높아지는 문제가 발생했습니다.
즉, 초기 속도를 위해 희생한 설계 부재가, 장기적으로는 유지보수성과 확장성 저하라는 형태로 돌아왔습니다.

한 파일에 모든 로직이 다 들어있어서 300줄이 넘는 거대한 컴포넌트를 예시로 들어보겠습니다.

개선 지표

항목Before (바이브 코딩)After (리팩토링)개선율
파일 줄 수1개 파일 300+ 줄11개 파일 평균 50줄가독성 +
useEffect 개수7개 (복잡한 의존성)훅별 1-2개 (단순)복잡도 -70%
useState 개수6개 (한 곳에 집중)훅별 분산 관리관리성 +
재사용 가능 컴포넌트0개8개재사용성 +∞
테스트 가능성불가능각 훅/컴포넌트 독립 테스트테스트성

1. 단일 책임 원칙 적용

// ❌ Before: 모든 로직이 하나의 컴포넌트에
const TimerDetail = () => {
  // 타이머 로직
  const [remainingSeconds, setRemainingSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(true);
  
  // TODO 로직  
  const [isAdding, setIsAdding] = useState(false);
  const [inputValue, setInputValue] = useState('');
  
  // 바텀시트 로직
  const [isBottomSheetExpanded, setIsBottomSheetExpanded] = useState(false);
  
  // 7개의 useEffect...
  // 300줄의 JSX...
};

// ✅ After: 역할별로 완전 분리
const TimerDetail = () => {
  const timer = useTimer(minutes);           // 타이머 로직만
  const todoManager = useTodoManager(timerId); // TODO 로직만  
  const bottomSheet = useBottomSheet();      // UI 상태만
  
  return (
    <TimerDisplay {...timer} />              // 렌더링만
    <BottomSheet {...bottomSheet}>
      <TodoSection {...todoManager} />
    </BottomSheet>
  );
};

2. 커스텀 훅으로 완벽한 로직 분리

// ✅ 각 훅이 하나의 책임만 담당
export const useTimer = (initialMinutes: number) => {
  // 타이머 관련 상태와 로직만
  return { remainingSeconds, isRunning, isCompleted, toggleTimer, formatTime };
};

export const useTodoManager = (timerId: number) => {
  // TODO 관련 상태와 로직만  
  return { isAdding, inputValue, startAdding, submitTodo, cancelAdding };
};

export const useBottomSheet = () => {
  // 바텀시트 관련 상태와 로직만
  return { isExpanded, expand, collapse, handleDragEnd };
};

3. 컴포넌트 단위 분할로 재사용성 극대화

// ✅ 각 컴포넌트가 독립적으로 재사용 가능
<TimerDisplay 
  timerName="공부하기"
  formattedTime="25:00" 
  isCompleted={false}
  onEnd={() => {}}
  // 다른 페이지에서도 동일하게 사용 가능
/>

<TodoSection 
  todos={todoList}
  isAdding={false}
  // 다른 타이머나 프로젝트에서도 재사용 가능
/>

✔️ 실제 사용법 - 새로운 기능 추가가 쉬워짐

새 타이머 타입 추가하기

// ✅ 1분만에 새로운 타이머 컴포넌트 생성
const Timer = () => {
  const timer = useTimer(25); // 25분 포모도로
  
  return (
    <TimerDisplay 
      {...timer}
      timerName="포모도로"
      variant="pomodoro" // 새로운 스타일
    />
  );
};

새로운 TODO 기능 추가하기

// ✅ useTodoManager에 새 기능만 추가하면 끝
export const useTodoManager = (timerId: number) => {
  // 기존 로직...
  
  // 새 기능 추가
  const prioritizeTodo = (todoId: number) => {
    // 우선순위 로직
  };
  
  const categorizeTodo = (todoId: number, category: string) => {
    // 카테고리 로직  
  };
  
  return {
    // 기존 반환값...
    prioritizeTodo,
    categorizeTodo,
  };
};

Before: 새 기능 추가할 때

1. 300줄 파일 열기 
2. 어디에 코드를 넣을지 고민 
3. 기존 로직과 충돌 확인 
4. 7개 useEffect 의존성 체크 
5. 전체 파일 테스트 

After: 새 기능 추가할 때

1. 해당 훅/컴포넌트 파일 열기 
2. 새 함수 추가 
3. 독립적이라 충돌 위험 없음 
4. 해당 훅만 테스트 

After: 각 기능별 독립 테스트

// ✅ 훅별 독립 테스트
test('타이머 로직 테스트', () => {
  const { result } = renderHook(() => useTimer(25));
  
  act(() => {
    result.current.toggleTimer();
  });
  
  expect(result.current.isRunning).toBe(false);
});

test('TODO 추가 로직 테스트', () => {
  const { result } = renderHook(() => useTodoManager(1));
  
  act(() => {
    result.current.startAdding();
  });
  
  expect(result.current.isAdding).toBe(true);
});

// ✅ 컴포넌트별 독립 테스트
test('TimerDisplay 렌더링 테스트', () => {
  render(
    <TimerDisplay 
      timerName="테스트 타이머"
      formattedTime="25:00"
      isCompleted={false}
      onEnd={jest.fn()}
      isEnding={false}
      onBack={jest.fn()}
    />
  );
  
  expect(screen.getByText('테스트 타이머')).toBeInTheDocument();
  expect(screen.getByText('25:00')).toBeInTheDocument();
});

참고 자료 :
토스가 꿈꾸는 React Native 기술의 미래

profile
[궁금한 것들 이리저리..쿵]

0개의 댓글