어라? 함수는 객체일 뿐이기 때문에 App 함수 전체가 재실행되면 하위에 있는 함수들도 모두 재실행되어 새롭게 생성된다고?
그러면 이제 이런 의문이 생길 수도 있다.
리액트에서 state(상태)
는 가장 중요한 개념이다. 상태는 컴포넌트를 재렌더링하고 화면에 표시되는 것들을 바꾼다. 따라서 컴포넌트와 상태의 상호작용은 리액트의 핵심적인 개념이다.
상태
와 컴포넌트
둘 다 리액트가 관리한다는 것에 주목해야 한다.
컴포넌트의 개념은 리액트 라이브러리에서 나왔으며, 리액트는 이 컴포넌트와 연결된 상태도 함께 처리한다. useState() 훅
을 사용하는 것이 그 예시이다. 가장 일반적인 형태의 상태 관리 방법이 바로 useState() 훅
을 사용하는 방법이다. useState() 훅
을 통해 컴포넌트와 상태간의 상호작용을 처리할 수 있다.
import React, { useState } from "react";
function App () {
const [state, setState] = useState();
return (...);
}
export default App;
useState()훅
을 사용하면 새로운 상태를 만들어서 자동적으로 useState
를 호출한 컴포넌트에 이를 연결할 수 있다. 이러한 연결도 리액트가 담당하는 역할이다.
useState
를 호출하면 리액트는 백그라운드에서 이를 관리하고 컴포넌트와 이를 묶어줄 새로운 상태 변수를 만든다.
함수는 객체일 뿐이므로 App 컴포넌트가 재실행될 때 새롭게 정의되므로 기본적으로 새롭게 생성되어야 한다. 그런데 왜 다른 함수들과 달리 state(상태)는 어떤 종류의 변경이 발생한다거나 초기화가 발생한다거나 하는 변화가 없는 걸까?
상태를 관리
하고, 컴포넌트와의 연결을 관리
한다.useState
와 여기에 전달된 기본 값
에 대해서는 한 번만 고려하도록 처리된다. 컴포넌트가 처음 렌더링될 때 즉, App 컴포넌트가 최초 실행될 때 한 번만 처리된다.useState
가 실행되면 리액트가 관리하는 새로운 state(상태) 변수를 만든다. 그러고 나서 리액트는 이 변수가 어느 컴포넌트에 속하는지를 기억해 둔다. 위 경우에는 App 컴포넌트이다.useState
가 호출되면 새로운 상태는 생성되지 않는다❗️관리
와 갱신(update)
만 담당한다.import React, { useState } from "react";
function App () {
const [state, setState] = useState("SUGA");
const onChangeNameHandler = (e) => {
e.preventdefault();
setState("AGUST-D");
}
return (
<div>
<p>{state}</p>
<button onClick={onChangeNameHandler}>이름 변경</button>
</div>
);
}
export default App;
useState 훅을 통해 상태를 관리하고 있다고 해보자. 이때, 리액트가 상태를 관리해주고 있다. 상태를 업데이트할 때는 useState를 통해 값을 반환하는 상태 업데이트 함수인 setState 함수
를 사용하면 된다.
그런데, setState("AGUST-D")
가 실행이 완료되더라도, state는 바로 SUGA
에서 AGUST-D
로 변경되지 않는다❗️
대신에 상태 업데이트 함수인 setState
가 호출되면, 새로운 상태로 업데이트를 하도록 scheduled(예약)
된다.
즉, 새로운 상태로 업데이트 될 수 있도록, 상태 변경 예약(Scheduled State Change)된다.
리액트는 이를 인지하고 처리할 계획을 준비하지만, 즉시! 처리하지는 않는다.
사람이 보기에는 바로 처리된 것으로 보이지만, 실제로 리액트는 상태 변화를 지연시킨다.
하지만 대부분의 경우 상태 변경이 발생하면 상태 변경에 대한 스케줄 작업은 매우 빠르게 발생하기 때문에 거의 즉각적인거나 다름 없다. 실제로는 순간적으로 발생한다.
대다수 성능 기반의 작업들은 거의 동시에 진행되는데, 잠재적으로 리액트는 상태를 더 높은 우선순위를 갖는 것으로 간주한다.
화면에 사용자 입력 input이 있다고 해보자. 사용자의 입력에 응답하는 것이 화면에 문자를 변경하는 것보다는 우선순위가 높다. 그러므로 리액트는 예정된 상태 변경을 지연시킬 수 있다.
리액트가 하는 일은 상태 변화가 발생하면, 상태 변화의 순서를 명확히 하여 같은 타입임을 보증한다.
이번에는 setState()를 다시 호출하여 "Yunki Min"
을 보내보자.
import React, { useState } from "react";
function App () {
const [state, setState] = useState("SUGA");
const onChangeNameHandler = (e) => {
e.preventdefault();
setState("AGUST-D");
//
setState("Yunki Min");
}
return (
<div>
<p>{state}</p>
<button onClick={onChangeNameHandler}>이름 변경</button>
</div>
);
}
export default App;
그러면 상태 변화는 이전의 상태가 업데이트 되기 전까지는 일어나지 않는다.
즉, 순서가 유지된다.
즉각 실행이 필수적인 것은 아니다. 결국 언젠가는 처리되고 state는 AGUST-D
로 바뀔 것이다.
그리고 새로운 상태인 AGUST-D
가 활성화 되고 상태 변화가 처리되면, 리액트가 컴포넌트를 재평가하고 컴포넌트 함수를 재실행한다.
이런 스케줄링(예약) 때문에 다수의 예약 상태 변화가 동시에 일어날 수 있다. 동시에 여러 번의 업데이트가 스케줄될 수 있기 때문에 상태를 업데이트할 때는 ✅ 함수 형태를 이요하여 업데이트 해야 한다.
특히 이전 상태의 스냅샷에 의존한다면 특히 그래야 한다.
setState((prev) => !prev);
이론상 스케줄링 작업은 지연될 수 있고, 상태변경은 순서대로 처리되므로 이전 상태를 기반으로 매번 상태를 변경하는 경우, 위 처럼 코드를 작성해야 안전하게 가장 최신의 스냅샷을 얻을 수 있다.
위 처럼 함수 형태를 사용해야 리액트가 미완료된 상태 업데이트 작업에 대하여 최신 상태를 사용하고, 컴포넌트가 재렌더링되었을 때 최신의 상태를 사용할 수 있게 된다.
이렇게 이전 상태의 스냅샷을 기반으로 상태를 업데이트하기 위해 함수 형식을 사용하는 것과 비슷한 것이 useEffect()의 작동 방법이다. useEffect는 상태 또는 종속된 값이 변경될 때마다 의존성 매커니즘을 통해 내부에서 선언한 이펙트 함수가 재실행된다. 따라서 컴포넌트 함수가 재실행되면 컴포넌트가 재실행되어 매번 이펙트가 다시 실행되기 때문에 미완료된 상태 업데이트 작업이 빠짐없이 실행된다.
실제로 이펙트는 이 이후에 실행되므로 이런 작업을 수행할 때마다 가장 최신의 결과를 얻을 수 있는 것이다.
setState() 로 상태 업데이트 예약함 -> 컴포넌트 다시 실행되고 나서야 사용가능한 최신 상태로 업데이트됨
const [name, setName] = useState("RM");
const [age, setAge] = useState(29);
const changeHandler = () => {
setName("NamJoon");
// 상태가 아직 업데이트 된건 아님, 상태 업데이트 예약됨
setAge(30);
// 상태가 아직 업데이트 된건 아님, 상태 업데이트 예약됨
}
//...
상태 업데이트 함수를 사용하면 각각 상태 업데이트가 예약된다.
이는 컴포넌트가 재실행되고 재평가된다는 것을 의미한다.
그런데 사실 이는 리액트가 하는 역할은 아니다.
이렇게 두개의 상태 업데이트가 같은 sync 코드 조각에 존재한다면, 즉 같은 함수가 서로 다른 promise 블록에 있지 않고 같은 곳에 존재한다면, 이 둘 사이에 시간지연 현상은 발생하지 않을 것이다.
두 코드가 같은 곳에 있으면서 상태 업데이트 함수를 호출한다면, 리액트는 이들에 대한 상태 업데이트를 하나의 동기화 프로세스에서 같이 실행한다.
예를들어, 하나의 함수가 처음부터 끝까지 어떤 콜백이나 프로미스 없이 실행된다면, 리액트는 이 함수로부터 발생하는 모든 상태 업데이트 작업을 하나의 상태 업데이트 작업으로 처리한다. 즉 일괄 처리(batch together)한다.