React의 핵심 개념인 컴포넌트와 props를 정리했다. 컴포넌트를 왜 나누는지부터, props로 데이터를 전달하는 방법, 구조 분해 할당, children props까지 다룬다.
컴포넌트는 "화면의 한 조각을 만드는 함수"다. 지금까지 계속 써온 App도 사실 하나의 컴포넌트였다.
컴포넌트는 이렇게 정의한다.
app X, App O)return한다.function Welcome() {
return <h2>환영합니다!</h2>;
}
function App() {
return (
<div>
<h1>나의 첫 컴포넌트 나누기</h1>
<Welcome />
<Welcome />
<Welcome />
</div>
);
}
export default App;
<Welcome />처럼 태그 형태로 컴포넌트를 3번 사용하면, "환영합니다!"가 화면에 3번 출력된다.
컴포넌트 이름은 왜 꼭 대문자로 시작해야 할까? JSX는 태그 이름의 첫 글자로 "이게 원래 있던 HTML 태그인지, 내가 만든 컴포넌트인지"를 구분한다. <welcome />처럼 소문자로 쓰면 JSX는 이걸 <div>, <span>처럼 브라우저가 원래 알고 있는 HTML 태그로 착각해서 찾다가 오류를 낸다. 그래서 직접 만든 컴포넌트는 항상 대문자로 시작해야 한다.
방금 예시처럼 Welcome을 한 번만 만들어두면 몇 번이든 재사용할 수 있다. 만약 컴포넌트로 나누지 않았다면 <h2>환영합니다!</h2>를 필요할 때마다 똑같이 복사-붙여넣기 해야 했을 것이다.
컴포넌트를 안 나누고 App 하나에 다 몰아넣으면 왜 안 될까? 처음에는 코드가 짧아서 괜찮아 보이지만, Todo 앱처럼 기능이 늘어나면 App 함수 하나가 수백 줄이 되어버린다. 그러면 "지금 고치려는 버튼이 코드의 어디에 있는지" 찾는 데만 한참 걸리게 된다. 컴포넌트를 기능 단위로 나눠두면 필요한 부분만 빠르게 찾아 수정할 수 있다.
지금까지 만든 Welcome은 항상 같은 문구만 보여준다. 컴포넌트마다 다른 값을 보여주고 싶을 때는 props를 사용한다. props는 부모 컴포넌트가 자식 컴포넌트에게 전달하는 데이터다.
function Profile(props) {
return (
<div>
<h2>{props.name}</h2>
<p>{props.bio}</p>
</div>
);
}
function App() {
return (
<div>
<Profile name="한코딩" bio="프론트엔드 개발자" />
<Profile name="김리액트" bio="주니어 개발자" />
</div>
);
}
export default App;
<Profile name="한코딩" bio="프론트엔드 개발자" />처럼 태그에 속성을 적으면, Profile 함수는 { name: "한코딩", bio: "프론트엔드 개발자" }라는 객체를 props라는 이름으로 통째로 받는다.
props.name, props.bio처럼 매번 props.을 붙이는 대신, 자바스크립트의 구조 분해 할당 문법을 사용하면 코드를 더 간결하게 쓸 수 있다.
function Profile({ name, bio }) {
return (
<div>
<h2>{name}</h2>
<p>{bio}</p>
</div>
);
}
function App() {
return (
<div>
<Profile name="한코딩" bio="프론트엔드 개발자" />
<Profile name="김리액트" bio="주니어 개발자" />
</div>
);
}
export default App;
왜 굳이 구조 분해 할당을 쓸까? props.name, props.bio처럼 매번 props.을 반복해서 적으면 코드가 길어지고, 이 컴포넌트가 어떤 props를 받는지도 함수 내용을 다 읽어야 알 수 있다. { name, bio }처럼 함수의 매개변수 자리에서 바로 구조 분해를 하면, 함수 이름만 봐도 "아, 이 컴포넌트는 name과 bio를 받는구나"를 한눈에 알 수 있다.
컴포넌트 태그 사이에 무언가를 넣으면, 그 내용은 자동으로 props.children이라는 이름으로 전달된다.
function Card({ children }) {
return <div style={{ border: "1px solid gray", padding: "12px" }}>{children}</div>;
}
function App() {
return (
<Card>
<h2>공지사항</h2>
<p>이번 주 금요일은 휴강입니다.</p>
</Card>
);
}
export default App;
<Card>와 </Card> 사이에 적은 <h2>, <p> 태그들이 통째로 Card 컴포넌트의 children으로 전달되어, 테두리가 있는 박스 안에 그대로 렌더링된다.
src/App.jsx의 내용을 모두 지운다.name, job, bio 세 가지 props를 받는 Profile 컴포넌트를 구조 분해 할당 방식으로 작성한다.App 컴포넌트 안에서 <Profile />을 사용해 자신의 이름, 직업(또는 꿈), 소개를 props로 전달해본다.Profile 컴포넌트를 서로 다른 props 값으로 2~3번 더 사용해본다.children을 받아서 테두리와 배경색이 있는 박스로 감싸주는 Box 컴포넌트를 만든다.<Box> 태그 사이에 방금 만든 <Profile /> 컴포넌트를 넣어서, 프로필 카드가 박스 안에 예쁘게 감싸지는지 확인한다.{ name, bio }처럼 구조 분해 할당으로 받으면 코드가 훨씬 간결해진다.children으로 전달되며, 이는 박스·카드처럼 내용물을 감싸는 컴포넌트를 만들 때 유용하다.지금까지 배운 React 입문 개념을 총정리하고, 이를 활용해 사진·직업 정보가 담긴 프로필 카드 3개를 나란히 보여주는 미니 프로젝트를 만들어봤다.
지금까지 배운 내용을 순서대로 이으면 이런 흐름이었다.
| 노트 | 핵심 개념 | 한 줄 요약 |
|---|---|---|
| 웹 개발의 흐름과 React 등장 배경 | Virtual DOM, 선언적 UI | 화면을 직접 조작하지 않고, "이렇게 보여야 한다"만 선언하면 React가 알아서 반영해준다 |
| 개발 환경 구축 | 개발 환경 | Vite로 프로젝트를 만들고 npm run dev로 실행한다 |
| JSX(JavaScript XML) 문법 | JSX | { }로 자바스크립트 값을 화면에 끼워 넣는, HTML을 닮은 자바스크립트 문법이다 |
| 컴포넌트(Component)와 Props | 컴포넌트, props | 화면을 재사용 가능한 조각(컴포넌트)으로 나누고, props로 서로 다른 데이터를 전달한다 |
앞에서 만든 Profile 컴포넌트는 이 네 가지 개념이 전부 합쳐진 결과물이었다. JSX로 구조를 짜고, props로 사람마다 다른 값을 받아서, 하나의 컴포넌트를 여러 번 재사용한 것이다.
지금까지 만든 Profile 컴포넌트를 확장해서, 사진과 직업 정보까지 포함한 프로필 카드 3개를 나란히 보여주는 목록을 완성한다.
카드 3개를 만드는데 컴포넌트는 왜 하나만 있으면 될까? Profile 컴포넌트 없이 만들었다면 카드 3개 분량의 HTML 구조(이미지, 이름, 직업, 소개, 버튼)를 세 번 반복해서 작성해야 했을 것이다. 컴포넌트 하나를 만들어두고 props만 다르게 넘기면, 코드는 한 번만 작성하고 데이터만 바꿔서 여러 번 재사용할 수 있다.
앞에서 만든 Profile은 name, bio만 받았다. 여기에 job(직업)과 photo(사진 URL)를 추가한다.
function Profile({ name, job, photo, bio }) {
return (
<div>
<img src={photo} alt={name} style={{ width: "80px", height: "80px", borderRadius: "50%" }} />
<h3>{name}</h3>
<p>{job}</p>
<p>{bio}</p>
<button>프로필 보기</button>
</div>
);
}
App 컴포넌트 안에서 Profile을 3번 사용하되, 이름·직업·소개·사진을 각각 다르게 전달한다. 사진은 아직 실제 이미지가 없어도 되니, 임시 이미지 서비스 주소를 사용해도 된다.
function App() {
return (
<div>
<Profile
name="한코딩"
job="프론트엔드 개발자"
photo="https://placehold.co/80x80"
bio="React를 가르치고 있어요."
/>
<Profile
name="김리액트"
job="주니어 개발자"
photo="https://placehold.co/80x80"
bio="이제 막 React를 배우기 시작했어요."
/>
<Profile
name="이자바"
job="백엔드 개발자"
photo="https://placehold.co/80x80"
bio="프론트엔드에도 관심이 많아요."
/>
</div>
);
}
export default App;
지금은 카드 3개가 세로로 길게 쌓여 있다. App을 감싸는 <div>에 style을 추가해서 가로로 나란히 정렬한다.
function App() {
return (
<div style={{ display: "flex", gap: "16px" }}>
<Profile
name="한코딩"
job="프론트엔드 개발자"
photo="https://placehold.co/80x80"
bio="React를 가르치고 있어요."
/>
<Profile
name="김리액트"
job="주니어 개발자"
photo="https://placehold.co/80x80"
bio="이제 막 React를 배우기 시작했어요."
/>
<Profile
name="이자바"
job="백엔드 개발자"
photo="https://placehold.co/80x80"
bio="프론트엔드에도 관심이 많아요."
/>
</div>
);
}
export default App;
display: "flex"는 안의 요소들을 가로 방향으로 나란히 배치해주고, gap: "16px"는 카드 사이 간격을 벌려준다.
Profile 컴포넌트의 최상위 <div>에도 style을 추가해서, 카드처럼 테두리와 여백이 있는 모양으로 꾸며본다.
function Profile({ name, job, photo, bio }) {
return (
<div style={{ border: "1px solid #ddd", borderRadius: "8px", padding: "16px", width: "180px" }}>
<img src={photo} alt={name} style={{ width: "80px", height: "80px", borderRadius: "50%" }} />
<h3>{name}</h3>
<p>{job}</p>
<p>{bio}</p>
<button>프로필 보기</button>
</div>
);
}
Profile 컴포넌트 자체는 그대로 두고 데이터만 추가하면 된다.display: "flex" + gap은 카드형 UI를 가로로 나란히 배치할 때 자주 쓰이는 조합이니 기억해두면 좋다.React의 상태 관리 기본인 useState를 정리했다. 왜 일반 변수로는 화면이 안 바뀌는지부터, useState 문법, 리렌더링 원리, 여러 state를 동시에 다루는 방법까지 다룬다.
React에서는 일반 변수의 값을 바꿔도 화면이 자동으로 다시 그려지지 않는다. 아래 코드로 직접 확인해보자.
let count = 0;
function App() {
const handleClick = () => {
count = count + 1;
console.log(count);
};
return (
<div>
<p>현재 값: {count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
export default App;
버튼을 눌러보면, 크롬 개발자 도구의 Console 탭에는 1, 2, 3 ... 값이 잘 찍히는데, 정작 화면의 "현재 값: 0"은 그대로다. count 변수의 값은 분명히 바뀌었는데, React는 그 변화를 모르기 때문에 화면을 다시 그려주지 않는 것이다.
이 문제를 해결하는 것이 바로 state다. state는 "값이 바뀌면 화면도 자동으로 다시 그려달라"고 React에게 등록해두는 특별한 변수다.
state를 만들 때는 React가 제공하는 useState라는 함수를 사용한다. 위 코드를 이렇게 바꿔보자.
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
return (
<div>
<p>현재 값: {count}</p>
<button onClick={handleClick}>+1</button>
</div>
);
}
export default App;
이제 버튼을 클릭하면 화면의 숫자가 실제로 올라가는 것을 확인할 수 있다.
| 코드 | 의미 |
|---|---|
useState(0) | state의 초기값을 0으로 설정 |
count | 현재 state 값 (읽기 전용) |
setCount | state 값을 바꿀 때 사용하는 함수 |
useState(0)을 호출하면 [현재값, 값을바꾸는함수] 형태의 배열이 돌아오는데, 이걸 const [count, setCount] = ...처럼 구조 분해 할당으로 한 번에 꺼내서 사용한다.
setCount를 호출하면 React는 "이 컴포넌트, 다시 그려야겠다"고 인식하고 App 함수를 처음부터 다시 실행한다. 이때 useState(0)은 이제 처음 값(0)이 아니라 가장 최근에 저장된 값을 돌려준다. 그래서 화면에 최신 값이 반영되는 것이다.
count = count + 1처럼 변수를 직접 바꾸면 왜 안 될까? React는 setCount처럼 정해진 함수를 통해서만 "값이 바뀌었다"는 사실을 알아챌 수 있다. count = count + 1처럼 변수를 직접 바꾸는 방식은 앞서 확인했듯 React가 전혀 눈치채지 못한다. 그래서 state 값을 바꿀 때는 반드시 useState가 만들어준 함수(setCount)를 사용해야 한다.
컴포넌트 하나에 useState를 여러 번 사용해서, 서로 다른 값을 각각의 state로 관리할 수 있다.
import { useState } from "react";
function App() {
const [name, setName] = useState("한코딩");
const [likeCount, setLikeCount] = useState(0);
return (
<div>
<p>이름: {name}</p>
<p>좋아요: {likeCount}</p>
<button onClick={() => setLikeCount(likeCount + 1)}>좋아요</button>
</div>
);
}
export default App;
name과 likeCount는 서로 완전히 독립된 state다. likeCount가 바뀌어도 name은 그대로 유지된다.
src/App.jsx에 "일반 변수" 카운터 코드를 붙여넣어 실행한다.useState를 사용해 진짜로 동작하는 클릭 카운터를 만든다.+1 버튼 옆에 -1 버튼과 리셋 버튼을 추가한다.-1 버튼은 setCount(count - 1)을, 리셋 버튼은 setCount(0)을 호출하도록 만든다.useState를 두 번 사용해서, 좋아요 수(숫자)와 좋아요 여부(true/false) 두 가지 state를 각각 만든다.true로 바뀌도록 만들어본다.const [값, set함수] = useState(초기값) 패턴을 기본 틀로 외워두면 어떤 state를 만들든 헷갈리지 않는다.set함수로만 변경해야 하며, 변수 = 새값처럼 직접 대입하면 React가 변화를 감지하지 못한다.useState를 여러 번 호출해서 각각 독립된 state로 관리하는 게 기본 패턴이다.React에서 클릭, 입력 같은 사용자 동작을 다루는 방법을 정리했다. 주요 이벤트 종류부터 이벤트 핸들러 작성 패턴, 이벤트 객체(e.target.value)까지 다룬다.
사용자가 버튼을 클릭하거나, 입력창에 글자를 타이핑하거나, 폼을 제출하는 것처럼 화면에서 일어나는 동작을 이벤트(event)라고 부른다. React에서는 HTML 태그에 이벤트별로 정해진 속성을 붙여서 처리한다.
| 이벤트 속성 | 언제 발생하는가 |
|---|---|
onClick | 요소를 클릭했을 때 |
onChange | input, textarea 등의 값이 바뀔 때 |
onSubmit | form이 제출될 때 |
onMouseEnter | 마우스가 요소 위에 올라갔을 때 |
이벤트가 발생했을 때 실행할 함수를 이벤트 핸들러라고 부른다. 작성 방식은 크게 두 가지다.
function App() {
// 방식 1: 분리된 함수로 작성
const handleClick = () => {
alert("버튼을 클릭했습니다!");
};
return (
<div>
<button onClick={handleClick}>분리된 함수</button>
{/* 방식 2: 인라인(그 자리에서 바로) 작성 */}
<button onClick={() => alert("인라인으로 작성했습니다!")}>인라인 함수</button>
</div>
);
}
export default App;
onClick={handleClick}이라고 써야지 왜 onClick={handleClick()}이라고 쓰면 안 될까? onClick={handleClick()}처럼 괄호를 붙이면 "클릭했을 때 실행해줘"가 아니라 "지금 당장 실행해줘"라는 뜻이 된다. 그러면 버튼을 누르기도 전에 화면이 그려지는 순간 함수가 바로 실행되어버린다. 이벤트 핸들러는 함수를 "지금 호출"하는 게 아니라 "나중에 클릭됐을 때 실행할 함수 자체"를 전달해야 하기 때문에 괄호 없이 함수 이름만 적어야 한다.
이벤트 핸들러 함수는 React가 자동으로 이벤트 객체를 첫 번째 매개변수로 넘겨준다. 이 객체 안에는 "무슨 일이 일어났는지"에 대한 정보가 담겨 있다. 특히 입력창의 경우, e.target.value로 사용자가 입력한 값을 읽을 수 있다.
import { useState } from "react";
function App() {
const [text, setText] = useState("");
const handleChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input onChange={handleChange} placeholder="아무거나 입력해보세요" />
<p>입력한 값: {text}</p>
</div>
);
}
export default App;
input에 글자를 입력할 때마다 onChange가 실행되고, e.target은 "이벤트가 일어난 그 input 태그 자신"을 가리킨다. 그래서 e.target.value는 지금 그 input 안에 있는 값을 의미한다.
| 상황 | 추천 방식 |
|---|---|
로직이 한 줄로 아주 간단할 때 (예: setCount(count + 1)) | 인라인 방식 |
| 로직이 여러 줄이거나, 조건문이 들어갈 때 | 분리된 함수 방식 |
| 같은 로직을 여러 버튼에서 재사용할 때 | 분리된 함수 방식 |
alert()로 원하는 문구가 뜨는 코드를 분리된 함수 방식으로 작성한다.useState와 onChange를 사용해서, input에 입력한 글자가 바로 아래 문장에 그대로 표시되도록 만든다.text.length)도 함께 표시해본다.onClick={handler}처럼 함수 자체를 전달해야 하며, handler()처럼 괄호를 붙이면 즉시 실행되어버린다는 점을 꼭 기억하자.onChange + e.target.value + useState 조합은 input 값을 실시간으로 화면에 반영하는 React의 가장 기본적인 패턴이다.