React 함수형 컴포넌트에서 가장 중요한 개념은 Hook이다. 함수형 컴포넌트가 각광받게 된 이유는 무엇보다도 클래스형 컴포넌트보다 간결하다는 이유이다.
useState는 함수형 컴포넌트 내부에서 상태를 정의하고 상태를 관리할 수 있게 해주는 Hook이다.
useState의 인수로는 사용할 state의 초깃값을 넘겨준다. useStat 반환값은 배열이며, 배열의 첫 번째 원소로는 state 값이며, 두 번째 원소로는 setState 함수를 사용해 해당 state의 값을 변경한다.
function Componenet() { let state = 'hello'; function handleButtonClick() { state = 'hi'; } return ( <> <h1>{state}</h1> <button onClick={handleButtonClick}>hi</button> </> ) }
위 코드는 렌더링 발생 시 리렌더링이 되지 않는다. 리액트에서 렌더링은 함수형 컴포넌트의 return과 클래스형 컴포넌트의 render 함수를 실행한 다음, 이 실행 결과를 이전의 리액트 트리와 비교해 리렌더링이 필요한 부분만 업데이트해 이뤄진다. 위 코드는 두 조건을 만족하지 않아 리렌더링이 발생하지 않는다.
function Componenet() { const[, triggerRender] = useState(); let state = 'hello'; function handleButtonClick() { state = 'hi'; trigerRender(); } return ( <> <h1>{state}</h1> <button onClick={handleButtonClick}>hi</button> </> ) }
useState를 사용하면서, 리액트에서 렌더링이 일어나게끔 변경하였지만, state 값은 hi가 아닌 hello로 할당되어 있을 것이다. state가 업데이트 되었지만, 렌더링이 되지 않는 이유는 함수형 컴포넌트는 렌더링될 때마다 함수를 다시 새롭게 실행하기 때문이다. 새롭게 실행되는 함수에서 state는 매번 hello로 초기화되므로 아무리 state를 변경해도 다시 hello로 초기화되는 것이다.
리액트의 내부 구현을 하나도 모른다고 가정하고 useState가 어떤 구조를 가지고 있을 지 상상해보자
//const [value, setValue] = useState(0); //setValue(1); //console.log(value); // 0 // 신기하네 왜지? value는 1이 할당되야 하는데?
이러한 결과 발생하는 이유는 setValue로 값을 변경했음에도 이미 구조 분해 할당[value, setValue]으로 state의 값을 이미 할당해 놓은 상태이기 떄문에 훅 내부 useState를 호출하더라도 변경된 새로운 값을 반환하지 못하는 것이다.
리액트는 클로저를 사용해서, 어떤 함수(useState) 내부에 선언된 함수(setState)가 함수의 실행이 종료된 이후에도(useState가 종료된 이후) 지역변수인 state를 계속 참조할 수 있다.
useState 내부의 모습을 구현한 모습
const MyReact = function() { const global = {}; let index = 0; function useState(initialState) { if(!global.states){ // 애플리케이션 전체의 states 배열을 초기화한다. // 최초 접근이라면 빈 배열로 초기화한다. global.states = []; } // states 정보를 조회해서 현재 상태값이 있는 지 확인하고, // 없다면 초깃값으로 설정한다. const currentState = global.states[index] || initialState; globla.state[index] = currentState; // 즉시 실행 함수로 setter를 만든다. const setState = (function () { // 현재 index를 클로저로 가둬놔서 이후에도 계속해서 동일한 index에 // 접근할 수 있도록 한다. ) let currentIndex = index; return function (value) { global.states[currentIndex] = value; // 렌더링 하는 과정 생략 } })() // useState를 쓸 때마다 index를 하나씩 추가한다. // index는 setState에서 사용된다. // 하나의 state마다 index가 할당돼 있어 그 index가 배열의 값을 // 가리키고 필요할 때마다 그 값을 가져오게 된다. index += 1; return [currentState, setState] } // 실제 useState를 사용하는 컴포넌트 function Component() { const [value, setValue] = useState(0); }
MyReact의 실행이 끝났음에도 불구하고 useState 함수를 사용할 수 있는 이유는 클로저덕분이다. MyReact 클로저 내부에 useState와 관련된 정보를 저장해 두고 이를 필요할 때마다 꺼내 놓는 형식으로 구성돼있다.
useState는 자바스크립트의 특징 중 하나인 클로저에 의존해 구현돼 있을 것이라는 사실을 짐작해볼 수 있다. 클로저를 사용함으로써 외부에 해당 값을 노출시키지 않고 오직 리액트에서만 쓸 수 있었고, 함수형 컴포넌트가 매번 실행되더라도 useState에서 이전의 값을 정확하게 꺼내 쓸 수 있다.
일반적으로 우리는 useState를 사용하는 경우 인수로 원시값을 넣는 경우가 대다수일 것이다. 하지만, 인수로 특정한 값을 넘기는 함수를 인수로 넣어줄 수도 있다. useState의 인수를 변수가 아닌 함수를 넘겨주는 경우를 게으른 초기화라고 한다. 게으른 초기화 함수는 오로지 state가 처음 만들어질 때만 사용된다. 만약 이후에 리렌더링이 발생된다면 이 함수의 실행은 무시된다.
리액트는 렌더링이 실행될 때마다 함수형 컴포넌트의 함수가 다시 실행된다. 즉, useState의 값도 재실행될 것이다(useState 내부에는 클로저가 존재하고, 클로저를 통해 값을 갖고오며 초기값은 최초에만 사용된다.). 만약 useState 인수로 자바스크립트에 많은 비용을 요구하는 작업이 들어가 있다면 이는 계속해서 실행될 위험이 존재할 것이다. 그러나 우려와는 다르게 useState 내부에 함수를 넣으면 이는 최초 렌더링 이후에는 실행되지 않고, 최초의 state 값을 넣을 때만 실행된다.
useState 인수로 많은 비용이 드는 값을 있다고 가정하면, 초기값뿐만 아니라 리렌더링 시에 동일하게 값이 전달되므로 비용이 증가할 것이다. 이러한 경우에는 값으로 인수를 전달하기보단, 함수 형태로 넘겨주는 것이 경제적일 것이다.
주로 게으른 초기화는 Storage 접근이나, 고차함수 같은 배열에 대한 접근, 함수 호출이 필요할 때와 같이 무거운 연산을 포함해 실행 비용이 많이 드는 경우에 사용하는 것이 좋다.
useEffect 정의
- useEffect는 두 개의 인수를 받는데, 첫 번째는 callback 두 번째는 의존성 배열이다. 두 번째 의존성 배열의 값이 변경하면, 첫 번째 인수인 callback을 실행한다.
- 두 번째 인수인 의존성 배열에 빈 배열을 전달하면, mount(최초 렌더링 시)될 때마다 실행된다.
- useEffect는 클린업 함수를 반환할 수 있는데, 이 클린업 함수는 컴포넌트가 unMount(컴포넌트가 더 이상 존재하지 않을 때)될 때 사용된다.
useEffect의 정의를 정확하게 내리자면 useEffect는 애플리케이션 내 컴포넌트의 여러 값을 활용해 동기적으로 부수 효과를 만드는 메커니즘이다.
일반적인 useEffect 형태
function Componenet() { //... useEffect(() => { // do something },[props, state]); //.. }
첫 번째 인수로는 실행할 부수 효과가 포함된 함수를, 두 번째 인수로는 의존성 배열을 전달한다. 우리는 의존성 배열이 변경될 때마다 useEffect의 첫 번째 인수인 콜백을 실행한다는 것은 널리 알려진 사실이다. 하지만 useEffect는 어떻게 의존성 배열이 변경된 것을 알고 실행될까? 함수형 컴포넌트는 매번 함수를 실행해 렌더링을 수행한다.
function Component() { const [counter, setCounter] = useState(0); function handleClick() { setCounter((prev) => prev + 1) } useEffect(() => { function addMouseEvent() { console.log(counter); } window.addEventListener('click', addMouseEvet); // Clean Up Function return () => { console.log('클린업 함수 실행!', counter); window.removeEventListener('click', addMouseEvent); } }, [counter]); return ( <> <h1>{counter}</h1> <button onClick={handleClick}>+</button> </> ); }
useEffect는 렌더링할 때마다 의존성에 있는 값을 보면서 의존성의 값이 이전과 다른게 하나라도 있으면, 부수 효과를 실행하는 평범한 함수라 볼 수 있다. useEffect는 state나 props의 변화 속에서 일어나는 렌더링 과정에서 실행되는 부수 효과 함수라고 볼 수 있다.
클린업 함수는 이전 counter 값 이전 state를 참조해 실행된다. 클린업 함수는 비록 새로운 값을 기반으로 렌더링 뒤에 실행되지만 이 변경된 값을 읽는 것이 아니라 함수가 정의됐을 당시에 선언됐던 이전 값을 보고 실행된다는 것이다.
함수형 컴포넌트의 useEffect는 그 콜백이 실행될 때마다 이전의 클린업 함수가 존재한다면 그 클린업 함수를 실행한 뒤에 콜백을 실행한다. 이벤트를 추가하기 전에 이전에 등록했던 이벤트 핸들러를 학제하는 코드를 클린업 함수에 추가하는 것이다.
의존성 배열은 보통 빈 배열을 두거나, 아예 아무런 값도 넘기지 않거나, 혹은 사용자가 직접 원하는 값을 넣어줄 수 있다. 빈 배열시, 비교할 의존성이 없다고 판단해 최초 렌더링 직후에 실행된 다음 더 이상 실행되지 않는다. 아무런 값도 넘겨주지 않는다면 의존성을 비교할 필요가 없어 렌더링 발생할 때마다 실행된다.
의존성 배열이 없는 useEffect가 매 렌더링마다 실행된다면 그냥 useEffect 없이 사용해도 되지 않나요?
// 1. useEffect 없이 렌더링 확인 함수 function ComponenetA() { console.log("render"); } // 2. 의존성 배열 없는 useEffect 렌더링 확인 함수 function ComponentB(){ console.log('render'); }
- 이후에 소개할 서버 사이드 렌더링 관점에서 useEffect는 클라이언트 사이드에서 실행되는 것을 보장해준다. useEffect 내부에서 window 객체의 접근에 의존하는 코드를 사용해도 된다.
- useEffect는 컴포넌트 렌더링이 완료된 이후에 실행된다. 반면 직접 실행은 컴포넌트가 렌더링 되는 도중에 실행된다. 따라서 1번과 달리 서버 사이드 렌더링의 경우에 서버에서도 실행된다. 그리고 이 작업은 함수형 컴포넌트의 반호나을 지연시키는 행위다. 즉 무거운 작업일 수록 렌더링을 방해하므로 성능에 악영향을 줄 수 있다.
useEffect는 컴포넌트가 렌더링된 후에 어떠한 부수 효과를 일으키고 싶을 때 사용하는 Hook이다.
const myReact = (function() { const global = {}; let index = 0; function useEffect(callback, dependencies) { const hooks = global.hooks; // 이전 훅 정보가 있는 지 확인하기 let previousDependencies = hooks[index]; let isDependenciesChanged = previousDependencies ? dependencies.some( (value, idx) => !Object.is(value, previousDependencies[idx]) ) : true if(isisDependenciesChanged) { callback(); } hooks[index++] = dependencies; } return { useEffect } })()
useEffect 핵심은 의존성 배열의 이전 값과 현재 값의 얕은 비교다. 이전과 현재 의존성 배열의 변경 사항이 있다면 callback으로 선언한 부수 효과를 실행한다.
useEffect는 리액트 코드를 작성할 때 가장 많이 사용하는 훅이면서 동시에 가장 주의해야 할 훅이다.
eslint-disable-line react-hooks/exhaustive-deps 주석은 자제하자
ESLint 룰은 useEffect 인수 내부에서 사용하는 값 중 의존성 배열에 포함돼 있지 않은 값이 있을 때 경고를 발생시키다.
컴포넌트가 마운트 하는 시점에만 무언가 실행시키고 싶을 때 주로 빈 배열을 의존성 배열로 전달한다. 의존성 배열로 빈 배열을 채우는 것은 왠만해서 사용하지 않는 편이 좋다. useEffect의 주요 목적은 의존성 배열로 전달한 값의 변경에 의해 실행되는 훅이다. 그러나 의존성 배열을 넘기지 않은 채 콜백 함수 내에 특정 값을 사용한다는 것은 부수 효과가 실제로 관찰해서 실행되야 하는 값(state, prop)과 별개로 작동한다는 것을 의미한다.
function Component({ log } : { log: string }) { useEffect(() => { logging(log); }, []) }
위 코드는 log가 props로 넘어와 최초 렌더링 시에 실행된다. 코드를 작성한 의도는 최초 렌더링시에만 logging을 실행하고 싶어서 일 것이다. 하지만 위 코드는 props인 log에 변동 사항에 useEffect에 부수 효과가 실행되지 않고 useEffect의 흐름과 props.log의 흐름이 맞지 않는다.
useEffect에 빈 배열을 넘기기 전에는 정말로 useEffect의 부수 효과가 컴포넌트의 상태와 별개로 작동해야만 하는지, 혹은 여기서 호출하는 게 최선인 지 한 번 더 검토해 봐야 한다.
빈 배열이 아닐 때도 항상 조심해야한다. useEffect 조심해야할 게 너무 많다. 특정 값을 사용하지만 해당 값의 변경 시점을 피할 목적이라면 메모이제이션을 적절히 활용해 해당 값의 변화를 막거나 적당한 실행 위치를 다시 한 번 고민해 보는 것이 좋다.
즉 useEffect 내부에 props를 사용할 때는, 의존성 배열에 props를 추가하는 편이 좋다. -> 사실 이 방법도 props 변경으로 인해 상위 컴포넌트에 영향을 줄 수 있으니 memo 사용하는 것이 좋다
useEffect의 첫 번째 인수(Callback)에 함수명을 부여하라
대부분의 useEffect는 첫 번째 인수로 익명 함수를 넘긴다. useEffect의 코드가 복잡하고 많아질수록 useEffect가 어떤 역할을 하는 지 파악하기 힘들다. 그래서 우리는 callback 함수를 익명 함수가 아닌 기명 함수로 바꾸는 것이 좋다.
// callback 함수가 익명 함수가 아닌 기명 함수 useEffect( function logActiveUser() { logging(log); }, [user.id], )
거대한 useEffect를 만들지 말자
useEffect는 의존성 배열을 바탕으로 렌더링 시 의존성이 변경될 때마다 부수 효과를 실행한다. 부수 효과의 크기가 커질수록 애플리케이션 성능에 악영향을 미친다. 부득이하게 큰 useEffect를 만들게 되면 여러 개의 useEffect & 적은 의존성 배열를 이용하는 것이 좋다.
불필요한 외부 함수를 만들지 마라
useEffect 외부에 관련 함수를 useEffect 내부로 가져오면 가독성이 좋아지고, 불필요한 의존성 배열을 줄일 수 있으며, 무한루프에 빠지기 위해 넣었던 코드인 useCallback도 삭제할 수 있다.
useMemo는 비용이 큰 연산에 대한 결과를 저장해두고 이 저장된 값은 반환하는 훅이다. 첫 번째 인수로는 어떠한 값을 반환하는 생성 함수, 두 번째 인수로는 해당 함수가 의존하는 값의 배열을 전달한다. useMemo는 렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면 함수를 재실행하지 않고 이전에 기억해 둔 해당 값을 반환하고, 이존성 배열의 값이 변경됐다면 첫 번째 인수의 함수를 실행한 후에 그 값을 반환하고 그 값을 다시 기억해 둘 것이다.
import { useMemo } from 'react' const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b])
useMemo가 값을 기억했다면 useCallback은 인수로 넘겨받은 콜백 자체를 기억한다. useCallback은 특정 함수를 새로 만들지 않고 다시 재사용한다는 의미다.
const ChildComponent = memo({ name, value, onChange }) => { useEffect(() => { console.log('rendering!', name)); }) return ( <> <h1> {name} {value ? "켜짐" : "꺼짐"} </h1> <button onClick={onChange}>toggole</button> </> ) }); fucntion App() { const [status1, setStatus1] = useState(false); const [status2, setStatus2] = useState(false); const toggle1 = useCallback( function toggle1() { setStatus1(!status1); }, [status1], ) const toggle2 = useCallback( function toggle2() { setStatus2(!status2); }, [status2], ) return ( <> <ChildComponenet name='1' value={status1} onChange={toggle1} /> <ChildComponenet name='2' value={status2} onChange={toggle2} /> </> ) } // 주의사항 <React.StrictMode>는 개발모드에서 // (개발 단계시 오류를 잘 잡기위해) 두 번씩 렌더링됩니다.
만약 toggle1 / toggle2 함수에 useCallback을 사용하지 않는다면, Event 발생 시, state가 변경되는 데 이 과정에서 리렌더링이 발생한다. 그럼 function App은 다시 실행될 것이며, function toggle1, 2 또한 재 실행될 것이다. memo의 의존성 배열이 변경되었기 때문에 ChildrenComponent 또한 렌더링될 것이다. 우리는 useCallback을 사용함으로써, 오류를 막을 수 있다.
useCallback을 추가하면 해당 의존성이 변경됐을 때만 함수가 재생성되는 것을 볼 수 있다. 이처럼 함수의 재생성을 막아 불필요한 resource 또는 rendering을 방지하고 싶을 때 useCallback을 사용해 볼 수 있다.
useMemo와 useCallback의 유일한 차이는 메모이제이션 하는 대상이 변수냐 함수냐일 뿐이다. useMemo로도 useCallback을 구현할 수 있는데, 가독성이 떨어지기 때문에 함수를 메모이제이션 하는 경우에는 useCallback을 사용하자
useRef는 useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있다.
useRef와 useState 차이점
- useRef는 반환 값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
- useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
렌더링에 영향을 미치지 않는 고정된 값을 관리하기 위해 useRef를 사용하는 것보단 함수 외부에 값을 선언해서 관리하는 것도 동일한 기능을 수행할 수도 있지 않을까?
함수 외부에서 값을 관리하면, 렌더링이 되지 않았음에도 불구하고 값을 갖고 있어서, 메모리 측면에서 악영향을 미친다. 또한, 여러 Component의 인스턴스가 여러 개면 각각 별개의 값이 필요할 때, 함수 외부에 값을 선언해서 관리하는 방법은 옳지 않다.
useRef는 최초에 넘겨받은 기본값을 가지고 있다. useRef를 사용할 수 있는 유용한 경우는 렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있다는 특징을 활용해 useState의 이전 값을 저장하는 usePrevious() 같은 훅을 구현할 때이다.
useEffect는 렌더링이 된 이후 부수효과를 실행함
import logo from "./logo.svg"; import "./App.css"; import { useState, useCallback, useEffect, memo, useRef } from "react"; function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } function App() { const [counter, setCounter] = useState(0); const previousCounter = usePrevious(counter); function handleClick() { setCounter((prev) => prev + 1); } return ( <button onClick={handleClick}> {counter} {previousCounter} </button> ); } export default App;
useRef에서 중요한 두 가지 - 값이 변경되어도 렌더링이 안 된다 / 실체 값은 { current : value }와 같은 객체 형태로 있다.
React는 기본적으로 Parent Component와 Child Component로 이루어진 트리 구조를 갖고 있기 때문에 Parent Component에서 사용한 data를 Child Component에서도 사용하고 싶으면, props로 넘겨주는 게 일반적이다. 부모와 자식간의 거리가 멀면 서로 불편하기 때문에, 이를 극복하기 위해 React는 Context라는 개념을 도입했다. props 전달 없이도, Child Component에서 자유롭게 원하는 값을 사용할 수 있다.
useContext로 전역 상태 관리하는 건,,, 코드의 복잡성을 높이는 결과를 야기하고, 렌더링 측면에서 문제가 생겨 memo를 사용해야하므로 다른 전역 상태 관리 라이브러리로 사용할게요
useReducer는 useState의 심화 버전이다.
useState와 useReducer 비교
- 반환값은 useState와 동일하게 길이가 2인 배열이다.
- state : 현재 useReducer가 가지고 있는 값을 의미한다. useState와 마찬가지로 배열을 반환하는데, 동일하게 첫 번째 요소가 값이다.
- dispatcher : state를 업데이트 해주는 함수 useReducer가 반환하는 두 번째 요소이다. setState와 차이점은 setState는 단순히 값을 넘겨주지만, useReducer는 action을 넘겨준다.
- useState의 인수와 달리 2개에서 3개의 인수를 필요로 한다.
- reducer: userReducer의 기본 action을 정의하는 함수이다. 이 reducer는 useReducer의 첫 번째 인수로 넘겨주어야 한다.
- initialState : 두 번째 인수로 useReducer의 초기값을 의미한다.
- init : useState의 인수로 함수를 넘겨줄 때처럼 초깃값을 지연해서 생성시키고 싶을 때 사용하는 함수다. 이 함수는 필수값이 아니며, 만약 여기에 인수로 넘겨주는 함수가 존재한다면 useState와 동일하게 게으른 초기화가 일어나며 initialState를 인수로 init 함수가 실행된다.
// useReducer가 사용할 state 정의 interface StateType { count: number } // state의 변화를 발생시킬 action의 타입과 넘겨줄 값을 정의 // 꼭 type과 payload라는 네이밍을 지킬 필요도 없으며 굳이 객체일 필요도 없다. // 다만 이러한 네이밍이 가장 널리 쓰인다. interface ActionType { type: 'up' | 'down' | 'reset'; payload?: StateType; // 무거운 연산이 포함된 게으른 초기화 함수 function init(count: StateType): StateType { // count: StateType를 받아와서 초기값을 어떻게 정의할 지 연산하면 된다. return count; } // 초깃값 const initialState: StateType = { count : 0 }; // 앞서 선언한 state와 action을 기반으로 state가 어떻게 변경될지 정의 function reducer(state: StateType, action: actionType): StateType { switch(action.type){ case 'up': return { count: state.count + 1 } case 'down': return { count: state.count - 1 > 0 ? state.count - 1 : 0 } case 'reset' return init(action.payload || { count: 0 }) default: throw new Error(`Unexpected action type ${action.type}`); } } export default function App() { const [state, dispatcher] = useReducer(reducer, initialState, init); function handleUpButtonClick(){ dispatcher({ type: 'up' }); } function handleDownButtonClick(){ dispatcher({ type: 'down' }); } function handleResetButtonClick(){ dispatcher({ type: 'reset', payload: { count: 1 } }); } return ( <div className='App'> <h1>{state.count}</h1> <button onClick={handleUpButtonClick}>+</button> <button onClick={handleDownButtonClick}>-</button> <button onClick={handleResetButtonClick}>reset</button> </div> ); }
useState에 비해 상대적으로 복잡해 보일 수도 있지만, useReducer는 복잡한 형태의 state를 사전에 정의한 dispatcher로만 수정할 수 있게 만들어줌으로써 state 값에 대한 접근은 컴포넌트에서만 가능하고, 변경하는 바법에 대한 상세 정의는 컴포넌트 밖에 둔 다음, state의 업데이트를 미리 정의해 둔 dispatcher로만 제한하는 것이다. state 값을 변경한느 시나리오를 제한적으로 두고 이에 대한 변경을 빠르게 확인할 수 있게끔 하는 것이 useReducer의 목적이다.
간단한 원시값을 관리하는 경우 useState를 사용하는 편이 낫지만, state 하나가 가져야 할 값이 복잡하고 이를 수정하는 경우의 수가 많아진다면 state 관리가 어려울 것이다. useReducer를 사용해 state 관리하면서 state를 사용하는 로직과 이를 관리하는 비즈니스 로직을 분리할 수 있어 state를 관리하기가 한결 쉬워진다.
이 파트는 잘 안 쓰이기도 하고, ref를 하위 컴포넌트로 전달하는 경우,,, problems가 너무 많아서 생략
useLayoutEffect 정의 - function signature == useEffect 하지만 DOM 변경 후에 동기적으로 발생한다.
useEffect & useLayoutEffect의 사용법 비교
function App() { const [count, setCount] = useState(0); useEffect(() => { console.log('useEffect', count); }, [count]); useLayoutEffect(() => { console.log('useEffect', count); }, [count]); function handleClick() { setCount((prev) => prev + 1); } return ( <> <h1>{count}</h1> <button onClick={handleClick}>+</button> </> ); }
리액트 리렌더링 과정 정리
Event로 인한 State 변경:
이벤트 (예: 버튼 클릭, 입력 등)가 발생하면 해당 EventHandler에서 setState 또는 관련된 상태 업데이트 함수를 호출하여 상태를 변경합니다.
State변경에 따른 리렌더링:
상태가 변경되면 해당 컴포넌트가 리렌더링되어 가상 DOM 트리(progressInTree)가 재생성됩니다.
Reconciliation (조화):
리액트는 새로운 가상 DOM 트리와 이전에 렌더링된 트리를 비교하고 변경된 부분을 찾아냅니다. 이 과정은 리액트 파이버 트리 내부의 progressInTree와 currentTree에서 처리됩니다.
가상 DOM에서 실제 DOM으로 이동:
변경된 부분에 대한 업데이트는 가상 DOM을 기반으로 이루어지며, 실제 DOM에 반영됩니다. (이 때, useLayoutEffect 실행)
실제 DOM에 UI 적용:
업데이트된 내용이 실제 DOM에 적용되어 사용자가 화면에 보이게 됩니다.
끝나면 (useEffect 실행)
useLayoutEffect는 동기적으로 발생하기 때문에, 컴포넌트에 버벅거림이 보일 수 있다. 하지만, useLayoutEffect의 특징상 DOM은 계산됐지만, 이것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용하는 것이 좋다.
ex) DOM 요소를 기반으로 하는 에니메이션 / 스크롤 위치를 제어하는 등 화면에 반영되기 전에 하고 싶은 작업
- 최상위에서만 훅을 호출해야 한다. 반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다. 이 규칙을 따라야만 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장할 수 있다.
- 훅을 호출할 수 있는 것은 리액트 함수형 컴포넌트 혹은 사용자 정의 훅의 두 가지 경우뿐이다. 일반 자바스크립트 함수에서는 훅을 사용할 수 없다.
## 3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
개발자라면 누구나 중복 코드를 피해야 한다는 말에 대해 십분 공감할 것이다. 리액트에서 사용자 관리 훅과 고차 컴포넌트는 재사용할 수 있는 로직을 관리할 수 있는 방법이다
### 3.2.1 사용자 정의 훅
서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용되는 것이 바로 사용자 정의 훅이다. 사용자 정의 훅 규칙 중 하나는 이름이 반드시 use로 시작하는 함수를 만들어야 한다.