리액트 기초 강좌 #4 컴포넌트와 props
지난 시간에는 JSX 문법을 살펴봤습니다. 그 과정에서 우리는 자연스럽게 App이라는 이름의 함수를 봤습니다. 사실 이 App이 바로 리액트의 가장 중요한 단위인 **컴포넌트(Component)**입니다. 이번 시간에는 컴포넌트가 무엇이고 어떻게 만드는지, 그리고 컴포넌트끼리 데이터를 주고받는 통로인 props에 대해 알아보도록 하겠습니다.
컴포넌트는 왜 필요한가? #
화면 전체를 하나의 거대한 함수에 다 작성한다고 상상해보세요. 헤더, 사이드바, 메인 컨텐츠, 푸터, 버튼, 입력창 … 코드는 금방 수백, 수천 줄이 되고, 어디를 고쳐야 할지 찾기조차 어려워집니다. 같은 모양의 버튼이 화면에 10개 있다면 똑같은 코드도 10번 작성해야 합니다.
리액트는 이 문제를 컴포넌트라는 개념으로 해결합니다. 컴포넌트는 화면의 한 조각을 표현하는 재사용 가능한 단위입니다. 헤더, 버튼, 카드, 입력창 같은 화면 요소를 각각 하나의 컴포넌트로 만들어두면, 필요한 곳에서 마치 HTML 태그처럼 가져다 쓸 수 있습니다.
첫 컴포넌트 만들기 #
리액트에서 컴포넌트는 결국 JSX를 반환하는 자바스크립트 함수입니다. 우리가 지난 시간 내내 본 App도 그렇습니다.
function Greeting() {
return <h1>안녕하세요, 리액트!</h1>;
}
function App() {
return (
<div>
<Greeting />
</div>
);
}
export default App;Greeting이라는 새 함수를 정의하고, App 안에서 마치 HTML 태그처럼 <Greeting />을 사용했습니다. 이렇게 함수 하나가 그대로 하나의 컴포넌트가 됩니다.
<greeting />처럼 소문자로 시작하면 리액트는 이것을 일반 HTML 태그로 인식해서 의도와 다르게 동작합니다. 작명 규칙: PascalCase(UserCard, LoginButton)이 표준입니다.컴포넌트를 별도 파일로 분리하기 #
컴포넌트가 늘어나면 한 파일에 다 쓰기보다 파일별로 나누는 게 좋습니다. 보통 컴포넌트 하나당 파일 하나로 만듭니다.
src/Greeting.jsx라는 파일을 새로 만들고 다음과 같이 작성합니다.
function Greeting() {
return <h1>안녕하세요, 리액트!</h1>;
}
export default Greeting;그리고 App.jsx에서는 이렇게 가져옵니다.
import Greeting from './Greeting';
function App() {
return (
<div>
<Greeting />
</div>
);
}
export default App;export default로 내보내고 import로 받아오는 일반적인 자바스크립트 모듈 패턴 그대로입니다. 파일 확장자(.jsx)는 생략해도 Vite가 알아서 찾아줍니다.
Greeting.jsx, UserCard.jsx 같은 식입니다. 폴더 구조는 프로젝트마다 다르지만, 작은 프로젝트는 src/components/ 아래 모아두는 것이 보편적입니다.컴포넌트에 데이터 전달하기 — props #
Greeting 컴포넌트는 항상 “안녕하세요, 리액트!“만 출력합니다. 그런데 사용자마다 다른 인사말을 보여주고 싶다면 어떻게 해야 합니까? props를 사용하면 됩니다.
props는 컴포넌트의 매개변수 같은 개념입니다. 부모가 자식 컴포넌트에 데이터를 내려보낼 때 사용합니다. HTML 속성을 쓰듯이 자연스럽게 작성하면 됩니다.
import Greeting from './Greeting';
function App() {
return (
<div>
<Greeting name="철수" />
<Greeting name="영희" />
<Greeting name="민수" />
</div>
);
}
export default App;Greeting을 세 번 사용하면서 매번 다른 name 값을 전달했습니다. 이제 Greeting 컴포넌트가 이 값을 받을 수 있게 수정합니다.
function Greeting(props) {
return <h1>안녕하세요, {props.name}님!</h1>;
}
export default Greeting;함수의 첫 번째 매개변수로 props라는 객체가 들어옵니다. 부모가 전달한 모든 속성이 이 객체의 프로퍼티로 담깁니다. name="철수"로 전달했으니 props.name은 '철수'입니다.
화면에는 다음과 같이 출력됩니다.
안녕하세요, 철수님!
안녕하세요, 영희님!
안녕하세요, 민수님!같은 컴포넌트가 props만 바꿔서 세 번 재사용된 것입니다. 이게 컴포넌트의 핵심 가치입니다.
다양한 타입의 props #
props로는 문자열뿐 아니라 숫자, 불리언, 배열, 객체, 심지어 함수도 전달할 수 있습니다. 문자열이 아닌 값은 중괄호 { }로 감싸야 한다는 점만 기억하세요.
function App() {
const user = { name: '철수', email: 'cheolsu@example.com' };
return (
<UserCard
name="철수"
age={30}
isAdmin={true}
hobbies={['독서', '코딩', '여행']}
profile={user}
/>
);
}function UserCard(props) {
return (
<div>
<h2>{props.name} ({props.age}세)</h2>
{props.isAdmin && <p>관리자 권한이 있습니다.</p>}
<p>이메일: {props.profile.email}</p>
<p>취미: {props.hobbies.join(', ')}</p>
</div>
);
}
export default UserCard;문자열은 따옴표로 (name="철수"), 그 외 자바스크립트 값은 중괄호로 (age={30}) 전달한다고 기억하면 됩니다.
구조분해 할당으로 깔끔하게 받기 #
props.name, props.age처럼 매번 props.을 붙이는 게 번거롭다면 자바스크립트의 **구조분해 할당(destructuring)**을 사용할 수 있습니다.
function Greeting({ name }) {
return <h1>안녕하세요, {name}님!</h1>;
}
export default Greeting;매개변수 부분에서 바로 분해해서 받으면 함수 본문에서는 name만으로 사용할 수 있어 코드가 짧아집니다. 여러 props라면 그냥 나열하면 됩니다.
function UserCard({ name, age, isAdmin, hobbies, profile }) {
return (
<div>
<h2>{name} ({age}세)</h2>
{isAdmin && <p>관리자 권한이 있습니다.</p>}
<p>이메일: {profile.email}</p>
<p>취미: {hobbies.join(', ')}</p>
</div>
);
}
export default UserCard;실제 리액트 코드에서는 이 방식이 훨씬 자주 보입니다. 앞으로 우리도 이 스타일로 쓰겠습니다.
기본값 지정하기 #
prop이 전달되지 않을 수도 있는 상황이라면 구조분해 할당과 함께 기본값을 지정할 수 있습니다.
function Greeting({ name = '손님' }) {
return <h1>안녕하세요, {name}님!</h1>;
}<Greeting />처럼 name 없이 사용하면 자동으로 '손님'이 사용됩니다.
children — 컴포넌트 사이에 들어가는 자식 #
지금까지 우리는 <Greeting />처럼 자체 닫는 형태로만 컴포넌트를 사용했습니다. 그런데 HTML처럼 여는 태그와 닫는 태그 사이에 무언가를 넣고 싶을 때가 있습니다.
import Card from './Card';
function App() {
return (
<Card>
<h2>공지사항</h2>
<p>오늘은 휴무일입니다.</p>
</Card>
);
}이때 <Card>와 </Card> 사이에 들어간 내용은 자동으로 **children**이라는 특별한 prop에 담겨 전달됩니다.
function Card({ children }) {
return (
<div className="card" style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '8px' }}>
{children}
</div>
);
}
export default Card;Card 컴포넌트는 안에 무엇이 들어올지 모르지만, children을 그 위치에 출력해주기만 하면 됩니다. 이 패턴은 레이아웃(Card, Modal, Layout 등)이나 래퍼(Wrapper) 성격의 컴포넌트에서 매우 자주 사용됩니다.
props는 읽기 전용입니다 #
마지막으로 가장 중요한 규칙입니다. 컴포넌트는 자신이 받은 props를 절대 수정해서는 안 됩니다. 다음 코드는 잘못된 예입니다.
function Greeting({ name }) {
name = name.toUpperCase(); // 🚫 props를 직접 수정
return <h1>안녕하세요, {name}님!</h1>;
}props는 부모에서 흘러내려오는 데이터의 사본이며, 자식이 마음대로 바꿀 수 있는 값이 아닙니다. 가공이 필요하면 새로운 변수에 담아서 사용하세요.
function Greeting({ name }) {
const upperName = name.toUpperCase();
return <h1>안녕하세요, {upperName}님!</h1>;
}직접 해보기 #
지난 시간에 만든 src/App.jsx를 다음 구조로 바꿔보세요.
src/UserCard.jsx를 새로 만들고:
function UserCard({ name, age, hobbies }) {
return (
<div style={{ border: '1px solid #ccc', padding: '12px', margin: '8px', borderRadius: '8px' }}>
<h2>{name} ({age}세)</h2>
<p>취미: {hobbies.join(', ')}</p>
</div>
);
}
export default UserCard;src/App.jsx는 이렇게:
import UserCard from './UserCard';
function App() {
return (
<>
<h1>회원 목록</h1>
<UserCard name="철수" age={30} hobbies={['독서', '코딩']} />
<UserCard name="영희" age={28} hobbies={['여행', '요리', '사진']} />
<UserCard name="민수" age={35} hobbies={['게임']} />
</>
);
}
export default App;저장하면 세 명의 회원 카드가 화면에 그려집니다. 같은 UserCard 컴포넌트가 props만 바뀌면서 세 번 재사용된 것입니다. 이름이나 취미를 바꿔보고, 새 카드를 추가해보세요.
마무리 #
이번 글에서는 리액트의 핵심 단위인 컴포넌트를 만들고, 별도 파일로 분리하고, props로 데이터를 전달하는 방법을 살펴봤습니다. 구조분해 할당, 기본값, children, props의 읽기 전용 규칙까지 다뤘습니다. 이제 화면을 작은 조각들로 쪼개고 재사용할 수 있게 됐습니다.
지금까지 우리가 다룬 컴포넌트는 모두 정적이었습니다. 한 번 그려지고 나면 절대 변하지 않았습니다. 하지만 실제 앱은 사용자 입력에 따라, 시간에 따라, 서버 응답에 따라 끊임없이 모습이 바뀝니다. 다음 글인 “리액트 기초 강좌 #5 State와 useState"에서는 컴포넌트가 변할 수 있는 데이터를 다루는 방법, 즉 state의 개념과 useState 훅을 배워보도록 하겠습니다.