React props와 state 차이, 언제 무엇을 써야 할까
빠른 답
- props는 부모가 자식에게 내려주는 읽기 전용 데이터다.
- state는 컴포넌트가 직접 관리하며 바뀌면 화면이 다시 렌더링된다.
- 자식이 값을 바꿔야 하면 props를 수정하지 말고 부모의 state 변경 함수를 전달한다.
- 입력값, 로딩 여부, 토글 같은 변하는 UI 데이터는 대부분 state로 관리한다.
목차
props와 state가 처음 React에서 가장 헷갈리는 이유
React를 처음 배우면 props와 state가 비슷해 보입니다. 둘 다 JSX 안에서 값을 출력할 때 사용하고, 둘 다 화면 결과에 영향을 주기 때문입니다. 그래서 “둘 다 데이터면 그냥 아무 데나 넣어도 되는 것 아닌가?”라는 생각이 들기 쉽습니다.
하지만 둘의 차이는 문법보다 소유권에 있습니다. 어떤 값을 부모가 가지고 자식에게 전달하면 props이고, 어떤 값을 컴포넌트가 자기 내부에서 직접 관리하면 state입니다. 이 기준만 분명히 잡히면, 어디에 어떤 값을 둬야 하는지 대부분 정리됩니다.
초보자가 가장 자주 막히는 지점도 여기입니다. 자식 컴포넌트에서 버튼을 눌렀는데 부모가 넘겨준 값을 바꾸고 싶을 때, props를 직접 수정하려고 시도합니다. 하지만 React는 단방향 데이터 흐름을 전제로 설계되어 있어서, 값의 변경은 값을 소유한 쪽에서 일어나야 합니다. 자식은 값을 바꾸는 대신, “이 값을 바꿔 달라”는 요청을 부모에게 보내는 구조를 사용합니다.
한눈에 비교
비교형으로 정리하면 props와 state의 역할 차이가 더 빨리 보입니다.
- 소유자:
props는 부모가 소유하고 자식에게 전달합니다.state는 현재 컴포넌트가 소유합니다. - 변경 가능성:
props는 자식 입장에서 읽기 전용입니다.state는 setter 함수로 변경할 수 있습니다. - 데이터 흐름:
props는 부모에서 자식으로 내려옵니다.state는 컴포넌트 내부 이벤트나 비동기 처리 결과로 바뀝니다. - 리렌더링 기준: 부모가 새로운
props를 내려주면 자식은 다시 렌더링될 수 있습니다.state가 바뀌면 해당 컴포넌트는 다시 렌더링됩니다. - 대표 용도:
props는 표시용 데이터, 설정값, 콜백 함수 전달에 적합합니다.state는 입력값, 로딩 상태, 모달 열림 여부, 서버 응답 데이터에 적합합니다. - 실수하기 쉬운 지점:
props를 직접 수정하기, 받은props를 그대로state로 복사하기, 계산 가능한 값을 굳이state로 저장하기가 대표적입니다.
짧게 말하면 이렇습니다. 외부에서 받으면 props, 내부에서 바꾸면 state입니다. 여러 컴포넌트가 함께 써야 하면, 가장 가까운 공통 부모로 state를 올려서 필요한 자식에게 props로 내려주면 됩니다.
props를 써야 하는 상황
props는 자식 컴포넌트가 스스로 결정하면 안 되는 값을 받을 때 사용합니다. 예를 들어 프로필 카드의 사용자 이름, 버튼 문구, 카드의 강조 여부, 리스트 아이템 데이터 같은 것들입니다.
핵심은 자식이 이 값을 소유하지 않는다는 점입니다. 자식은 받은 값을 렌더링하거나 조건 분기에 사용할 수는 있지만, 원본을 바꾸지는 않습니다. 그래서 props를 잘 쓰면 컴포넌트 재사용성이 높아집니다.
이럴 때는 props를 먼저 떠올리면 됩니다.
- 부모가 이미 가지고 있는 데이터를 자식에게 보여주고 싶을 때
- 자식의 모양이나 동작을 바깥에서 설정하고 싶을 때
- 자식이 어떤 이벤트를 발생시켰을 때 부모의 함수를 실행해야 할 때
- 같은 컴포넌트를 여러 화면에서 다른 데이터로 재사용하고 싶을 때
여기서 중요한 점 하나가 더 있습니다. props에는 문자열이나 숫자만 담는 것이 아닙니다. 함수도 props로 전달할 수 있습니다. 실무에서는 오히려 이 패턴이 매우 자주 등장합니다. 자식이 부모의 상태를 직접 바꾸는 대신, 부모가 내려준 함수를 호출하는 방식입니다.
state를 써야 하는 상황
state는 컴포넌트가 살아 있는 동안 변할 수 있는 값을 담습니다. 사용자가 입력창에 타이핑하면 값이 바뀌고, 버튼을 누르면 모달이 열리거나 닫히고, API 요청이 시작되면 로딩 상태가 켜집니다. 이런 값들은 시간이 지나며 바뀌므로 state가 적합합니다.
중요한 점은 state가 단순한 자바스크립트 변수가 아니라는 것입니다. 일반 변수는 값이 바뀌어도 React가 화면을 다시 그릴 이유를 알지 못합니다. 반면 state는 setter 함수로 변경하면 React가 이를 감지하고 컴포넌트를 다시 렌더링합니다.
실무에서 state로 관리하는 대표적인 값은 다음과 같습니다.
- 폼 입력값
- 체크 여부, 탭 선택 상태, 드롭다운 열림 여부
- 로딩 중인지 아닌지
- 서버에서 받아온 데이터
- 에러 메시지, 성공 메시지 같은 UI 상태
반대로 모든 값을 state로 만들 필요는 없습니다. 예를 들어 firstName과 lastName이 있다면 fullName은 매번 계산해서 써도 됩니다. 이미 있는 값으로부터 만들 수 있는 결과를 또 state로 저장하면 값이 어긋날 가능성만 커집니다.
예제를 위한 최소 구성
작은 예제로 시작하면 props와 state의 역할이 바로 드러납니다. 입문 단계에서는 큰 프로젝트보다 부모 컴포넌트 하나, 자식 컴포넌트 하나로 흐름을 보는 편이 훨씬 효과적입니다.
아래처럼 Vite로 React 예제를 만들면 가장 빠르게 실습할 수 있습니다.
npm create vite@latest props-state-demo -- --template react
cd props-state-demo
npm install
npm run dev
이제 App.jsx를 부모 컴포넌트로, ProfileCard.jsx를 자식 컴포넌트로 두고 보면 됩니다. 부모는 값을 소유하고, 자식은 그 값을 받아서 표시하는 구조입니다. 이 기본 구조를 이해하면 폼, 모달, 탭, 필터링 UI도 같은 관점으로 해석할 수 있습니다.
실전 코드로 보기: 부모의 state를 자식에 props로 전달하기
아래 예제는 가장 중요한 패턴을 그대로 보여줍니다. 부모는 state를 가지고 있고, 자식은 그 값을 props로 받습니다. 자식에서 버튼을 눌렀을 때도 props로 내려받은 함수를 실행해 부모의 상태를 변경합니다.
// App.jsx
import { useState } from 'react';
import ProfileCard from './ProfileCard';
export default function App() {
const [nickname, setNickname] = useState('seji');
const [isFollowing, setIsFollowing] = useState(false);
function handleToggleFollow() {
setIsFollowing((prev) => !prev);
}
return (
<main>
<h1>{nickname}</h1>
<ProfileCard
nickname={nickname}
isFollowing={isFollowing}
onToggleFollow={handleToggleFollow}
/>
<button onClick={() => setNickname('frontend-dev')}>
닉네임 변경
</button>
</main>
);
}
// ProfileCard.jsx
export default function ProfileCard({ nickname, isFollowing, onToggleFollow }) {
return (
<section>
<p>사용자: {nickname}</p>
<p>상태: {isFollowing ? '팔로우 중' : '미팔로우'}</p>
<button onClick={onToggleFollow}>
{isFollowing ? '언팔로우' : '팔로우'}
</button>
</section>
);
}
이 코드에서 nickname과 isFollowing의 실제 소유자는 App입니다. ProfileCard는 값을 직접 바꾸지 않고, 단지 렌더링에 사용하거나 이벤트를 통해 부모의 함수를 호출할 뿐입니다.
이 구조를 흔히 상태 끌어올리기라고 부릅니다. 형제 컴포넌트 여러 개가 같은 값을 함께 써야 할 때도, 결국 공통 부모가 state를 들고 각 자식에게 props로 내려주는 식으로 해결합니다.
입력값과 비동기 상태는 state가 왜 필요한지를 더 분명하게 보여줍니다. 아래 예제에서는 검색어, 로딩 상태, 결과 수가 모두 시간에 따라 변합니다.
import { useState } from 'react';
export default function SearchBox() {
const [keyword, setKeyword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [resultCount, setResultCount] = useState(0);
async function handleSearch() {
setIsLoading(true);
await new Promise((resolve) => setTimeout(resolve, 700));
setResultCount(keyword.length * 3);
setIsLoading(false);
}
return (
<div>
<input
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
placeholder="검색어를 입력하세요"
/>
<button onClick={handleSearch} disabled={isLoading}>
{isLoading ? '검색 중...' : '검색'}
</button>
<p>결과 수: {resultCount}</p>
</div>
);
}
이 예제에서 keyword, isLoading, resultCount는 모두 현재 컴포넌트가 직접 관리해야 자연스럽습니다. 다만 페이지 전체에서 검색 조건을 공유해야 한다면 이야기가 달라집니다. 그때는 이 상태를 상위 컴포넌트로 올리고, 필요한 곳에 props로 전달하는 편이 맞습니다.
자주 하는 실수와 예외 케이스
가장 흔한 실수는 받은 props를 그대로 state로 복사하는 것입니다. 처음에는 편해 보이지만, 부모가 새로운 값을 내려줘도 내부 state가 자동으로 맞춰지지 않아 버그가 생기기 쉽습니다.
아래 코드는 전형적인 반례입니다.
import { useState } from 'react';
export default function UserEditor({ user }) {
const [draftName, setDraftName] = useState(user.name);
return (
<div>
<p>원본 이름: {user.name}</p>
<input
value={draftName}
onChange={(e) => setDraftName(e.target.value)}
/>
</div>
);
}
이 컴포넌트는 처음 렌더링될 때만 user.name을 복사합니다. 이후 부모가 다른 user를 넘기더라도 draftName은 이전 값을 계속 들고 있을 수 있습니다. 그래서 아래 질문을 먼저 해야 합니다.
- 이 값은 정말 로컬에서 따로 편집해야 하는 임시 상태인가
- 아니면 그냥
props를 바로 보여주면 되는가
예외적으로, 편집 폼처럼 원본과 분리된 임시 초안이 필요한 경우라면 props를 기반으로 한 로컬 state가 필요할 수 있습니다. 다만 이때도 “복사본이 왜 필요한지”가 분명해야 합니다. 무심코 props를 state로 옮기는 습관은 피하는 편이 좋습니다.
또 다른 흔한 오해는 “props는 읽기 전용이니 바뀌지 않는다”는 생각입니다. 정확히는 자식이 직접 수정할 수 없을 뿐, 부모가 새로운 값을 내려주면 props는 달라질 수 있고 자식도 다시 렌더링됩니다.
정리하면 디버깅할 때는 아래를 먼저 확인하면 됩니다.
- 값이 바뀌는데 화면이 안 바뀌면 일반 변수만 수정한 것은 아닌지 확인합니다.
- 부모의 값은 바뀌었는데 자식이 예전 값을 보여주면
props를 복사한 로컬state가 있는지 확인합니다. - 자식에서 값을 바꾸고 싶을 때
props를 직접 수정하려고 하고 있지는 않은지 확인합니다.
실무 선택 기준: 어디에 둬야 하는가
실무에서는 이론보다 질문 몇 개로 판단하는 편이 더 빠릅니다. 어떤 값을 봤을 때 아래 순서대로 묻는 습관을 들이면 설계가 단순해집니다.
- 이 값의 주인은 누구인가
- 이 값은 외부에서 전달되는가
- 이 값은 현재 컴포넌트 안에서 자주 바뀌는가
- 여러 컴포넌트가 함께 써야 하는가
- 저장해야 하는 상태인가, 아니면 계산 가능한 값인가
이 질문을 기준으로 고르면 대부분 정리됩니다.
- 부모가 내려주는 데이터면
props - 현재 컴포넌트에서 직접 바꾸는 값이면
state - 형제 컴포넌트가 함께 써야 하면 공통 부모로
state를 올리기 - 앱 전체에서 넓게 공유해야 하면 그다음에 Context나 상태 관리 라이브러리 검토
- 다른 값으로 계산 가능한 결과라면
state보다 계산식 우선
결국 중요한 것은 문법 암기가 아니라 데이터의 소유권과 변경 위치를 분리해서 생각하는 습관입니다. 이 기준이 잡히면 props와 state를 어디에 둬야 하는지 훨씬 빨리 판단할 수 있습니다.
원문 참고
https://www.maeil-mail.kr/question/16
댓글
댓글 쓰기