자바스크립트로 화면을 만들어 본 사람은 안다. 요소 하나 띄우려면 document.createElement로 태그를 만들고, 거기에 글자를 넣고, appendChild로 붙이는 절차를 손으로 다 밟아야 한다. 화면이 열 조각쯤 되면 이 코드가 수십 줄로 불어나고, 나중에 어디를 고쳐야 뭐가 바뀌는지 나조차 헷갈린다. 리액트(React)는 이 화면을 컴포넌트(부품)라는 조각으로 쪼갠 다음, 각 조각을 함수처럼 다루게 해준다. 사고방식 자체가 다르니, 이걸 먼저 붙잡아야 나머지가 술술 풀린다.


컴포넌트는 화면 한 조각을 돌려주는 함수다. 리액트에서 컴포넌트는 별게 아니다. 그냥 화면에 그릴 내용을 return하는 함수 하나다. 아래는 인사말을 그리는 가장 단순한 컴포넌트다.


function Greeting() {
return <h1>안녕하세요</h1>;
}


함수 이름이 Greeting이고, 안에서 <h1>안녕하세요</h1>를 돌려준다. 이 태그처럼 생긴 문법이 JSX(자바스크립트 확장 문법)인데, 지금은 그냥 화면에 그릴 모양이라고만 보면 된다. 이 컴포넌트를 화면에 올리면 결과는 이렇다. 브라우저에 큰 제목 크기로 '안녕하세요'라는 글자가 하나 나타난다. 그게 전부다. createElement도, appendChild도 한 줄 없이, 그릴 모양만 적었을 뿐이다.


명령형 DOM 조작과 뭐가 다른가. 같은 걸 순수 자바스크립트로 하면 이렇게 된다. 만들고, 채우고, 붙이는 세 동작을 내가 순서대로 지시해야 한다.


const el = document.createElement("h1");
el.textContent = "안녕하세요";
document.body.appendChild(el);


결과는 리액트 쪽과 똑같이 '안녕하세요' 제목 하나다. 하지만 코드가 말하는 방식이 다르다. 이쪽은 '요소를 만들어라, 글자를 넣어라, 몸통에 붙여라'라고 어떻게 할지를 단계별로 명령한다. 리액트 쪽은 '화면은 이렇게 생겼다'라고 결과 모양만 선언한다. 조각이 하나일 땐 차이가 사소해 보이지만, 화면이 복잡해지고 내용이 계속 바뀌기 시작하면 이 차이가 코드의 운명을 가른다.


컴포넌트는 재사용된다. 함수라서 좋은 점이 여기서 나온다. 한 번 만든 Greeting을 몇 번이고 다시 쓸 수 있다. 태그를 쓰듯 <Greeting />라고 적으면 그 자리에 인사말이 하나 그려진다.


function App() {
return (
<div>
<Greeting />
<Greeting />
</div>
);
}


결과는 '안녕하세요' 제목이 위아래로 두 개 나타난다. 복사 붙여넣기로 태그를 늘린 게 아니라, 같은 부품을 두 번 꽂았을 뿐이다. 나중에 인사말 디자인을 바꾸고 싶으면 Greeting 함수 한 곳만 고치면, 그걸 꽂아둔 모든 자리가 한꺼번에 바뀐다. 예전에 게시글 카드를 순수 자바스크립트로 스무 개 찍어내다가, 디자인 수정 한 번에 스무 군데를 손보느라 밤을 샜던 기억이 있다. 컴포넌트를 알았으면 한 줄이면 끝날 일이었다.


컴포넌트를 조립해 화면을 만든다. 위의 App이 바로 그 조립의 예다. Appdiv 안에 Greeting들을 품고 있다. 이렇게 큰 컴포넌트가 작은 컴포넌트를 담고, 그 작은 것이 더 작은 것을 담는 식으로 화면 전체가 하나의 나무 구조가 된다. 헤더, 사이드바, 게시글 목록, 각 게시글 카드처럼 눈에 보이는 덩어리마다 컴포넌트를 하나씩 만들어 끼워 맞추면 페이지가 완성된다. 화면을 통짜로 보지 않고 조각의 조립으로 보는 것, 이게 리액트로 생각한다는 말의 핵심이다.


이름은 반드시 대문자로 시작한다. 사소해 보이지만 이걸 어기면 조용히 안 된다. Greeting은 되고 greeting은 안 된다. 리액트는 소문자로 시작하는 태그는 divh1 같은 진짜 HTML 태그로 취급하고, 대문자로 시작해야만 내가 만든 컴포넌트로 알아본다. 소문자로 이름을 지으면 에러도 안 나고 그냥 화면에 아무것도 안 나오는 경우가 많아서, 초보 때 이걸로 삼십 분씩 날린다. 컴포넌트 이름은 대문자, 이건 예외 없는 규칙이다.


상태를 가지면 스스로 다시 그린다. 컴포넌트가 함수라는 말이 아직 밋밋하게 들릴 수 있다. 진짜 힘은 컴포넌트가 상태(state, 변하는 값)를 품고, 그 값이 바뀌면 알아서 다시 그린다는 데 있다. 버튼을 누를 때마다 숫자가 오르는 카운터를 보자.


import { useState } from "react";

function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}


useState(0)은 처음 값이 0인 상태를 하나 만들고, 현재 값 count와 그 값을 바꾸는 함수 setCount를 돌려준다. 결과를 보자. 처음엔 버튼에 '0'이 적혀 있다. 버튼을 한 번 누르면 setCount가 값을 1로 올리고, 리액트가 이 컴포넌트를 다시 그려 버튼 글자가 '1'로 바뀐다. 또 누르면 '2', '3'으로 계속 오른다. 여기서 눈여겨볼 건, 내가 textContent를 손으로 바꾸는 코드를 한 줄도 안 썼다는 점이다. 값만 바꿨더니 화면이 따라온다. 이게 다음 편에서 이어질 선언형의 맛보기다.


컴포넌트는 보통 파일 하나에 하나씩 담는다. 실무에선 컴포넌트마다 파일을 따로 두고, 그 파일에서 export로 내보낸 뒤 쓸 곳에서 import로 가져온다. Greeting.jsx라는 파일에 export default function Greeting()이라고 적어두면, 다른 파일에서 import Greeting from "./Greeting"; 한 줄로 불러와 <Greeting />처럼 꽂을 수 있다. 화면을 조각으로 나눈다는 말이 곧 파일을 조각으로 나눈다는 말이기도 하다. 조각 하나가 고장 나면 그 파일만 열어 보면 되니, 순수 자바스크립트로 화면 로직이 한 파일에 뒤엉켜 있던 시절보다 어디를 봐야 할지가 훨씬 분명해진다.


함수형 컴포넌트가 요즘의 표준이다. 옛 리액트에는 class로 컴포넌트를 만들던 방식이 있었다. 코드가 길고 규칙도 까다로웠는데, 지금은 위에서 본 것처럼 함수로 만들고 useState 같은 훅(hook, 함수형 컴포넌트에 기능을 다는 도구)으로 상태를 다루는 방식이 표준이 됐다. 새로 배우는 사람이라면 옛 class 방식은 신경 쓸 필요 없이 함수형만 익히면 된다. 이 시리즈의 코드도 전부 함수형이다.


결국 컴포넌트는 화면을 함수로 잘게 나눈 것이다. 정리하면 컴포넌트는 화면 한 조각을 돌려주는 함수고, 이름은 대문자로 시작하며, 서로 조립되어 페이지를 이루고, 상태가 바뀌면 스스로 다시 그린다. 순수 자바스크립트로 요소를 일일이 만들고 붙이던 습관을 잠시 내려놓고, '이 화면은 어떤 조각들로 이루어졌나'부터 그려보는 연습을 하면 리액트가 훨씬 빨리 손에 붙는다. 처음엔 함수가 태그를 돌려주는 게 어색하지만, 며칠 지나면 이 방식 없이 화면 짜던 시절이 오히려 아득하게 느껴진다.