React 컴포넌트는 UI와 상호작용하면서 상태가 변해야 하는 경우가 많습니다.
예를 들어 버튼 클릭 시 값이 변해야 하지만 화면에 즉시 반영되지 않는 문제가 발생할 수 있습니다.
버튼 클릭 시 counter
값은 증가하지만 화면에는 반영되지 않습니다.
let counter = 1;
function Counter() {
const handleCounter = () => {
counter++;
console.log(counter);
};
return <button onClick={handleCounter}>Counter: {counter}</button>;
}
export default Counter;
🔍 문제점
console.log
를 통해서는 값이 증가하는 것을 확인할 수 있음.React 컴포넌트는 현재 입력값, 이미지, 장바구니와 같은 데이터를 '기억'해야 합니다.
React는 이러한 컴포넌트별 메모리를 State라고 부릅니다.
상태를 생성하기 위해서는 useState
를 사용할 수 있습니다.
useState
는 컴포넌트에 state 변수를 추가할 수 있는 React Hook입니다.
const [index, setIndex] = useState(0);
📌 구문 규칙
const [something, setSomething]
과 같은 구조로 이름을 지정합니다.
이름은 자유롭게 지을 수 있지만, 규칙을 따르는 것이 가독성을 높입니다.
useState
로 생성된 메모리는 컴포넌트 인스턴스별로 관리됩니다.
각각의 Counter
가 별도의 상태로 관리되는 것을 확인할 수 있습니다.
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;
useState
를 사용해 total
과 setTotal
을 생성합니다. handleTotal
을 통해 total
을 업데이트합니다. 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;
useState
를 사용해 counter
와 setCounter
를 생성합니다. counter
값을 업데이트하고 onTotal
을 실행시킵니다.Main
컴포넌트에서 total
값을 통해 전체 상태를 관리할 수 있습니다.useState
를 사용하면 React 컴포넌트의 상태를 생성하고 관리할 수 있습니다. useState
의 첫 번째 인자는 초기값, 두 번째 인자는 업데이트 함수입니다. ✨ 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 업데이트는 없을 수 있습니다.
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 />);
메서드를 호출하면 렌더링이 발생합니다.forceUpdate()
를 호출하면 해당 컴포넌트가 강제로 리렌더링됩니다.컴포넌트 렌더링 과정은 크게 세 가지 단계로 나눠집니다:
1. 렌더링 트리거
2. 컴포넌트 렌더링
3. DOM 커밋
리렌더링 과정에서는 변경된 DOM 노드만 업데이트됩니다.
렌더링은 DOM 업데이트를 의미하는 것은 아닙니다!
실제로 DOM 업데이트가 없더라도 컴포넌트에서는 렌더링이 발생할 수 있습니다. 😉
React에서는 모든 컴포넌트를 순수 함수로 간주합니다!
즉, 같은 입력(예: props
, state
, context
)을 넣으면 동일한 JSX 출력이 나와야 한다는 의미입니다.
먼저 비순수 컴포넌트를 만들어 보겠습니다!
/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에서는 항상 동일한 입력값에는 동일한 출력값을 보장하는 순수 컴포넌트를 작성해야 합니다.
이제 순수 컴포넌트를 작성해 보겠습니다.
/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 덕분에 추가적인 렌더링이 발생하여, 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씩 증가하게 된 것입니다.
React.StrictMode
는 개발 중에 애플리케이션에서 발생할 수 있는 잠재적인 문제를 식별하도록 도와주는 도구입니다. 이를 통해 컴포넌트를 더욱 안전하고 순수하게 만들 수 있습니다.
useState
를 사용해 할 일 목록을 정의하고, 이를 props
로 TodoList
컴포넌트에 전달합니다.
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;
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;
🖼️ 렌더링 결과
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>
);
🖼️ 결과 확인
아래와 같이 동일한 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: "포트폴리오 사이트 만들기" });
🖼️ 수정 후 결과
<StrictMode>
로 감싸면 애플리케이션 전체에 StrictMode가 적용됩니다.<StrictMode>
로 감쌀 수 있습니다.State
변수는 일반적인 JavaScript 변수처럼 읽고 쓸 수 있을 것 같지만, 사실은 스냅샷처럼 동작합니다.
State
값을 설정한다고 해서 기존 state
변수가 즉시 변경되는 것이 아닙니다. 대신 리렌더링이 발동되고 새로운 값으로 업데이트됩니다.
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함수를 넣어주면 됩니다!
State
변경은 즉시 반영되지 않고, 리렌더링을 트리거하여 업데이트된 값을 계산합니다.React에서 여러 개의 state
업데이트를 하나의 리렌더링으로 묶어서 처리하는 최적화 기법을 Batching이라고 합니다.
이를 통해 불필요한 리렌더링을 줄이고 성능을 최적화할 수 있습니다.
```jsx
const handleCounter = () => {
setCounter(counter + 1);
setCounter(counter + 1);
setCounter(counter + 1);
};
```
위 코드에서는 첫 번째 setCounter
호출 이후 바로 리렌더링이 발생하지 않습니다.
React는 모든 setCounter
호출을 모은 뒤, 한 번에 리렌더링을 처리합니다.
state
업데이트가 발생할 때마다 즉시 리렌더링하지 않습니다.💡 비유
레스토랑에서 종업원이 손님이 주문한 첫 번째 메뉴를 듣자마자 주방으로 달려가지 않고, 모든 주문을 들은 뒤 주방으로 전달하는 것과 같습니다!
React에서 상태를 업데이트할 때 직접 값을 전달하거나, 업데이터 함수를 사용할 수 있습니다.
```jsx
setCounter(counter + 5); // 현재 값(0) + 5 = 5
setCounter((c) => c + 1); // 이전 값(5) + 1 = 6
setCounter(42); // 최종적으로 42로 덮어씀
```
setCounter(counter + 5)
로 인해 counter
는 5가 됩니다.setCounter((c) => c + 1)
는 이전 상태 값을 참조해 5 + 1 = 6으로 업데이트합니다.setCounter(42)
가 호출되어 최종 값은 42로 설정됩니다.