본문 바로가기

컴포넌트의 개념과 종류

UI를 독립적인 컴포넌트로 분리하는 기준을 익히고 함수형·클래스형 컴포넌트의 구조와 이름 규칙을 비교합니다.

앞 절에서 우리는 리액트의 UI를 구성하는 핵심 문법인 JSX에 대해 알아보았습니다.

이제 JSX를 활용하여 실제로 UI를 만들고 재사용하는 리액트의 가장 중요한 개념인 컴포넌트(Component)에 대해 깊이 있게 탐구해 볼 시간입니다.

리액트를 배운다는 것은 곧 컴포넌트를 이해하고 활용하는 방법을 익히는 것이라고 할 수 있습니다.

같은 컴포넌트도 받은 props에 따라 다른 UI를 반환합니다

COMPONENT CONTRACT

같은 컴포넌트도 받은 props에 따라 다른 UI를 반환합니다

본문의 App은 WelcomeMessage를 두 번 사용합니다. 컴포넌트 이름과 입력·출력 계약을 먼저 정하고, 반복되는 UI 책임을 한 정의로 묶습니다.

같은 정의에 두 입력 전달App→WelcomeMessageA및App→WelcomeMessageB의3노드2간선이다. 각사용위치가title/message를다르게전달하며A에서B로전달하거나두함수정의를만드는것이아니다.App두 사용 위치의 입력WelcomeMessage A제목: 환영합니다!WelcomeMessage B제목: 다시 오셨군요!각각 title · message
같은 정의에 두 입력 전달 · 모바일App→WelcomeMessageA및App→WelcomeMessageB의3노드2간선이다. 각사용위치가title/message를다르게전달하며A에서B로전달하거나두함수정의를만드는것이아니다. 모바일에서도 같은 입력·노드·간선 의미를 유지한다.App두 위치에 다른 입력WelcomeMessage A환영합니다!WelcomeMessage B다시 오셨군요!A의 입력B의 입력

A의 입력 → 출력

title="환영합니다!"
message="즐거운 리액트 학습 되세요!"

WelcomeMessage는 title을 h2에, message를 p에 표시합니다.

B의 입력 → 출력

title="다시 오셨군요!"
message:
리액트 컴포넌트 재사용 예시입니다.

같은 정의를 사용하며 B가 받은 두 값을 표시합니다.

함수형 · 이 교재의 기본

function WelcomeMessage(props) {
  return /* JSX */;
}

props를 인자로 읽고 렌더할 node를 반환합니다. 상태·동기화가 필요하면 함수 컴포넌트와 Hooks를 사용합니다.

클래스형 · 기존 코드 읽기

class Greeting extends Component {
  render() { /* this.props */ }
}

render()와 this.props를 읽는 구조를 익힙니다. 컴포넌트 경계는 재사용·UI 책임·입력·테스트 기준으로 판단합니다.

JSX에서 소문자 태그는 DOM 태그, 대문자로 시작한 이름은 컴포넌트로 해석합니다. 표시만 하는 WelcomeMessage에는 이벤트 콜백이 반드시 필요한 것은 아닙니다.


React 컴포넌트란 무엇인가?

다시 한번, 웹사이트를 레고 블록으로 만든다고 상상해 봅시다.

이때 각각의 레고 블록은 특정 기능을 수행하고, 독립적으로 존재하는 작은 부품들입니다.

이 레고 블록들을 조립하여 하나의 멋진 구조물을 완성할 수 있습니다.

리액트에서 컴포넌트는 이 레고 블록과 같습니다.

웹 애플리케이션 UI의 일부를 입력과 출력으로 정의하는 재사용 단위입니다. 크기가 정해진 최소 단위는 아니며, 하나의 컴포넌트가 여러 요소나 다른 컴포넌트를 포함할 수 있습니다.

즉, 웹 페이지를 여러 개의 독립적인 조각으로 나누어 개발하는 방식이며, 각각의 조각이 바로 컴포넌트입니다.

예를 들어, 온라인 쇼핑몰 웹사이트를 생각해 보겠습니다.

  • 헤더(Header) 영역: 로고, 검색창, 로그인 버튼, 장바구니 아이콘
  • 상품 목록(ProductList) 영역: 여러 개의 상품 카드
  • 상품 카드(ProductCard) 하나: 상품 이미지, 상품 이름, 가격, 장바구니 담기 버튼
  • 푸터(Footer) 영역: 회사 정보, 저작권 표시

이 모든 요소들을 각각 하나의 컴포넌트로 만들 수 있습니다.

상품 카드 컴포넌트는 여러 번 재사용될 수 있으며, 상품 목록 컴포넌트 안에는 여러 개의 상품 카드 컴포넌트가 포함될 수 있습니다.

컴포넌트 기반 개발의 장점
  • 재사용성(Reusability): 한 번 만든 컴포넌트는 필요한 곳 어디에서든 다시 사용할 수 있어 개발 시간을 단축하고 코드 중복을 줄입니다.
  • 유지보수성(Maintainability): 관련 UI와 처리 책임을 한곳에 모으면 변경 위치와 테스트 범위를 좁히기 쉽습니다. 컴포넌트를 분리한 것만으로 렌더 오류가 자동 격리되지는 않으며, 필요하면 별도의 Error Boundary를 설계합니다.
  • 모듈성(Modularity): 복잡한 UI를 작은 단위로 분리하여 개발함으로써, 여러 개발자가 동시에 다른 컴포넌트를 개발할 수 있어 협업 효율성을 높입니다.
  • 가독성(Readability): UI를 구조적으로 파악하기 쉬워 코드를 이해하고 관리하기 용이합니다.
  • 예측 가능성(Predictability): 데이터의 변화에 따라 컴포넌트가 어떻게 렌더링될지 예측하기 쉬워 디버깅에 유리합니다.

컴포넌트의 종류

리액트 컴포넌트는 크게 두 가지 방식으로 정의할 수 있습니다.

리액트 16.8에서 Hooks가 도입된 뒤 함수 컴포넌트도 상태와 동기화 기능을 사용할 수 있습니다. 모든 클래스 API와 일대일로 같은 것은 아니며, 오류 경계의 클래스 메서드에는 직접 대응하는 Hook이 없습니다. 이 장에서는 함수 컴포넌트로 일반 UI를 작성하고 클래스형은 기존 코드의 구조를 읽는 범위로 다룹니다. Component 공식 API가 두 방식의 지원 범위를 설명합니다.

하지만 코드 작성 방식에서 차이가 있으므로 두 가지 모두 알아두는 것이 좋습니다.

함수형 컴포넌트 (Functional Component)

함수형 컴포넌트는 가장 기본적인 형태의 리액트 컴포넌트입니다.

말 그대로 자바스크립트 함수로 컴포넌트를 정의합니다.

이 함수는 JSX를 반환하며, 외부에서 전달받는 데이터(props)를 함수의 인자로 받습니다.

함수형 컴포넌트 기본 구조
import React from 'react'; // 이 Vite 프로젝트의 JSX 자동 변환 설정에서는 JSX만을 위해
                           // React import가 필요하지 않습니다. 기존 JSX 변환 방식에서는 필요합니다.
                           // React 버전과 변환 설정을 함께 확인합니다.

function MyFunctionComponent(props) { // (1) 함수 정의: 함수의 첫 글자는 대문자로 시작해야 합니다.
  return ( // (2) JSX 반환: 하나의 루트 요소로 감싸져야 합니다.
    <div>
      <h1>안녕하세요, {props.name}님!</h1>
      <p>함수형 컴포넌트입니다.</p>
    </div>
  );
}

export default MyFunctionComponent; // (3) 컴포넌트 내보내기 (다른 파일에서 사용하기 위함)
특징
  • 간결성: 클래스형 컴포넌트보다 코드가 간결하고 읽기 쉽습니다.
  • Hooks 사용: 리액트 16.8 버전 이후 Hooks의 등장으로 state(상태) 관리, life cycle(생명 주기) 관리 등 클래스형 컴포넌트에서만 가능했던 기능들을 함수형 컴포넌트에서도 사용할 수 있게 되었습니다. 현대 리액트 개발에서는 함수형 컴포넌트와 Hooks를 조합하여 사용하는 것이 주류입니다.
  • 성능 판단: 함수형이라는 이유만으로 더 빠르다고 보장하지 않습니다. 실제 렌더 비용·입력 변경·필요한 최적화는 작성한 화면에서 측정합니다.

예시

앞 장의 Vite 프로젝트에서 src/components 폴더를 만들고, JSX가 들어갈 파일은 WelcomeMessage.jsx로 저장합니다. 아래 원 예제의 src/components/WelcomeMessage.js 표기는 기존 프로젝트를 읽기 위한 이름입니다. App.jsx에서 가져오면 확장자 없는 import 경로는 그대로 사용할 수 있습니다.

src/components/WelcomeMessage.js
import React from 'react'; // 이 Vite 설정에서는 JSX만을 위한 import는 필수가 아닙니다.

function WelcomeMessage(props) {
  return (
    <div>
      <h2>{props.title}</h2>
      <p>{props.message}</p>
    </div>
  );
}

export default WelcomeMessage;

그리고 App.js에서 이 컴포넌트를 불러와 사용합니다. 아래 파일명은 기존 JavaScript 프로젝트 표기이며, 앞 장의 Vite 프로젝트에서는 JSX가 든 파일을 WelcomeMessage.jsx, App.jsx로 저장합니다.

src/App.js
import React from 'react';
import './App.css'; // App.css가 있다면 그대로 둡니다.
import WelcomeMessage from './components/WelcomeMessage'; // WelcomeMessage 컴포넌트 불러오기

function App() {
  return (
    <div className="App">
      <h1>나 혼자 React!</h1>
      <WelcomeMessage title="환영합니다!" message="즐거운 리액트 학습 되세요!" />
      <WelcomeMessage title="다시 오셨군요!" message="리액트 컴포넌트 재사용 예시입니다." />
    </div>
  );
}

export default App;

위 코드를 저장하면 WelcomeMessage를 두 번 사용한 결과로 제목 나 혼자 React! 아래에 두 묶음이 표시됩니다. 첫 호출의 title은 환영합니다!, message는 즐거운 리액트 학습 되세요!이므로 각각 h2와 p의 내용이 됩니다. 둘째 호출은 같은 함수 정의를 사용하면서 다시 오셨군요!와 리액트 컴포넌트 재사용 예시입니다.를 표시합니다. 두 입력을 구별하는 것은 함수가 두 벌 있기 때문이 아니라, 각 사용 위치가 다른 props를 전달하기 때문입니다.

이 예제를 직접 옮겨 적을 때는 JSX를 처리하도록 구성한 학습 프로젝트를 전제로 합니다. 파일을 저장한 뒤에는 첫 카드의 제목·본문과 둘째 카드의 제목·본문을 각각 확인합니다. 두 카드가 같은 내용을 보인다면 함수 정의를 복사하기 전에 App의 두 사용 위치에서 전달한 속성 이름과 값을 비교합니다. export default WelcomeMessage는 그 파일에서 내보내는 값이며, 다른 파일의 import WelcomeMessage from ...와 짝을 이룹니다.

작은 컴포넌트 두 개를 같은 파일에 둘 수도 있습니다. 다만 자식 함수 정의는 App 함수의 바깥에 둡니다. App 안에서 자식 컴포넌트 함수를 매 렌더마다 새로 정의하면 React가 다른 컴포넌트 타입으로 취급하여 상태를 잃을 수 있습니다. 첫 컴포넌트의 공식 설명이 사용 위치의 중첩과 함수 정의의 중첩을 구별합니다.

이 함수 안에서는 전달받은 값을 읽고 UI를 계산합니다. 컴포넌트를 이벤트 핸들러에서 일반 함수처럼 직접 실행하기보다, JSX의 <WelcomeMessage /> 형태로 React가 렌더 과정에서 호출하도록 사용합니다.

클래스형 컴포넌트 (Class Component)

클래스형 컴포넌트는 ES6의 클래스 문법을 사용하여 정의합니다.

React.Component를 상속받아 render() 메서드 안에서 JSX를 반환하는 형태입니다.

클래스형 컴포넌트 기본 구조
import React, { Component } from 'react'; // React에서 Component를 임포트합니다.

class MyClassComponent extends Component { // (1) 클래스 정의: Component를 상속받습니다.
  render() { // (2) render() 메서드 안에서 JSX를 반환합니다.
    return (
      <div>
        <h1>안녕하세요, {this.props.name}님!</h1>
        <p>클래스형 컴포넌트입니다.</p>
      </div>
    );
  }
}

export default MyClassComponent;
특징
  • render() 메서드: 반드시 render() 메서드를 포함해야 하며, 이 메서드 안에서 JSX를 반환합니다.
  • this.props 사용: props는 this.props를 통해 접근합니다.
  • state와 생명주기 메서드: 과거에는 state(상태) 관리와 life cycle(생명 주기) 메서드를 사용하기 위해서는 클래스형 컴포넌트를 사용해야만 했습니다. (예: componentDidMount, componentDidUpdate 등) 하지만 Hooks의 등장으로 함수형 컴포넌트에서도 이들을 대체하는 기능을 사용할 수 있게 되었습니다.
현대 리액트 개발에서는 대부분 함수형 컴포넌트와 Hooks를 사용하여 애플리케이션을 구축합니다.

하지만 기존 프로젝트가 클래스형 컴포넌트로 작성됐거나 특정 상황에서 클래스형 컴포넌트가 사용될 수 있으므로 기본 구조는 알아두는 것이 좋습니다.

이 책에서는 함수형 컴포넌트와 Hooks를 중심으로 학습을 진행할 것입니다.


컴포넌트 이름 규칙

리액트 컴포넌트의 이름은 반드시 대문자로 시작해야 합니다.

이는 JSX가 일반 HTML 태그(소문자)와 리액트 컴포넌트(대문자)를 구분하는 방식입니다.

  • <div>는 일반 HTML DOM 태그로 인식됩니다.
  • <MyComponent>는 리액트 컴포넌트로 인식됩니다.

만약 컴포넌트 이름을 소문자로 시작하면, 리액트는 이를 HTML 태그로 인식하여 오류를 발생시키거나 예상치 못한 동작을 할 수 있습니다.

JSX에서 태그 이름을 볼 때는 첫 글자가 소문자인지 대문자인지만 확인해도, 브라우저 DOM 태그인지 직접 만든 컴포넌트인지 빠르게 구분할 수 있습니다.


컴포넌트를 언제 사용해야 할까?

어떤 부분을 컴포넌트로 만들어야 할지 고민될 수 있습니다.

다음 경우들을 고려하여 컴포넌트를 분리하면 좋습니다.

컴포넌트를 분리할 때는 다음 기준을 확인합니다.

컴포넌트를 분리한 뒤에는 함수형을 기본 선택지로 두고, 기존 클래스형 코드를 읽을 수 있을 정도로 구조를 익혀 두면 충분합니다.

  • 재사용 가능한 부분: 여러 곳에서 반복적으로 사용되는 UI (버튼, 입력 필드, 카드 등)
  • 복잡한 UI의 특정 부분: 하나의 페이지가 너무 복잡하다면, 그 페이지를 논리적으로 분리할 수 있는 작은 단위로 나눕니다.
  • 변경 이유가 모이는 부분: 같은 입력에서 함께 계산되는 UI를 묶고, 상태를 어느 컴포넌트가 소유할지 정할 때. 단순히 분리했다고 그 부분만 재렌더링되는 것은 아닙니다.
  • 독립적인 기능 블록: 예를 들어, 사용자 정보를 표시하는 블록, 댓글 목록을 표시하는 블록 등 명확한 기능을 가진 단위

컴포넌트 분리는 소프트웨어 개발의 모듈화 원칙과 일맥상통합니다.

처음부터 완벽하게 분리하기는 어렵지만, 코드를 작성하면서 중복되는 부분이 보이거나, 하나의 파일이 너무 길어진다면 컴포넌트로 분리하는 것을 고려해 보세요.


컴포넌트를 만들기 전에는 이름, 입력, 출력, 재사용 가능성을 한 번에 점검해 보면 좋습니다.


2장 컴포넌트의 개념과 종류는 여기까지입니다.

컴포넌트의 중요성과 함수형/클래스형 컴포넌트의 차이점, 그리고 컴포넌트 이름 규칙까지 자세히 다루었습니다.

특히 현대 리액트 개발의 주류인 함수형 컴포넌트에 집중하여 설명했습니다.

입력·출력 계약을 혼자 정하기

기존 WelcomeMessage를 세 번째로 사용하여 제목은 오후 수업, 본문은 2시에 시작합니다.로 표시하려고 합니다. 함수 정의를 복사해야 하는지, App의 어느 입력을 추가해야 하는지 적어 보세요. 또 두 상품 카드가 같은 모양이지만 이름과 가격만 다르다면 어떤 값을 입력으로 받고 어떤 구조를 재사용할지 정합니다. 상품 카드의 렌더 오류가 나면 분리만으로 다른 화면이 반드시 보존되는지도 판단하세요.

재사용과 경계 해설

함수는 그대로 두고 App에 <WelcomeMessage title="오후 수업" message="2시에 시작합니다." />를 추가합니다. 함수가 읽는 두 속성 이름을 유지했으므로 새 h2와 p 내용만 달라집니다. 상품 카드도 이름·가격을 props로 전달하고 동일한 표시 구조를 재사용할 수 있습니다. UI 책임의 분리는 오류 격리 보장이 아니며, 렌더 오류를 처리할 범위는 별도의 오류 경계에서 정합니다.

직접 완성하기: 상품 요약을 한 정의로 만들기

  1. JSX를 처리하는 학습 프로젝트의 src/App.jsx 한 파일에서 ProductSummary와 App을 작성합니다. 연필은 1200원, 공책은 2500원입니다. 상품마다 article 안에 h2로 이름, p로 가격: 숫자원을 표시하고, 같은 함수 정의를 두 번 사용해야 합니다. 가격 입력은 원 단위 정수이며 통화 형식화나 할인 계산은 이 문제에 포함하지 않습니다. 두 컴포넌트의 함수 정의 위치와 JSX 사용 위치를 구별하여 작성하세요.
  2. 아래 MissingGreeting을 App에서 <MissingGreeting />으로 사용했는데 문장이 나타나지 않습니다. JavaScript가 함수에서 실제로 반환하는 값과, return의 어느 위치를 고쳐야 하는지 적으세요. 파일의 JSX 변환과 import는 정상이라고 가정합니다.
  3. 완성한 상품 요약에서 공책 가격만 3000원으로 바꾸려고 합니다. 함수 정의와 사용 위치 중 어디를 바꾸고, 연필의 출력이 어떻게 유지되는지 설명하세요. 이 표시 전용 UI를 재사용한 것만으로 클릭 기능이나 오류 격리가 생기는지도 구별합니다.
연습 2: 의도적으로 잘못된 return
function MissingGreeting() {
  return
    (<p>상품을 골라 주세요.</p>);
}
상품 요약의 완성 코드와 반환값 해설
  1. 함수 정의는 파일의 최상위에 두고 App의 JSX 안에서 두 번 사용합니다.
src/App.jsx: 상품 요약 연습 정답
function ProductSummary({ name, price }) {
  return (
    <article>
      <h2>{name}</h2>
      <p>가격: {price}원</p>
    </article>
  );
}

export default function App() {
  return (
    <main>
      <ProductSummary name="연필" price={1200} />
      <ProductSummary name="공책" price={2500} />
    </main>
  );
}

첫 article은 연필 / 가격: 1200원, 둘째는 공책 / 가격: 2500원입니다. 입력 이름 name, price가 함수가 읽는 이름과 일치해야 합니다. 이 두 입력의 자세한 전달 문법은 다음 절에서 이어집니다.

  1. 잘못된 함수는 return 뒤의 줄바꿈에서 문장이 끝나므로 undefined를 반환합니다. JSX 문법 오류와 별개의 JavaScript 반환 문제입니다. 여는 괄호를 return과 같은 줄에 두면 p 요소를 반환합니다.
MissingGreeting의 정답
function MissingGreeting() {
  return (
    <p>상품을 골라 주세요.</p>
  );
}
  1. 공책을 사용하는 JSX의 price={2500}만 price={3000}으로 바꿉니다. 함수와 연필의 입력은 같으므로 연필은 계속 1200원입니다. 이 컴포넌트는 입력을 표시하는 계약만 갖습니다. 클릭 동작은 이벤트 처리가 있어야 하고, 렌더 오류 처리 범위는 별도로 설계합니다.

다음 절에서는 컴포넌트에 데이터를 전달하는 핵심 방법인 props를 중심으로 부모-자식 간 데이터 흐름을 구체적으로 살펴보겠습니다.