
Cursor IDE와 GPT를 활용해서 2주일 만에 MVP를 다 만들고 앱스토어에 어플리케이션을 출시했습니다. 그 과정에서 제가 느낀 점을 회고하고 리팩토링 개선한 점은 써보겠습니다.
참고자료
https://github.com/PatrickJS/awesome-cursorrules
세 가지 룰셋(Clean Code Guidelines, React Best Practices, Tailwind CSS Best Practices)을 적용했습니다.
프론트엔드 개발은 단순히 “동작하는 코드”를 작성하는 것이 아니라, 읽기 쉽고, 유지보수 가능하며, 팀 전체가 일관성 있게 사용할 수 있는 코드를 작성하는 것이 목표입니다.
이번 글에서는 Clean Code, React, Tailwind CSS의 세 가지 영역에서 지켜야 할 핵심 베스트 프랙티스를 정리했습니다.
1) 매직 넘버 대신 상수 사용
코드 중간에 갑자기 7이나 0.8 같은 값이 튀어나오면, 그 의미를 이해하기 어렵습니다.
이럴 땐 의도를 설명하는 상수를 만들어 사용하세요.
const MAX_RETRY_COUNT = 3;
상수는 파일 상단이나
constants.ts같이 한 곳에 모아두면 관리가 쉬워집니다.
2) 의미 있는 이름
변수나 함수 이름만 보고도 무엇을 하는지, 왜 존재하는지를 알 수 있어야 합니다.
temp 대신 selectedUserdata 대신 userList3) 똑똑한 주석처리
주석은 코드가 무엇을 하는지가 아니라, 왜 그렇게 작성했는지를 설명해야 합니다.
API, 복잡한 알고리즘, 특이한 비즈니스 로직은 꼭 이유를 남겨두세요.
4) 하나의 함수, 하나의 책임
하나의 함수는 하나의 역할만 수행하도록 만들고, 너무 길어지면 분리하세요.
“함수가 길어서 주석이 필요하다면, 나눌 때라는 신호”입니다.
5) 중복 제거(DRY)
같은 코드를 여러 번 쓰기보다, 재사용 가능한 함수나 컴포넌트로 추출하세요.
중복은 버그 발생 시 수정 범위를 넓히고, 유지보수 난이도를 높입니다.
6) 깔끔한 구조
관련 있는 코드는 가까이, 관련 없는 코드는 분리하세요.
폴더/파일 네이밍 컨벤션을 일관성 있게 유지하는 것도 중요합니다.
7) 캡슐화
구현 세부사항은 숨기고, 명확한 인터페이스만 외부에 제공합니다.
중첩 조건문은 함수로 추출해 이름만 봐도 의미가 드러나게 하세요.
8) 테스트 & 버전 관리
1) 컴포넌트 구조
2) 훅(Hooks) 활용
useEffect의 의존성 배열은 정확하게3) 상태 관리
useReducer 활용4) 성능 최적화
React.memo, useMemo, useCallback)5) 폼 처리
react-hook-form 같은 라이브러리 고려6) 접근성 & 에러 처리
1) 프로젝트 설정
tailwind.config.js에서 테마 확장, 색상, 브레이크포인트 정의2) 컴포넌트 스타일링
@apply로 그룹화hover:, focus:)은 일관성 있게 적용3) 레이아웃
4) 타이포그래피 & 색상
5) 성능 최적화
이 세 가지 규칙은 단순한 “코드 예쁘게 쓰기”를 넘어서, 협업 효율과 프로젝트 수명을 좌우합니다.
프로젝트를 시작할 때부터 팀원들과 이 규칙을 합의하고, 리뷰 시 꾸준히 적용하면 코드 품질이 꾸준히 유지됩니다.
좋은 코드는 동작할 뿐 아니라, 읽는 사람을 배려합니다.
Apple 로그인 → WebView 토큰 전달 → 서버 통신 → 타이머 로딩까지의 과정에서 지연이 발생함. 사용자 입장에서 웹뷰가 로드되는 동안 빈 화면을 마주함
저는 두가지 방식으로 접근했습니다.

처음엔 RN에서 첫 페이지 데이터 미리 준비해서 데이터와 함께 주입해서 같이 그릴 수 있는 방식으로 생각했습니다.
위 방식은 리팩토링하기엔 모노레포가 아니였기에 중복 로직 관리 부담이 됐고,(원래 WebView(웹) 쪽에서 타이머를 불러오는 API 로직이 있었는데, RN에서도 동일한 API를 호출하게 되면 중복 관리가 필요하겠죠) 또한, API 변경이나 파라미터 수정 시 RN + 웹 양쪽을 다 고쳐야 한다는 단점이 있었습니다.
또한 저는 처음 설계를 다음과 같이 하였기 때문에 책임 영역이 혼동될 거 같아 1차 접근 방식은 사용하지 않았습니다.
"누가 뭘 관리하지?" 혼란을 야기할 수 있고 & 버그 발생시 어디를 먼저 확인할지 모호
React Native = 컨테이너
Web App = 비즈니스 로직
Apple 로그인 → WebView 토큰 전달 → 서버 통신 → 타이머 로딩 과정에서 발생하는 지연 문제에 대해, 미리 RN에서 데이터를 받아 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개 | 재사용성 +∞ |
| 테스트 가능성 | 불가능 | 각 훅/컴포넌트 독립 테스트 | 테스트성 |
// ❌ 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>
);
};
// ✅ 각 훅이 하나의 책임만 담당
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 };
};
// ✅ 각 컴포넌트가 독립적으로 재사용 가능
<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" // 새로운 스타일
/>
);
};
// ✅ useTodoManager에 새 기능만 추가하면 끝
export const useTodoManager = (timerId: number) => {
// 기존 로직...
// 새 기능 추가
const prioritizeTodo = (todoId: number) => {
// 우선순위 로직
};
const categorizeTodo = (todoId: number, category: string) => {
// 카테고리 로직
};
return {
// 기존 반환값...
prioritizeTodo,
categorizeTodo,
};
};
1. 300줄 파일 열기
2. 어디에 코드를 넣을지 고민
3. 기존 로직과 충돌 확인
4. 7개 useEffect 의존성 체크
5. 전체 파일 테스트
1. 해당 훅/컴포넌트 파일 열기
2. 새 함수 추가
3. 독립적이라 충돌 위험 없음
4. 해당 훅만 테스트
// ✅ 훅별 독립 테스트
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 기술의 미래