카테고리 없음

리액트 기초 01 - 핵심 요소 2 : props란?

lamarcK 2025. 4. 6. 20:42

 1. Props(프롭스)의 정의

props(properties)

이전 컴포넌트 설명에서 한번 언급했었는데 부모 요소가 자식 요소에게 물려주는 특정 데이터를 props(프롭스)라고 한다.

상속과는 다르게 특정 데이터만 물려받기 때문에 부분적 상속이라고 할 수 있다. 예를 들면 외모만 물려 받고 재산은 물려 받지 않는다거나 하는 것이다.

사용 목적

실제로 ParentChild 2개의 컴포넌트가 존재한다면 부모 요소가 가지는 특정 데이터 들이 존재할 것이다.

예를 들면 내부에서 선언한 지역 변수들이다. 일반적인 경우 지역 변수는 해당 스코프(Scope)에서만 유효하다.

하지만 특정 컴포넌트끼리 서로 상관 관계에 있다면 해당 변수의 값을 활용해야 하는 경우가 있을 것이다.

이 경우 다른 스코프 내에 똑같은 변수를 그대로 선언하는 것은 코드 관리 측면에서 문제가 있다.

코드 중복, 데이터 일관성, 메모리 비효율성, 데이터 추적의 어려움 등의 문제를 발생시킨다는 것이다.

function Parent() {
    const traits = {
        height: 180,
        eyeColor: "brown",
        personality: "friendly",
        wealth: 1000000,
        hobby: "reading"
    };

    return (
        <Child 
            height={traits.height}     // 키만 전달
            eyeColor={traits.eyeColor} // 눈 색깔만 전달
            hobby={traits.hobby}       // 취미만 전달
        />
    );
}

function Child({ height, eyeColor, hobby }) {
    return (
        <div>
            <p>키: {height}</p>
            <p>눈색: {eyeColor}</p>
            <p>취미: {hobby}</p>
        </div>
    );
}

그런데 만약 부모 요소의 데이터를 자식 요소에게 전달한다면 어떻게 될까? 그럴 경우 변수는 1번의 선언으로 충분할 것이며 데이터의 일관성을 확보할 수 있게된다.

 

또한 데이터를 전달하는 방식으로 여러가지 컴포넌트를 돌려쓸 수 있기 때문에 중복되는 코드를 줄이고 코드 재사용성을 증가 시켜서 유지보수 용이성을 향상 시킬 수 있다.

예를 들면 범용적으로 사용이 가능한 컴포넌트를 만들어서 프롭스를 받아 재사용하는 것이다.

// App.jsx
// 다양한 상황에서 재사용
//부모 컴포넌트
function App() {
  return (
    <div>
      <Button 
        label="확인" 
        onClick={() => alert('확인')} 
      />
      <Button 
        label="취소" 
        color="red" 
        onClick={() => alert('취소')} 
      />
    </div>
  );
}

//범용성 있는 자식 컴포넌트
function Button({ label, onClick, color = 'blue' }) {
  return (
    <button 
      style={{ backgroundColor: color }}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

export default App;

중요 특징

  1. 부모 → 자식 컴포넌트로 데이터 전달
  2. 컴포넌트 호출 시 반드시 태그 사용
  3. 중괄호 {} 사용해 변수/표현식 전달
  4. Props는 객체로 전달
  5. 매개변수 이름은 자유롭게 지정 가능
  6. 전달받은 Props 전체/일부/미사용 모두 가능
  7. 단방향 데이터 흐름(참조를 전달하는 거라 원본 데이터 직접 변경을 차단한다.)

2. Props(프롭스)의 작동방식

그렇다면 실제로 프롭스는 어떻게 작동할까?

 

보통은 실제 컴포넌트 실행을 위한 main.jsx와 컴포넌트들이 담겨있는 app.jsx 등으로 jsx 파일을 분리하게 된다.

// main.jsx
import { createRoot } from 'react-dom/client'
import Components from './App.jsx';

createRoot(document.getElementById('root')).render(
  <Components />
)

 

여기서 중요한 점은 main.jsx로 넘기는 default 컴포넌트가 자식 컴포넌트가 아니라 부모 컴포넌트라는 점이다.

// App.jsx
function Parent() {
  const userName = "John"; // userName은 "John"
  const userAge = 25;
  // Child에게 name="John", age=25 전달
  return <Child name={userName} age={userAge} />;
}

function Child(props) {
  // 어떤 props를 받았는지 전체 객체를 확인
  // props.name 값을 명시적으로 확인
  const name = props.name;
  return <div>{name}, {props.age}세</div>;
}

export default Parent;

 

어째서 부모 컴포넌트일까?

착각하기 쉬운 점

데이터 전달이나 부분적 상속이라는 말때문에 실제로 자식요소가 부모요소로 부터 받은 데이터를 독립적으로 사용할 수 있다고 착각할 수 있는데 전혀 아니다.

 

실제로는 부모 컴포넌트가 자식 컴포넌트를 사용하는 것에 가깝다.

 

프롭스를 전달하기 위해서는 부모 요소에서 자식 컴포넌트를 호출하는 과정이 반드시 필요하며

➡️return <Child name={userName} age={userAge} />; 부분

실제 자식 컴포넌트의 동작도 부모 컴포넌트 안에서 작동하게 된다.

 

즉 부모 컴포넌트는 붕어빵 장사꾼이고 자식 컴포넌트는 붕어빵의 틀이다. 프롭스는 부모가 제공하는 데이터, 즉 재료다.

  • 부모 = 붕어빵 장사꾼 (데이터와 실행의 주체)
  • 자식 = 붕어빵 틀 (구조와 로직 제공)
  • Props = 재료 (부모가 제공하는 동적 데이터)

붕어빵 장사꾼이 틀(자식 컴포넌트)에 재료(부모의 데이터)를 넣고 붕어빵을 만드는 용도로 틀을 이용하는 것이다.

틀이 재료를 바꾸거나 단독으로 재료를 가져다가 붕어빵을 만들거나 할 수 없지 않은 것처럼

자식 컴포넌트가 원본 데이터 자체를 변경하는 등의 접근은 불가능하다.

 

함수 내 함수 실행이라고 생각하면 된다. 실제로 부모 컴포넌트는 외부 함수이고 자식 컴포넌트는 내부에서 호출된 함수이다.

➡️A와 B 함수가 있을 경우 A함수 내에서 B 함수를 호출하는 경우

 

때문에 자식 컴포넌트는 독립적으로 실행할 수도 있고 이 경우 내부 로직도 수행하지만 부모의 프롭스는 전혀 받지 못해서 해당 데이터를 undefined로 처리하게 된다.

실제 동작 예시

1️⃣export default Parent;

main.jsx에서 Parent 컴포넌트 실행 → 내부의 Child 호출도 실행 → 매개 변수(props) 전달 → 온전한 데이터 출력

// main.jsx
import { createRoot } from 'react-dom/client'
import Components from './App.jsx';

createRoot(document.getElementById('root')).render(
  <Components />
)
// App.jsx
function Parent() {
  const userName = "John"; // userName은 "John"
  const userAge = 25;
  // Child에게 name="John", age=25 전달
  return <Child name={userName} age={userAge} />;
}

function Child(props) {
  // 어떤 props를 받았는지 전체 객체를 확인
  // props.name 값을 명시적으로 확인
  const name = props.name;
  return <div>{name}, {props.age}세</div>;
}

export default Parent;

Parent 컴포넌트가 실행되어 정보 전달이 제대로 이루어 지기 때문에 Child 컴포넌트도 정상적으로 동작한다.

 

2️⃣export default Child;

main.jsx에서 Child  컴포넌트 실행 → 매개 변수 props를 전달받는 과정이 없음 빈 데이터 출력

export default Child;

Child 컴포넌트가 단독으로 실행되어 Parent 컴포넌트의 정보가 전달이 안되기 때문에 컴포넌트가 불완전하게 동작한다.

실제로 return <div>{name}, {props.age}세</div>; 문은 실행이 되지만 {name}과 {props.age}는 부모로부터 정보를 받지 못했기 때문에 undeifed 값을 반영하여 실제로는 아무런 데이터도 입력되지 않고 <div>, 세</div> 만 남게 된다.

3. Props의 전달 방법

1. 기본적인 Props 전달

function Parent() {
  // 변수 선언
  const userName = "John";
  const userAge = 25;

  // 선언된 변수를 Props로 전달
  return <Child name={userName} age={userAge} />; 
}

function Child(props) {
  // props 객체에서 name과 age 접근
  return <div>{props.name}, {props.age}세</div>;
}

✅ function Parent() 구조 분석:

  • <Child>: 자식 컴포넌트 호출 - react에서 컴포넌트를 호출할때는 반드시 < />(태그 필요/ 프래그먼트 아님)
  •  name={userName}:
    • name은 Props의 키(속성명)
    • {userName}은 실제 전달할 값
    • 중괄호 {} 사용해 변수/표현식 직접 전달
  • age={userAge}:
    • age는 Props의 키
    • {userAge}는 전달할 값
    • 숫자도 중괄호로 감싸서 전달

✅ function Child(props) 구조 분석:

props는 매개변수 이름으로 관습적으로 사용되지만 어떤 이름을 해도 상관은 없다.

  • {props.name}, {props.age}
    • props: 매개변수로 받은 객체
    • .name: props 객체의 name 속성 호출
    • .age: props 객체의 age 속성 호출

📌 곧바로 return에서 사용하는 방식이 아니라 변수로 선언해서 값을 이용해도 상관없다.

function Child(props) {
  const a = props.name;
  const b = props.age;
  
  // 반드시 return으로 렌더링
  return <div>{a}, {b}세</div>;
}

 

📌 또한 매개변수를 받아서 사용하지 않거나 일부만 사용하는 것도 가능하다.

 

1️⃣변수는 모두 선언하지만 일부만 사용할 경우

  • b만 사용하기 때문에 사실상 a 변수는 필요없음
//일부만 사용
function Child(props) {
  const a = props.name;
  const b = props.age;
      return <div>{b}세</div>; //b만 사용
}

 

2️⃣아예 값을 사용하지 않는 경우

  • 이 경우 굳이 변수를 선언할 이유도 없다
//값을 사용하지 않음
function Child(props) {
	return <div></div>; //값을 사용하지 않음
}

 

3️⃣매개변수로만 받고 아예 아무런 활용을 하지 않는 경우

//매개변수 활용 자체를 안함
function Child(props) {
      return <div></div>; //값을 사용하지 않음
}

2. 구조 분해 할당 Props 전달

function Parent() {
  // 동일하게 Props 전달 (변경 없음)
  return <Child name="John" age={25} />; 
}

function Child({ name, age }) {
  // Props를 직접 구조 분해로 받음
  // 개별 변수로 바로 사용 가능
  // 이경우 매개변수의 키를 그대로 변수명으로 사용
  return <div>{name}, {age}세</div>;
}

📌 매개변수 자체를 구조분해 할당으로 받아서 사용할 수도 있다.

  • 이 경우 구조 분해 할당에서 Props로 받은 변수는 기본적으로 const로 선언된 것과 동일하다.
    • Props는 읽기 전용이어서 불변성을 유지하기 위해서 재할당이 안되는 const로 취급하는 것이다.
  • 변수 명은 구조분해 할당 당시의 속성 값을 그대로 사용하게 된다.

3. 스프레드 연산자로 Props 전달

function Parent() {
  const userInfo = { name: 'John', age: 30, email: 'john@example.com' };
  	// 모든 속성을 한 번에 전달
    return <Child {...userInfo} />;
}

function Child(props) { // 전체 props 객체로 받음
  return <div>{props.name}, {props.age}, {props.email}</div>;
}

📌스프레드 연산자(전개 구문)으로 전달할 경우엔 배열의 경우 배열이나 객체의 모든 속성을 전달하게 된다.

 

1️⃣주로 긴 배열이나 객체에서 모든 속성을 한번에 전달하는 경우에 사용한다.

function Parent() {
  const userInfo = { name: 'John', age: 30, email: 'john@example.com' };
  	// 모든 속성을 한 번에 전달
    return <Child {...userInfo} />;
}

function Child({ name, age, email }) {
  return <div>{name}, {age}, {email}</div>;
}
  • 이 경우엔 매개변수를 받을 때 구조분해할당을 사용했다.
    • function Child({ name, age, email })의 { name, age, email } 부분
  • 사용하지 않았다면 맨 처음 예시처럼 매개변수.속성 식으로 접근해야한다.
    • 예시 : Props.name

➡️때문에 스프레드 연산자(전개 구문)을 사용할 경우 다음과 같은 장점이 있다.

  1. 긴 배열이나 객체의 모든 속성을 간편하게 전달 가능
  2. 코드를 더 간결하고 읽기 쉽게 만듦
  3. 개별 속성에 직접 접근 가능
  4. 불필요한 중첩 접근(.)을 줄여줌

4. 객체로 Props 묶어서 전달

스프레드를 사용하지 않고 전체 객체를 전달할 수도 있다.

return <Child user={userInfo} />; 식으로 전달하는 것인데 user라는 props 이름으로 userInfo 객체를 전달한다는 뜻이다.

이 경우 user라는 객체로 userInfo가 넘어간 것이기 때문에 props.user.name 식으로 매개변수.객체명.속성 식으로 접근해야한다.

function Parent() {
  const userInfo = { name: 'John', age: 30, email: 'john@example.com' };
  return <Child user={userInfo} />;
}
function Child(props) {
  return <div>{props.user.name}, {props.user.age}, {props.user.email}</div>;
}

하지만 당연히 이건 너무 불편하기 때문에 구조분해할당을 사용하는 것이 좋다.

function Parent() {
  const userInfo = { name: 'John', age: 30, email: 'john@example.com' };
  return <Child user={userInfo} />;
}
function Child({user}) {
  return <div>{user.name}, {user.age}, {user.email}</div>;
}

이 경우 받아오는 매개변수의 이름을 부모 컴포넌트에서 설정한 이름과 동일하게 설정해야한다. 그렇지 않으면 오류가 난다.

⛔return <Child user={userInfo} />;의 user 부분

⛔구조 분해 할당의 경우에는 props 객체의 키 이름과 일치해야 하기 때문이다.

 

실제로는 아래와 같은 형태로 데이터를 받아오는 것이기 때문에 애초에 매개 변수로 받아오는 즉시 구조분해할당을 해주는 것이 좋다.

function Parent() {
  const userInfo = { name: 'John', age: 30, email: 'john@example.com' };
  return <Child user={userInfo} />;
}
function Child(props) {
  return <div>{props.user.name}, {props.user.age}, {props.user.email}</div>;
}

구조 분해 할당을 할 경우 객체의 깊이가 줄어들어 props.user.name 형태를 user.name 형태로 접근할 수 있도록 바꿔준다.

  • 구조분해 할당 전 : 매개변수.객체.속성
  • 구조분해 할당 후 : 새로 생성된 변수.속성
function Parent() {
  const userInfo = { name: 'John', age: 30, email: 'john@example.com' };
  return <Child user={userInfo} />;
}
function Child({user}) {
  return <div>{user.name}, {user.age}, {user.email}</div>;
}

5. 함수형 Props 전달

function Parent() {
  // 클릭 이벤트 핸들러 함수 정의
  const handleClick = () => console.log("clicked"); 
  
  // 함수를 onClick Props로 전달
  return <Child onClick={handleClick} />;
}

function Child({ onClick }) {
  // 전달받은 함수를 버튼 클릭 이벤트에 연결
  return <button onClick={onClick}>클릭</button>;
}

📌 () => console.log("clicked") 라는 함수 자체를 전달하게 된다.

즉 자식 요소는 props.onClick의 형태로 함수를 참조하게 된다.

function Child(props) {
  return <button onClick={props.onClick}>클릭</button>;
}

실제로는 이런 형태이며 구조분해 할당을 통해서 {onClick} 형태로 매개변수를 사용하게 된다.

6. 조건부 Props 전달

// 부모 컴포넌트
function Parent() {
  // 관리자 여부 결정 (현재 true)
  const isAdmin = true;

  return (
    <Child
      name="John"
      // isAdmin 값에 따라 role Prop의 값을 "admin" 또는 undefined로 설정
      role={isAdmin ? "admin" : undefined}
      //role={isAdmin ? "admin" : null}
    />
  );
}

// 자식 컴포넌트
function Child(props) {
  // 전달받은 props 객체를 사용하여 이름과 역할을 출력
  // props = { name: "John", role: "admin" } (isAdmin이 true일 경우)
  // props = { name: "John", role: undefined } (isAdmin이 false일 경우)
  return <div>{props.name} {props.role}</div>;
}

예를 들어 어떤값이 true인지 false인지에 따라서 값을 다르게 하고 싶을 경우 사용한다.

대표적으로 3개가 있다.

  1. 삼항 연산자
  2. 논리 AND 연산자
  3. 객체 스프레드 연산자

1️⃣삼항 연산자

삼항 연산자를 사용해서 true나 fasle 값에 해당하는 값을 반환하고 role 속성의 값으로 할당한다.

  • ⭐삼항 연산자의 구조
    • 조건식 ? 참일경우 반환되는 값 : 거짓일경우 반환되는 값;
    • condition ? exprIfTrue : exprIfFalse;
    • 조건식, 참일 경우 반환되는 값, 거짓일 경우 반환되는 값 모두 '값'이기 때문에 표현식(expression)이다.
    • 조건식의 경우 true나 false로 반환된다.
function Parent() {
  const isAdmin = true;
  const isAdmin2 = false;
  return (
    <Child
      name="John"
      name2="Sol"
      role={isAdmin ? "true" : "false"} 
      role2={isAdmin2 ? "yes" : "no"} 
    />
  );
}

// 자식 컴포넌트
function Child(props) {
  // 전달받은 props 객체를 사용하여 이름과 역할을 출력
  return <>
  <div>{props.name} {props.role}</div>
  <div>{props.name2} {props.role2}</div>
  </>
  ;
}

 

role={isAdmin ? "true" : "false"} : isAdmin의 값이 true여서 "true" 값을 반환한다.

❌role2={isAdmin2 ? "yes" : "no"} : isAdmin2의 값이 false여서 "no"를 반환한다.

2️⃣AND 연산자 (&&)

논리곱 연산자이다. 두 조건이 모두 true여야 값을 반환한다.

// App.jsx
// 부모 컴포넌트
function Parent() {
  const isAdmin = true;
  const isAdmin2 = false;
  return (
    <Child
      name="John"
      name2="Sol"
      role={isAdmin && "true"}
      role2={isAdmin2 && "false"}
    />
  );
}

// 자식 컴포넌트
function Child(props) {
  // 전달받은 props 객체를 사용하여 이름과 역할을 출력
  return <>
  <div>{props.name} {props.role}</div>
  <div>{props.name2} {props.role2}</div>
  </>
  ;
}

export default Parent;

✅role={isAdmin && "true"}

  • isAdmin이 true이기 때문에 true를 반환한다.
  • "true"도 true이기 떄문에 true를 반환한다.
    • falsy 값들: false, 0, "" (빈 문자열), null, undefined, NaN
    • Truthy 값들 (위 Falsy 값 제외한 모든 값): "a" (비어있지 않은 문자열), 숫자 (0 제외), 배열 [], 객체 {}, true
  • 때문에 true && true로 값을 반환하게 되는데 논리곱의 경우 마지막으로 평가한 값을 반환하기 때문에 문자열인 "true" 부분만 남게 된다.
  • 만약에 "true" && isAdmin 형태였다면 true를 반환했을 것이다. 그럴 경우 true는 렌더링되지 않는 값이기 때문에 John만 렌더링 됐을 것이다.

❌role2={isAdmin2 && "false"}

  • isAdmin2이 false이기 때문에 false를 반환한다.
  • "false"는 true를 반환하지만 논리곱상 하나라도 false일 경우 false를 반환하기 때문에 결과는 false가 된다.
  • false는 true와 마찬가지로 렌더링 되지 않는 값이기 때문에 Sol만 반환된다.

3️⃣객체 스프레드 연산자

// App.jsx
// 부모 컴포넌트
function Parent() {
  const isAdmin = true;
  const isAdmin2 = false;
  return (
    <Child
      {...( isAdmin && { role: 'admin' })}
      {...( isAdmin2 && { role2: 'user' })}
      name="John"
      name2="Sol"
    />
  );
}

// 자식 컴포넌트
function Child(props) {
  // 전달받은 props 객체를 사용하여 이름과 역할을 출력
  return <>
  <div>{props.name} {props.role}</div>
  <div>{props.name2} {props.role2}</div>
  </>
  ;
}

export default Parent;

✅ {...( isAdmin && { role: 'admin' })}

  • isAdmin이 true이고 role: 'admin' 이 true이기 떄문에 role = "admin"이 반환된다.

❌{...( isAdmin2 && { role2: 'user' })}

  • isAdmin2가 false이기 때문에 논리곱 연산자의 특성상 바로 false를 반환한다.

7. 자식 컴포넌트(children) Props

function Parent() {
  return (
    // Child 컴포넌트 사이에 요소/컴포넌트 삽입
    <Child>
      <p>이것은 children Props입니다</p>
    </Child>
  );
}

function Child({ children }) {
  // children Props를 컴포넌트 내부에서 렌더링
  return <div>{children}</div>;
}

✅실제로 props로 지정해서 전달하는 이름이 없는데도 불구하고 오직 { children }만을 통해서 props를 받을 수 있다.

이는 children이 컴포넌트 태그 사이에 포함된 내용(텍스트, 요소, 다른 컴포넌트 등)을 받는 특별한 Props이기 때문이다. <>여기에 있는 내용</> 에서 "여기에 있는 내용"이 props.children으로 전달된다.

때문에 매개변수를 props로 전달한다면 props.children로 값을 호출할 수 있고 children으로 전달한다면 {children} 구조분해 할당으로 바로 {children}을 값으로 쓸수 있게 된다.

function Child(props) {
  // children Props를 컴포넌트 내부에서 렌더링
  return <div>{props.children}</div>;
}

function Child(children) {
  // children Props를 컴포넌트 내부에서 렌더링
  return <div>{children}</div>;
}

8. 타입스크립트와 함께 사용하는 Props

// Props의 타입을 인터페이스로 정의
interface UserProps {
  name: string;   // name은 문자열
  age: number;   // age는 숫자
}

function Child({ name, age }: UserProps) {
  // 타입이 지정된 Props 사용
  // 타입과 다른 값 전달 시 컴파일 에러 발생
  return <div>{name}, {age}세</div>;
}

9. 기본값이 있는 Props

function Parent() {
  return (
    <Child
      name="jo"
      age="10"
    />
  );
}

function Child({ 
  name = "Guest",    
  age = 0  
}) {
  // Props 미전달 시 기본값 사용
  return <div>{name}, {age}세</div>;
}

export default Parent;

전달되는 값이 있다면 제대로 나오지만

function Parent() {
  return (
    <Child
    />
  );
}

function Child({ 
  name = "Guest",    
  age = 0  
}) {
  // Props 미전달 시 기본값 사용
  return <div>{name}, {age}세</div>;
}

export default Parent;

전달되는 값을 없애면 기본값으로 출력된다.