기본 콘텐츠로 건너뛰기

React props와 state 차이, 언제 무엇을 써야 할까

React props와 state 차이, 언제 무엇을 써야 할까

빠른 답

  • props는 부모가 자식에게 내려주는 읽기 전용 데이터다.
  • state는 컴포넌트가 직접 관리하며 바뀌면 화면이 다시 렌더링된다.
  • 자식이 값을 바꿔야 하면 props를 수정하지 말고 부모의 state 변경 함수를 전달한다.
  • 입력값, 로딩 여부, 토글 같은 변하는 UI 데이터는 대부분 state로 관리한다.

props와 state가 처음 React에서 가장 헷갈리는 이유

React를 처음 배우면 propsstate가 비슷해 보입니다. 둘 다 JSX 안에서 값을 출력할 때 사용하고, 둘 다 화면 결과에 영향을 주기 때문입니다. 그래서 “둘 다 데이터면 그냥 아무 데나 넣어도 되는 것 아닌가?”라는 생각이 들기 쉽습니다.

하지만 둘의 차이는 문법보다 소유권에 있습니다. 어떤 값을 부모가 가지고 자식에게 전달하면 props이고, 어떤 값을 컴포넌트가 자기 내부에서 직접 관리하면 state입니다. 이 기준만 분명히 잡히면, 어디에 어떤 값을 둬야 하는지 대부분 정리됩니다.

초보자가 가장 자주 막히는 지점도 여기입니다. 자식 컴포넌트에서 버튼을 눌렀는데 부모가 넘겨준 값을 바꾸고 싶을 때, props를 직접 수정하려고 시도합니다. 하지만 React는 단방향 데이터 흐름을 전제로 설계되어 있어서, 값의 변경은 값을 소유한 쪽에서 일어나야 합니다. 자식은 값을 바꾸는 대신, “이 값을 바꿔 달라”는 요청을 부모에게 보내는 구조를 사용합니다.

한눈에 비교

비교형으로 정리하면 propsstate의 역할 차이가 더 빨리 보입니다.

  • 소유자: 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로 만들 필요는 없습니다. 예를 들어 firstNamelastName이 있다면 fullName은 매번 계산해서 써도 됩니다. 이미 있는 값으로부터 만들 수 있는 결과를 또 state로 저장하면 값이 어긋날 가능성만 커집니다.

예제를 위한 최소 구성

작은 예제로 시작하면 propsstate의 역할이 바로 드러납니다. 입문 단계에서는 큰 프로젝트보다 부모 컴포넌트 하나, 자식 컴포넌트 하나로 흐름을 보는 편이 훨씬 효과적입니다.

아래처럼 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>
  );
}

이 코드에서 nicknameisFollowing의 실제 소유자는 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가 필요할 수 있습니다. 다만 이때도 “복사본이 왜 필요한지”가 분명해야 합니다. 무심코 propsstate로 옮기는 습관은 피하는 편이 좋습니다.

또 다른 흔한 오해는 “props는 읽기 전용이니 바뀌지 않는다”는 생각입니다. 정확히는 자식이 직접 수정할 수 없을 뿐, 부모가 새로운 값을 내려주면 props는 달라질 수 있고 자식도 다시 렌더링됩니다.

정리하면 디버깅할 때는 아래를 먼저 확인하면 됩니다.

  • 값이 바뀌는데 화면이 안 바뀌면 일반 변수만 수정한 것은 아닌지 확인합니다.
  • 부모의 값은 바뀌었는데 자식이 예전 값을 보여주면 props를 복사한 로컬 state가 있는지 확인합니다.
  • 자식에서 값을 바꾸고 싶을 때 props를 직접 수정하려고 하고 있지는 않은지 확인합니다.

실무 선택 기준: 어디에 둬야 하는가

실무에서는 이론보다 질문 몇 개로 판단하는 편이 더 빠릅니다. 어떤 값을 봤을 때 아래 순서대로 묻는 습관을 들이면 설계가 단순해집니다.

  • 이 값의 주인은 누구인가
  • 이 값은 외부에서 전달되는가
  • 이 값은 현재 컴포넌트 안에서 자주 바뀌는가
  • 여러 컴포넌트가 함께 써야 하는가
  • 저장해야 하는 상태인가, 아니면 계산 가능한 값인가

이 질문을 기준으로 고르면 대부분 정리됩니다.

  • 부모가 내려주는 데이터면 props
  • 현재 컴포넌트에서 직접 바꾸는 값이면 state
  • 형제 컴포넌트가 함께 써야 하면 공통 부모로 state를 올리기
  • 앱 전체에서 넓게 공유해야 하면 그다음에 Context나 상태 관리 라이브러리 검토
  • 다른 값으로 계산 가능한 결과라면 state보다 계산식 우선

결국 중요한 것은 문법 암기가 아니라 데이터의 소유권과 변경 위치를 분리해서 생각하는 습관입니다. 이 기준이 잡히면 propsstate를 어디에 둬야 하는지 훨씬 빨리 판단할 수 있습니다.

원문 참고

https://www.maeil-mail.kr/question/16

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...