개발/프론트엔드

30개의 상태가 달린 하나의 컴포넌트를 마주친 적 있으신가요?

paice 2025. 5. 14. 00:53

30개의 상태관리가 달려있는 하나의 컴포넌트를 마주하신 적 있으신가요?

최근 제가 맡은 업무 중 하나는, 파일 변환 기능을 담당하는 컴포넌트

  1. 기능 추가
  2. 구조 개선 < 이를 중점적으로 글을 쓸 거예요.
  3. 배포 및 테스트

이 세 가지를 모두 책임지는 일이었어요.
처음에는 “기존 코드에 기능 몇 개만 추가하면 되겠지”라는 마음으로 가볍게 시작했지만, 실제로 마주한 코드는 예상과는 조금 달랐어요.


return 문만 보면 현기증이…
 

이 컴포넌트에는 useState가 30개가 나열돼 있었고,
UI 구성은 Tailwind CSS 라이브러리를 사용한 형태로 되어 있었어요.
Tailwind는 스타일을 클래스 단위로 태그에 바로 작성하는 방식이라, 작은 컴포넌트에선 굉장히 생산적일 수 있지만,
return문 안에 여러 레이아웃과 조건부 렌더링이 얽혀 있을 경우 한눈에 파악하기가 쉽지 않은 느낌이었어요.
예시로 들면 ...

return (
  <div className="flex flex-col gap-4 w-full bg-white p-6 rounded-xl shadow-md">
    {isUploading && <p className="text-sm text-blue-500">업로드 중...</p>}
    <input
      type="file"
      onChange={handleFileChange}
      className="border border-gray-300 rounded-md p-2"
    />
    {error && <p className="text-sm text-red-500">{error}</p>}
    {/* ... 이런 코드가 계속 반복 */}
  </div>
);

그리고 이 컴포넌트 안에…

const [file, setFile] = useState(null);
const [fileName, setFileName] = useState('');
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState('');
// ... 그 외 20여개가 더

 
이런 useState가 무더기로 선언되어 있었어요.
그 상태들은 대부분 자식 컴포넌트에도 props로 전달되고 있었고요.
 
이쯤 되면 감이 오겠죠? 상태 관리의 복잡성이 문제의 핵심이었어요.
이 문제를 방치하면 나중에는 프롭스 드릴링(props drilling) 즉, 상위 컴포넌트에서 선언한 상태를 하위 컴포넌트로 계속해서 깊게 전달해야 하는 구조로 인해 컴포넌트 간 결합도가 높아지고, 컴포넌트 재사용성은 떨어지며, 버그 추적이나 유지보수가 매우 어려워지는 상황에 빠지게 될 수 있어요.


내가 아닌 다른 사람이 만든 코드를 고쳐나가는 것은 처음입니다만 ...

 

코드를 보며 저는 고민에 빠졌어요.
 

작동이 되는 코드를 괜히 고쳤다가... 안 되면 어떡하지... 

 
해당 소스는 이미 프로젝트에서 사용 중인 기능이기도 했고,
무엇보다 "내가 감히 다른 개발자의 코드를 손봐도 괜찮을까?" 라는 막연한 부담감도 있었어요.
내가 쓴 코드라면 언제든 뜯어고치고, 고장 나도 다시 고치면 되지만, 남이 만든 코드에 손을 댄다는 건 '이 구조가 틀렸다'고 판단해야 하는 일이기도 하니까요.
그래서 처음엔 파일을 열어두고도 며칠을 머뭇거렸어요. "정말 바꿔야만 하나?", "정 내가 힘들어도 그냥 참고 쓰는 게 낫지 않을까?"
하는 생각을 계속 반복했어요.


"이대로 두면, 나도 나중에 발목 잡힌다"
 

그렇게 며칠을 고민하다가 결국 리팩토링을 결심한 데에는, 단순히 지금 눈앞에 있는 문제 때문만은 아니었어요.
 
1. 언젠가는 이 컴포넌트를 남에게 설명해야 한다
 
앞으로 제가 다른 프로젝트로 넘어가거나, 혹은 누군가 이 기능을 인수받게 되는 순간이 분명 올 텐데,
어떤 기능이 어떤 흐름으로 동작하는지,이 상태는 왜 여기 있고, 왜 이렇게 전달되고 있는지 설명하려면 그 누구보다 제가 이 구조를 잘 이해하고 있어야 하고, 결국엔 “남이 보기 쉬운 코드”로 바꿔놔야 내가 설명할 수 있겠다는 생각이 들었어요.
 
2. 기능을 추가하려면 지금부터 정리돼 있어야 한다
 
그리고 무엇보다 이 컴포넌트는 단발성 기능이 아니었어요. 앞으로도 계속 이 코드를 재활용하고 유용하게 쓰기 위해서는 구조가 명확하고 깔끔해야 한다는 생각했어요.
그런데 지금처럼 useState가 30개씩 흩어져 있고, 컴포넌트 구조도 무겁게 얽혀 있는 상태라면 기능 하나 추가할 때마다 다른 기능이 망가질 수도 있다는 불안함이 생겼어요.
즉, 지금 손대지 않으면 나중에는 더 손도 못 댈 정도로 커지겠다는 위기감이 들었어요.

 

그리고 제가 항상 생각해왔지만, 저는 개발을 할 때 ‘불가능이란 없다’라고 믿고 시작하면 사고가 확 열리면서 신기하게도 길이 보이기 시작하더라고요. 이번에도 마찬가지였어요.
어떻게든 해내야겠다는 생각뿐이었습니다.

 
“일단 나눠보자. 나누다 보면 흐름이 보이겠지.”

그렇게 상태를 하나하나 분류하고, 컴포넌트의 역할을 정리하면서 점점 더 코드가 이해되기 시작했고,
마침내 내가 책임질 수 있는 구조로 리팩토링을 마칠 수 있었어요.


어떻게 나눴는가: 리팩토링의 방향을 잡기 위해 생각한 것들

 

막연히 “나눠야겠다”는 생각만으로는 리팩토링이 되지 않을거에요.
그래서 논리적으로, 효율성 있게 생각해보려고 노력했어요.
 
무엇을 기준으로 나눌 것인가, 그리고 나누면 어떤 이점이 생길 것인가에 대한 기준을 만들었고,
다음과 같은 질문을 스스로에게 던지며 방향을 잡아갔어요.


✅ 1. 기능이 분리되는가? → API 호출 로직부터 훅으로 추출
 

가장 먼저 눈에 들어온 건 API 호출 로직이었어요.
컴포넌트 내부에 주기능 함수들이 모두 들어있었고, 동작의 흐름은 명확하게 나뉘어 있었지만, UI와 무관한 로직까지 컴포넌트 안에 섞여 있는 상태였어요.
 

"이건 굳이 컴포넌트 안에 있을 필요가 있을까?"

 
그래서 구조를 정리하기 시작했어요.

  1. 컴포넌트 내부의 주요 기능 함수들은 역할(기능) 단위로 나누어 커스텀 훅으로 분리했고,
  2. API 요청과 관련된 로직은 api 폴더 안으로 이동시켜
    • apiClient.ts에서 공통 axios 인스턴스를 관리하고,
    • 기능별로 각각의 API 요청은 파일 하나씩 나누어 정리했어요.

이렇게 나누고 나니, 테스트가 훨씬 쉬워졌고 비즈니스 로직과 UI 로직이 자연스럽게 분리되어
코드가 눈에 훨씬 잘 들어오고 유지보수도 편한 구조가 되었어요.


✅ 2. UI의 책임이 명확한가? → 화면 영역 기준으로 컴포넌트 분리
 

다음으로 살펴본 건 화면 구조였어요.
초기에 기능 단위로 로직을 분리하면서, 자연스럽게 각 UI가 담당하는 역할도 명확하게 드러났어요.
 
그래서
"시각적으로 구분되는 영역 = 하나의 컴포넌트"
라는 기준을 세우고 리팩토링을 진행했어요.
 
이 기준에 따라 UI를 나눠보니 각 컴포넌트가 어떤 일을 담당하는지 한눈에 파악할 수 있었고, 더 이상 거대한 컴포넌트를 해석하느라 스트레스를 받을 일도 없어졌어요.


3. 상태 관리, 제대로 하고 있는가?
 

마지막으로 남은 숙제는 상태 관리였어요.
처음 코드를 봤을 때, 최상위 컴포넌트가 너무 많은 useState를 가지고 있었고, 그걸 하위 컴포넌트로 계속 props로 넘기는 구조였어요.
 
문제는,
일부 상태는 특정 UI에서만 쓰이는데도
전체 컴포넌트가 그 상태의 영향을 받아 불필요하게 리렌더링되고 있었던 거예요.
그래서 아래 기준을 세워 상태의 위치를 정리했어요.
 
1. 여러 컴포넌트에서 공유되는 상태 → 최상위로 올리기
2. 특정 기능에서만 쓰이는 상태 → 그 기능 내부 컴포넌트로 내리기
3. 재사용 가능한 전역 상태 → 전역 상태 관리 도구 Recoil로 분리
 
이렇게 상태의 위치를 정리함으로써, 비효율적인 렌더링을 줄이고 코드의 흐름을 명확히 했으며, 결과적으로 새로운 기능을 추가하거나 버그를 수정할 때도 훨씬 더 효율적으로 작업할 수 있게 되었어요.


 
이번 작업을 통해 느낀 건, 리팩토링은 단순히 코드를 보기 좋게 정리하는 일이 아니라는 점이에요.
진짜 중요한 건 왜 그렇게 나눴는지, 그 구조가 어떤 문제를 해결했는지를 논리적으로 설명할 수 있는 코드로 만드는 거였어요.
그래야 나중에 다른 누군가가 이 코드를 보더라도 쉽게 이해할 수 있고, 무엇보다 내가 다시 봤을 때도 낯설지 않고 자신 있게 수정할 수 있다고 느꼈어요.
 
그래서 리팩토링을 진행하면서 계속 스스로에게 질문을 던졌어요.
"이 로직은 여기 있어야 할까?" "이 상태는 왜 이 위치에 있지?" 이런 고민을 하면서 기준을 세우고, 그 기준에 맞춰 구조를 하나씩 정리해 나갔어요.
 
그 과정을 통해 결국 내가 설명할 수 있는 코드, 두렵지 않고 자신 있게 다룰 수 있는 코드를 만들 수 있었고,
이번 경험이 단순한 코드 정리를 넘어 개발자로서 한 걸음 더 성장하는 계기가 되었다고 느꼈어요.
아마 다음에도 이런 상황에 마주하면 자신감 있게 맞설 수 있겠단 생각을 했습니다!
다음 글에는 이 세가지 과정을 구체적으로 설명하는 글을 써보도록 할게요.
 

그 전에 저 야근 좀 그만하고 싶어요................ (누구보다 야근을 즐기는 광기를 보이며)

살려줘요