[Opinion] 오늘날 프론트엔드는 쉽게 엉망이 될 수 있고, 우리는 이를 정리해야 한다

발행: (2026년 2월 1일 오후 03:25 GMT+9)
19 min read
원문: Dev.to

Source: Dev.to

Disclaimer: 이 게시물은 현대 프론트엔드의 복잡성에 대한 불평과 이를 해결하기 위한 내 생각을 섞어 놓은 것입니다. 나는 프론트엔드/React 세계에서 잠시 떨어져 있었기 때문에 이 게시물에는 다소 오래된 아이디어가 포함될 수 있습니다. 읽는 중에 그런 아이디어를 발견하면 알려 주세요. 여기서는 React/Next.js에 대해서만 논의하지만, 이 주장은 프레임워크와 관계없이 전체 프론트엔드 생태계에도 적용될 수 있다고 생각합니다. 따라서 글 전체에서 frontendReact를 서로 교체해서 사용할 것입니다.

React는 정말 복잡해

최근에 프론트엔드가 Next.js로 구축된 애플리케이션을 개발하는 작업에 참여했습니다. React와 Next.js를 사용한 작업을 많이 해봤지만, 그때는 디자인 부분을 다룰 필요가 없었습니다. 백엔드에서 데이터를 받아 React 컴포넌트에 전달해 보여주는 정도였죠. 이번이 레이아웃부터 작은 텍스트 조각의 폰트 색상까지 시각 디자인을 진지하게 고려해야 하는 첫 번째 경험이었고, 정말 힘들었습니다!

왜 그럴까요?
MDN에 있는 방대한 CSS 문서가 압도적이었던 것(그것도 도움이 됐지만)만은 아닙니다. 더 어려운 점은 React 컴포넌트를 금방 엉망으로 만들 수 있다는 점입니다. 여기서 말하는 “엉망 코드”란 목적이나 책임이 파악하기 어렵고, 동작과 상태 업데이트를 추적하기 힘든 컴포넌트를 의미합니다.

내 실력 문제라고 비난하기 전에, 최근에 마주한 React/Next.js 코드를 떠올려 보세요. 기억이 나지 않으면 공식 React 문서 홈페이지의 고급 주제들을 살펴보세요. 예를 들어, 아래 발췌는 입력과 상태 관리에 관한 페이지에서 가져온 것입니다:

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return 

맞아요!

; }

async function handleSubmit(e) { e.preventDefault(); setStatus(‘submitting’); try { await submitForm(answer); setStatus(‘success’); } catch (err) { setStatus(‘typing’); setError(err); } }

function handleTextareaChange(e) { setAnswer(e.target.value); }

return ( <>

City quiz

    In which city is there a billboard that turns air into drinkable water?
  
  
    
    

    
      Submit
    
    {error !== null && (
      

{error.message}

    )}
  

); }


이 예제가 비교적 단순함에도 불구하고 이미 다음과 같은 요소들을 포함하고 있습니다:

- 세 개의 상태 조각  
- 중첩된 HTML 요소  
- 두 개의 이벤트 핸들러  
- 조건부 렌더링  

왜 이렇게 보기에 간단해 보이는 것이 복잡하게 느껴질까요?

React(및 프론트엔드 전반)에서 극복하기 어려운 근본적인 문제가 있다고 생각합니다: React 컴포넌트는 관리되는 상태의 정보를 JSX에 표현해야 하는데, JSX는 본질적으로 “더 깔끔한” HTML 버전입니다. 자연스러운 접근처럼 보이지만, 근본적인 긴장을 야기합니다.

## 프론트엔드 = 상태 + 계층적 프레젠테이션

모든 프론트엔드 기술—웹이든 모바일이든—은 본질적으로 **현재 상태를 관리**하고(**클라이언트든 서버든**) **그 상태를 계층적인 뷰로 렌더링**하는 것에 관한 것입니다. 계층 구조는 다음과 같은 여러 도전을 안겨줍니다:

- **레이아웃** – 형제 HTML 노드들은 서로 어떻게 관계를 맺나요? `` 요소는 몇 개의 자식을 가져야 할까요? 반응형 디자인은 어떻게 처리하나요?  
- **상태 관리** – 모든 데이터를 한 곳에서 가져와 여러 컴포넌트에 배포해야 할까요, 아니면 각 작은 컴포넌트가 자체적으로 데이터를 가져오게 해야 할까요? 업데이트와 재렌더링은 어떻게 처리하나요? 데이터 페칭은 부모에서 할지, 자식에서 할지?

상태 관리와 레이아웃이 긴밀히 연결되어 있으면서 UI는 계층 구조를 유지해야 하기 때문에 복잡성(또는 “지저분함”)이 빠르게 증가합니다.

이전 `Form` 예제를 생각해 보세요. 백엔드에서 동적으로 가져온 목록 중에서 사용자가 답을 선택할 수 있도록 ``을 ``으로 교체하고 싶다고 가정합니다. 자연스러운 첫 번째 단계는 같은 `Form` 컴포넌트 안에 `useEffect`를 추가하는 것일 수 있습니다:

```tsx
export default function Form() {
  // …
  const [countries, setCountries] = useState([]);

  useEffect(() => {
    async function fetchCountries() {
      try {
        const response = await fetch('GET_COUNTRY_LIST_API');
        const data = await response.json();
        setCountries(data.countries || []);
      } catch (error) {
        setError(error);
      }
    }

    fetchCountries();
  }, []);

  // …
}

이것이 좋은 해결책일까요? 일부는 그렇다고 말할 것이고, 다른 일부는 동의하지 않을 것입니다. countries 목록을 데이터를 렌더링하고 제출하는 동일한 Form 컴포넌트에서 가져와야 할까요, 아니면…? (논의는 계속됩니다…)

컨테이너‑프레젠테이션 패턴 재조명

Dan Abramov의 유명한 “프레젠테이션‑컨테이너” 패턴은 이 복잡함을 정리하는 데 유용한 통찰을 제공합니다(쉽게 시작하고 싶다면 patterns.dev의 이 글을 읽어보세요).

제가 이해한 바로는, React 컴포넌트를 작성하는 두 가지 패턴이 있습니다:

TypeDescription
Stateful (또는 non‑functional)애플리케이션의 내부 상태를 관리합니다. 이는 순수 클라이언트‑사이드 상태(예: 입력창의 현재 텍스트 값)일 수도 있고, 백엔드나 서드‑파티 API에서 가져온 데이터일 수도 있습니다. 요컨대, useState, useEffect, fetch 등을 사용하는 모든 컴포넌트는 stateful합니다.
Stateless (또는 purely functional)순수 함수형 – 읽는 데이터는 불변이며 useEffect에 의해 발생하는 부수 효과가 없습니다. 주어진 데이터를 시각화하는 역할만 담당합니다.

Form 컴포넌트에 패턴 적용하기

Form 컴포넌트는 여러 상태를 useState로 관리하기 때문에 명백히 stateful합니다. 여기서 후보 국가 목록을 가져오기 위해 useEffect를 추가한다면, 해당 컴포넌트는 백엔드에서 가져온 데이터 처리도 담당하게 됩니다.

이러한 관심사의 분리는 유지보수에 특히 유용합니다:

  • 추가적인 데이터 제출 로직을 추가하려면 Form 컴포넌트를 수정하면 됩니다.
  • 국가 텍스트 제출에 문제가 있다면, 버그는 반드시 이 Form 내부에 있습니다.

프레젠테이션‑컨테이너 패턴에 따라 컴포넌트를 리팩터링하면, 마크업(프레젠테이션 부분)과 상태‑처리 로직(컨테이너 부분)을 분리하게 됩니다. 프레젠테이션 컴포넌트는 다음과 같이 구현될 수 있습니다:

export const FormBox = ({
  title,
  description,
  answer,
  status,
  error,
  handleSubmit,
  handleTextareaChange,
}: Props) => {
  return (
    <>
      

{title}

{description}

...
          Submit
...

Note: 컨테이너 컴포넌트는 FormBox를 import하고 적절한 props(상태 값 및 콜백)를 전달합니다.

Hierarchical concerns

이러한 논리적 분리를 적용하더라도, 프런트엔드 계층 구조는 상태를 가진 컴포넌트와 상태가 없는 컴포넌트 사이의 경계를 여전히 흐리게 만들 수 있습니다. 상태가 있는 요소를 상태가 없는 요소 안에 중첩하는 데는 제한이 없습니다. 다음 예시를 살펴보세요:

export function FormLayout() {
  return (
    
      {/* some other components */}
      
    
  );
}

FormLayout 자체는 제출 로직을 포함하고 있지 않지만, 개념적으로 폼을 그룹화하기 때문에 디버깅 중에 확인하게 될 가능성이 높습니다. 이는 프런트엔드 코드를 조직하기 위해 보다 포괄적인 사고 모델이 필요함을 보여줍니다.

Source: https://atomicdesign.bradfrost.com/chapter-2/

원자 디자인 다시 살펴보기

Brad Frost’s Atomic Design 은 React 프로젝트를 구조화하는 또 다른 유용한 관점을 제공합니다. Frost가 다섯 단계의 컴포넌트(원자, 분자, 유기체, 템플릿, 페이지)를 정의하지만, 제가 얻은 교훈은 전체 프론트엔드 페이지를 두 가지 측면으로 생각할 수 있다는 점입니다:

측면설명
Layout시각적 컴포넌트가 화면에 어떻게 배치되는지(크기, 위치, flexbox 정렬 등)를 다룹니다. 이는 주로 CSS와 관련된 문제이며, 각 컴포넌트는 자체적으로 포함되어 형제 컴포넌트에 의도치 않게 영향을 주지 않도록 해야 합니다(예: overflow나 리사이징으로 인한 영향).
Feature page제품 관점에서 컴포넌트가 사용자에게 무엇을 전달하려는지를 다룹니다. 각 기능은 단일 책임 원칙(Single‑Responsibility Principle)을 따라야 합니다. 하나의 기능은 여러 하위 기능으로 구성될 수 있습니다(예: 텍스트 입력, 파일 업로드 등을 포함한 폼 페이지). 각 하위 기능은 자체 UI와 데이터 상태를 관리합니다.

명명 고려 사항

FormLayout이라는 이름은 폼 자체뿐 아니라 네비게이션 바나 광고 배너와 같은 무관한 요소까지 포함할 수 있기 때문에 오해를 불러일으킬 수 있습니다. 이런 경우 QuizPageLayout처럼 더 구체적인 이름이 적절할 수 있습니다.

모두 합치기

이제 우리는 관심사를 분리하기 위한 정신 모델을 갖게 되었습니다:

  1. 계층적 기능 구조 – 전체 프로젝트가 기능 트리 형태로 조직됩니다.
  2. 페이지‑수준 레이아웃 – 각 최상위 기능은 레이아웃 컴포넌트에 의해 자체 페이지가 할당됩니다.
  3. 기능‑수준 상태 – 각 기능은 자체 데이터를 가져오고 업데이트하여 상태를 격리합니다.

컨테이너‑프레젠테이션 패턴을 원자 디자인 원칙과 결합함으로써, 깔끔하고 유지보수가 용이하며 확장 가능한 프론트엔드 아키텍처를 구현할 수 있습니다.

레이아웃 – 페이지 모델

앞서 다룬 Container‑Presentational 패턴과 함께 Layout‑Page 모델에 대해 자세히 논의해 보겠습니다.

레이아웃이란?

  • 레이아웃은 Atomic Design의 organisms, templates, pages에 해당합니다.
  • 전체 화면에 여러 컴포넌트를 배치하는 역할만 담당합니다: 각 컴포넌트의 위치, 표시 방식, 크기를 결정합니다.
  • 구분선과 같은 시각적 도우미를 포함할 수 있지만, 이는 드물게 사용됩니다.
  • 레이아웃은 개별 컴포넌트(즉, 페이지)를 어떻게 렌더링할지 혹은 그 컴포넌트의 마진·패딩 속성을 다루지 않습니다.

페이지란?

  • 페이지는 단일 책임 원칙에 따라 제품 내 하나의 기능을 나타냅니다.
  • 백엔드에서 해당 기능과 관련된 데이터를 가져오고 업데이트하는 역할을 하며, 필요할 경우 클라이언트 측 UI 상태를 관리합니다.
    • 전달된 데이터를 시각화만 하는 무상태 순수 함수형 페이지도 유효합니다.
  • 페이지는 두 요소로 구성됩니다: 자체 레이아웃서브 페이지.
    • 페이지가 기본 HTML 요소만 포함하고 다른 React 컴포넌트를 포함하지 않으면 리프 페이지로 간주할 수 있습니다.

각 페이지가 자체 레이아웃과 서브 페이지를 가질 수 있기 때문에 구조는 재귀적이며, 레이아웃과 서브 페이지로 논리적으로 구분된 페이지 트리 형태가 됩니다.

예시 트리

QuizPage
├── @AdsBanner
│   ├── @Page
│   └── Layout
├── @QuizSubmitPage
│   ├── @Page   #  will be in this page
│   └── Layout
└── Layout

이 트리는 Next.js App Router의 구조를 반영하고 있지만, Next.js는 특정 설계 원칙을 강제하지 않습니다. App Router의 파일 기반 라우팅은 Layout‑Page 모델과 자연스럽게 맞아떨어지며, 이 모델은 부분적으로 그것에 영감을 받았습니다.

Source:

Next.js 파일 라우팅 매핑

quiz
├── @adsbanner
│   ├── page.tsx
│   └── layout.tsx
├── submit
│   ├── page.tsx   #  will be in this page
│   └── layout.tsx
├── layout.tsx
└── page.tsx
  • 광고 배너병렬 라우트를 사용합니다(자세한 내용은 Next.js 병렬 라우트 문서를 참고). 이를 통해 배너가 독립적인 라우트로 노출되지 않도록 합니다.
  • 배너가 quiz/submit/이 아니라 quiz/ 아래에 위치하기 때문에, quiz/의 모든 하위 라우트에서 배너가 표시됩니다.
  • 병렬 라우트는 Layout‑Page 모델에 필수적이며, 하나의 제품 기능이 여러 하위 기능을 포함할 수 있도록 합니다.

전체 프로젝트 재귀

Project
├── Layout
├── Page0
│   ├── Layout
│   ├── Page00
│   │   ├── Layout
│   │   ├── Page000
│   │   …
│   ├── Page01
│   ├── Page02
│   …
├── Page1
├── Page2
├── Page3

마무리 생각

제가 이 정신 모델의 원래 창시자는 아닐 수도 있습니다; 다른 사람이 이미 다른 이름으로 유사한 아이디어를 발표했을지도 모릅니다. 복잡한 프론트‑엔드를 정리할 효과적인 방법을 오래 찾은 끝에 이 모델이 저에게 떠올랐습니다. 여러분이 가지고 계신 의견이나 참고 자료를 듣고 싶습니다. 감사합니다!

Back to Blog

관련 글

더 보기 »

새해 복 많이 받으세요!

소개 이 작품은 Google AI가 주최한 New Year, New You Portfolio Challenge에 제출하는 것입니다. 안녕하세요, 저는 몬트리올에 거주하는 software developer 현우입니다.