웹 프로그래밍

컴포넌트(Component)

2025-12-08

컴포넌트

컴포넌트는 UI를 재사용 가능한 작은 단위로 나누어 관리하는 요소로, 각각의 컴포넌트가 독립적으로 화면을 구성할 수 있습니다. 이러한 컴포넌트는 입력 값(props)을 받아 그에 맞는 UI 엘리먼트를 반환하는 함수와 같은 역할을 합니다.​

함수(컴포넌트)

앞선 수많은 예제에서 소흘히 넘어갔지만, 확실한 것은 컴포넌트는 JavaScript 함수 입니다. 따라서 모든 컴포넌트는 JavaScript 함수 입니다. 우리는 TypeScript를 사용하지만, 기본적으로 TypeScript는 JavaScript를 확장한 언어이므로, 결과론적으로 JavaScript 함수라 할 수 있습니다.

// 함수 선언
function App() {
  return (
    <div>
      <h1>Hello React</h1>
    </div>
  );
}

앞선 예제를 화살표 함수 표현식으로 작성해 보겠습니다.

  • 화살표 함수(arrow function)
    • ES6(ECMAScript 2015)에서 도입된 익명 함수 표현식으로, 기존 함수보다 더 간결한 문법으로 작성할 수 있음
    • 자신만의 this 바인딩을 만들지 않고, 외부 스코프의 this 값을 그대로 사용한다는 점에서 기존 함수와 차별화
// 화살표 함수 표현식
const App = () => {
  return (
    <div>
      <h1>Hello React</h1>
    </div>
  );
};

JSX만 반환하는 경우 중괄호와 return 문을 생략할 수 있습니다. 이는 간결한 바디(concise body)라고 합니다.

// 간결한 바디 (암시적 return)
const App = () => (
  <div>
    <h1>Hello React</h1>
  </div>
);

정의와 호출(선언과 인스턴스화)

함수는 선언과 호출로 구분됩니다. 그리고 컴포넌트는 선언(declaration)과 인스턴스화(instantiation)로 구분됩니다.

// 컴포넌트 선언
function List() {
  return (
    <ul>
      <li>Item 1</li>
      <li>Item 2</li>
    </ul>
  );
}

// 컴포넌트 인스턴스화
function App() {
  return (
    <div>
      <List />  {/* 인스턴스 생성 */}
      <List />  {/* 또 다른 인스턴스 */}
    </div>
  );
}

React루 구성된 페이지는 컴포넌트 트리(component tree)로 구성됩니다.

function App() {
  return (
    <div>
      <h1>My Hacker Stories</h1>
      <Search />  {/* App의 자식 */}
      <List />    {/* App의 자식, Search의 형제 */}
    </div>
  );
}

function Search() { ... }
function List() { ... }
  • 루트 컴포넌트: 최상위 컴포넌트 (예: App)
  • 부모/자식 컴포넌트: 계층 관계
  • 형제 컴포넌트: 같은 부모를 가진 컴포넌트
<App>
├── <h1>My Hacker Stories</h1>
├── <Search />
└── <List />

매개변수(Props)

함수의 매개변수를 사용해서 외부 값을 받아 올 수 있습니다. React 컴포넌트에서는 이를 Props라고 합니다. Props는 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달하는 수단입니다.

// 부모 컴포넌트
function App() {
  const stories = [
    { title: 'React', objectID: 0 },
    { title: 'Redux', objectID: 1 },
  ];

  return (
    <div>
      <List list={stories} />  {/* props 전달 */}
    </div>
  );
}

// 자식 컴포넌트
function List(props) {
  return (
    <ul>
      {props.list.map((item) => (
        <li key={item.objectID}>{item.title}</li>
      ))}
    </ul>
  );
}

Props 객체를 구조 분해하여 더 간결하게 사용할 수 있습니다.

  • 구조 분해 할당(destructuring assignment)
    • JavaScript에서 배열이나 객체의 값을 쉽게 개별 변수에 할당할 수 있게 해주는 문법
    • 배열의 요소들을 순서대로 변수에 할당하거나, 객체의 프로퍼티를 변수명과 매칭하여 한 번에 변수에 담을 수 있어 코드가 간결하고 가독성이 높아짐
// 함수 본문에서 구조 분해
function List(props) {
  const { list } = props;
  return (
    <ul>
      {list.map((item) => (
        <li key={item.objectID}>{item.title}</li>
      ))}
    </ul>
  );
}
// 함수 시그니처에서 구조 분해 (권장)
function List({ list }) {
  return (
    <ul>
      {list.map((item) => (
        <li key={item.objectID}>{item.title}</li>
      ))}
    </ul>
  );
}
  • 읽기 전용: Props는 변경할 수 없음(immutable)
  • 단방향 데이터 흐름: 부모 \(to\) 자식으로만 전달
  • JavaScript 객체: Props는 일반 JavaScript 객체

TypeScript 컴포넌트 주의사항

컴포넌트의 props는 별도의 타입(또는 인터페이스)으로 정의해 지정합니다.

// 인터페이스/타입 선언
type HelloProps = {
  name: string;
  age?: number; // 선택적 prop
};

// 함수형 컴포넌트에서 사용
function Hello({ name, age }: HelloProps) {
  return <div>Hello, {name}! {age && `Age: ${age}`}</div>;
}
  • FC(Functional Component) 타입 명시는 권장되지 않습니다. 함수형 컴포넌트는 타입 별칭(명시적 props 타입)만으로 충분해서 React.FC(또는 React.FunctionComponent) 사용은 권장되지 않습니다.

이벤트 핸들러의 타입도 명확하게 지정합니다.

function MyButton(props: { onClick: (e: React.MouseEvent<HTMLButtonElement>) => void }) {
  return <button onClick={props.onClick}>Click</button>;
}

useRef, useState 등 훅을 사용할 때 generic 타입 매개변수로 명시적으로 타입을 넣을 수 있습니다.

const [count, setCount] = useState<number>(0);
const inputRef = useRef<HTMLInputElement>(null);

TypeScript에서는 propTypes나 defaultProps 대신 인터페이스(type)와 기본 매개변수를 활용합니다. children을 받는 컴포넌트라면 아래처럼 지정합니다.

type Props = {
  children: React.ReactNode;
}
  • 모든 props, state, 이벤트 객체 타입을 명시적으로 선언
  • any 타입 남용은 피하고, 가능한 구체적인 타입을 사용
  • JSX 반환 타입은 JSX.Element로 추론됨(명시 필요 없음)

React 컴포넌트 연습

연습(1) - app/layout.tsx 읽어보기

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}
  • RootLayout 함수는 Next.js에서 사용되는 레이아웃 컴포넌트
  • 최상단(/) 세그먼트이며, 하위 <children> 세그먼트를 인자로 받아서 화면을 구성
  • 함수의 매개변수(props)는 구조 분해 할당(destructuring)을 통해 children만 추출
  • export default를 사용, 기본 내보내기(default export) 형식으로 제공함
  • Readonly<{ children: React.ReactNode }>로 지정, Readonly<T>는 객체의 속성이 변경될 수 없음(불변 객체)
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) { ... }

children은 React에서 컴포넌트의 자식 노드를 전달할 때 사용하는 표준 속성, 타입은 React.ReactNode (= 렌더링할 수 있는 모든 React 요소, string, number, null 등) 입니다.

  • 예를 들어, <Layout>내용</Layout>처럼 Layout 컴포넌트가 있을 때, 내용 부분이 자동으로 children으로 전달
  • children을 활용하면, 여러 컴포넌트에서 동일한 레이아웃이나 래퍼 구조를 재사용할 수 있음
  • 함수형 컴포넌트에서 props의 일부로 전달되며, 보통 다음처럼 사용하며 children은 부모가 자식에게 콘텐츠를 끼워넣을 수 있게 해주는 매우 유용한 리액트의 기본 패턴임
type Props = { children: React.ReactNode };
function MyComponent({ children }: Props) { return <div>{children}</div>; }

함수(컴포넌트)는 HTML 마크업을 반환합니다.

  • 최상단에 <html lang="en"> 태그로 감싸고 있음
  • <body> 엘리먼트에 여러 클래스가 props 형태로 전달되는데, 이는 보통 폰트나 스타일 적용을 위한 것(geistSans.variable, geistMono.variable)
  • children을 그대로 body 안에서 렌더링하여, 이 레이아웃을 사용하는 페이지의 실제 콘텐츠가 해당 위치에 삽입됨

연습(2) - app/layout.tsx 읽어보기

Next.js 13+의 app 디렉터리에서 이 레이아웃 파일(app/layout.tsx)은 앱 전체 혹은 특정 라우트 그룹에 공통으로 적용되는 레이아웃 역할을 합니다.

  • 레이아웃에서 자식 요소(children)를 받아 지정한 HTML 구조에 끼워 넣는 방식으로, 모든 하위 페이지에 일관된 구조를 제공함
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
  <main>
    //...
  </main>
</div>
  • flex: flexbox 레이아웃을 활성화, 자식 요소들이 플렉스 컨테이너 안에서 정렬, 배치, 크기 조정이 유연하게 동작함
    • items-center: flexbox에서 자식 요소들을 수직(교차축) 중앙 정렬, flex direction이 기본값(row)이므로 위아래(세로축) 중앙에 위치함
    • justify-center: flexbox에서 자식 요소들을 수평(주축) 중앙 정렬, 좌우(가로축) 기준으로 중앙에 놓임
  • min-h-screen: 컨테이너의 최소 높이(min-height)를 뷰포트 전체 높이(100vh, 즉 화면 전체 높이)로 설정
  • bg-zinc-50: 배경색을 zinc 팔레트의 50단계로 지정, 아주 연한 회색(zinc-50) 계열이며, Tailwind CSS 컬러 스케일의 하나임
  • font-sans: 글꼴을 산세리프 계열로 지정, 시스템에서 산세리프 폰트를 사용하게 되며, 깔끔하고 현대적인 느낌을 줌
  • dark:bg-black: 다크 모드가 활성화된 경우(dark 프리픽스) 배경색을 검정색(black)으로 설정, Light 모드에서는 bg-zinc-50, 다크 모드에선 bg-black이 적용되어 테마를 자연스럽게 전환할 수 있음
<main className="flex w-full max-w-3xl flex-col justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
</main>
  • flex-col: flexbox 방향을 기본 가로(row)에서 세로(column)로 변경, 자식들이 위에서 아래로 쌓임
  • justify-between: flexbox의 주 축(main axis, 여기서는 세로축)에서 자식 요소들 사이를 가능한 멀리 떨어뜨려 배치
  • w-full: 요소의 가로 너비를 부모 요소 기준으로 100%로 설정
  • max-w-3xl: 최대 너비를 Tailwind에서 지정한 3xl(즉, 약 48rem)로 제한, 너무 넓어지지 않게 함
  • py-32: 위아래(padding-top, padding-bottom)에 각각 32단계(preset, 약 8rem)의 내부 여백 추가
  • px-16: 좌우(padding-left, padding-right)에 각각 16단계(약 4rem)의 내부 여백 추가
  • bg-white: 배경색을 흰색으로 지정
  • dark:bg-black: 다크 모드에서는 배경색을 검정색으로 변경
  • sm:items-start: 작은(sm, 약 640px 이상) 화면에서는 flexbox의 교차축(여기서는 가로축) 기준으로 자식 요소들을 시작점(왼쪽) 정렬
    • sm: 40rem (640px), @media (width >= 40rem) { … }
    • md: 48rem (768px), @media (width >= 48rem) { … }
    • lg: 64rem (1024px), @media (width >= 64rem) { … }
    • xl: 80rem (1280px), @media (width >= 80rem) { … }
    • 2xl: 96rem (1536px), @media (width >= 96rem) { … }
  • 클라이언트 사이드에서 렌더링되는 <Image> 태그를 사용, 이미지를 렌더링할 때 사용
<Image
  className="dark:invert"
  src="/next.svg"
  alt="Next.js logo"
  width={100}
  height={20}
  priority
/>

참고문헌