리액트 함수 컴포넌트에서 가장 중요한 개념이 바로 훅이다.
리액트 훅에 대해 알아보자.
3.1.1 useState
useState
함수 컴포넌트 내부에서 상태를 정의하고, 이 상태를 관리할 수 있게 해주는 훅
useState 구현 살펴보기
기본적인 사용법
import (useState } from 'react'
const [state , setState ] = useState(initialState)
useState의 인수로는 사용할 state의 초깃값을 넘겨준다. > 아무런 값을 넘겨주지 않으면 초깃값은 undefined
useState 훅의 반환 값은 배열이다.
배열의 첫 번째 원소로 state 값을 사용할 수 있고 , 두번째 원소인 setState 함수로 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
// states의 값을 위에서 조회한 현재 값으로 업데이트한다.
global.states[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가 배열의 값(global.states)을
// 가리키고 필요할 때마다 그 값을 가져오게 한다.
index = index + 1
return [currentState, setState]
}
// 실제 useState를 사용하는 컴포넌트
function Component() {
const [value, setValue] = useState(0)
// ...
}
})
함수의 실행이 끝났음에도 함수가 선언된 환경을 기억할 수 있는 방법 >> 1장에서 소개한 클로저이다.
매번 실행되는 함수 컴포넌트 환경에서 state 값을 유지하고 사용하기 위해 클로저를 활용한다.
게으른 초기화
useState에 변수 대신 함수를 넘기는 것을 게으른 초기화(lazy initialization)라고 한다.
//일반적인 useState 사용
//바로 값을 집어넣는다.
const [count, setCount ] = useState(
Number.parseInt(window.localStorage.getItem(cacheKey)),
)
//게으른 초기화
//위 코드와 차이점 : 함수를 실행해 값을 반환한다.
const [count, setCount ] = useState(() =>
Number.parseInt(window.localStorage.getItem(cacheKey)),
)
리액트 공식 문서에는 이러한 게으른 초기화는 useState의 초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하라고 한다.
게으른 초기화 함수는 state가 처음 만들어질 때만 사용된다.
이후에 리렌더링이 발생하면 이 함수의 실행은 무시된다.
useState 내부에 함수를 넣으면 최초 렌더링 이후에는 실행되지 않고 state 값을 넣을 때만 실행된다.
3.1.2 useEffect
- 두 개의 인수를 받는데, 첫 번째는 콜백, 두 번째는 의존성 배열이다. 이 두 번째 의존성 배열의 값이 변경되면 첫 번째 인수인 콜백을 실행한다.
- 클래스 컴포넌트의 생명주기 메서드와 비슷한 작동을 구현할 수 있다. 두 번째 의존성 배열에 빈 배열을 넣으면 컴포넌트가 마운트 될 때만 실행된다.
- 클린업 함수를 반환할 수 있는데, 이 클린업 함수는 컴포넌트가 언마운트될 때 실행된다.
useEffect 의 정확한 정의는
애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 메커니즘이다.
이 부수효과가 언제 일어나는지보다 어떤 상태값과 함께 실행되는지 살펴보는 것이 중요하다.
useEffect의 일반적인 형태
function Component() {
//...
useEffect(() => {
//do something
}, [props, state])
//...
}
첫 번째 인수로는 실행할 부수 효과가 포함된 함수를, 두 번째 인수로는 의존성 배열을 전달한다.
useEffect는 어떻게 의존성 배열이 변경된 것을 알고 실행될까?
function Component() {
const [counter, setCounter] = useState(0)
function handleClick() {
setCounter((prev) => prev + 1)
}
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+<button>
</>
)
)
버튼을 클릭하면 counter에 값을 1씩 올리는 평범한 컴포넌트다.
함수 컴포넌트는 렌더링 시마다 고유의 state와 props 값을 갖고 있다.
여기에 useEffect가 추가된다면
function Component() {
const [counter, setCounter] = useState(0)
useEffect(() => {
console.log(counter) //1,2,3,4
}
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+<button>
</>
)
)
useEffect는 렌더링할 때마다 의존성에 있는 값을 보면서
이 의존성의 값이 이전과 다른 게 하나라도 있으면 부수 효과를 실행하는 것이다.
클린업 함수의 목적
클린업 함수는 무엇이고 어떤 일을 할까?
클린업 함수는 보통 이벤트를 등록하고 지울 때 사용한다고 알려져 있다.
import { useState, useEffect } from 'react'
export default function App() {
const [counter, setCounter] = useState(0)
function handleClick() {
setCounter((prev) => prev + 1)
}
useEffect(() => {
function addMouseEvent() {
console.log(counter)
}
window.addEventListener('click', addMouseEvent)
// 클린업 함수
return () => {
console.log('클린업 함수 실행!', counter)
window.removeEventListener('click', addMouseEvent)
}
}, [counter])
return (
<>
<h1>{counter}</h1>
<button onClick={handleClick}>+</button>
</>
)
}
//실행결과
클린업 함수 실행! 0
1
클린업 함수 실행! 1
2
클린업 함수 실행! 2
3
클린업 함수 실행! 2
4
클린업 함수는 이전 counter 값, 즉 이전 state를 참조해 실행된다는 것을 알 수 있다.
클린업 함수는 새로운 값과 함께 렌더링된 뒤에 실행된다.
useEffect는 콜백이 실행될 때마다 이전의 클린업 함수가 존재한다면 그 클린업 함수를 실행한 뒤에 콜백을 실행한다.
이벤트를 추가하기 전에 등록했던 이벤트 핸들러는 삭제하는 코드를 클린업 함수에 추가해 특정 이벤트 핸들러가 무한히 추가되는 것을 방지한다.
'언마운트' 특정 컴포넌트가 DOM에서 사라지는 것
클린업 함수는 언마운트라기 보다는 함수 컴포넌트가 리렌더링됐을 때 의존성 변화 이전의 값을 기준으로 실행되는,
즉 이전 상태를 청소해주는 개념으로 보는 것이 옳다.
의존성 배열
의존성 배열은 빈 배열, 아무런 값도 넘기지 않거나, 원하는 값을 넣어줄 수 있다.
빈 배열 >> 최초 렌더링때 실행된 이후 실행되지 않는다.
아무런 값 x >> 렌더링 할 때 마다 실행
원하는 값 >> 값이 바뀔 때 마다 실행
그렇다면 의존성 배열에 아무런 값이 없는 useEffect가 매 렌더링마다 실행된다면
useEffect 없이 써도 되는 게 아닐까? 라고 생각할 수 있다.
useEffect는 컴포넌트의 렌더링이 완료된 이후에 실행되지만 함수 내부의 직접 실행은 컴포넌트가 렌더링 되는 도중에 실행되고, 서버 사이드 렌더링의 경우에는 서버에세도 실행된다.
무거운 작업일 경우에는 렌더링을 방해하기 때문에 성능에 악영향을 미칠 수 있다.
useEffect를 사용할 때 주의할 점
- eslint-disable-line react-hooks/exhaustive-deps 주석은 최대한 자제하라
- useEffect의 첫 번째 인수에 함수명을 부여하라
- 거대한 useEffect를 만들지 마라
- 불필요한 외부 함수를 만들지 마라
3.1.3 useMemo
비용이 큰 연산에 대한 결과를 저장(메모이제이션)해 두고, 저장된 값을 반환하는 훅.
흔히 리액트에서 최적화를 할 때 가장 먼저 언급되는 훅이다.
import { useMemo } from 'react'
const memoizedValue = useMemo(() => expensiveComputation(a,b), [a,b])
첫 번째 인수로는 어떠한 값을 반환하는 생성함수를 , 두 번째 인수로는 해당 함수가 의존하는 값의 배열을 전달한다.
렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면 > 함수 재실행 x, 이전에 기억해 둔 값을 반환
의존성 배열의 값이 변경 > 첫 번째 인수의 함수를 실행한 후 그 값을 반환하고 그 값을 다시 기억한다.
3.1.4 useCallback
useMemo가 값을 기억했다면, useCallback은 인수로 넘겨받은 콜백 자체를 기억한다.
즉, 특정 함수를 새로 만들지 않고 다시 재사용한다는 의미다.
useCallback 의 첫 번째 인수로 함수를, 두 번째 인수로 의존성 배열을 집어 넣으면 useMemo와 마찬가지로 의존성 배열이 변경되지 않는 한 함수를 재생성하지 않는다.
함수의 재생성을 막아 불필요한 리소스 또는 리렌더링을 방지하고 싶을 때 useCallback을 사용할 수 있다.
useCallback을 useMemo로도 구현할 수 있지만 불필요하게 코드가 길어지고 혼동을 야기할 수 있어 별도로 제공하는 것으로 추측된다.
하지만 사실상 동일한 역할을 한다.
'모던 리액트 Deep Dive 스터디' 카테고리의 다른 글
사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야할까? (1) | 2024.11.16 |
---|---|
리액트 훅 깊게 살펴보기(useReducer, useRef) (7) | 2024.11.15 |
1장 발표자료 (1) | 2024.11.10 |
선택이 아닌 필수, 타입스크립트 (0) | 2024.11.07 |
리액트에서 자주 사용하는 자바스크립트 문법 (0) | 2024.11.07 |