[TIL] React 알아가기 (3) : props / state

invisibleVoice·2025년 1월 21일

리액트

목록 보기
4/14
post-thumbnail

props

props는 properties의 준말로, 컴포넌트끼리 데이터를 마치 속성을 사용하듯이 주고받을 수 있다.

정확히는, 부모 컴포넌트가 자식 컴포넌트에게 물려주는 데이터다. 이를 단방향 데이터 바인딩이라 한다. 상태 관리 라이브러리 같은 외부 라이브러리를 사용하지 않는 이상 자식 컴포넌트에서 부모 컴포넌트로 데이터를 보낼 방법은 없다.

전달 받은 props는 반드시 읽기 전용이어야 하기에 변경하지 않는다. 변경할 수는 있지만, 리액트는 상태를 직접 변경하는 것을 권장하지 않는다. 후술하겠지만, 리액트는 상태의 불변성을 깨뜨리지 않는 것을 지향한다.

props 사용하기

전달 받은 props는 객체 형태로 전달된다!

import React from "react";

function App() {
  return <Father />
}

function Father() {
  const lastName = '신';
  const fullName = '신형만';
  return <Child lastName={lastName} fullName={fullName} />
  // props로 fullName, lastName을 전달!
}

function Child(p) {
  const name = '짱구'
  console.log(p) 	// {lastName: '신', fullName: '신형만'}
  return <div>{p.fullName}의 아들 {p.lastName}{name}</div>
}

export default App;

prop drilling

props는 부모에서 자식으로만 보낼 수 있다. 그렇다면 저 위의 조상님이 저 아래 자손에게 데이터를 보내려면 자식의 자식을 거쳐 보내는 수 밖에 없다. 이렇게 자식들을 거쳐거쳐 데이터를 보내는 것을 prop drilling이라 한다.

아래 예시를 보자. 이 얼마나 무시무시하고 끔찍한 코드인가? 우리는 이런 패턴을 피하며 코드를 작성해야한다. ‘Redux’와 같은 데이터 상태관리 툴을 이용한다면 이런 상황을 더욱 잘 피할 수 있다.

function App() {
  return (
    <div className="App">
      <FirstComponent content="Who needs me?" />
    </div>
  );
}

function FirstComponent({ content }) {
  return (
    <div>
      <h3>I don't need you</h3>
      <SecondComponent content={content} />
    </div>
  );
}

function SecondComponent({ content }) {
  return (
    <div>
      <h3>I don't need you</h3>
      <ThirdComponent content={content} />
    </div>
  );
}

function ThirdComponent({ content }) {
  return (
    <div>
      <h3>I don't need you</h3>
      <ComponentNeedingProps content={content} />
    </div>
  );
}

function ComponentNeedingProps({ content }) {
  return 
  	<div>
      <h3>I need you!!!!!!!</h3>
      <p>{content}</p>
    </div>
}

props children

속성을 명시적으로 사용하여 props를 전달하는 방법이 있는가 하면, 명시적인 속성명 없이 반드시 children이라는 속성명의 값으로 데이터를 넘겨주는 방법도 존재한다.

function User(props) {
  console.log(props.children)	// { children: 안녕하세요 }
  return <div>{props.children}</div>
}

function App() {
  return <User>안녕하세요</User>	// <User/>가 아니라 열림 닫힘을 따로 표시
}
export default App;

위 코드처럼 자식 태그를 열고 닫는 사이에 어떤 값을 넣으면, 이 값은 자동으로 children이라는 속성의 값으로 들어가게 된다. 그래서 props를 콘솔에 출력해보면 children을 속성으로 가진 객체가 나온다.

props children은 언제 사용하면 좋을까?

보통 Layout을 구성할 때 사용한다. 아래는 조금 난잡한 예시이긴 한데, 중요한 것은 Layout컴포넌트가 쓰여지는 모든 곳에서 <Layout> ... </Layout> 안에 있는 정보를 받아서 가져올 수 있다는 것이다.

About, App에서 Layout을 사용하는데, 각각 다른 데이터를 Layout으로 props를 보내는 것을 확인 할 수 있다.

// App.jsx
import React from "react";
import About from "./About";
import Layout from "./components/Layout";

function App() {
  return (
    <>
      <header>
        <Layout>
          여긴 App의 컨텐츠가 들어갑니다.
        </Layout>
      </header>
      <main>
        <About />
      </main>
    </>
  );
}

export default App;
// About.jsx
import React from "react";
import Layout from "./components/Layout.jsx";

function About() {
    return (
        <Layout>
            여긴 About의 컨텐츠가 들어갑니다.
        </Layout>
    );
}
export default About;
// ./components/Layout.jsx
function Layout(props) {
    console.log(props)
    const children = props.children
    return (
        <div>
            {children}
        </div>
    )
}

export default Layout

props 추출

구조분해할당을 사용하면 props에 정보가 아무리 많더라도 필요한 정보만 뽑아서 사용가능하다.

// props에 title이란 속성과 이외에도 많은 속성이 있을 경우
function Todo(props){
	return <div>{props.title}</div>
}
// 구조분해할당을 활용하면? 필요한 속성만 빼내 쓸 수 있다.
function Todo({ title }){
	return <div>{title}</div>
}

state

React에서 JS로 선언된 여러 값들을 변경하기 위해서는 특별한 방법이 필요한데, 바로 state를 이용하는 것이다.
state는 컴포넌트 내부에서 바뀔 수 있는 값을 의미한다. 만약 const name = "Bob"이라는 정보를 만들어는데, 이 name이 바뀌어야만 하는 정보일 경우, state로 생성해야한다.

state 생성

state를 생성하기 위해서는 useState라는 Hooks의 한 종류를 사용해야한다. Hooks는 기존의 함수형 컴포넌트에서 할 수 없었던 다양한 작업을 할 수 있게 해주는 일종의 함수라고 생각하면 된다.

useState상태상태를 변경할 수 있는 함수가 담긴 배열을 반환한다. 인자로는 상태의 초기값을 설정할 수 있다.

// useState는 반드시 import!
import { useState } from "react"

// 상태 name과 이 name을 변경할 수 있는 함수 setName
// name의 초기값은 Bob. 없어도 에러는 일어나지 않는다.
const [name, setName] = useState("Bob")

state 변경

위에서 말한대로, 상태를 변경할 수 있는 함수를 이용하여 state를 변경할 수 있다. 이 함수도 props로 하위 컴포넌트로 전달 가능하다!

🪄setState는 props로 전달 가능하지만, 권장하지 않는다!

setStateprops로 자식에게 직접 내려주는 건 부모의 상태관리 책임이 흐려진다는 느낌이 있다. 그래서 setState를 실행시키는 handler함수를 전달하는 것이 보편적으로 좋은 방법이다.

// App.jsx
import { useState } from "react";
import Child from "./Child"

const App = () => {
  const [value, setValue] = useState("");

  return (
    <Child val={value} setVal={setValue}/>
  );
};

export default App;
//Child.jsx
const Child = ({ val, setVal }) => {
  const onChangeHandler = (event) => {
    const inputValue = event.target.value;
    setVal(inputValue);
    console.log(inputValue);
  };

	console.log(val)	// inputValue와 상태 value(=val)이 똑같이 나온다

  return (
    <div>
      <input type="text" onChange={onChangeHandler} value={val} />
    </div>
  );
};

export default Child;

제어 컴포넌트 (Controlled Component)

여기서 중요하게 볼 부분은 input태그의 value속성이다. 이 속성에 상태 value(=val)를 넣은 것을 확인할 수 있다. React에서는 이렇게 inputvalue속성과 onCahnge이벤트를 함께 사용하여 입력 필드의 상태를 관리하는 것이 일반적인데, 이런 패턴을 제어 컴포넌트 라고 한다.

제어 컴포넌트에서는 React의 상태가 'source of truth' 역할을 하며, 이를 통해 입력 필드의 현재 값이 항상 React 컴포넌트의 상태와 동기화됨을 보장한다. 참고로 'source of truth'란 어떤 정보의 상태를 정확하게 최신 버전으로 유지하는 주된 위치를 말한다. 위 예제의 경우, inputvalue속성이 React의 상태와 엮여 있으므로 그 역할을 한다.

정리하자면, React(=state)에 의해 값이 제어되는 컴포넌트가 제어 컴포넌트고, 이는 데이터의 일관성과 정확성을 유지하기 위해 꼭 필요하다.

비제어 컴포넌트

value속성이 없더라도 동작은 잘 된다! 다만 이는 비제어 컴포넌트와 유사한 동작을 하게 된다. input요소는 자체적으로 내부 상태를 유지하게 되고, React의 상태는 input입력값과 동기화 되지 않는다.

비제어 컴포넌트는 특정 상황에선 유용할 수 있다. 그러나 일반적으로 form데이터를 명확하게 제어할 때는 제어 컴포넌트 방식이 더 안전하고 관리하기가 용이해진다.


불변성 Immutablility

기본형 데이터의 특징인 불변성이다. 반대로 참조형 데이터는 불변성이 없다. 참조형 데이터는 메서드에 따라 원본 데이터가 보장되지 않는다.

불변성을 깨뜨리는 일은 코드를 예측 불가능하게 만든다. 데이터 구조를 변경했을 때 프로그램의 다른 부분에서 이를 사용하고 있었다면 예상치 못한 결과가 발생할 수 있다. 또한 버그가 발생했을 경우 원본 데이터가 여러 곳에서 변경될 수 있는 상황이면 어떤 곳에서 버그를 일으켰는지 추적하기가 어려워진다.

따라서 최대한 불변성을 유지하도록 코드를 작성해야한다. 새로운 객체를 생성하여 기본 객체를 놔두는 등..

리액트에서의 불변성

리액트에서는 화면을 리렌더링 할지 말지 결정할 때 state의 변화를 확인한다.
리액트에서는 화면을 리렌더링 할지 말지 결정할 때 state의 변화를 확인한다!!

너무 중요한 문장이라 두 번 적었다. state의 변화리렌더링이란 단어에 집중해보자.

리액트는 state가 변하는지 감시한다. 변했으면 리렌더링을 하고, 변하지 않았으면 리렌더링을 하지 않는다. 이때, 리액트는 state가 변경되었는지 확인하기 위해 state의 메모리 주소를 본다.
그래서 불변성을 유지하는 것이 리액트에서 중요하다. 참조형 데이터를 수정할 때 불변성을 지키지 않고 원본 데이터를 직접 변경하면 값은 변하지만, 메모리주소는 변함이 없다. 그래서 리액트는 state가 변했다고 인지하지 못하게 되어 의도한 리렌더링은 일어나지 않게 된다.

오해하면 안 되는 것은, 리액트가 인식을 못해 리렌더링이 일어나지 않을 뿐, 상태는 실제로 바뀐다는 점이다.

import React, { useState } from "react";

function App() {
  const [dogs, setDogs] = useState(["말티즈"]);

  function onClickHandler() {
    // setDogs([...dogs, "시고르자브종"] 	// 불변성 유지하면서 클릭마다 리렌더링 된다.
    dogs.push("시고르자브종")
    console.log(dogs);		// 클릭할때마다 dogs에 시고르자브종이 추가된다!
  }							// 그러나 화면에 렌더링 되지 않는다. 인식을 못하니까!
  
  return (
    <div>
      <button onClick={onClickHandler}>버튼</button>
    </div>
  );
}

export default App;

그래서 값이 바뀐 dogs를 새로운 배열에 넣어(주소값을 바꿔야 리액트가 변화를 인식하니까) setDog에 집어넣으면 그제서야 렌더링이 될 것이다. 밑의 코드처럼 말이다!

function App() {
  const [dogs, setDogs] = useState(["말티즈"]);
  function onClickHandler() {
    dogs.push("시고르자브종")
    console.log(dogs);
  }
  function onUpdate() {
    setDogs([...dogs])
  }
  
  return (
    <div>
      <button onClick={onClickHandler}>버튼</button>
      <button onClick={onUpdate}>결과</button>
      {dogs}
    </div>
  );
}

Q. 이런식으로 리렌더링 할 수 있다면 꼭 불변성을 지키지 않아도 되겠네요!
A. 그렇지 않다. 불변성은 꼭 지키는 것이 좋다.
특히 UI와 연결된 데이터라면 리렌더링을 통해 React의 상태 관리 흐름에 맞춰 작업하는 것이 더 안전하고 유지보수에 용이하다. 불변성은 리액트의 동작 원칙과도 관련이 있어 성능에 영향이 생길 수 있다.
불변성을 지키지 않으면 React의 성능 최적화와 디버깅 효율성을 잃을 수 있으므로, 불변성을 유지하는 것이 React를 사용할 때 더 도움이 된다.

위와 같이 의도적으로 리렌더링을 피하고 싶은 경우에도 props와 state의 설계에 따라 불변성을 지키면서 피할 수 있는 방법이 존재한다. React.memo, useRef, 상태 분리, useCallbackuseMemo를 활용하여 리렌더링 범위를 최소화할 수 있다.

profile
내배캠 React 9기 수료 후 Wecommit 풀스택 개발자로 근무중

0개의 댓글