리액트 상태와 불변성
당연하게 사용하는 훅이지만 깊게 공부는 안해본 것 같아서 남기는 간단한 메모
동일한 제목의 비슷한 내용이 많은 키워드이다. 검색해보면 ‘리액트에서 불변성을 왜 지켜주어야 하는가?’라는 제목의 글이 많은데, 막상 살펴보면 set state 함수의 매개변수로 얕은 복사를 해야한다는 내용이 대부분이다. 실은 state에서 불변성이 어떻게 구현되는지, 더 깊게는 왜 state를 사용하는지 궁금해졌다. 조금 더 깊게는 state에서 클로져가 사용되는 이유, 불변성 같은 함수형 프로그래밍의 개념이 리액트와 얼마나 연관이 있는지 궁금했다.
리액트 Docs에 따르면 State의 정의는 다음과 같다.
- State는 컴포넌트의 메모리이다.
- 지역변수는 렌더링이 일어나는 동안 지속되지 않는다.
- 지역변수의 변화는 렌더링을 트리거하지 않는다.
- 새로운 데이터로 컴포넌트를 업데이트 시키기 위해서는 두가지가 필요하다.
- 렌더링 동안 데이터를 유지
- 새로운 데이터를 렌더링하기 위한 트리거
- useState 훅은 이 2가지를 제공한다
- 렌더링 사이에 데이터를 보관할 상태 변수 (state)
- 변수를 업데이틀하고 리액트에 컴포넌트를 다시 렌더링하도록 트리거시킬 상태 정의 함수 (state action)
이하 부터는 리액트 docs에서 인상 깊은 내용을 노트하고 주관적인 생각을 덧붙였다.
- State는 독립isolated적이고 비공개private이다
상태state는 화면상의 컴포넌트 인스턴스에 제한됩니다. 즉 같은 컴포넌트를 2번 렌더링하더라도, 복제된 동일한 두 컴포넌트는 완전히 독립된 상태를 갖습니다.
이것이 상태(State)가 일반적인 변수와 구분되는 점입니다. 일반적인 변수는 코드 모듈 상단에서 선언할 수 있지만, 상태는 특정한 함수 호출이나 코드의 위치와 연결되지 않습니다. 대신, 상태는 화면상의 특정한 위치에 "로컬(local)"하게 존재합니다.
- 자식 컴포넌트 동일한 props로 연결되지 않은 상태에 대해서도 완전히 독립된 상태를 갖는다.
자주 사용되는 일상적인 문법이지만, 리액트 설계의 중요한 지점으로 다가온다. 서버 데이터에 의존하지 않는 ui 요소만의 상태를 관리하고, 필요에 따라 부모-자식의 상태를 독립적으로 관리할 경우도 분명 있기 때문이다.
리액트의 렌더링은 3단계로 구분된다
- 트리거
- 렌더링
- 커밋
단계 1: 트리거
컴포넌트를 렌더링할 2가지 이유
- 초기 렌더링
- 리액트는 createRoot()를 타겟 DOM에 호출하고, render()를 통해 컴포넌트를 렌더링한다.
- 업데이트 렌더링 (리렌더링)
- Set 함수를 이용해 state를 업데이트하여 렌더링을 트리거할 수 있다.
- 컴포넌트의 상태를 업데이트 하면 자동으로 렌더링이 대기(queue)된다
단계 2: 렌더링
렌더링을 트리거한 후에, React는 화면에 어떤 내용을 표시할지 결정하기 위해 컴포넌트를 호출한다. "렌더링(Rendering)"은 React가 컴포넌트를 호출하는 것을 의미한다.
- 초기 렌더링에 리액트는 root 컴포넌트를 호출한다
- 이어진 렌더에서 리액트는 상태 업데이트로 인해 렌더링을 트리거한 함수형 컴포넌트를 호출한다.
이 과정은 재귀적으로 진행됩니다. 업데이트된 컴포넌트가 다른 컴포넌트를 반환하면, React는 해당 컴포넌트를 다음으로 렌더링하고, 그 컴포넌트 또한 무언가를 반환하면 그 다음 컴포넌트를 렌더링하게 됩니다. 이러한 과정은 중첩된 컴포넌트가 더 이상 없을 때까지 계속되며, React는 정확히 화면에 표시해야 할 내용을 알게 됩니다.
- 리액트 컴포넌트가 JSX가 담긴 render() 메서드를 반환하는 함수이고, 부모의 반환값에 또다른 컴포넌트가 들어있다면, 그 자식이 반환하는 render()도 호출한다. 즉 계속 중첩된 함수를 차례로 호출한다. 이 부분에서 리액트가 함수형을 고려하여 짜여진게 아닐까 생각이 든다. 곧이어 다음과 같은 노트가 이어진다.
**함정
렌더링은 항상 순수한 계산이어야 합니다:
동일한 입력, 동일한 출력. 주어진 동일한 입력에 대해 컴포넌트는 항상 동일한 JSX를 반환해야 한다. … 렌더링 이전에 존재한 객체나 변수를 변경해서는 안된다. 그렇지 않으면 코드베이스가 복잡해질수록 혼란스러운 버그와 예측할 수 없는 동작이 발생할 수 있습니다. "Strict Mode(엄격 모드)"에서 개발할 때, React는 각 컴포넌트의 함수를 두 번 호출하여 순수하지 않은 함수로 인해 발생하는 오류를 찾아낼 수 있습니다.
과연 함수형이 떠오르지 않을 수 없는 대목이다. 처음 궁금증이었던 리액트 상태의 불변성에 대한 정의와 사용 이유-에 대한 단서가 되는 것 같다.
본문에서는 샐러드, 토마토 등등으로 설명하고 있지만, 비유는 쉬운데 적용이 어렵다. 훨씬 더 단순하게 생각한다면, 1이라는 숫자가 props로 들어온다면 항상 <p>1</p>가 리턴되어야 한다. 이 1 컴포넌트는 항상 1을 뱉어야한다. 이 컴포넌트가 어떤 모종의 (전역)변수를 참조하여 1을 넣었는데 어떨 때는 2가 나오고 3이 나오는 경우가 없어야 한다.
단계 3: React가 DOM에 변경 사항을 반영(commit)
컴포넌트를 렌더링(호출)한 후, React는 DOM을 수정합니다.
초기 렌더링(initial render)에서, React는 생성한 모든 DOM 노드를 appendChild() DOM API를 사용하여 화면에 배치합니다.
재렌더링(re-renders)에서, React는 최소한의 필요한 작업(렌더링 중에 계산된 작업)만을 적용하여 DOM을 최신의 렌더링 결과와 일치시킵니다.
export default function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}
React는 렌더링 사이에 차이가 있는 경우에만 DOM 노드를 변경합니다. 예를 들어, 부모로부터 초당 한 번씩 다른 props를 전달 받아 재렌더링되는 컴포넌트가 있다고 가정해봅시다. <input>에 텍스트를 추가하고 해당 값을 업데이트해도, 컴포넌트가 재렌더링되어도 텍스트가 사라지지 않는 것을 관찰할 수 있습니다.
초기 렌더링 시에는 appendChild() api를 통해 DOM을 생성한다. 이후 리렌더링이 발생할 시 (상태가 업데이트 되어 렌더링이 트리거되고, render()가 호출되어 리렌더링이 발생할 시) 리액트는 변경된 부분만 다시 렌더링한다. 아마 이 부분이 리액트의 핵심이라고도 추측되고, 처음 궁금증을 가졌던 불변성과도 연관이 있어보인다.
… 이어집니다