중간중간 들어가는 🤔 표시는 공식문서에는 없지만 더 알면 좋을 것 같아 내가 정리해본 내용,
📄 는 공식문서임을 의미한다.
바쁘신 분들은 🌟 은 꼭 읽어보시길 추천드린다.
e.stopPropagation()
을 호출해 중지할 수 있다.📑 브라우저는 특정 화면 요소에서 이벤트가 발생했을 때 그 이벤트를 최상위에 있는 화면 요소까지 전파시킨다.
버블링(bubbling)은, 이런 브라우저의 이벤트 전파 방식으로 인해 발생하는 특징이다.
드물지만 stop propagation을 호출한 경우에도, children에 대한 이벤트를 잡아야하는 경우가 있다. (ex - 화면내의 모든 click event에 log를 걸어야하는 경우) 이때는 Capture라는 이벤트를 추가하면 된다.
이벤트 캡쳐(Event Capture)는 버블링과 반대로, parent > children로 이벤트가 전파되는 방식이다.
e.stopPropagation()
이벤트 전파(bubbling) 막기e.preventDefault()
몇몇 이벤트에 대해 브라우저가 기본적으로 가진 동작을 막기(ex- 위 예시 form onSumit시 page reload)위 예시에서 handleClick
이벤트는 지역 변수, index
를 업데이트 하는데, 두가지 요인으로 변화가 보이지 않는것을 확인할 수 있다.
새로운 데이터로 업데이트 하기 위해서는, 아래 두가지 조건이 필요하다.
useState Hook은 아래 2개를 제공한다.
📑 use로 시작하는 Hook은 컴포넌트의 top level이나 customHook을 통해서만 호출 가능합니다.
Hook을 조건문이나, 반복문, 함수 내에서 호출할 수 없습니다.
[0, setIndex]
를 반환. 리액트는 0
을 가장 최근 상태값으로 저장한다.setIndex(1)
을 호출하게된다. setter function는 index
의 값을 1
로 저장하고, 다른 렌더를 트리거한다.useState(0)
을 확인하지만, index
를 1
로 바꾼것을 기억하고 있으므로 [1, setIndex]
를 반환한다.🤔 진짜 React Hook 파일은 어떻게 구성되어 있을까?
- useState & resolveDispatcher : initialState과 함께 ReactCurrentDispatcher.current 호출 → 클로저를 활용해 함수 외부값에 접근
- ReactCurrentDispatcher : 전역으로 선언된 객체 프로퍼티
- dispatcher.useState 내부 함수
- 실행할 hook을 가져온 뒤, 저장되어 있는 state(state variable)을 불러온다. 없으면 initialState를 가져온다.
- 함수가 호출되면 로그 정보를 hookLog 배열에 순서대로 추가한다. 이때 현재 값(state), primitive, 호출된 위치의 스택정보(stackError) 도 함께 저장된다.
- setter function 호출 시, hookLog 스택을 돌며 렌더를 트리거하고 값을 업데이트 한다
(→ 이 내용부터는 코드가 너무 복잡해져서 다른 글로 작성해보는게 좋을듯하다)
useState를 호출할 때, 자신이 참조하는 state에는 아무 정보가 없다는 것을 확인할 수 있다.
위 예시로 살펴보자면 index
- 0, showMore
- false로 매칭되는데 useState를 호출할 때는 아무런 “identifier”가 없다.
다시 말해, 0이 index로 매칭되고 showMore가 어떻게 false로 매칭될까?
따로 key를 선언한 것도 아니고 그냥 useState를 두 개 선언했을 뿐인데!
그럼 state마다 어떤 value를 반환할지 도대체 어떻게 알 수 있는걸까?
React의 Hooks은 동일한 컴포넌트를 렌더링할 때마다 안정적인 호출 순서에 의존한다. 위 내용에서 “Hook은 최상위 수준에서만 호출가능”하다고 했었는데, 이 특징으로 인해 Hook은 매번 같은 순서로 호출되게 된다. (추가적으로 linter plugin이 실수를 잡아낸다.)
내부적으로 React는 모든 컴포넌트에 대한 state pair 배열을 보유하고 있다.
이 배열은 렌더 전부터 0으로 초기화 된 현재 pair 인덱스를 유지하는데, 이를 사용해 useState를 호출할 때마다, 다음 state pair를 제공하고 인덱스를 증가시킨다.
이 내용에 대한 자세한 내용은 React hook: not magic, just arrays 에서 확인할 수 있다.
아래는 실제 React에서 사용되는 코드는 아니지만, useState의 동작 원리에 맞춰 설계한 코드이다. (공식문서에 나와있는 코드 그대로)
let componentHooks = [];
let currentHookIndex = 0;
function useState(initialState) {
let pair = componentHooks[currentHookIndex];
if (pair) {
// 첫번째 렌더가 아니므로, state pair가 이미 존재한다.
// 이 pair를 반환하고 다음 훅이 호출되면 사용한다.
currentHookIndex++;
return pair;
}
// 첫번째 렌더링시 생성 및 저장되는 pair (초기값, setState)
pair = [initialState, setState];
function setState(nextState) {
// state를 변경하면, pair[0] - value에 새로운 값을 할당하고 DOM을 업데이트한다
pair[0] = nextState;
updateDOM();
}
// pair를 저장하고, 다음으로 호출될 Hook 준비 (index+1)
componentHooks[currentHookIndex] = pair;
currentHookIndex++;
return pair;
}
function Gallery() {
// useState를 사용해 pair값을 받을 수 있다
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
function handleNextClick() {
setIndex(index + 1);
}
function handleMoreClick() {
setShowMore(!showMore);
}
let sculpture = sculptureList[index];
return {
onNextClick: handleNextClick,
...
};
}
function updateDOM() {
// 렌더링 전, currentHookIndex 초기화 (저장/업데이트 된 pair 불러옴)
currentHookIndex = 0;
let output = Gallery();
// Update the DOM to match the output.
// This is the part React does for you.
nextButton.onclick = output.onNextClick;
...
}
let nextButton = document.getElementById('nextButton');
...
updateDOM();
state는 화면 컴포넌트 구성요소에 local로 존재한다. 동일한 컴포넌트를 2번 렌더링하면, 각 복사본(컴포넌트)은 완전히 독립적인 state를 가진다.
내부의 독립적인 state들은 서로 영향을 주지 않는다.
이 점이 모듈 상단에서 선언할 수 있는 일반 변수(지역변수)와 다른 점이다. State는 화면의 특정 위치에 local로 존재한다. 위 예제에서 두 개의 <Gallery />
구성요소를 렌더링했으므로, 각 state가 별도로 저장된다.
또한 페이지 컴포넌트는 <Gallery />
state에 관해 아무것도 알지 못한다. props와 달리 state는 이를 선언하는 컴포넌트(Page)에도 완전히 비밀로 유지된다. parent 컴포넌트에서 이를 바꿀 수 없다.
두 <Gallery />
상태를 동일하게 유지하고 싶다면, 각 컴포넌트에 있는 state를 제거해 parent에서 이를 관리해야한다.
컴포넌트가 렌더되는 2가지 원인은 아래와 같다.
초기 렌더링(Initial render)
렌더할 DOM 노드에 [createRoot](https://react.dev/reference/react-dom/client/createRoot)
와 render
method를 함께 호출하면 된다.
컴포넌트(혹은 부모 컴포넌트)의 state가 업데이트
초기 렌더링 후에, set function
으로 state를 업데이트 할 수 있다.
state를 업데이트하면 렌더링이 자동으로 대기열에 추가된다.
📄 ”렌더링” 이란 리액트가 컴포넌트를 호출하는 것을 의미한다.
초기 렌더링
리렌더링
이전 렌더와 비교해 변경된 내용이 있는지 계산
📄 렌더링은 순수 함수여야한다.
- 동일한 input, 동일한 output
- 현재 주어진 일만 처리1
초기 렌더링
[appendChild()](https://developer.mozilla.org/ko/docs/Web/API/Node/appendChild)
DOM API를 사용해 DOM 노드를 생성
리렌더링
렌더링이 끝나고 리액트가 DOM을 업데이트하면, 브라우저가 화면을 repaint 한다.
(이 프로세스 전체를 “브라우저 렌더링” 이라고 하지만, 문서 내에서는 혼동을 제거하기 위해 “페인팅” 이라고 명명)
렌더링 후 반환되는 JSX는 그 시간에 찍힌 UI 스냅샷 같은 함수이다.
이때 찍힌 스냅샷에는 이벤트 핸들러, props, 지역 변수등이 모두 포함되어 있다.
컴포넌트를 re-render 하면 아래 과정이 실행된다.
state는 컴포넌트 메모리로서 함수 반환과 함께 삭제되지 않는다.
컴포넌트는 렌더링 한 state 값을 사용해 props, 이벤트 핸들러를 포함한 JSX 스냅샷을 계산한다.
setNumber
함수가 3번 실행된다. 따라서 버튼을 눌러도 1만 증가한다.위 예제 코드를 실행하면 alert에 무엇이 뜰까?
정답은….. “0”이 뜬다!
(위에서 다 나온 얘기지만) setTimeout
내의 number
는 0
으로 스냅샷이 저장되어 있으므로, 렌더링 전 값인 0을 반환하게 된다.
state 변수값은 이벤트 핸들러가 비동기적이여도 렌더 전에는 절대 변경되지 않는다.
React는 한 렌더의 이벤트 핸들러 내에서 state 값을 “고정”으로 유지한다.
이전 예제에서, 각 렌더마다 state 상태는 고정이기 때문에 결국 setNumber(0 + 1);
이 3번 호출된다고 했었는데, 숨겨진 사실이 하나 더 있다.
리액트는 state 업데이트를 처리하기 전에, 모든 이벤트 핸들러 코드가 실행되는 것을 기다린다. 이것이 모든 setNumber() 호출후에 리렌더링 되는 이유이다.
이를 batching 이라 하며, 다발적인 state 변화를 많은 re-render 없이 업데이트할 수 있게 해주므로 앱을 더욱 빠르게 만들어준다.
또한 모든 이벤트 핸들러가 실행 및 완료되고 나서야 UI를 업데이트하는 특징이 있다.
n => n + 1
을 update function이라 한다. 이를 사용하면,
update function 외에 다른값이 들어오면 이미 대기열에 있는 항목은 무시되고 replace 함수가 대기열에 추가된다.
update function은 렌더링 중에 실행되므로, 순수 함수여야하며 결과를 반환해야 한다.
object state 내부 value를 스스로 변경이 가능한 위와 같은 형태를 mutation이라고 한다.
하지만 object가 변경 가능 하더라도 리액트 내에서는 불변성을 지켜줘야하므로, 값을 넣는 대신 변경해야한다.
중첩된 객체가 많아서 코드가 복잡해졌을때, immer를 사용하면 불변성을 신경쓰지 않으면서 가독성있는 코드를 작성할 수 있다.
🤔 이외에도 다수 있는데.. 그냥 사용하지 말자.
slice
만 사용하자!나머지 내용은, 아는내용 + object 에 있는 내용이라 패스