React Components vs Spaghetti: UI가 유지보수 불가능해지는 5가지 신호

발행: (2026년 3월 24일 PM 05:30 GMT+9)
13 분 소요
원문: Dev.to

Source: Dev.to

위 링크에 있는 글의 내용을 번역하려면 실제 텍스트를 제공해 주시겠어요?
링크만으로는 본문을 가져올 수 없으니, 번역이 필요한 부분을 복사해서 알려주시면 한국어로 번역해 드리겠습니다.

지난 주에 React 컴포넌트를 열었는데… 바로 닫았습니다.

복잡해서가 아니라,
적대감을 느꼈기 때문입니다.

그 느낌을 아시죠? 파일이 계속 스크롤되고, props가 여기저기 날아다니며, 작은 변경 하나가 전혀 관련 없는 무언가를 깨뜨릴 것 같은 느낌.
그건 복잡함이 아니라 엔트로피입니다.

UI를 오래 만들어 왔다면, 이런 현상이 서서히, 거의 눈에 보이지 않게 일어나는 것을 보았을 겁니다.
상황이 악화되기 전에 신호에 대해 이야기해 봅시다.

React 컴포넌트가 읽기 어렵고, 깨지기 쉬우며, 예측할 수 없게 느껴진다면, UI가 유지보수가 어려워지고 있다는 신호입니다.
가장 흔한 신호는 다음과 같습니다:

  • 과도하게 큰 컴포넌트
  • props‑드릴링
  • 불명확한 책임 분담
  • 중복
  • 복잡한 조건문

재작성은 필요 없습니다—작고 일관된 리팩터링 습관만 있으면 됩니다.

“It Still Works”의 문제점

  1. The God Component (Too Big to Understand)갓 컴포넌트 (이해하기엔 너무 큼)
  2. Props Drilling EverywhereProps Drilling이 여기저기
  3. Confusing Responsibilities혼란스러운 책임 분담
  4. UI Logic DuplicationUI 로직 중복
  5. Conditional Rendering Hell조건부 렌더링 지옥

무시하면 안 되는 추가 징후

(필요한 경우 여기 추가 경고 신호를 적으세요.)

Source:

Mini Refactoring: From Spaghetti to Clean

1. Split the God Component

// Before
export default function Dashboard({ user, posts }: Props) {
  // fetching logic
  // filtering logic
  // UI rendering
  // event handlers
  // conditionals everywhere

  return (
    <>
      <h2>{user.name}</h2>
      {/* hundreds of lines */}
    </>
  );
}

왜 문제가 되는가:
파일이 너무 커서 책임이 뒤얽혀 있고 경계가 없습니다.

경험 법칙: 컴포넌트를 한 문장으로 설명할 수 없으면, 너무 많은 일을 하고 있다는 뜻.

// After – clear boundaries
export function Dashboard() {
  return (
    <>
      {/* sub‑components go here */}
    </>
  );
}

이제 각 조각마다 목적 한계가 있습니다.

2. Stop Unnecessary Props Drilling

// Props drilling example
// (placeholder – actual code omitted)

일부 컴포넌트는 실제로 사용하지도 않는 데이터를 파이프라인처럼 전달받기만 합니다.
보통의 해결책은 “Context를 사용하자”인데, Context를 과도하게 사용하면 또 다른 마찰이 생깁니다:

  • 데이터가 덜 명시적이 된다
  • 재렌더링 제어가 어려워진다
  • 데이터 출처 추적이 복잡해진다

Context는 의도적으로 사용하세요. 진정으로 전역적인 데이터(인증, 테마, 사용자) 혹은 깊게 공유되는 상태에만 사용합니다.

// Custom hook + Context
function useUser() {
  return useContext(UserContext);
}

function UserAvatar() {
  const user = useUser();
  return <img src={user.avatar} alt={user.name} />;
}

이제 의존성이 정확히 필요한 곳에 존재합니다.

3. Separate Concerns (Fetching, State, UI)

다음과 같이 섞인 컴포넌트:

  • 데이터 패칭
  • 상태 관리
  • 변환
  • 렌더링

은 컨텍스트 전환을 계속하게 만듭니다.

// Bad: everything in one component
function Dashboard() {
  // …fetch, state, UI all together
}

훅과 순수 UI 컴포넌트로 리팩터링:

// Hook for data
function usePosts() {
  // fetch + transform
}

// UI component
function PostList() {
  const posts = usePosts();
  return (
    <ul>
      {posts.map(p => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  );
}

이제 컴포넌트가 퍼즐이 아니라 이야기를 읽는 듯합니다.

4. Eliminate Repeated Patterns

다음과 같은 반복 스니펫:

if (isLoading) return <Spinner />;
if (error) return <ErrorMessage />;

은 중복된 로직을 초래합니다.

공유 컴포넌트로 추출:

function DataState({ loading, error, children }: Props) {
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage />;
  return children;
}

이제 모든 UI가 하나의 일관된 구현을 공유합니다.

5. Tame Conditional Rendering

중첩된 삼항 연산자는 읽기 어렵습니다:

return isAdmin
  ? isActive
    ? <AdminActive />
    : <AdminInactive />
  : <UserView />;

JSX 밖으로 로직을 이동:

function renderContent() {
  if (isAdmin && isActive) return <AdminActive />;
  if (isAdmin) return <AdminInactive />;
  return <UserView />;
}

return renderContent();

또는 가드 절을 사용해 선형 흐름을 만들 수 있습니다:

if (!isAdmin) return <UserView />;
if (!isActive) return <AdminInactive />;
return <AdminActive />;

실용적인 리팩터링 의식

의식적용 시점예시
컴포넌트 크기 검사> 200 LOC 또는 책임 2개 초과서브 컴포넌트 또는 훅으로 분리
Props 드릴링 감사동일한 prop이 2단계 이상 전달될 때Context 또는 커스텀 훅 도입
중복 스캔동일한 JSX/로직이 2번 이상 나타날 때공유 컴포넌트 또는 유틸리티 추출
조건문 단순화중첩된 삼항 연산자 또는 JSX에서 &&가 많이 사용될 때로직을 함수나 가드 절로 분리
책임 검토컴포넌트가 데이터와 UI를 혼합할 때데이터를 가져오는 로직을 훅으로 분리

이러한 검사를 정기적으로 실행하세요(예: 각 PR 병합 전) 엔트로피를 방지하기 위해.

최종 생각

대부분의 복잡한 UI는 처음부터 복잡하게 시작하지 않는다.
그들은 작고, 명확하며, 심지어 우아하게 시작한다. 그런 다음:

  1. 기능이 추가된다.
  2. 또 다른 기능이 뒤따른다.
  3. 마감 전에 급히 수정한다.

극적인 일은 없고—그 순간에 타당한 작은 결정들일 뿐이다.
그러다 어느 날 파일을 열어보니 더 이상 제대로 이해하지 못한다는 것을 깨닫는다.

그때가 바로 “아직 동작한다”는 말이 위험해지는 순간이다.
이제 모든 변경은 위험을 동반하고, 코드를 건드리는 비용이 그대로 두는 이점보다 크다.

대부분의 프로젝트에는 모두가 피하는 파일이 하나씩 있다.
그 파일은 크다. 정말 크다. 데이터, UI, 상태, 이벤트를 모두 처리하고, 아마도 몇 가지 부수 효과도 포함하고 있다.

위의 습관—분할, 범위 지정, 추출, 보호—을 적용하면 명확성을 회복하고 위험을 줄이며 UI를 장기적으로 유지 보수 가능하게 만든다. 리팩토링을 즐기세요!

“너무 많은 일이 진행되고 있다”가 경고가 될 때

“여기엔 조금 너무 많은 일이 진행되고 있어요.”

당신도 이런 경험을 해봤을 겁니다:

  • 파일을 열기 전에 주저합니다.
  • 뭔가를 바꾸는 것이 두렵습니다.
  • 버그를 고치는 것이 위험하게 느껴집니다.
  • “이건 건드리지 말자” 라고 한 번 이상 말했습니다.

원본 컴포넌트

export default function Dashboard({ user, posts }: Props) {
  const [filter, setFilter] = useState("all");

  const filteredPosts = posts.filter(p =>
    filter === "all" ? true : p.type === filter
  );

  return (
    <>
      <h2>{user.name}</h2>

      <select value={filter} onChange={e => setFilter(e.target.value)}>
        <option value="all">All</option>
        <option value="tech">Tech</option>
      </select>

      {filteredPosts.length === 0 ? (
        <p>No posts</p>
      ) : (
        filteredPosts.map(p => <div key={p.id}>{p.title}</div>)
      )}
    </>
  );
}

기술적으로는 문제가 없지만, 모든 것이 뒤섞여 있습니다:

  • 상태
  • 로직
  • UI

작은 리팩터: 훅 추출

function useFilteredPosts(posts: Post[]) {
  const [filter, setFilter] = useState("all");

  const filtered = posts.filter(post =>
    filter === "all" ? true : post.type === filter
  );

  return { filter, setFilter, filtered };
}

이제 컴포넌트는 필터링이 어떻게 작동하는지에 대해 신경 쓰지 않는다.

작은 리팩터링: UI 조각 추출

function PostFilter({ value, onChange }: Props) {
  return (
    <select value={value} onChange={e => onChange(e.target.value)}>
      <option value="all">All</option>
      <option value="tech">Tech</option>
    </select>
  );
}

리팩터링된 컴포넌트

export default function Dashboard({ user, posts }: Props) {
  const { filter, setFilter, filtered } = useFilteredPosts(posts);

  return (
    <>
      <h2>{user.name}</h2>

      <PostFilter value={filter} onChange={setFilter} />

      {filtered.length === 0 ? (
        <p>No posts</p>
      ) : (
        filtered.map(p => <div key={p.id}>{p.title}</div>)
      )}
    </>
  );
}

같은 기능이지만 전혀 다른 느낌입니다.

왜 이것이 중요한가

그것이 리팩터링의 진정한 목표입니다. 스파게티 코드를 고치기 위해 전체 재작성은 필요하지 않으며, 습관이 필요합니다.

채택할 간단한 습관

  • 문제가 느껴질 때 조기에 추출한다.
  • 컴포넌트를 단일 책임에 집중시킨다.
  • 증가하는 로직을 커스텀 훅으로 이동한다.
  • 명명에 시간을 투자하여 명확하게 한다.

이 중 어느 것도 혁신적이지 않지만, 함께 적용하면 코드베이스를 더 다루기 쉽게 만든다.

결론

Spaghetti UI는 실패가 아니라, 실제 기능이 실제 제약과 마주할 때 발생하는 현상입니다. 이를 무시하면 점점 코드베이스가 신뢰하기보다 피하고 싶은 것이 됩니다.

좋은 소식은, 완벽함을 추구할 필요는 없으며, 작고 일관된 개선만으로 충분합니다.

이 내용이 공감된다면

  • ❤️ 반응 남기기
  • 🦄 유니콘 남기기
  • 댓글에 지금까지 작성(또는 본) 가장 안 좋은 컴포넌트를 공유해 주세요

그리고 이런 콘텐츠가 마음에 든다면, DEV에서 저를 팔로우해 주세요.

0 조회
Back to Blog

관련 글

더 보기 »