Ray Book
프레임워크의 철학

컴포넌트 모델, UI를 어떻게 나누는가

함수, 클래스, SFC, .svelte 파일, 각 프레임워크가 컴포넌트를 정의하고 통신하는 방법을 비교합니다

frameworkcomponentpropsslotslifecycle

핵심 질문

컴포넌트란 무엇이고, 어떻게 나누고 조합하는가?

모든 프레임워크가 "컴포넌트"라는 단어를 사용합니다. 하지만 컴포넌트를 정의하는 방법 , 데이터를 전달하는 방법 , 내부 콘텐츠를 끼워 넣는 방법은 프레임워크마다 다릅니다.

문제: UI가 커지면 한 파일로는 한계가 온다

<!-- 이 파일이 3000줄이라면? -->
<div id="app">
  <header>...</header>
  <nav>...</nav>
  <main>
    <section class="todo-list">...</section>
    <section class="sidebar">...</section>
  </main>
  <footer>...</footer>
</div>

한 파일에 모든 UI를 넣으면 수정할 때마다 전체를 읽어야 합니다. 재사용도 불가능합니다. 해결책은 UI를 독립된 단위 로 쪼개는 것, 그것이 컴포넌트입니다.

네 프레임워크가 컴포넌트를 정의하는 방식을 한 줄로 요약하면:

  • React : 함수가 곧 컴포넌트, function → JSX 반환
  • Vue : 단일 파일 컴포넌트 (SFC), <script> + <template> + <style>을 하나의 .vue 파일에
  • Angular : 클래스 + 데코레이터, @Component 데코레이터가 메타데이터를 부여
  • Svelte : .svelte 파일, <script> + 마크업 + <style>이 컴파일러 입력이 됨

컴포넌트의 정의, 같은 TodoItem, 네 가지 표현

아래 코드는 모두 동일한 기능을 구현합니다. 텍스트를 보여주고, 완료 여부에 따라 취소선을 긋고, 클릭하면 토글하는 TodoItem입니다.

// 함수 = 컴포넌트
function TodoItem({ text, done, onToggle }) {
  return (
    <li onClick={onToggle} style={{ textDecoration: done ? 'line-through' : 'none' }}>
      {text}
    </li>
  );
}

공통점이 보입니다. 네 프레임워크 모두 **입력 (props)**을 받고, 마크업을 출력하고, 이벤트를 외부로 전달합니다. 형태만 다를 뿐 역할은 같습니다.

React 는 가장 직접적입니다. 함수의 매개변수가 props이고 반환값이 UI입니다. JavaScript 함수 그 자체이므로 별도의 API를 배울 필요가 거의 없습니다.

Vue 의 SFC는 관심사를 영역으로 분리합니다. <script setup>에서 로직을, <template>에서 마크업을 다룹니다. definePropsdefineEmits라는 컴파일러 매크로가 타입 안전한 인터페이스를 만들어 줍니다.

Angular 는 클래스 기반입니다. @Component 데코레이터가 메타데이터 (selector, template) 를 선언하고, @Input@Output이 외부와의 계약을 명시합니다. 가장 명시적이지만 보일러플레이트가 많습니다.

Svelte.svelte 파일 자체가 컴포넌트입니다. Svelte 5의 $props() 룬은 구조 분해로 props를 꺼내며, 이벤트는 콜백 prop으로 처리합니다. 컴파일러가 나머지를 처리하므로 코드가 가장 짧습니다.

Props와 Events, 데이터는 아래로, 이벤트는 위로

컴포넌트를 나눴으면 통신이 필요합니다. 네 프레임워크 모두 단방향 데이터 흐름 이라는 원칙을 공유합니다. 데이터 (props) 는 부모에서 자식으로, 이벤트는 자식에서 부모로 흐릅니다.

1. Parent가 상태를 보유1 / 5
Parent
state = { count: 0 }
props
Child
props.count → render
event
Props (아래로)Events (위로)
Parent 컴포넌트가 상태 (state) 를 소유합니다. 모든 데이터의 원천은 상위 컴포넌트입니다.

이 패턴이 왜 중요할까요? 양방향 바인딩이 편해 보이지만, 앱이 커지면 "이 값을 누가 바꿨는지" 추적하기 어려워집니다. 단방향 흐름은 데이터의 출처를 명확하게 만듭니다.

각 프레임워크의 구현 방식:

  • React : props는 함수 인자, 이벤트는 콜백 함수, onClick={handleClick}처럼 함수 자체를 prop으로 넘깁니다
  • Vue : defineProps로 선언, defineEmits로 이벤트 발생, emit('toggle')은 부모의 @toggle 핸들러를 호출합니다
  • Angular : @Input()으로 수신, @Output() + EventEmitter로 발신, 타입 시스템이 계약을 강제합니다
  • Svelte : $props()로 수신, 콜백 prop으로 발신, React와 유사하지만 컴파일러가 최적화합니다

Children과 Slots, 컴포넌트 안에 콘텐츠 넣기

Props로는 데이터를 전달할 수 있지만, "컴포넌트 안에 다른 컴포넌트나 마크업을 끼워 넣고 싶다"면 다른 메커니즘이 필요합니다. HTML의 <div>내용</div>처럼, 컴포넌트도 여는 태그와 닫는 태그 사이에 콘텐츠를 넣을 수 있어야 합니다.

function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="body">{children}</div>
    </div>
  );
}

// 사용
<Card title="공지">
  <p>내용이 여기에 들어갑니다</p>
</Card>

네 가지 방식은 이름이 다르지만 개념은 같습니다, "이 자리에 외부 콘텐츠를 렌더링하라" 는 것입니다.

  • React : children은 특별한 prop입니다. JSX에서 태그 사이에 넣은 모든 것이 children으로 전달됩니다. JavaScript 값이므로 조건부 렌더링, 매핑 등 어떤 조작이든 가능합니다.
  • Vue : <slot />은 템플릿의 "구멍"입니다. 이름 있는 슬롯 (<slot name="header" />) 으로 여러 위치에 콘텐츠를 배치할 수 있습니다.
  • Angular : <ng-content />콘텐츠 프로젝션 (Content Projection) 이라 부릅니다. CSS 선택자로 특정 콘텐츠를 특정 위치에 매핑할 수 있습니다.
  • Svelte : Svelte 5에서는 children을 snippet으로 받아 {@render children()}으로 렌더링합니다. 명시적인 함수 호출 방식입니다.

라이프사이클, 태어나고, 바뀌고, 사라진다

컴포넌트는 DOM에 나타나고 (mount), 상태가 바뀌면 업데이트되고 (update), DOM에서 제거됩니다 (destroy). 이 생명주기에 개입하는 방법이 프레임워크마다 다릅니다.

MountUpdateDestroy
ReactuseEffect(() => {}, [])useEffect(() => {}, [deps])useEffect return cleanup
VueonMounted()onUpdated() / watch()onUnmounted()
AngularngOnInit()ngOnChanges()ngOnDestroy()
Svelte$effect()$effect()$effect return cleanup

ReactSvelte는 흥미로운 공통점이 있습니다. 별도의 mount/update/destroy 함수가 아니라, 하나의 effect 시스템 으로 모든 라이프사이클을 표현합니다. 의존성 배열 (React) 이나 시그널 추적 (Svelte) 으로 "언제 실행할지"를 결정하고, cleanup 함수로 정리합니다.

Vue 는 명시적인 훅 이름을 제공합니다. onMounted, onUpdated, onUnmounted, 이름만 봐도 언제 실행되는지 알 수 있습니다. 직관적이지만 API 표면이 넓어집니다.

Angular 는 클래스 메서드로 라이프사이클을 구현합니다. 인터페이스 (OnInit, OnChanges, OnDestroy) 를 구현하면 Angular가 적절한 시점에 호출합니다. 가장 객체지향적인 접근입니다.

트레이드오프 정리

ReactVueAngularSvelte
컴포넌트 단위함수SFC (.vue)클래스 (standalone).svelte 파일
Props 전달함수 인자defineProps@Input$props()
이벤트 전달콜백 함수emitEventEmitter콜백 함수
슬롯/Childrenchildren prop<slot /><ng-content />{@render children()}
스타일 스코핑CSS Modules/외부<style scoped>ViewEncapsulation<style> (기본 스코핑)

React와 Svelte는 "함수/콜백" 중심이고, Vue와 Angular는 "선언적 API" 중심입니다. React는 JavaScript에 가장 가깝고, Angular는 가장 구조화되어 있으며, Vue와 Svelte는 그 사이에서 균형을 잡습니다.

어떤 방식이 "정답"이라기보다, 팀의 규모와 프로젝트의 성격에 따라 적합한 트레이드오프가 달라집니다. 소규모 팀이라면 React나 Svelte의 유연함이, 대규모 팀이라면 Angular의 명시적 구조가 장점이 될 수 있습니다.

다음 글에서는

컴포넌트를 정의하고 조합하는 방법을 살펴봤습니다. 다음 글에서는 렌더링 전략 을 다룹니다, Virtual DOM은 왜 등장했고 Svelte는 왜 버렸는가, 각 프레임워크가 DOM을 다루는 방식의 차이를 살펴보겠습니다.