특징화 테스트, 작은 안전 단계, 상태 정리를 통해 동작을 바꾸지 않고 Claude Code로 React 컴포넌트를 안전하게 리팩터링하는 방법을 배우세요.

React 리팩터는 대부분의 컴포넌트가 작고 깔끔한 빌딩 블록이 아니라 살아 있는 UI, 상태, 효과, 그리고 "한 가지 더 필요한 prop" 수정을 품고 있기 때문에 위험하게 느껴집니다. 구조를 바꾸면 의도치 않게 타이밍, 정체성(identity), 데이터 흐름이 바뀌기 쉽습니다.
리팩터가 동작을 바꾸는 경우는 대개 다음과 같습니다:
key가 바뀌어서 상태가 리셋된다.리팩터는 또한 "정리(cleanup)"가 "개선"과 섞이면 재작성(rewrite)으로 바뀝니다. 컴포넌트를 추출하는 것으로 시작해 이름을 바꾸고 상태 형태를 고치고 훅을 교체하면 곧 레이아웃과 로직을 동시에 바꾸게 됩니다. 안전장치가 없으면 어느 변경이 버그를 일으켰는지 알기 어렵습니다.
안전한 리팩터의 약속은 간단합니다: 사용자에게 동일한 동작을 제공하고 코드가 더 명확해져야 합니다. Props, 이벤트, 로딩 상태, 에러 상태, 엣지 케이스는 동일하게 동작해야 합니다. 동작이 바뀌어야 한다면 의도적이고 작으며 명확히 표시되어야 합니다.
Claude Code(또는 어떤 코딩 어시스턴트)를 사용해 React 컴포넌트를 리팩터링할 때는 자동 조종장치가 아니라 빠른 페어 프로그래머로 대하세요. 변경 전에 위험을 설명하게 하고, 작은 단계의 계획을 제안하게 하며, 동작이 동일했는지를 어떻게 확인했는지 설명하게 하세요. 그다음엔 직접 검증하세요: 앱을 실행하고 이상한 경로들을 클릭해 보고, 현재 컴포넌트가 하는 일을 잡아두는 테스트에 의존하세요.
시간을 잡아먹는 한 컴포넌트를 고르세요. 전체 페이지나 "UI 레이어" 또는 막연한 "정리"가 아니라 읽기 어렵고 변경하기 어렵거나 취약한 상태와 사이드 이펙트를 가진 단일 컴포넌트를 선택하세요. 좁은 대상은 어시스턴트의 제안을 검증하기 쉽게 만듭니다.
5분 안에 확인할 수 있는 목표를 작성하세요. 좋은 목표는 결과가 아니라 구조에 관한 것입니다: "더 작은 컴포넌트로 분리한다", "상태를 따라가기 쉽게 만든다", "앱 절반을 목킹하지 않고 테스트 가능하게 만든다" 같은 식입니다. "더 좋게 만들기"나 성능 개선은 구체적인 지표와 병목이 있을 때만 목표로 삼으세요.
에디터를 열기 전에 경계를 설정하세요. 가장 안전한 리팩터는 지루합니다:
그런 다음 코드 이동 시 동작을 조용히 깨뜨릴 수 있는 의존성들을 나열하세요: API 호출, 컨텍스트 제공자, 라우팅 파라미터, 기능 플래그, 분석 이벤트, 공유 글로벌 상태 등.
구체적 예: 600라인짜리 OrdersTable가 데이터 페칭, 필터링, 선택 관리, 상세 드로어 표시를 한다고 합시다. 명확한 목표는 "행 렌더링과 드로어 UI를 컴포넌트로 추출하고 선택 상태를 하나의 리듀서로 옮기되 UI 변경 없음"일 수 있습니다. 이 목표는 무엇이 완료인지와 범위를 알려줍니다.
리팩터하기 전에 컴포넌트를 블랙박스로 취급하세요. 당신의 임무는 현재 그것이 무엇을 하는지 캡처하는 것입니다. 이렇게 하면 리팩터가 재설계로 바뀌는 것을 막을 수 있습니다.
먼저 현재 동작을 평문으로 작성하세요: 이 입력이 주어지면 UI가 저 출력을 보여준다. Props, URL 파라미터, 기능 플래그, 컨텍스트나 스토어에서 오는 모든 데이터를 포함하세요. Claude Code를 사용한다면 작은 집중된 코드 스니펫을 붙여 넣고 나중에 확인할 수 있는 정확한 문장들로 동작을 재진술하게 하세요.
사람들이 실제로 보는 UI 상태를 덮으세요. 컴포넌트는 해피 패스에서는 괜찮아 보이지만 로딩, 빈 상태, 에러 순간에 깨질 수 있습니다.
또한 암묵적인 규칙들을 기록하세요. 이 규칙들은 종종 리팩터를 깨뜨립니다:
예: 사용자 테이블이 결과를 로드하고 검색을 지원하며 "Last active"로 정렬한다고 합시다. 검색이 비어 있을 때, API가 빈 목록을 반환할 때, API가 에러를 반환할 때, 두 사용자의 "Last active"가 동일할 때 어떻게 되는지 적으세요. 정렬이 대소문자 무시인지, 필터 변경 시 현재 페이지를 유지하는지 같은 작은 디테일도 적으세요.
메모가 지루하고 구체적으로 느껴지면 준비된 것입니다.
특징화 테스트는 "지금 이 컴포넌트가 하는 것"을 검증하는 테스트입니다. 이상하고 일관성 없고 장기적으로는 원치 않는 동작조차도 현재 동작을 설명합니다. 역설적으로 들리지만, 리팩터가 조용히 재작성으로 바뀌는 것을 막아줍니다.
React 컴포넌트를 Claude Code로 리팩터링할 때 이 테스트들은 안전 장치입니다. 도구는 코드를 재구성하는 데 도움을 주지만 무엇을 바꿀 수 없는지는 당신이 결정합니다.
사용자(및 다른 코드)가 의존하는 것들에 초점을 맞추세요:
테스트는 구현이 아닌 결과를 검증하도록 작성하세요. "Save 버튼이 비활성화되고 메시지가 나타난다"는 식으로 assert 하세요. 내부 훅 호출이나 setState 호출 같은 구현 세부를 검증하면 리팩터에 취약합니다.
비동기 동작은 리팩터가 타이밍을 바꿔버리기 쉬운 곳입니다. 명시적으로 다루세요: UI가 안정될 때까지 기다린 후 assert 하세요. 디바운스된 검색이나 지연된 토스트처럼 타이머가 있으면 가짜 타이머를 사용해 시간을 전진시키세요. 네트워크 호출이 있으면 fetch를 목킹하고 성공/실패 후 사용자가 보는 것을 assert 하세요. Suspense 같은 흐름은 폴백과 해결된 뷰를 모두 테스트하세요.
예: "Users" 테이블이 검색 완료 후에만 "No results"를 보여준다면, 특징화 테스트는 그 순서를 고정해야 합니다: 먼저 로딩 인디케이터, 그다음 행 또는 빈 메시지.
승리는 "더 큰 변경을 더 빨리 하는 것"이 아니라 컴포넌트가 무엇을 하는지 명확히 파악한 뒤 한 번에 한 가지 작은 것을 바꾸며 동작을 유지하는 것입니다.
먼저 컴포넌트를 붙여넣고 책임의 평문 요약을 요청하세요. 구체적으로 요구하세요: 어떤 데이터를 보여주는지, 어떤 사용자 동작을 다루는지, 어떤 사이드 이펙트(페칭, 타이머, 구독, 분석)를 트리거하는지. 이것은 리팩터를 위험하게 만드는 숨은 일을 드러냅니다.
다음으로 의존성 맵을 요청하세요. 모든 입력과 출력을 목록화하세요: props, 컨텍스트 읽기, 커스텀 훅, 로컬 상태, 파생 값, 효과, 모듈 수준 헬퍼 등. 유용한 맵은 이동해도 안전한 것(순수 계산)과 "끈적이는(sticky)" 것(타이밍, DOM, 네트워크)을 구분해줍니다.
그다음 추출 후보를 제안하게 하되 한 가지 엄격한 규칙을 적용하세요: 순수한 뷰 조각과 상태가 있는 컨트롤러 조각을 분리하세요. JSX가 많고 props만 필요로 하는 부분은 첫번째로 추출하기 좋습니다. 이벤트 핸들러, 비동기 호출, 상태 업데이트가 섞인 섹션은 보통 추출하기에 적합하지 않습니다.
현실에서 작동하는 워크플로:
체크포인트가 중요합니다. Claude Code에 각 단계가 커밋 가능하고 되돌릴 수 있게 최소 계획을 요청하세요. 실용적인 체크포인트 예: "정렬 로직을 건드리기 전에 <TableHeader>를 논리 변경 없이 추출" 같은 것.
구체적 예: 고객 테이블이 있고 필터를 제어하고 데이터를 페칭한다면, 먼저 테이블 마크업(헤더, 행, 빈 상태)을 순수 컴포넌트로 추출하세요. 그 다음 필터 상태나 페치 효과를 옮기세요. 이 순서는 JSX와 함께 버그가 전파되는 것을 막습니다.
큰 컴포넌트를 분할할 때 위험은 JSX를 옮기는 것 자체가 아니라 데이터 흐름, 타이밍, 이벤트 연결을 실수로 바꾸는 것입니다. 추출을 먼저 복사-and-연결(copy-and-wire) 작업으로 처리하고 나중에 정리하세요.
먼저 UI에 이미 존재하는 경계를 찾아보세요. 한 문장으로 "자체적인 것"으로 설명할 수 있는 부분을 찾으세요: 액션이 있는 헤더, 필터 바, 결과 리스트, 푸터의 페이지네이션 등.
안전한 첫걸음은 순수한 프리젠테이션 컴포넌트 추출: props 입력, JSX 출력. 일부러 지루하게 유지하세요. 새로운 상태, 새로운 효과, 새로운 API 호출 금지. 원래 컴포넌트에 클릭 핸들러가 세 가지 일을 했다면 그 핸들러는 부모에 두고 내려보내세요.
보통 잘 맞는 경계: 헤더 영역, 리스트와 행 아이템, 입력만 있는 필터, 푸터 컨트롤(페이지네이션, 합계, 벌크 액션), 다이얼로그(열기/닫기와 콜백 전달).
이름 짓기는 중요합니다. UsersTableHeader나 InvoiceRowActions처럼 구체적인 이름을 선택하세요. "Utils"나 "HelperComponent" 같은 이름은 책임을 숨기고 관심사 혼합을 초래합니다.
컨테이너 컴포넌트는 진짜 필요할 때만 도입하세요: 상태나 효과를 소유해야 일관성을 유지하는 UI 청크일 때. 그때도 좁게 유지하세요. 좋은 컨테이너는 하나의 목적(예: "필터 상태")만 소유하고 나머지는 props로 전달합니다.
어수선한 컴포넌트는 보통 세 종류의 데이터를 섞습니다: 실제 UI 상태(사용자가 바꾸는 것), 파생 데이터(계산으로 얻을 수 있는 것), 서버 상태(네트워크에서 오는 것). 모두를 로컬 상태로 다루면 리팩터가 위험해집니다.
각 데이터 조각에 라벨을 붙이세요. 사용자가 편집하는가, 아니면 props/상태/페치된 데이터로 계산 가능한가? 또한 이 값이 여기서 소유되는가, 아니면 그저 전달되는 것인가?
파생 값은 useState에 두지 마세요. 작은 함수나 비용이 크면 메모이제이션된 셀렉터로 옮기세요. 이렇게 하면 상태 업데이트가 줄고 동작 예측이 쉬워집니다.
안전한 패턴:
useState에 보관useMemo로 감싸기효과는 너무 많은 일을 하거나 잘못된 의존성에 반응할 때 동작을 깨뜨립니다. 목적별로 하나의 효과를 목표로 하세요: localStorage 동기화 하나, 페칭 하나, 구독 하나. 효과가 많은 값을 읽는다면 보통 숨은 책임이 있습니다.
Claude Code를 사용한다면 작은 변경을 요청하세요: 하나의 효과를 둘로 나누거나 한 책임을 헬퍼로 옮기기. 각 이동 후 특징화 테스트를 실행하세요.
프롭 드릴링(prop drilling)을 조심하세요. 컨텍스트로 대체하는 것은 반복적인 배선을 줄이고 소유권을 명확히 할 때만 도움이 됩니다. 컨텍스트가 적합한 표시는 앱 수준 개념(현재 사용자, 테마, 기능 플래그)일 때입니다.
예: 테이블 컴포넌트가 rows와 filteredRows를 상태로 가지고 있다면 rows만 상태로 두고 filteredRows는 rows와 query로부터 계산하세요. 필터링 코드를 순수 함수로 두면 테스트하기 쉽고 깨지기 어렵습니다.
리팩터가 잘못되는 대부분의 경우는 너무 많이 바꾼 뒤에야 발견됩니다. 해결책은 간단합니다: 작은 체크포인트로 작업하고 각 체크포인트를 미니 릴리스처럼 다루세요. 같은 브랜치에서 작업하더라도 변경을 PR 크기로 유지해 무엇이 깨졌는지, 왜 깨졌는지 쉽게 보게 하세요.
의미 있는 이동(컴포넌트 추출, 상태 흐름 변경) 후엔 멈추고 동작이 바뀌지 않았음을 증명하세요. 자동화된 증명(테스트)과 수동 증명(브라우저 빠른 체크)이 있습니다. 목표는 완벽함이 아니라 빠른 감지입니다.
실용적인 체크포인트 루프:
Koder.ai 같은 플랫폼을 사용하면 스냅샷과 롤백이 반복하는 동안 안전장치 역할을 합니다. 그래도 정상 커밋은 필요합니다. 스냅샷은 실험이 망했을 때 "알던 작동 버전"과 비교하거나 되돌릴 때 유용합니다.
간단한 동작 장부(behavior ledger)를 유지하세요. 검증한 내용의 짧은 메모입니다. 같은 것을 반복 점검하는 일을 줄여줍니다.
예:
무언가 깨지면 장부가 무엇을 다시 확인해야 하는지 알려주고 체크포인트가 있으면 저렴하게 되돌릴 수 있습니다.
대부분의 리팩터 실패는 작고 지루한 방식으로 발생합니다. UI는 작동하는 것 같지만 간격 규칙이 사라지거나 클릭 핸들러가 두 번 실행되거나 입력 중 리스트가 포커스를 잃는 식입니다. 어시스턴트가 코드를 더 깔끔하게 만들어주더라도 동작이 미세하게 drift할 수 있습니다.
한 가지 흔한 원인은 구조 변경입니다. 컴포넌트를 추출하면서 추가 \u003cdiv\u003e로 래핑하거나 \u003cbutton\u003e를 클릭 가능한 \u003cdiv\u003e로 바꾸면 CSS 선택자, 레이아웃, 키보드 네비게이션, 테스트 쿼리가 바뀝니다.
가장 자주 동작을 깨뜨리는 함정들:
{} 또는 () => {}) 불필요한 재렌더나 자식 상태 초기화가 발생할 수 있습니다. 예전에는 안정적이던 props를 주시하세요.useEffect, useMemo, useCallback으로 옮기면 의존성이 달라져 오래된 값이나 무한 루프가 생길 수 있습니다. 클릭 시 실행되던 것을 "무언가가 바뀔 때마다" 실행되는 것으로 바꾸지 마세요.구체적 예: 테이블을 분할하면서 행 키를 ID에서 배열 인덱스로 바꾸면 정렬할 때 선택 상태가 깨질 수 있습니다. "깨끗함"은 보너스입니다. "동일한 동작"이 필수조건입니다.
병합 전 리팩터가 동작을 유지했음을 증명하세요. 간단한 신호는 지루합니다: 테스트를 수정하지 않아도 모든 것이 여전히 작동하는가.
마지막 작은 변경 후 빠른 점검:
빠른 건전성 검사: 컴포넌트를 열고 에러를 트리거한 뒤 재시도하고 필터를 지우는 이상한 흐름을 하나 시도해보세요. 리팩터는 주요 경로가 작동할 때 전환을 깨뜨리는 경우가 많습니다.
어떤 항목이 실패하면 마지막 변경을 되돌리고 더 작은 단계로 다시 시도하세요. 큰 diff를 디버그하는 것보다 보통 빠릅니다.
ProductTable 컴포넌트가 데이터 페칭, 필터 관리, 페이지네이션, 삭제 확인 다이얼로그, 행 액션(edit, duplicate, archive)을 모두 담당한다고 상상해보세요. 처음엔 작았지만 900라인 파일로 커졌습니다.
증상은 익숙할 것입니다: 상태가 useState 여러 곳에 흩어져 있고, 몇 개의 useEffect가 특정 순서로 실행되며, 사소한 변경이 특정 필터가 활성화됐을 때만 페이지네이션을 깨뜨립니다. 사람들이 만지는 걸 멈추는 이유는 예측 불가능하기 때문입니다.
구조를 바꾸기 전에 몇 가지 React 특징화 테스트로 동작을 고정하세요. 사용자가 하는 일을 중심으로:
이제 작은 커밋들로 리팩터할 수 있습니다. 깔끔한 추출 계획 예:
FilterBar는 컨트롤을 렌더링하고 필터 변경을 방출TableView는 행과 페이지네이션 렌더링RowActions는 액션 메뉴와 확인 다이얼로그 UI 소유useProductTable 훅이 지저분한 로직(쿼리 파라미터, 파생 상태, 사이드 이펙트)을 소유순서가 중요합니다. 먼저 덤 UI(TableView, FilterBar)를 추출하고 props를 바꾸지 말고 전달하세요. 위험한 부분인 상태와 효과를 useProductTable로 옮기는 작업은 마지막에 하세요. 그때도 옛 prop 이름과 이벤트 형태를 유지해 테스트가 통과하게 하세요. 테스트가 실패하면 스타일 문제가 아니라 동작 변경을 찾은 것입니다.
Claude Code로 React 컴포넌트를 리팩터링하는 것을 매번 안전하게 느끼게 하려면 방금 한 방법을 작은 템플릿으로 만드세요. 목표는 더 많은 프로세스가 아니라 더 적은 놀람입니다.
어떤 컴포넌트에서든 따라할 수 있는 짧은 플레이북을 작성하세요:
이걸 노트나 레포에 스니펫으로 보관하면 다음 리팩터가 동일한 안전장치로 시작됩니다.
컴포넌트가 안정되고 읽기 쉬워지면 사용자 영향에 따라 다음 단계를 선택하세요. 일반적인 순서: 접근성(레이블, 포커스, 키보드) → 성능(메모이제이션, 무거운 렌더) → 정리(타입, 명명, 죽은 코드). 한 PR에 세 가지를 섞지 마세요.
Koder.ai(koder.ai) 같은 워크플로를 사용하면 계획 모드에서 단계 개요를 작성하고 스냅샷/롤백을 체크포인트로 삼아 반복할 수 있습니다. 끝나면 소스 코드를 내보내 최종 diff를 검토하고 깔끔한 히스토리를 유지하세요.
잠재적으로 깨질 것들이 테스트로 커버되고 다음 변경이 기능 추가나 제품 결정 수정이라면 멈추고 배포하세요. 예: 큰 폼을 분할해 얽힌 상태를 제거했고 테스트가 검증과 제출을 커버한다면 배포합니다. 남은 개선 아이디어는 짧은 백로그로 남기세요.
React 리팩터링은 눈에 보이는 UI가 같아도 *정체성(identity)*과 타이밍을 바꿔서 문제가 생기는 경우가 많습니다. 흔한 동작 변화 예시는 다음과 같습니다:
key가 바뀌어 상태가 리셋됩니다.구현을 바꾸기 전까지 구조적 변경이 동작 변경이 될 수 있다고 가정하세요.
모호하지 않고 검증 가능한 구조 중심의 목표를 세우세요. 좋은 목표 예시는:
“더 좋게 만든다” 같은 목표는 피하세요. 측정 가능한 지표와 병목이 있을 때만 성능 목표를 세우세요.
컴포넌트를 블랙박스로 다루고 사용자가 보는 동작을 적어 두세요. 포함할 항목:
세부적이고 지루하게 느껴질수록 쓸모있습니다.
**특징화 테스트(characterization tests)**를 추가해 컴포넌트가 “지금 하는 대로” 동작한다는 걸 고정하세요. 실무에서 안전을 주는 테스트 대상:
테스트는 구현이 아니라 UI 결과를 검증하세요(예: 버튼이 비활성화되고 메시지가 보인다).
조심스러운 페어 프로그래머로 사용하세요:
큰 한 번의 리팩터 스타일 변경(diff)을 받아들이지 말고 검증 가능한 작은 변경을 밀어붙이세요.
먼저 순수한 프리젠테이션 부분을 추출하세요:
복사해서 연결(copy-and-wire)한 뒤에 정리(cleanup)를 하세요. UI가 안전하게 분리된 다음에야 상태와 효과를 옮기세요.
실제 식별자(ID)에 기반한 안정적 키를 사용하세요. 배열 인덱스를 키로 쓰면 정렬·필터·삽입·삭제 시 컴포넌트가 잘못 재사용되어 다음과 같은 버그가 생깁니다:
리팩터링하면서 키를 바꾼다면 높은 위험으로 보고 재정렬 케이스를 꼭 테스트하세요.
파생 데이터(derived data)는 대부분 useState에 넣지 마세요. 안전한 접근법:
useState에 보관rows + query처럼 파생값은 계산해서 전달useMemo로 감싸기이렇게 하면 불필요한 업데이트가 줄고 동작 예측이 쉬워집니다.
작은 체크포인트를 남기면 리팩터가 재작성으로 흐르는 것을 막을 수 있습니다:
Koder.ai를 쓴다면 스냅샷/롤백 기능이 실험 실패 시 유용합니다.
동작이 고정되고 코드가 수정하기 쉬워졌다면 멈추고 배포하세요. 멈출 신호:
리팩터 후 접근성, 성능, 정리 작업은 별도의 작업으로 기록하세요.