React State 기초, 고급 스

김명원·2024년 12월 18일
0

learnReact

목록 보기
3/26

🎯 State: 컴포넌트의 기억 저장소


📌 State란?

React 컴포넌트는 UI와 상호작용하면서 상태가 변해야 하는 경우가 많습니다.
예를 들어 버튼 클릭 시 값이 변해야 하지만 화면에 즉시 반영되지 않는 문제가 발생할 수 있습니다.

state 개념 이미지


상태가 관리되지 않을 때의 문제

버튼 클릭 시 counter 값은 증가하지만 화면에는 반영되지 않습니다.

let counter = 1;

function Counter() {
  const handleCounter = () => {
    counter++;
    console.log(counter);
  };

  return <button onClick={handleCounter}>Counter: {counter}</button>;
}

export default Counter;

🔍 문제점

  • 값은 증가하지만 화면에는 반영되지 않음.
  • console.log를 통해서는 값이 증가하는 것을 확인할 수 있음.

console 확인


State의 필요성

React 컴포넌트는 현재 입력값, 이미지, 장바구니와 같은 데이터를 '기억'해야 합니다.
React는 이러한 컴포넌트별 메모리State라고 부릅니다.

상태를 생성하기 위해서는 useState를 사용할 수 있습니다.
useState컴포넌트에 state 변수를 추가할 수 있는 React Hook입니다.

const [index, setIndex] = useState(0);

📌 구문 규칙
const [something, setSomething]과 같은 구조로 이름을 지정합니다.
이름은 자유롭게 지을 수 있지만, 규칙을 따르는 것이 가독성을 높입니다.


🎯 State를 사용한 예시

useState로 생성된 메모리는 컴포넌트 인스턴스별로 관리됩니다.

컴포넌트 상태 관리

각각의 Counter가 별도의 상태로 관리되는 것을 확인할 수 있습니다.

인스턴스별 상태


🛠️ State를 활용해 Total 값 구현하기

🔹 Main 컴포넌트

import { useState } from "react";
import Counter from "./Counter";

function Main() {
  const [total, setTotal] = useState(0);

  const handleTotal = () => {
    setTotal(total + 1);
  };

  return (
    <main>
      <h2>total: {total}</h2>
      <Counter onTotal={handleTotal} />
      <br />
      <br />
      <Counter onTotal={handleTotal} />
    </main>
  );
}

export default Main;
  1. useState를 사용해 totalsetTotal을 생성합니다.
  2. 이벤트 핸들러 handleTotal을 통해 total을 업데이트합니다.
  3. Counter 컴포넌트에 onTotal이라는 prop으로 handleTotal을 전달합니다.

🔹 Counter 컴포넌트

import { useState } from "react";

function Counter({ onTotal }) {
  const [counter, setCounter] = useState(0);

  const handleCounter = () => {
    setCounter(counter + 1);
    onTotal();
  };

  return <button onClick={handleCounter}>Counter: {counter}</button>;
}

export default Counter;
  1. useState를 사용해 countersetCounter를 생성합니다.
  2. 버튼 클릭 시 counter 값을 업데이트하고 onTotal을 실행시킵니다.

📊 결과 화면

Total 값 구현

  • 각 버튼의 상태는 개별적으로 관리됩니다.
  • 동시에 Main 컴포넌트에서 total 값을 통해 전체 상태를 관리할 수 있습니다.

🚀 정리

  1. useState를 사용하면 React 컴포넌트의 상태를 생성하고 관리할 수 있습니다.
  2. useState의 첫 번째 인자는 초기값, 두 번째 인자는 업데이트 함수입니다.
  3. 상태는 컴포넌트 인스턴스별로 독립적으로 관리됩니다.
  4. 부모-자식 컴포넌트를 통해 상태를 효율적으로 공유하고 업데이트할 수 있습니다.

React의 State를 통해 UI와 상호작용하는 데이터 관리를 깔끔하게 해결해보세요!

🎯 렌더링 단계 이해하기

우선 렌더링은 화면에 표시할 UI를 그리는 것을 의미합니다.
리액트에서는 화면에 표시할 UI를 만들기 위해서는 컴포넌트 함수를 호출해야 합니다.
다시 말해서 React에서 렌더링이란 컴포넌트를 호출하는 것을 의미합니다.

React 공식 문서에서는 3가지의 단계로 정의합니다:
1. 렌더링 트리거
2. 컴포넌트 렌더링
3. DOM에 커밋


🔄 렌더링 트리거

리액트 컴포넌트가 렌더링을 시작하게 만드는 것을 의미합니다.
여기서 초기 렌더링리렌더링으로 나뉩니다.

🛠️ 초기 렌더링

초기 렌더링에서는 render() 메서드를 호출하여 작업을 실행할 수 있습니다.

import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import AppCounter from "./AppCounter.jsx";

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <AppCounter />
  </React.StrictMode>
);

여기서 AppCounter라는 루트 컴포넌트를 렌더링하는 걸 확인할 수 있습니다.
이러한 render() 함수가 초기 렌더링이라고 할 수 있습니다.


🔄 리렌더링

컴포넌트가 초기 렌더링된 후, 다양한 조건에 따라 리렌더링이 발생합니다.
리렌더링 트리거의 예시:

import { useState } from "react";
import Counter from "./Counter";

function Main() {
  const [total, setTotal] = useState(0);

  const handleTotal = () => {
    setTotal(total + 1);
  };

  return (
    <main>
      <h2>total: {total}</h2>
      <Counter onTotal={handleTotal} />
      <hr />
      <Counter onTotal={handleTotal} />
      <hr />
      <Counter />
    </main>
  );
}

export default Main;

Counter 컴포넌트에서는:

import { useState } from "react";

function Counter({ onTotal }) {
  const [counter, setCounter] = useState(0);

  const handleCounter = () => {
    setCounter(counter + 1);
    if (onTotal) {
      onTotal();
    }
  };

  return <button onClick={handleCounter}>Counter: {counter}</button>;
}

export default Counter;

리렌더링은 컴포넌트의 상태가 변경되면 리렌더링이 발생하고, 해당 컴포넌트가 다시 호출됩니다.
Counter 컴포넌트에서 console.log()를 확인하면 상태 변화에 따라 리렌더링이 발생하는 것을 볼 수 있습니다.


📊 리렌더링 확인 방법

리렌더링을 확인하려면 크롬 개발자 도구에서 Components 탭을 열고,
환경설정에서 Highlight updates when components render를 체크해 보세요.

리렌더링이 발생하면 검은색 테두리로 하이라이트가 표시됩니다.
이것은 리렌더링이 발생했음을 나타냅니다.


🧩 컴포넌트 렌더링

렌더링 트리거가 발생하면, 리액트는 해당 컴포넌트를 호출하여 화면에 표시할 내용을 파악합니다.
컴포넌트 렌더링 단계에서는 초기 렌더링 시 DOM 노드를 생성하고,
커밋 단계에서는 생성된 DOM 노드를 화면에 표시합니다.

리렌더링의 경우 변경된 DOM 노드만 계산하여 업데이트합니다.
따라서, DOM 업데이트가 없으면 렌더링만 발생하고 DOM 업데이트는 없을 수 있습니다.


💡 왜 Counter의 값은 0으로 초기화되지 않나요?

import { useState } from "react";

function Counter({ onTotal }) {
  const [counter, setCounter] = useState(0);
  console.log("counter");

  const handleCounter = () => {
    setCounter(counter + 1);
    if (onTotal) {
      onTotal();
    }
  };

  return <button onClick={handleCounter}>Counter: {counter}</button>;
}

export default Counter;

useState()를 사용하여 상태를 기억하고 있기 때문에,
counter 값은 0이 아닌 상태로 유지됩니다.


🚀 리렌더링이 발생하는 조건

  • 초기 렌더링: root.render(<App />); 메서드를 호출하면 렌더링이 발생합니다.
  • 상태(State) 변화: 컴포넌트의 상태가 변경되면 해당 컴포넌트와 하위 컴포넌트가 리렌더링됩니다.
  • 프롭스(Props) 변화: 부모 컴포넌트에서 전달된 프롭스가 변경되면 자식 컴포넌트가 리렌더링됩니다.
  • 부모 컴포넌트 리렌더링: 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 리렌더링됩니다.
  • 컨텍스트(Context) 변화: 컨텍스트 값이 변경되면 해당 값을 사용하는 컴포넌트가 리렌더링됩니다.
  • 강제 업데이트(Force Update): forceUpdate()를 호출하면 해당 컴포넌트가 강제로 리렌더링됩니다.

🔑 중요한 사실!

컴포넌트 렌더링 과정은 크게 세 가지 단계로 나눠집니다:
1. 렌더링 트리거
2. 컴포넌트 렌더링
3. DOM 커밋

리렌더링 과정에서는 변경된 DOM 노드만 업데이트됩니다.
렌더링은 DOM 업데이트를 의미하는 것은 아닙니다!
실제로 DOM 업데이트가 없더라도 컴포넌트에서는 렌더링이 발생할 수 있습니다. 😉


✨ 렌더링의 흐름을 잘 이해하고, React의 효율적인 상태 관리를 통해 최적화된 애플리케이션을 만들 수 있습니다!


🧐 순수한 컴포넌트와 Strict 모드란?

React에서는 모든 컴포넌트를 순수 함수로 간주합니다!
즉, 같은 입력(예: props, state, context)을 넣으면 동일한 JSX 출력이 나와야 한다는 의미입니다.


🎯 Impure Component (비순수 컴포넌트)

먼저 비순수 컴포넌트를 만들어 보겠습니다!

/Main.jsx

import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./AppPure.jsx";

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

/AppPure.jsx

import "./App.css";
import PullUpImpure from "./components/PullUpImpure";

function AppPure(props) {
  return (
    <div>
      <PullUpImpure />
      <PullUpImpure />
      <PullUpImpure />
    </div>
  );
}

export default AppPure;

/PullUpImpure.jsx

let counter = 10;

function PullUpImpure() {
  counter = counter + 1;
  return <p>나는 턱걸이를 {counter}개 했습니다.</p>;
}

export default PullUpImpure;

위 코드를 실행하면 PullUpImpure 컴포넌트가 호출될 때마다 counter 값이 1씩 증가하는 것을 확인할 수 있습니다.
하지만 결과를 보면 동일한 컴포넌트를 사용했음에도 출력값이 다릅니다!

🔎 원인 분석

PullUpImpure.jsx에서 counter 변수가 전역으로 정의되어 있습니다.
그래서 내부에서 전역 변수를 직접 수정하여 결과가 달라지게 되는 것입니다.

👉 이러한 컴포넌트를 "비순수 컴포넌트" 라고 부릅니다!
React에서는 항상 동일한 입력값에는 동일한 출력값을 보장하는 순수 컴포넌트를 작성해야 합니다.


🎯 Pure Component (순수 컴포넌트)

이제 순수 컴포넌트를 작성해 보겠습니다.

/PullUpPure.jsx

function PullUpPure({ counter }) {
  counter = counter + 1;
  return <p>나는 턱걸이를 {counter}개 했습니다.</p>;
}

export default PullUpPure;

/AppPure.jsx 수정

import "./App.css";
import PullUpPure from "./components/PullUpPure";

function AppPure(props) {
  return (
    <div>
      <PullUpPure counter={11} />
      <PullUpPure counter={11} />
      <PullUpPure counter={11} />
    </div>
  );
}

export default AppPure;

실행 결과

동일한 입력값을 전달했을 때, 이제 동일한 출력값을 확인할 수 있습니다!


🛠️ Strict Mode와 Impure Component의 관계

Strict Mode가 활성화된 상태에서 Impure Component를 사용하면 어떤 문제가 발생할까요?

Impure 컴포넌트의 출력 결과

Strict Mode 덕분에 추가적인 렌더링이 발생하여, counter 값이 1씩이 아닌 2씩 증가하는 것을 볼 수 있습니다.

🔎 이유

Strict Mode는 개발 중에 발생할 수 있는 일반적인 버그를 빠르게 찾도록 도와줍니다.
1. 순수하지 않은 렌더링 버그 감지
2. Effect 클린업 누락 감지
3. 더 이상 사용되지 않는 API 탐지


import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./AppPure.jsx";

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

StricMode 덕분에 1씩 증가한게 아니라 2씩 증가하게 된 것입니다.


🛠️ StrictMode로 버그를 찾아보자!

1️⃣ StrictMode란?

React.StrictMode는 개발 중에 애플리케이션에서 발생할 수 있는 잠재적인 문제를 식별하도록 도와주는 도구입니다. 이를 통해 컴포넌트를 더욱 안전하고 순수하게 만들 수 있습니다.


2️⃣ StrictMode로 버그를 찾는 과정

1. AppTodo.jsx 만들기

useState를 사용해 할 일 목록을 정의하고, 이를 propsTodoList 컴포넌트에 전달합니다.

import { useState } from "react";
import "./App.css";
import TodoList from "./components/todo/TodoList";

function AppTodo(props) {
  const [todos, setTodos] = useState([
    { id: 0, label: "HTML&CSS 공부하기" },
    { id: 1, label: "자바스크립트 공부하기" },
  ]);

  return (
    <div>
      <h2>할일 목록</h2>
      <TodoList todos={todos} />
    </div>
  );
}

export default AppTodo;

2. TodoList.jsx 만들기

TodoList에서 todos를 받아 목록에 새로운 항목을 추가하고, map 함수로 렌더링합니다.

function TodoList({ todos = [] }) {
  const items = todos;
  items.push({ id: 2, label: "포트폴리오 사이트 만들기" });

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.label}</li>
      ))}
    </ul>
  );
}

export default TodoList;

🖼️ 렌더링 결과
할일 목록


3. StrictMode 적용하기

main.jsx에서 StrictMode를 활성화합니다.

import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./AppTodo.jsx";

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

🖼️ 결과 확인
StrictMode 적용


3️⃣ 순수하지 않은 컴포넌트의 문제

동일한 props를 두 번 전달한 경우

아래와 같이 동일한 todos 배열을 전달하면 예기치 않은 결과가 발생합니다.

import { useState } from "react";
import "./App.css";
import TodoList from "./components/todo/TodoList";

function AppTodo(props) {
  const [todos, setTodos] = useState([
    { id: 0, label: "HTML&CSS 공부하기" },
    { id: 1, label: "자바스크립트 공부하기" },
  ]);

  return (
    <div>
      <h2>할일 목록</h2>
      <TodoList todos={todos} />
      <TodoList todos={todos} />
    </div>
  );
}

export default AppTodo;

🖼️ 출력 결과
문제 발생


문제 원인

TodoList에서 props로 받은 배열에 직접 push를 사용해 수정한 것이 문제입니다. React의 상태 및 props는 항상 읽기 전용으로 취급해야 합니다!

const items = todos; // todos 배열을 직접 참조
items.push({ id: 2, label: "포트폴리오 사이트 만들기" });

이 코드로 인해 첫 번째 TodoList가 렌더링될 때 배열이 수정되고, 두 번째 TodoList는 이미 수정된 배열을 받게 됩니다.


해결 방법: 깊은 복사 사용

push 대신 새로운 배열을 만들어 문제를 해결합니다.

const items = [...todos]; // 깊은 복사
items.push({ id: 2, label: "포트폴리오 사이트 만들기" });

🖼️ 수정 후 결과
문제 해결


4️⃣ StrictMode 활성화 요약

  1. 루트 컴포넌트를 <StrictMode>로 감싸면 애플리케이션 전체에 StrictMode가 적용됩니다.
  2. 일부분만 검사하고 싶다면 원하는 영역만 <StrictMode>로 감쌀 수 있습니다.
  3. 전체 애플리케이션에 적용하는 것이 권장됩니다.

📸 스냅샷처럼 동작하는 State

🧩 State는 무엇인가요?

State 변수는 일반적인 JavaScript 변수처럼 읽고 쓸 수 있을 것 같지만, 사실은 스냅샷처럼 동작합니다.
State 값을 설정한다고 해서 기존 state 변수가 즉시 변경되는 것이 아닙니다. 대신 리렌더링이 발동되고 새로운 값으로 업데이트됩니다.


🔍 Counter.jsx 예제

import { useState } from "react";

function Counter({ onTotal }) {
  const [counter, setCounter] = useState(0);
  console.log("counter");

  const handleCounter = () => {
    setCounter(counter + 1);
    setCounter(counter + 1);
    setCounter(counter + 1);

    if (onTotal) {
      onTotal();
    }
  };

  return <button onClick={handleCounter}>Counter: {counter}</button>;
}

export default Counter;

🤔 왜 setCounter를 3번 호출했는데 값이 1만 증가할까?

결과 화면
결과 화면


📝 이유: 렌더링 시점의 스냅샷

React는 렌더링 시점에서 state 값을 스냅샷으로 찍어 사용합니다.
렌더링이란 React가 컴포넌트를 호출하고 해당 컴포넌트에서 반환된 JSX를 UI로 만드는 과정입니다.

따라서, 이벤트 핸들러에서 호출된 모든 state 변경은 렌더링 당시의 스냅샷 값을 참조합니다.

const handleCounter = () => {
  setCounter(0 + 1);
  setCounter(0 + 1);
  setCounter(0 + 1);
};

위 코드에서 counter는 항상 렌더링 당시의 값(0)을 참조합니다.
즉, 세 번의 호출 모두 setCounter(0 + 1)로 해석되며, 최종적으로 1만 증가합니다.


해결법

	setCounter(counter + 1);
    setCounter(counter + 1);
    setCounter(counter + 1);

이러한 함수를 연속적으로 변경하기 위해서는 useState가 받아올 수 있는 counter의 값을 callback 함수를 사용하면 됩니다.
이러한 callback 함수의 인자로는 이전 state의 값을 받아옵니다.

    setCounter((c) => c + 1); // 0 + 1
    setCounter((c) => c + 1); // 1 + 1
    setCounter((c) => c + 1); // 2 + 1

그러면 한번의 클릭으로 +3이 된 것을 확인할 수 있습니다.

즉, 여러번 state를 변경해야 하는 경우에는 callback함수를 넣어주면 됩니다!

📚 정리

  • React에서의 렌더링은 특정 시점의 스냅샷을 기반으로 작동합니다.
  • State 변경은 즉시 반영되지 않고, 리렌더링을 트리거하여 업데이트된 값을 계산합니다.
  • 이 원리를 이해하면 React 상태 관리의 동작 방식을 더 명확히 알 수 있습니다!

⚙️ Batching & 업데이터 함수

🧩 Batching이란?

React에서 여러 개의 state 업데이트를 하나의 리렌더링으로 묶어서 처리하는 최적화 기법을 Batching이라고 합니다.
이를 통해 불필요한 리렌더링을 줄이고 성능을 최적화할 수 있습니다.


🔍 Batching 예제

```jsx
const handleCounter = () => {
setCounter(counter + 1);
setCounter(counter + 1);
setCounter(counter + 1);
};
```

위 코드에서는 첫 번째 setCounter 호출 이후 바로 리렌더링이 발생하지 않습니다.
React는 모든 setCounter 호출을 모은 뒤, 한 번에 리렌더링을 처리합니다.


📌 Batching의 동작 원리

  1. React는 state 업데이트가 발생할 때마다 즉시 리렌더링하지 않습니다.
  2. 여러 업데이트를 하나로 묶어 리렌더링을 한 번만 수행합니다.
  3. 이를 통해 불필요한 렌더링을 방지하고 애플리케이션의 성능을 최적화합니다.

💡 비유

레스토랑에서 종업원이 손님이 주문한 첫 번째 메뉴를 듣자마자 주방으로 달려가지 않고, 모든 주문을 들은 뒤 주방으로 전달하는 것과 같습니다!


🛠️ 업데이터 함수와 상태 변경

React에서 상태를 업데이트할 때 직접 값을 전달하거나, 업데이터 함수를 사용할 수 있습니다.

예제 코드

```jsx
setCounter(counter + 5); // 현재 값(0) + 5 = 5
setCounter((c) => c + 1); // 이전 값(5) + 1 = 6
setCounter(42); // 최종적으로 42로 덮어씀
```


🔍 결과 분석

  1. setCounter(counter + 5)로 인해 counter는 5가 됩니다.
  2. 이후, setCounter((c) => c + 1)이전 상태 값을 참조해 5 + 1 = 6으로 업데이트합니다.
  3. 마지막으로 setCounter(42)가 호출되어 최종 값은 42로 설정됩니다.

🚀 배운 점

  • Batching은 여러 상태 업데이트를 효율적으로 처리하여 성능을 최적화합니다.
  • 업데이터 함수는 이전 상태 값을 기반으로 안전하게 상태를 변경할 수 있습니다.
  • 상태 변경은 순차적으로 적용되지만, 최종 결과는 마지막 업데이트 값에 의해 결정됩니다.
profile
개발자가 되고 싶은 정치학도생의 기술 블로그

0개의 댓글