안녕하세요 YB 김가현입니다 ~! 😋
이번 3주차는 상호작용성 더하기 파트에서 이벤트에 응답하기, State: 컴포넌트의 기억 저장소, 렌더링 그리고 커밋, 스냅샷으로서의 state 부분에 대해 읽어보았는데요 !
읽으면서 매일 useState를 쓰면서도 잘 모르고 쓰고 있었구나 … 라는 생각을 하게 되었습니다 ……….🙀🙀🙀 그래서 state와 관련된 부분들을 공부해본 내용들을 정리해보았습니다
State ?
리액트에서 컴포넌트는 변화하는 데이터를 기억하고 관리할 수 있어야 한다 !
예를 들어, 폼에 텍스트를 입력할 때 입력한 값이 즉시 화면에 반영되거나, 구매 버튼을 클릭하면 상품이 장바구니에 담긴다던가 … 등등 동적으로 변하는 데이터를 관리할 수 있어야 한다.
이렇게 컴포넌트에서 동적인 값을 state 라고 한다.
🧐 일반 변수로 관리하면요 ?
일반 변수(지역 변수)를 사용하게 되면 다음 두가지 이유로 변경 사항이 표시되지 않는다.
- 지역 변수는 렌더링 간에 유지되지 않는다.
- React는 컴포넌트가 다시 렌더링될 때 컴포넌트 함수를 처음부터 다시 실행한다.
- 따라서 일반 변수는 컴포넌트가 다시 렌더링될 때마다 초기화되며, 이전에 변경된 값이 유지되지 않는다.
- 지역 변수를 변경해도 렌더링을 발생시키지 않는다
- 일반 변수를 변경해도 React는 데이터가 바뀌었다는 것을 감지하지 못하므로 컴포넌트를 다시 렌더링하지 않는다 (React는 state가 변경될 때만 컴포넌트를 다시 렌더링한다)
따라서 React에서는 상태(state)를 사용하는 것이 필수적이며, 이를 위해 useState 훅을 제공하여 상태를 쉽게 관리하고 렌더링을 효율적으로 처리할 수 있게 한다.
useState 사용하기
리액트에서 “use”로 시작하는 모든 함수를 훅이라고 한다.
훅을 사용할 때는 두 가지 규칙을 준수해야 한다.
1. 최상위(top level)에서만 훅을 호출해야 한다
- 반복문, 조건문, 중첩 함수 내에서 훅을 실행하면 안된다. 원하지 않은 사이드 이펙트가 발생할 수 있다. (조건부로 effect를 실행하길 원한다면, 조건문을 Hook 내부로 넣자 !!)
- 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장된다.
왜 훅의 호출 순서가 같아야 하는가 ?
→ React 에서 특정 state가 어떤 useState 호출에 해당하는지 알 수 있는 이유가 React 가 Hook이 호출되는 순서에 의존하기 때문이다. 모든 렌더링에서 Hook의 호출 순서가 같기 때문에 state를 구분할 수 있는 것이다.
2. 리액트 함수 컴포넌트 내에서만 훅을 호출해야 한다.
- custom hook을 제외하고 일반 자바스크립트 함수 내에서는 훅을 호출해서는 안된다.
useState의 경우 특정 값을 저장하고 해당 값을 변경하는 함수를 반환하여 주는 역할을 하는 react hook이다.
사용 법은 아래와 같다.
import { useState } from 'react';
function Counter() {
const [index, setIndex] = useState(0);
const handleClick = () => {
setIndex(index + 1);
};
return (
<div>
<p>Current index: {index}</p>
<button onClick={handleClick}>Increase</button>
</div>
);
}
export default Counter;
useState 함수를 호출하면 배열을 반환한다.
첫 번째 요소인 Index는 현재 상태 값 변수를, 두 번째 원소인 setIndex는 상태 값을 갱신해주는 setter 함수이다.
useState 괄호 안의 값(0)은 상태의 초기 값이다.
초기 값은 원시 값, 객체 뿐만 아니라 콜백 함수도 가능하다.
+) 상태 변수와 상태 설정 함수의 이름을 일정한 규칙에 맞게 지정하는 것이 좋다 !
상태 변수는 상태가 무엇을 나타내는지 명확하게 이름을 지정하고, 상태 설정 함수는 항상 set으로 시작하고, 그 뒤에 상태 변수의 이름을 이어서 작성하면 된다.
위 코드의 실제 작동 방식은 다음과 같다.
- 컴포넌트가 처음 렌더링 되며 [0, setIndex] 를 반환한다.
- 상태를 업데이트하는 동작이 수행되면 (사용자가 버튼을 누르면) setIndex(index + 1) 가 호출되고, index는 0에서 1로 업데이트된다. React는 상태가 업데이트되었다는 것을 감지하고 새로운 렌더링을 트리거합니다. 즉, 상태 값이 변경되었기 때문에 컴포넌트를 다시 렌더링한다.
- 컴포넌트가 두번째로 렌더링되며 [1, setIndex]를 반환한다.
- 반복반복반복한다 ~
하나의 컴포넌트에 여러 state 변수를 지정하는 것도 가능하다 !
다만 필드가 많은 폼의 경우 각 필드별로 state 변수를 사용하는 것보다, 하나의 객체 state 변수를 사용하는 것이 더 편리하다.
또, state는 자신이 속한 컴포넌트에 종속적이다.
만약 같은 컴포넌트(A)를 두 차례 렌더링 하더라도, 각 컴포넌트에 속한 state는 서로 독립적으로 작동한다. A 컴포넌트를 렌더링 하는 상위 컴포넌트도 A 컴포넌트 내의 state 가 어떻게 작동하는지 알 수 없다.
만약 부모 컴포넌트의 state 가 변경되었을 때 자식 컴포넌트에도 이를 적용하고 싶다면 ?
→ props drilling, Context API 와 같은 방법을 사용할 수 있다.
여기서 한 가지 질문이 생겼다. setState가 두 번 호출되면 두 번 리렌더링이 일어나는가 ?
답은 아니다 ! 값을 여러번 바꾸더라도 리렌더링은 한 번만 일어난다.
그 이유는 리액트의 배치(batching) 처리 때문이다.
batching ?
React 에서는 컴포넌트의 불필요한 리렌더링을 방지하기 위해 state를 변경하는 작업을 일괄적으로 처리한다. 이렇게 state의 업데이트 작업을 모아서 일괄적으로 처리하는 방식을 Batching 이라고 한다 !
import { useState , useEffect } from 'react'
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((count) => count + 1);
setCount((count) => count + 1);
setCount((count) => count + 1);
};
useEffect(() => {
console.log("count", count);
}, [count]);
return <button onClick={handleClick}>click !</button>;
}
export default App
해당 코드를 실행하면, 콘솔에는 어떻게 찍힐까 ?
실제 콘솔 화면에는 위와 같이 count 3만 찍힌다.
(count가 하나씩 증가할 때마다 콘솔이 찍힐 거라고 생각했는데 완전 착각이었음)
이 이유가 바로 Batching 때문이다.
그렇다면 Batching 은 어떤 단위로 발생할까 ?
- React 18 이전 : 이벤트 핸들러
- React 18 : 프로미스, setTimeout, 이벤트 핸들러, 그 외 이벤트 등
React 18 부터 createRoot를 통해, 모든 업데이트들은 어디서 왔는가와 무관하게 자동으로 Batching 된다.
How to Upgrade to React 18 – React
The library for web and native user interfaces
react.dev
코드를 통해 살펴보자.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
setCount((c) => c + 1); // 리렌더링 x
setFlag((f) => !f); // 리렌더링 x
// 이벤트 핸들링이 끝나고 콜백이 끝나면 리렌더링 (배칭!)
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style=>{count}</h1>
</div>
);
}
위 코드는 이벤트 핸들링이 끝난 후 콜백에서 상태 업데이트가 일어나고 있다.
React 18 이전 버전이었다면 배칭되지 않았겠지만, React 18 버전에서 자동 배칭 기능이 도입되면서 fetchSomething()과 같은 비동기 작업이 끝난 후에도 리액트는 그 안에서 발생하는 상태 업데이트를 한 번의 렌더링으로 묶어 처리한다.
따라서, setCount와 setFlag가 호출된 이후에 한 번의 렌더링만 수행되는 것이다.
최종적으로 발생하는 흐름
- 버튼 클릭 → handleClick 함수 실행
- fetchSomething() 호출 (비동기 요청)
- fetchSomething() 완료 후 .then() 콜백 실행 → setCount와 setFlag 호출
- Batching :
- setCount와 setFlag의 두 상태 업데이트는 하나의 렌더링 사이클로 묶여서 처리됨
- Re-rendering :
- 상태 값이 변경된 후 한 번의 리렌더링이 발생하고 h1 태그에 새로운 count 값이 표시됨
어렵네예
마지막으로 궁금한 점 !
최적화 .. 성능 향상 …. 과 관련된 말을 정말 많이 들었는데 !
성능 최적화에 대한 개념은 알고 있지만, 렌더링 작업을 줄이면 얼마나 효과가 있는지에 대해서는 잘 실감이 나지 않는 것 같아요. 혹여나 최적화에 대한 경험을 가지고 계신 분이 있다면 …!!!! 어떤 방식으로 최적화가 이루어졌고, 그로 인해 성능이 어떻게, 얼마나 개선되었는지 경험을 들려주시면 감사하겠습니다 🙇🏻♀️
'나야, 리액트 스터디' 카테고리의 다른 글
[week3]상호작용성 더하기 - 이벤트에 응답하기, State: 컴포넌트의 기억 저장소, 랜더링 그리고 커밋, 스냅샷으로서의 state (3) | 2024.11.10 |
---|---|
[week3] useState,,, 제대로 알고 계신가요?! (3) | 2024.11.10 |
[week2] 리액트의 트리구조 (6) | 2024.11.04 |
[week2] 컴포넌트의 순수성 (3) | 2024.11.03 |
[week 2] - 조건부 렌더링, 리스트 렌더링, 컴포넌트를 순수하게 유지하기, 트리로서의 UI (5) | 2024.11.03 |