Written by Paul
왜 react hooks의 콜백함수는 asynchronous promise를 지원하지 않는가?
가장 근본적인 이유는, react의 렌더링 및 state 업데이트가 동기적 특성을 유지해야 하기 때문
비동기 작업이 react의 렌더링 및 상태관리 흐름에 직접적인 영향을 미친다면, 부작용(side effect)가 발생할 수 있고, 동작이 예측가능하지 못하게 된다
리액트는 묶음처리(batching) 업데이트를 진행하는데, 이 부분은 조금 더 뒤에서 다루어 보겠다. 다시 돌아와서 비동기 콜백 실행으로 인해, 상태가 언제 업데이트 될지 모른다면 React의 내부적인 묶음처리(batching) 최적화가 제대로 동작하지 않을 수 있다.
리액트는 상태변경이 이루어질때마다, 새로운 값을 바탕으로 가상돔(Virtual DOM)을 업데이트 하고, 필요한 부분만 실제 DOM에 반영한다. 이 과정을 비동기로 처리할 경우, 상태가 바뀌지 않았음에도 불필요한 렌더링이 발생하거나, 반대로 필요한 상태 업데이트가 제대로 반영되지 않을 수 있다.
따라서, 비동기 작업은 useEffect의 콜백에서 처리하면 된다.
useEffect(() => { const fetchData = async () => { const result = await someAPICall(); setData(result); }; fetchData(); }, []);
이 방식에서는 deps로 인하여 사이드이펙트가 일어날수 있는 부분에 대해서, 비동기 콜백 함수를 실행한다. 상태 업데이트와 렌더링을 비동기 작업과 분리함으로써, 리액트의 상태관리와 렌더링 흐름이 비동기로 인해 방해받지 않도록 한다.
결론적으로, react hooks의 콜백함수가 async를 직접적으로 지원하지 않는 이유는, React의 상태 업데이트와 렌더링 흐름이 동기적으로 예측 가능하게 유지되도록 하기 위함입니다. 비동기 작업은 useEffect와 같은 effect hooks의 내부 콜백을 통해 처리하는 것이 권장되며, 이는 상태 업데이트와 비동기 작업이 충돌하지 않도록 설계된 것이다.
setState 함수는 비동기 함수일까? 동기 함수일까?
setState 함수는 비동기적으로 동작하는 경우가 많다. 이 점은 실제 setState의 동기성과 혼동을 줄 수 있는데, 여기서 말하는 비동기에서 Javascript의 비동기 작업 (ex. Promise, async/await)과는 차이가 있다.
setState 함수가 비동기적으로 보이는 이유는 리액트가 성틍 최적화를 위해 묶음처리(batching)을 사용하기 때문이다. 즉, 여러개의 상태 업데이트가 있을 때, 이를 하나로 묶어서 한 번에 처리하는 경우가 있다. 이런 최적화는 주로 다음과 같은 경우에 발생한다.
- 이벤트 핸들러에서
setState
를 호출할 때.
- 라이프사이클 메서드나 함수형 컴포넌트에서 상태를 변경할 때.
아래와 같이 상태를 두번 업데이트 하는 경우가 있다고 가정해보자.
function handleClick() { setState(prevState => prevState + 1); setState(prevState => prevState + 1); }
위 코드에서는 setState를 두번 호출 했지만, 리액트는 이를 묶어서 한번의 업데이트로 처리한다. 그 결과 상태는 1씩 두번 증가하는게 아니라, 최종적으로 1만 증가한다.
위와같이, setState는 실제로 상태가 변경되는 시점을 정확히 “보장”하지 않으며(이는 자바스크립트의 비동기와는 다르다), 리액트가 적절하다고 판단하는 시점에 상태를 업데이트 한다. 이것이 setState가 비동기적 동작처럼 보이는 이유이다.
React의 Batching Update(묶음 처리)는 무엇인가?
setState의 비동기성은 묶음 처리(batching)에서 비롯된다. 리액트는 성능을 최적화하기 위해 여러 상태 업데이트를 하나로 묶어 한 번의 리렌더링으로 처리하려고 한다. 이렇게 하면 여러 번의 상태 변경이 발생 하더라도, 그때마다 렌더링을 유발하지 않고, 한 번에 처리하여 성능을 향상시키기 때문이다.
setState의 비동기성은 Promise나
async/await
의 비동기성과는 다르다. setState는 JS의 이벤트 루프에 의해 비동기적으로 처리되는 것이 아니라, React의 내부 최적화에 따라 렌더링 사이클 동안 상태 업데이트가 지연되거나 묶음 처리 되는 것. 따라서, setState앞에 await 키워드를 붙이는 등 Promise처럼 사용할 수는 없다. 만약 setState가 완료된 다음에 작업을 수행하고 싶다면, 상태 업데이트가 반영 된 후 실행되는 콜백 함수나 useEffect를 사용해야 한다.React의 배치 업데이트는 이벤트 루프 내에서 발생한 상태 변화를 모아 한 번의 리렌더링으로 처리한다. 이를 가능하게 하는 메커니즘은 주로 React Fiber와 Transaction이라는 시스템을 기반으로 동작한다.
React는 왜 함수형 패러다임을 채택했는가?
리액트가 함수형 패러다임을 채택한 이유는, 여러가지가 있을 수 있겠지만 예측 가능성, 불변성, 구조적 단순성을 통해 복잡한 UI 상태관리 문제를 더 쉽게 해결하고, 유지보수를 용이하게 하기 위함이다.
- 순수 함수로 인한 예측 가능성
- 함수형 프로그래밍에서 핵심 개념 중 하나는 순수 함수
- 순수 함수는 동일한 입력이 주어지면 항상 동일한 출력을 반환하며, 외부 상태에 의존하거나 이를 변경하지 않는다
- React의 컴포넌트는 UI를 이러한 순수 함수처럼 취급하여, 입력(props)에 따라 동일한 UI(출력)를 반환하는 구조
- 이로 인해, 예측가능성이 높아지면서 버그를 줄이고 디버깅을 용이하게 만든다
- 순수함수는 부작용(side effect)가 없기 때문에 독립적으로 테스트가 가능하다. UI 컴포넌트도 동일한 props와 state가 주어지면 항상 동일한 UI를 반환하므로, 테스트가 훨씬 간단해진다
- 불변성
- 함수형 프로그래밍은 불변성을 중시
- 즉, 데이터는 변경되지 않고, 상태가 변경될 때는 기존 데이터를 수정하는 대신 새로운 데이터 객체를 생성하는 방식으로 처리
- React는 이 불변성을 활용하여 상태 관리와 성능 최적화를 쉽게 할 수 있다
- 이로 인해 상태의 변경을 쉽게 추적하고, 버그를 줄일 수 있다
- 이로 인해 리렌더링 최적화가 가능하다. 불변성을 활용하여 상태가 변경되었을 때에만 UI를 다시 렌더링 하기 때문. 객체의 참조값이 변경되었는지 확인하는 것이 쉬워지므로, 리렌더링을 효율적으로 관리할 수 있다.
- 선언형 프로그래밍 방식
- React는 UI를 선언형으로 구성
- 선언형 프로그래밍은 "무엇을 할지"에 집중하는 방식. 프로그래머가 특정 작업을 수행하는 방법을 명시하는 대신 최종 결과를 선언하는 방식. 함수형 프로그래밍은 선언형 프로그래밍의 대표적인 예이다. 조금 더 뒤에서 명령형 프로그래밍과의 비교를 해보겠다.
- React의 컴포넌트는 UI의 상태와 그에 따른 UI를 선언적으로 기술한다. 이는 UI가 특정 상태에 따라 어떻게 변화해야 하는지 명확하게 나타낼 수 있으며, 코드의 가독성을 높인다
- 과거에 주로 사용되던 명령형 프로그래밍 방식에서는 UI의 변화를 수동으로 관리해야 했지만, 선언형 프로그래밍 방식에서는 상태가 바뀔 때 React가 알아서 UI를 갱신. 이는 코드의 복잡도를 크게 줄여 준다
- 불변성과 상태 업데이트의 안전성
- 함수형 프로그래밍에서의 불변성과 상태를 명확하게 구분하는 방식은 상태 변경 시의 안전성을 보장
- React는 상태가 변경되면 컴포넌트를 다시 렌더링하지만, 상태를 직접 수정하지 않고, 새로운 상태를 반환하는 방식으로 상태를 관리
- 따라서, 시간여행 디버깅이 가능하다. 상태의 불변성 덕분에 이전 상태와 이후 상태를 쉽게 구분할 수 있으며, 이를 통해 "시간 여행 디버깅" 기능(상태 변경을 되돌리거나 재적용하는 기능)이 가능. 시간여행 디버깅도 조금 뒤에서 다루어 보겠다
- 직접적으로 상태를 변경하지 않기 때문에, 상태 관리의 안전성이 보장되고 예측 가능한 방식으로 상태가 변경
- 간결하고 직관적인 코드
- React Hooks는 함수형 패러다임을 더 강력하게 만드는 도구로, 상태와 사이드 이펙트를 보다 직관적으로 처리할 수 있다. 이를 통해 로직이 컴포넌트에 강하게 결합되지 않고, 독립적으로 관리되며 재사용성이 높아진다
(optional) 명령형 프로그래밍과 선언형 프로그래밍의 차이를 리액트를 빗대어서 설명
선언형 vs 명령형 비교
- 선언형: UI가 상태에 따라 어떻게 보일지 결과를 선언. React는 상태가 변경되면 자동으로 UI를 업데이트한다
- 명령형: 상태 변화에 따라 UI의 업데이트 과정을 직접 관리. 개발자가 상태 변경마다 어떤 DOM을 업데이트할지 명령을 내리는 방식
예제: 버튼 클릭 시 카운터가 증가하는 UI
선언형 방식 (React 코드)
import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
- 여기서
useState
를 통해 상태를 관리하고, React가 상태에 따라 UI를 알아서 갱신
- 우리는 UI가 어떻게 업데이트 되는지에 대한 구체적인 과정은 신경 쓰지 않는다. 대신 "상태가 바뀌면 React가 알아서 렌더링해준다"는 원칙만 따른다
명령형 방식
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Counter Example</title> </head> <body> <div id="app"> <p id="count">Count: 0</p> <button id="increment">Increment</button> </div> <script> let count = 0; // DOM 요소 선택 const countDisplay = document.getElementById('count'); const incrementButton = document.getElementById('increment'); // 상태 변경 시 UI를 직접 업데이트하는 함수 function updateUI() { countDisplay.textContent = 'Count: ' + count; } // 버튼 클릭 이벤트 리스너 추가 incrementButton.addEventListener('click', () => { count += 1; updateUI(); // 상태가 바뀔 때마다 UI를 직접 업데이트 }); </script> </body> </html>
- 여기서 우리는 버튼 클릭마다 상태(
count
)를 직접 업데이트하고, DOM 요소(countDisplay
)에 접근해서 해당 요소의 텍스트를 수동으로 변경해야 한다
- React의 선언형 방식과 달리, 상태가 변경될 때마다 수동으로 UI를 변경해야 하며, DOM 조작을 직접 관리해야 하기 때문에 복잡도가 증가한다
리액트는 내부적으로는 명령형 프로그래밍을 사용하여 DOM을 조작하며 동작한다. 하지만 이를 개발자에게 선언형 API로 사용할 수 있게 제공해준다.
(optional) 시간여행 디버깅을 리액트에 빗대어서 설명
시간여행 디버깅(Time-Travel Debugging)은 애플리케이션의 상태를 변경해 과거의 특정 시점으로 되돌아가거나, 다시 앞으로 이동할 수 있는 디버깅 기술이다. React 같은 상태 기반 프레임워크에서, 상태의 변화를 순서대로 기록하고, 해당 상태로 돌아가거나, 재실행하면서 애플리케이션을 디버깅하는 방식.
이 기능은 Redux DevTools와 같은 도구에서 구현되어 있으며, 상태 관리가 명확한 애플리케이션에서 특히 유용하다.
시간여행 디버깅의 주요 개념
- 상태 기록: 상태가 변경될 때마다 그 상태를 기록합니다.
- 과거 상태로 이동: 기록된 상태를 이용해, 애플리케이션을 과거의 특정 시점으로 되돌릴 수 있습니다.
- 다시 진행: 과거로 돌아갔다가 다시 원래의 최신 상태로 돌아올 수 있습니다.
- 버그 추적: 특정 버그가 발생한 시점까지 돌아가서 상태를 분석하고, 버그가 발생하기 이전과 이후의 상태를 비교할 수 있습니다.
시간여행 디버깅의 순수 JS 구현 예제
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Time Travel Debugging</title> <style> button { margin: 5px; } </style> </head> <body> <div id="app"> <p id="count">Count: 0</p> <button id="increment">Increment</button> <button id="decrement">Decrement</button> <button id="undo">Undo</button> <button id="redo">Redo</button> </div> <script> // 상태 관리 let currentState = 0; // 현재 상태 let history = [currentState]; // 상태의 히스토리 기록 let currentIndex = 0; // 현재 히스토리 인덱스 // DOM 요소 선택 const countDisplay = document.getElementById('count'); const incrementButton = document.getElementById('increment'); const decrementButton = document.getElementById('decrement'); const undoButton = document.getElementById('undo'); const redoButton = document.getElementById('redo'); // 상태를 업데이트하고, 히스토리를 기록하는 함수 function updateState(newState) { currentState = newState; // 현재 인덱스 이후의 히스토리는 버림 history = history.slice(0, currentIndex + 1); // 새 상태 추가 history.push(currentState); currentIndex++; render(); // UI 업데이트 } // 상태를 렌더링하는 함수 function render() { countDisplay.textContent = 'Count: ' + currentState; // Undo, Redo 버튼 활성/비활성화 undoButton.disabled = currentIndex === 0; redoButton.disabled = currentIndex === history.length - 1; } // Increment 버튼 클릭 이벤트 incrementButton.addEventListener('click', () => { updateState(currentState + 1); }); // Decrement 버튼 클릭 이벤트 decrementButton.addEventListener('click', () => { updateState(currentState - 1); }); // Undo 버튼 클릭 이벤트 undoButton.addEventListener('click', () => { if (currentIndex > 0) { currentIndex--; currentState = history[currentIndex]; render(); // UI 업데이트 } }); // Redo 버튼 클릭 이벤트 redoButton.addEventListener('click', () => { if (currentIndex < history.length - 1) { currentIndex++; currentState = history[currentIndex]; render(); // UI 업데이트 } }); // 초기 UI 렌더링 render(); </script> </body> </html>
가상돔(Virtual DOM)의 동작 방식
React의 Virtual DOM(가상 DOM)은 실제 DOM과 유사한 구조를 가지며, 효율적인 DOM 업데이트를 가능하게 하기 위해 설계되었다. Virtual DOM은 React가 UI 상태 변화에 따라 실제 DOM을 직접 조작하는 대신, 중간에 가상의 DOM을 활용해 최소한의 변경만 실제 DOM에 적용하는 핵심 개념이다.
가상돔의 라이프사이클
- Initial Render (초기 렌더링)
- React 컴포넌트가 처음 렌더링될 때, JSX 코드가 Virtual DOM 트리로 변환된다. 이 Virtual DOM 트리는 실제 DOM에 대응하는 데이터 구조
- React는 이 Virtual DOM을 기반으로 초기 실제 DOM을 생성하고, 브라우저에 렌더링한다
- State or Props 변경
- 컴포넌트의 상태(state)나 속성(props)이 변경되면, React는 변경된 상태를 기반으로 새로운 Virtual DOM 트리를 생성한다
- Virtual DOM 비교 (Diffing)
- React는 새로운 Virtual DOM과 이전 Virtual DOM을 비교한다. 이 과정에서 Diffing Algorithm(차이 알고리즘)을 사용해 두 트리 간의 차이를 찾는다.
- 이 비교 과정은 최소한의 변경 사항을 찾아내어 실제 DOM에 적용하려는 목적을 가지고 있습니다. React는 이를 재조정(reconciliation)이라고 부른다.
- Patch (패치)
- 비교 후, React는 변경된 부분만 실제 DOM에 반영한다. 전체 DOM을 다시 렌더링하지 않고, 최소한의 변경만 적용하여 성능을 최적화한다.
가상돔의 구조
Virtual DOM은 실제 DOM과 비슷한 구조를 가진다 하지만 이는 경량 객체로, DOM 요소의 속성, 스타일, 자식 요소들을 표현하는 자바스크립트 객체이다. 이러한 구조 덕분에 메모리 효율이 높고, 변경 사항을 추적하여 효율적으로 업데이트할 수 있다.
{ type: 'div', // DOM 요소의 태그 (예: div, span) props: { // 요소의 속성 (attributes) id: 'app', className: 'container' }, children: [ // 자식 요소들 (트리 구조) { type: 'h1', props: { style: 'color: red;' }, children: ['Hello, World!'] }, { type: 'button', props: { onClick: () => console.log('Clicked!') }, children: ['Click me'] } ] }
가상돔의 Diffing Algorithm
Virtual DOM의 핵심은 Diffing Algorithm을 통해 이전의 Virtual DOM과 새롭게 생성된 Virtual DOM 간의 차이를 찾아내는 것이다. 이 과정은 매우 최적화되어 있으며, 일반적으로 O(n) 복잡도로 동작한다. React는 두 Virtual DOM 트리 간의 차이를 계산할 때 다음과 같은 규칙을 사용한다.
- 다른 타입의 요소
- 예를 들어, 이전에
div
였던 요소가span
으로 변경되었다면, React는 전체 DOM 서브트리를 교체한다. 이는 DOM 트리의 서브트리가 완전히 다르다고 간주하기 때문
- 같은 타입의 요소
- 만약 요소의 타입이 같다면, React는 해당 요소의 속성(props)을 비교하여 달라진 부분만 업데이트한다
- 자식 노드가 있다면, 자식 노드 역시 재귀적으로 비교
- 키(key) 기반의 최적화
- React는 리스트를 렌더링할 때, 각 항목에
key
속성을 부여하는데, 이는 항목 간 순서가 바뀌거나 추가 및 삭제될 때 이를 더 효율적으로 처리하기 위함 key
값이 없다면 리스트 전체를 다시 렌더링하지만,key
를 제공하면 React는 리스트에서 추가된 항목이나 순서가 바뀐 항목만 찾아내어 업데이트한다
가상돔의 장점
Virtual DOM은 성능 최적화를 위해 만들어진 개념으로, 다음과 같은 장점이 있다.
- 최소한의 DOM 조작: 실제 DOM 조작은 비용이 크기 때문에, Virtual DOM을 통해 DOM 업데이트를 최소화하고, 필요한 부분만 실제 DOM에 반영
- 렌더링 최적화: 상태가 변경될 때 전체 UI를 다시 그리지 않고, 변화된 부분만 추적하여 렌더링이 가능하다
- 브라우저 호환성: Virtual DOM을 통해 브라우저 간의 DOM 처리 차이점을 추상화할 수 있어, 여러 브라우저 환경에서 일관된 동작을 보장할 수 있다
리액트의 가상돔(Virtual DOM) 최적화 전략
- Batching Updates: 여러 상태 업데이트가 한 번에 발생할 경우, React는 이를 묶어서 한 번에 처리하여 불필요한 DOM 업데이트를 방지
- Reconciliation: 상태나 props가 변경될 때, React는 Virtual DOM을 사용해 변경된 부분만을 효율적으로 다시 렌더링
- Memoization:
React.memo
,useMemo
,useCallback
등 최적화 도구를 사용하여, 리렌더링 비용을 줄이는 방법도 제공한다
리액트의 메모이제이션(memoization)
메모이제이션은 함수의 결과를 캐싱하여 불필요한 계산을 방지하는 기법이다. 동일한 인풋이 주어질 때는 이전에 계산된 결과를 반환하여 성능을 최적화할 수 있다. React에서 메모이제이션은 주로
useMemo
, useCallback
, 그리고 React.memo
같은 API를 통해 제공된다.useMemo
는 함수의 결과를 메모이제이션한다. 특정 의존성 배열이 변경되지 않는 한, 이전에 계산된 값을 반환한다
useCallback
은 함수의 참조를 메모이제이션하여, 동일한 함수가 계속해서 재생성되지 않도록 최적화한다
- 장점: 메모이제이션을 통해 불필요한 연산 및 리렌더링을 피할 수 있어 성능을 크게 개선할 수 있다
- 주의점: 무조건 메모이제이션을 사용하는 것이 좋은 것은 아니다. 메모이제이션된 값을 관리하는 오버헤드가 있기 때문에, 너무 자주 사용하면 성능 이점이 사라질 수 있다. 특히, 메모이제이션된 값이 거의 변경되지 않거나 연산이 무거운 경우에만 사용해야 한다
얕은비교(Shallow Compare)와 깊은비교(Deep Compare)
얕은비교
- 얕은비교는 객체의 참조값이나 1차원적인 속성을 비교하는 방식이다. React는
props
나state
가 변경되었는지 확인할 때, 기본적으로 얕은 비교를 사용한다. 이는 값의 메모리 주소(참조)를 비교하거나, 원시 값(숫자, 문자열 등)을 직접 비교하는 방법
const obj1 = { a: 1 }; const obj2 = { a: 1 }; console.log(obj1 === obj2); // false
위 코드는
obj1
과 obj2
가 내용적으로는 동일하지만, 참조가 다르기 때문에 얕은 비교에서는 false
로 평가됩된다. 객체 내부의 값을 비교하는 것이 아니라, 객체 자체의 참조 주소를 비교하기 때문이다.React에서
shallow compare
는 주로 React.memo
와 shouldComponentUpdate
에서 사용된다.여기에는 한계도 존재하는데 다음과 같다.
- 참조 무결성이 중요한 구조에서는, 값이 변경되지 않았더라도 참조가 변경되면 리렌더링이 발생할 수 있다
- 복잡한 구조의 객체나 중첩된 데이터에서는 값이 변경되어도 이를 감지하지 못할 수 있다. 예를 들어, 배열이나 객체 안의 내용이 바뀌어도 참조가 그대로일 경우, React는 변화를 감지하지 못한다
깊은비교
Deep compare는 객체 내부의 모든 속성과 중첩된 객체까지 재귀적으로 비교하는 방식. 이는 객체가 내용적으로 같은지를 판단하는 방식이며, 객체의 속성 하나하나를 비교하여 내부 값이 변경되었는지 확인한다.
const obj1 = { a: 1, b: { c: 2 } }; const obj2 = { a: 1, b: { c: 2 } }; function deepEqual(obj1, obj2) { // 재귀적으로 내부 값을 비교하는 코드 if (typeof obj1 === 'object' && typeof obj2 === 'object') { for (let key in obj1) { if (!deepEqual(obj1[key], obj2[key])) return false; } return true; } return obj1 === obj2; } console.log(deepEqual(obj1, obj2)); // true
위 코드에서
obj1
과 obj2
는 참조가 다르지만, 내용적으로는 동일하다. 깊은 비교에서는 이를 인식하여 true
로 반환한다.React에서의 Deep Compare
React는 기본적으로 얕은 비교만 사용하기 때문에, 깊은 비교를 적용하려면 직접 구현하거나 라이브러리를 사용해야 한다. 예를 들어,
shouldComponentUpdate
에서 깊은 비교를 수행하려면, props와 state를 직접 재귀적으로 비교해야 한다.리액트에서 prop 및 state 값의 변경을 감지하기 위해서 깊은비교를 택하지 않은 이유가 있는데 다음과 같다.
- 성능문제: 깊은비교는 객체의 중첩 구조가 복잡할수록 성능에 부담이 된다. 특히 대규모 데이터가 있거나 빈번하게 변경되는 경우, 깊은 비교는 성능 병목을 일으킬 가능서이 높다
- 비효율성: 모든 속성을 비교하므로, 필요이상으로 리소스를 낭비할 수 있다
const data = { a: 1, b: { c: 2 } }; function MyComponent({ data }) { return <div>{data.b.c}</div>; } <MyComponent data={data} />;
만약
data.b.c
가 변경되었을 때, 얕은 비교로는 이를 감지할 수 없다. 주소값이 직접적으로 바뀌지 않기 때문. 이럴 경우, 상태 관리 라이브러리나 사용자 정의 shouldComponentUpdate
를 통해 깊은 비교를 적용할 수 있습니다. 따라서 리액트는 함수형 패러다임과 불변성을 활용하여 기존의 값을 변경하는 것이 아닌, 전혀 새로운 값을 재 생성하여 불변성을 유지하면서 상태변경을 감지한다. 불변성을 유지하면, 객체 내부의 값이 변경될 때마다 새로운 객체를 생성하고, 기존 객체와 새로운 객체는 다른 참조를 가지게 된다. 이로 인해 얕은 비교를 통해 상태 변화가 감지될 수 있다.const data = { a: { b: { c: 1 } } }; // 불변성을 유지하여 상태 업데이트 const newData = { ...data, // 얕은 복사 (참조가 다름) a: { ...data.a, // 내부 객체도 복사 b: { ...data.a.b, // 가장 깊은 객체도 복사 c: 2 // 값 변경 } } }; console.log(data === newData); // false (참조가 다름) console.log(data.a === newData.a); // false console.log(data.a.b === newData.a.b); // false
(optional) useSyncExternalStore와 이벤트 리스너를 같이 사용하기
import React, { useSyncExternalStore } from 'react'; // 외부 스토리지에 접근하는 함수 function getStorageValue(key) { return localStorage.getItem(key); } // 구독하는 함수 function subscribe(key, callback) { const storageListener = () => { callback(); }; window.addEventListener('storage', storageListener); // 구독 해제 함수 return () => { window.removeEventListener('storage', storageListener); }; } // 컴포넌트 function StorageComponent() { const key = 'myData'; const value = useSyncExternalStore( subscribe.bind(null, key), // 구독 함수 () => getStorageValue(key), // 값을 가져오는 함수 ); return ( <div> <h1>Stored Value: {value}</h1> <button onClick={() => localStorage.setItem(key, 'New Value')}> Update Storage </button> </div> ); } export default StorageComponent;
useRef는 무엇인가
useRef
는 불변성을 유지하지 않는 객체를 생성하는 훅이다. 즉, useRef
로 생성된 객체의 current
속성은 변경 가능하며, 이 속성의 값을 언제든지 수정할 수 있습니다. 이는 불변성을 유지하는 React의 기본 원칙과는 다소 다른 개념이다.불변성과
useRef
useRef
의 동작 방식:useRef
를 사용하면{ current: ... }
형태의 객체가 생성된다. 이 객체의current
속성은 어떤 값으로든 변경할 수 있다- 예를 들어,
const myRef = useRef(initialValue);
라고 선언하면,myRef.current
의 초기값을 설정할 수 있으며, 이후에 이 값을 직접 수정할 수 있다
- React의 불변성 원칙은 주로 상태 관리 (
useState
,useReducer
)와 관련이 있다. 상태를 변경할 때는 새로운 객체를 반환하고, 이전 상태를 변경하지 않아야 한다. 이 불변성을 통해 React는 상태의 변경을 감지하고, 최적화된 방식으로 리렌더링을 수행한다
useRef
는 이러한 불변성 원칙과는 독립적으로 작동한다. 즉, 참조 객체를 사용하여 내부 상태를 변경할 수 있지만, 이로 인해 리렌더링이 발생하지 않기 때문에 성능에 긍정적인 영향을 줄 수 있다
useState는 무엇인가
useState
는 React에서 상태를 관리하기 위해 사용되는 훅으로, 상태의 변화를 감지하고 컴포넌트를 리렌더링하는 데 중요한 역할을 한다.useState의 내부 동작
- 상태 변수 및 업데이트 함수 생성
useState
를 호출하면 React는 현재 컴포넌트에 대한 상태 변수와 그 값을 업데이트하기 위한 함수를 생성한다- 예를 들어,
const [count, setCount] = useState(0);
라고 선언하면,count
는 현재 상태의 값이고,setCount
는 이 값을 업데이트하기 위한 함수이다
- 상태 업데이트:
setCount
를 호출하면 새로운 상태 값이 제공된다. 이때 React는 다음과 같은 작업을 수행한다- 내부적으로 상태 값을 업데이트하고, 해당 컴포넌트를 리렌더링해야 한다고 플래그를 설정
- 이 플래그는 React가 다음 렌더링 사이클에서 해당 컴포넌트를 업데이트해야 한다는 것을 의미한다
- Virtual DOM 업데이트
- React는 상태 업데이트가 발생한 후에 Virtual DOM을 사용하여 UI를 다시 그린다. 이는 다음과 같은 방식으로 이루어진다
- 새로운 Virtual DOM 트리 생성: 현재의 상태와 새로운 상태를 바탕으로 새로운 Virtual DOM 트리를 생성한다
- Diffing: 이전 Virtual DOM과 새로 생성된 Virtual DOM 간의 차이를 비교하여 어떤 부분이 변경되었는지를 계산한다. 이 과정에서 React는 효율적으로 변경된 부분만 실제 DOM에 반영한다
- DOM 업데이트
- diffing 과정을 통해 확인된 변경 사항을 실제 DOM에 반영하여 UI를 업데이트한다. 이 과정은 효율적이며 성능 최적화를 도와준다