본문 바로가기

컴포넌트의 개념과 종류

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

앞 글에서는 React의 UI를 이루는 핵심 문법인 JSX를 알아봤습니다. 이제 JSX로 UI를 만들고 재사용하게 해 주는, React에서 가장 중요한 개념인 컴포넌트(component)를 깊이 살펴봅니다. React를 배운다는 것은 곧 컴포넌트를 이해하고 활용하는 방법을 익히는 일이라고 해도 지나치지 않습니다.

같은 컴포넌트도 받은 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에는 이벤트 콜백이 반드시 필요한 것은 아닙니다.

위 그림은 이 글의 첫 예제에서 App이 같은 WelcomeMessage 정의를 두 번 쓰는 모습을 보여 줍니다. App이 첫 번째 사용 위치에는 title="환영합니다!"와 message="즐거운 리액트 학습 되세요!"를, 두 번째 사용 위치에는 title="다시 오셨군요!"와 message="리액트 컴포넌트 재사용 예시입니다."를 넘기고, WelcomeMessage는 받은 title을 h2에, message를 p에 표시합니다. 그림의 설명처럼 값이 A에서 B로 전달되는 것도 아니고 함수 정의가 두 벌 있는 것도 아니며, 하나의 정의가 사용 위치마다 다른 입력을 받아 다른 UI를 돌려줍니다.

그림의 아래쪽은 컴포넌트를 정의하는 두 방식을 비교합니다. 이 교재가 기본으로 쓰는 함수형은 props를 인자로 읽고 렌더할 내용을 돌려주며, 상태나 동기화가 필요하면 Hooks를 함께 씁니다. 클래스형은 기존 코드를 읽을 수 있도록 render()와 this.props의 구조를 익히는 정도로 다룹니다. 그림의 클래스 예시 이름 Greeting은 본문 코드의 MyClassComponent와 같은 구조를 가리킵니다. 또 JSX에서 소문자로 시작하는 태그는 DOM 태그로, 대문자로 시작하는 이름은 컴포넌트로 해석되며, WelcomeMessage처럼 표시만 하는 컴포넌트에는 이벤트 콜백이 꼭 필요하지 않습니다.


React 컴포넌트란 무엇인가?

웹사이트를 레고 블록으로 만든다고 상상해 봅시다. 블록 하나하나는 특정한 기능을 맡고 독립적으로 존재하는 작은 부품이고, 이 블록을 조립해 하나의 구조물을 완성합니다. React의 컴포넌트가 바로 이 블록과 같습니다. 컴포넌트는 웹 애플리케이션 UI의 일부를 입력과 출력으로 정의하는 재사용 단위입니다. 크기가 정해진 최소 단위는 아니어서, 하나의 컴포넌트가 여러 요소나 다른 컴포넌트를 품을 수 있습니다. 웹 페이지를 여러 개의 독립된 조각으로 나눠 개발하는 방식이며, 그 조각 하나하나가 컴포넌트입니다.

온라인 쇼핑몰을 예로 들어 봅니다. 로고와 검색창, 로그인 버튼, 장바구니 아이콘이 있는 헤더(Header), 여러 상품 카드가 놓인 상품 목록(ProductList), 상품 이미지와 이름, 가격, 장바구니 담기 버튼으로 이루어진 상품 카드(ProductCard), 회사 정보와 저작권 표시가 있는 푸터(Footer)를 각각 하나의 컴포넌트로 만들 수 있습니다. 상품 카드 컴포넌트는 여러 번 재사용되고, 상품 목록 컴포넌트 안에는 여러 개의 상품 카드가 들어갑니다.

컴포넌트로 개발하면 여러 이점이 생깁니다. 한 번 만든 컴포넌트를 필요한 곳 어디서든 다시 쓸 수 있어(재사용성, reusability) 개발 시간이 줄고 코드의 중복도 줄어듭니다. 관련된 UI와 처리를 한곳에 모으면 바꿀 위치와 테스트할 범위를 좁히기 쉽고(유지보수성, maintainability), 복잡한 UI를 작은 단위로 나눠 여러 개발자가 서로 다른 컴포넌트를 동시에 맡을 수 있습니다(모듈성, modularity). UI의 구조를 파악하기 쉬워 코드를 이해하고 관리하기 편하며(가독성, readability), 데이터가 바뀔 때 컴포넌트가 어떻게 렌더링될지 예측하기 쉬워 디버깅에도 유리합니다(예측 가능성, predictability).

다만 컴포넌트를 나눴다고 렌더 중의 오류가 저절로 격리되지는 않습니다. 한 부분의 렌더 오류가 화면 전체를 멈추지 않게 하려면 에러 바운더리(Error Boundary)를 따로 설계해야 합니다.


컴포넌트의 종류

React 컴포넌트는 크게 두 가지 방식으로 정의할 수 있습니다. React 16.8에서 훅(Hooks)이 도입된 뒤로는 함수 컴포넌트도 상태와 동기화 기능을 쓸 수 있게 되었습니다. 다만 모든 클래스 API와 일대일로 같지는 않아서, 에러 바운더리를 만드는 클래스 메서드에는 아직 직접 대응하는 Hook이 없습니다(React 문서의 Component API). 그래서 이 교재는 일반적인 UI를 함수 컴포넌트로 작성하고, 클래스형은 기존 코드의 구조를 읽는 범위로 다룹니다. 두 방식은 코드를 쓰는 모양이 다르므로 둘 다 알아 두면 좋습니다.

함수형 컴포넌트 (Functional Component)

함수형 컴포넌트는 가장 기본적인 형태의 React 컴포넌트로, 말 그대로 자바스크립트 함수로 컴포넌트를 정의합니다. 이 함수는 바깥에서 넘겨받는 데이터인 props를 인자로 받아 JSX를 돌려줍니다.

함수형 컴포넌트 기본 구조
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) 컴포넌트 내보내기 (다른 파일에서 사용하기 위함)

함수형 컴포넌트는 클래스형보다 코드가 간결하고 읽기 쉽습니다. React 16.8에서 훅이 나온 뒤로는 상태(state) 관리나 생명주기(life cycle) 관리처럼 예전에는 클래스형에서만 할 수 있던 기능도 함수형에서 쓸 수 있게 되었고, 지금은 함수형 컴포넌트와 훅을 조합하는 방식이 주류입니다. 그렇다고 함수형이라는 이유만으로 더 빠르다고 보장되지는 않으므로, 실제 렌더 비용과 입력의 변화, 필요한 최적화는 작성한 화면에서 측정해 판단합니다.

이제 예제를 만들어 봅니다. 앞 장의 Vite 프로젝트에 src/components 폴더를 만들고, 다음 컴포넌트를 WelcomeMessage.jsx로 저장합니다. Vite의 React 템플릿에서는 JSX가 들어간 파일의 확장자를 .jsx로 씁니다.

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

export default WelcomeMessage;

그리고 App.jsx에서 이 컴포넌트를 불러와 씁니다. import 경로에는 확장자를 적지 않아도 Vite가 .jsx 파일을 찾아 줍니다.

src/App.jsx
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;

저장하면 제목 “나 혼자 React!” 아래에 WelcomeMessage를 두 번 쓴 결과로 두 묶음이 나타납니다. 첫 번째 사용의 title은 “환영합니다!”, message는 “즐거운 리액트 학습 되세요!”이므로 각각 h2와 p의 내용이 되고, 두 번째 사용은 같은 함수 정의로 “다시 오셨군요!”와 “리액트 컴포넌트 재사용 예시입니다.”를 보여 줍니다. 두 묶음의 내용이 다른 것은 함수가 두 벌 있어서가 아니라, 사용 위치마다 다른 props를 넘기기 때문입니다. 두 묶음이 같은 내용을 보인다면 함수 정의를 복사하기 전에, App의 두 사용 위치에서 넘긴 속성의 이름과 값을 비교해 보세요.

export default WelcomeMessage는 그 파일이 밖으로 내보내는 값이며, 다른 파일의 import WelcomeMessage from ...와 짝을 이룹니다. 작은 컴포넌트 두 개를 같은 파일에 두어도 되지만, 이때 자식 컴포넌트의 함수 정의는 App 함수의 바깥에 둡니다. App 안에서 자식 컴포넌트 함수를 정의하면 렌더할 때마다 새 함수가 만들어져, React가 이를 다른 컴포넌트 타입으로 보고 상태를 잃을 수 있습니다(React 문서의 첫 번째 컴포넌트). 사용 위치를 중첩하는 것과 함수 정의를 중첩하는 것은 다른 일입니다.

컴포넌트 함수 안에서는 넘겨받은 값을 읽어 UI를 계산합니다. 이벤트 처리 함수 안에서 컴포넌트를 일반 함수처럼 직접 호출하지 말고, JSX의 <WelcomeMessage /> 형태로 써서 React가 렌더 과정에서 호출하게 합니다.

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

클래스형 컴포넌트는 ES6의 클래스 문법으로 정의하며, React.Component를 상속받아 render() 메서드 안에서 JSX를 돌려주는 형태입니다. 아래 코드의 (1)과 (2) 주석이 함수형 컴포넌트와 달라지는 두 부분입니다. 함수형이라면 함수 선언 하나로 충분했을 자리에 클래스 선언과 상속이 들어가고, 함수 본문의 return 대신 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() 메서드를 가져야 하고, 그 안에서 JSX를 돌려줍니다. props에는 this.props로 접근합니다. 예전에는 상태를 관리하거나 componentDidMount, componentDidUpdate 같은 생명주기 메서드를 쓰려면 클래스형 컴포넌트를 써야만 했지만, 훅이 나온 뒤로는 함수형 컴포넌트에서도 이를 대신하는 기능을 쓸 수 있습니다.

지금은 대부분 함수형 컴포넌트와 훅으로 애플리케이션을 만듭니다. 다만 기존 프로젝트가 클래스형으로 작성되어 있거나 특정한 상황에서 클래스형을 쓸 수 있으므로 기본 구조는 알아 두는 것이 좋으며, 이 교재는 함수형 컴포넌트와 훅을 중심으로 진행합니다.


컴포넌트 이름 규칙

React 컴포넌트의 이름은 반드시 대문자로 시작해야 합니다. JSX는 이 방법으로 일반 HTML 태그와 React 컴포넌트를 구분하기 때문입니다. <div>는 일반 HTML DOM 태그로, <MyComponent>는 React 컴포넌트로 해석됩니다. 컴포넌트 이름을 소문자로 시작하면 React가 이를 HTML 태그로 보고 그대로 DOM 요소로 만들려 하므로, 화면에 아무것도 나타나지 않거나 개발 모드에서 경고가 나는 등 예상하지 못한 동작이 생깁니다. 그래서 JSX의 태그 이름을 볼 때는 첫 글자가 소문자인지 대문자인지만 확인해도, 브라우저의 DOM 태그인지 직접 만든 컴포넌트인지 빠르게 구분할 수 있습니다.


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

어떤 부분을 컴포넌트로 만들지 고민된다면 다음 기준을 살펴봅니다. 먼저 버튼이나 입력 필드, 카드처럼 여러 곳에서 반복해서 쓰는 UI는 컴포넌트로 만들기 좋습니다. 하나의 페이지가 너무 복잡하다면 논리적으로 나눌 수 있는 작은 단위로 쪼갭니다. 같은 입력에서 함께 계산되는 UI는 하나로 묶고, 상태를 어느 컴포넌트가 가질지도 이때 정합니다. 다만 나눴다고 해서 그 부분만 다시 렌더링되는 것은 아닙니다. 사용자 정보를 보여 주는 블록이나 댓글 목록을 보여 주는 블록처럼 기능이 분명한 단위도 컴포넌트로 만들기 좋습니다.

컴포넌트를 나누는 일은 소프트웨어 개발의 모듈화 원칙과 같은 맥락에 있습니다. 처음부터 완벽하게 나누기는 어렵지만, 코드를 쓰다가 중복되는 부분이 보이거나 파일 하나가 너무 길어지면 컴포넌트로 나누는 것을 고려해 보세요. 컴포넌트를 만들기 전에는 이름과 입력, 출력, 재사용 가능성을 한 번에 점검하고, 함수형을 기본으로 쓰되 기존 클래스형 코드를 읽을 수 있을 정도로만 구조를 익혀 두면 충분합니다.


더 깊이: 컴포넌트를 직접 호출하지 않고 React에 맡기는 이유

{WelcomeMessage({ title: '안녕' })}처럼 컴포넌트를 일반 함수로 불러도 당장은 같은 JSX가 돌아오는 것처럼 보입니다. 하지만 React가 보기에 이 결과는 WelcomeMessage라는 컴포넌트가 아니라, 그 함수를 부른 부모가 돌려준 element의 일부일 뿐입니다. 그 안에서 쓴 훅도 부모가 렌더하는 도중에 불린 훅이 되고, 반복문이나 조건문 안에서 부르면 훅의 규칙을 어기기 쉽습니다. React 문서의 React가 컴포넌트와 훅을 호출한다는 규칙이 컴포넌트를 JSX로만 쓰라고 하는 것도 이 때문입니다.

컴포넌트를 React가 부르게 두면 React는 이 함수를 트리 안의 한 자리로 다룰 수 있습니다. 훅으로 만든 지역 state를 트리 안에서 그 컴포넌트의 정체성에 묶을 수 있고, 컴포넌트의 타입이 재조정에 참여하므로 <Feed>를 그리던 자리에 <Profile>이 오면 이전 것을 재사용하지 않습니다. 또 어떤 컴포넌트를 다시 렌더링해야 하는지 골라 나머지를 건너뛸 수 있고, 큰 트리를 다시 렌더링할 때 컴포넌트 호출 사이에 브라우저가 다른 일을 하게 해 메인 스레드를 오래 막지 않을 수도 있습니다. React Developer Tools가 컴포넌트 단위로 트리를 보여 줄 수 있는 것도 React가 컴포넌트를 알고 있기 때문입니다.


정리

  • 컴포넌트는 UI의 일부를 입력과 출력으로 정의하는 재사용 단위이며, 다른 컴포넌트를 품을 수도 있습니다.
  • 같은 컴포넌트 정의도 사용 위치마다 다른 props를 받으면 다른 UI를 돌려줍니다.
  • 함수형 컴포넌트는 props를 인자로 받아 JSX를 돌려주며, 훅과 함께 이 교재의 기본 방식입니다.
  • 클래스형 컴포넌트는 Component를 상속해 render()에서 JSX를 돌려주고 this.props로 props를 읽으며, 기존 코드를 읽는 범위로 익힙니다.
  • 컴포넌트 이름은 대문자로 시작해야 하며, 자식 컴포넌트의 함수 정의는 부모 함수의 바깥에 둡니다.
  • 컴포넌트로 나누는 것만으로 렌더 오류가 격리되지는 않으므로, 필요하면 에러 바운더리를 따로 설계합니다.

연습 문제

1. 세 번째 환영 메시지

기존 WelcomeMessage를 세 번째로 써서 제목은 “오후 수업”, 본문은 “2시에 시작합니다.”로 보여 주려고 합니다. 함수 정의를 복사해야 할까요? App에 무엇을 더해야 할까요?

해설 보기

함수 정의는 그대로 두고, App에 <WelcomeMessage title="오후 수업" message="2시에 시작합니다." />를 더합니다. 함수가 읽는 두 속성의 이름을 그대로 썼으므로 새로 나타나는 h2와 p의 내용만 달라집니다.

2. 상품 요약 컴포넌트 만들기

src/App.jsx 한 파일에 ProductSummary와 App을 작성합니다. 연필은 1200원, 공책은 2500원이며, 상품마다 article 안에 h2로 이름을, p로 가격: 숫자원을 보여 줍니다. 같은 함수 정의를 두 번 써야 하고, 가격은 원 단위 정수로 받으며 통화 형식이나 할인 계산은 다루지 않습니다. 함수를 정의하는 위치와 JSX에서 쓰는 위치를 구별해 작성해 보세요.

해설 보기

함수 정의는 파일의 최상위에 두고, 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가 함수가 읽는 이름과 같아야 합니다. 이 두 입력을 넘기는 자세한 문법은 다음 글에서 이어집니다.

3. 나타나지 않는 문장

아래 MissingGreeting을 App에서 <MissingGreeting />으로 썼는데 문장이 나타나지 않습니다. 이 함수가 실제로 돌려주는 값은 무엇이고, return의 어디를 고쳐야 할까요? 파일의 JSX 변환과 import는 정상이라고 가정합니다.

연습 3: 의도적으로 잘못된 return
function MissingGreeting() {
  return
    (<p>상품을 골라 주세요.</p>);
}
해설 보기

자바스크립트는 return 바로 뒤의 줄바꿈에서 문장이 끝났다고 보고 세미콜론을 자동으로 넣으므로, 이 함수는 undefined를 돌려주고 아래의 JSX는 실행되지 않습니다. JSX의 문법 오류가 아니라 자바스크립트의 반환 문제입니다. 여는 괄호를 return과 같은 줄에 두면 p 요소를 돌려줍니다.

MissingGreeting의 정답
function MissingGreeting() {
  return (
    <p>상품을 골라 주세요.</p>
  );
}

4. 공책 가격만 바꾸기

2번에서 완성한 상품 요약에서 공책의 가격만 3000원으로 바꾸려고 합니다. 함수 정의와 사용 위치 가운데 어디를 바꿔야 하고, 연필의 출력은 어떻게 될까요? 이 표시 전용 컴포넌트를 재사용한 것만으로 클릭 기능이나 오류 격리가 생길까요?

해설 보기

공책을 쓰는 JSX의 price={2500}만 price={3000}으로 바꿉니다. 함수 정의와 연필의 입력은 그대로이므로 연필은 계속 “가격: 1200원”을 보여 줍니다.

이 컴포넌트는 받은 입력을 보여 주는 일만 합니다. 클릭했을 때의 동작은 이벤트 처리를 더해야 생기고, 상품 카드에서 렌더 오류가 났을 때 다른 화면을 지키는 범위는 에러 바운더리로 따로 설계해야 합니다.

다음 글 props를 이용한 데이터 전달에서는 컴포넌트에 데이터를 넘기는 핵심 방법인 props를 중심으로, 부모와 자식 사이의 데이터 흐름을 구체적으로 살펴봅니다.