Learn React / Hooks
신현호
React Docs
목차
Hooks
#use
#use
는 promise
나 context
와 같은 데이터를 참조하는 Hook입니다. 기본 형태는 다음과 같습니다.
jsx
const value = use(resource)
아래의 예제는 각각 promise
와 context
를 적용한 예제입니다.
jsx
import { use } from 'react'
function MessageComponent({ messagePromise }) {
const message = use(messagePromise)
const theme = use(ThemeContext)
// ...
}
use
hook이 갖는 다른 훅과의 차이점은 다른 React Hook과 달리 use
는 if
와 같은 조건문과 반복문 내부에서도 호출할 수 있습니다.
use
hook을 사용하면 다음과 같은 작업이 가능해집니다.
use
를 사용하여 context
를 참조할 수 있습니다.
use
hook의 인자로 context
가 전달되면 useContext
와 유사하게 작동합니다. 차이가 있다면 useContext
는 컴포넌트의 최상위 수준에서 호출해야하지만 use
의 경우 if
같은 조건문이나 for
같은 반복문 내부에서도 호출할 수 있습니다.
이 케이스에서 use
는 useContext
보다 유연하게 사용할 수 있습니다.
jsx
// when use 'useContext' hook
import { useContext } from 'react'
function Button() {
const theme = useContext(ThemeContext)
// ...
}
// when use 'use' hook
import { use } from 'react'
function Button({ show, children }) {
if (show) {
const theme = use(ThemeContext)
const className = 'button-' + theme
return <button className={className}>{children}</button>
}
return false
}
use
를 사용하여 비동기처리를 진행할 수 있습니다.
promise
와 함께 사용되는 use
의 경우 리액트에서만 사용되는 await
로 이해하시면 됩니다.
이전까지 우리는 컴포넌트 내부에서 비동기 처리를 진행해주기 위해서는 useEffect
훅을 사용해야 했습니다.
jsx
function ClientComponent() {
useEffect(() => {
const doAsync = async () {
await doSomething('...')
}
doAsync()
}, [])
return <>...</>
}
하지만 use
hook을 사용해준다면 해당 로직을 다음과 같이 변경해 줄 수 있습니다.
jsx
import { use } from 'react'
function Component() {
const data = use(promise)
}
위에서도 말했듯이, use
는 if
나 for
문 내부에도 사용이 가능하다고 했으므로 아래와 같은 예시도 가능합니다.
jsx
function Note({ id, shouldIncludeAuthor }) {
const note = use(fetchNote(id))
let byline = null
// 조건부로 호출하기
if (shouldIncludeAuthor) {
const author = use(fetchNoteAuthor(note.authorId))
byline = <h2>{author.displayName}</h2>
}
return (
<div>
<h1>{note.title}</h1>
{byline}
<section>{note.body}</section>
</div>
)
}
useCallback
#useCallback
은 리렌더링 간에 함수 정의를 캐싱해 주는 Hook입니다.
jsx
const cachedFn = useCallback(fn, dependencies)
아래의 예시를 보겠습니다. 아래 로직에서 어떤 부분이 문제가 될까요?
jsx
import { useCallback } from 'react'
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
})
}
}
정답은 handleSubmit
이라는 함수가 ProductPage
가 리렌더링 될 때마다 재생성된다는 점 입니다.
useCallback
을 사용하면 이러한 함수를 캐싱하여 컴포넌트가 리렌더링되더라도 캐싱된 함수를 반환하여 불필요하게 함수를 추가 생성하는 일을 방지합니다.
jsx
import { useCallback } from 'react'
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback(
(orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
})
},
[productId, referrer]
)
// ...
}
useCallback
을 사용할 때는 두 가지 인자를 전달해야합니다.
- 리렌더링 간에 캐싱할 함수 정의
- 함수에서 사용되는 컴포넌트 내부의 모든 값을 포함하고 있는
의존성 목록
최초 렌더링에서 useCallback
으로 부터 반환되는 함수는 호출시에 전달할 함수입니다. useCallback
은 의존성 배열에 들어간 값이 변경되기 전까지 리렌더링 간에 함수를 캐싱합니다.
useContext
#useContext
는 컴포넌트에서 context를 읽고 구독할 수 있는 Hook입니다.
jsx
const value = useContext(SomeContext)
이를 사용하기 위해서는 기본적으로 상위 컴포넌트에서 createContext
를 통해 context를 생성한 뒤 해당 컴포넌트를 Provider
로 감싸줘야합니다.
useContext
는 특정 context에 대해 상위에서 가장 가까운 context provider를 찾습니다.
context를 변경하고 싶다면 state
를 선언한 뒤 provider에 전달하세요.
jsx
function MyPage() {
const [theme, setTheme] = useState('dark')
return (
<ThemeContext.Provider value={theme}>
<Form />
<Button
onClick={() => {
setTheme('light')
}}>
Switch to light theme
</Button>
</ThemeContext.Provider>
)
}
useDebugValue
#useDebugValue
는 React DevTools에서 커스텀 Hook에 라벨을 추가할 수 있게 해주는 Hook입니다.
jsx
useDebugValue(value, format)
useDebugValue
는 아무것도 반환하지 않으며, 설정시 React DevTools에서 다음과 같이 표시됩니다.
useDebugValue
는 내부 구조가 복잡하여 검사하기 어려운 커스텀 Hook에 유용합니다.
useDeferredValue
#useDeferredValue
는 UI의 일부 업데이트를 지연시킬 수 있는 Hook입니다.
jsx
const deferredValue = useDeferredValue(value)
useDeferredValue
는 설정한 값의 지연된 버전을 가져옵니다.
jsx
import { useState, useDeferredValue } from 'react'
function SearchPage() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
// ...
}
초기 렌더링 중에 반환된 지연된 값
은 사용자가 제공한 값과 같겠지만 업데이트가 발생하면 React는 먼저 이전 값으로 렌더링을 시도하고, 그 다음 백그다운드에서 다시 새 값으로 리렌더링을 시도합니다.
요약하자면 일종의 낮은 우선순위를 지정하기 위한 Hook이라고 볼 수 있습니다.
useDeferredValue
에 전달하는 값은 문자열 및 숫자와 같은 원시값이거나, 컴포넌트 외부에서 생성된 객체여야 합니다. 렌더링 과정중에 새 객체를 생성하고 이 값을 useDeferredValue
에 전달하게되면 렌더링ㅇ 할 때마다 값이 달라져 불필요한 백그라운드 리렌더링이 발생할 수 있습니다.
useDeferredValue
로 인한 백그라운드 리렌더링은 화면에 커밋될 때까지 Effects를 실행하지 않습니다. 백그라운드 리렌더링이 일시 중단되면 데이터가 로딩되고 UI가 업데이트된 후에 해당 Effects가 실행됩니다.
위의 예제에서는 input의 값이 변경될 때마다 바로 리스트가 갱신이 됩니다. 하지만 일반적인 대체 UI 패턴에서는 결과 목록 업데이트를 지연하고 새 결과가 준비될 때 까지 이전 결과를 계속 표시합니다.
useDeferredValue
를 사용하면 값이 변경되어도 잠시동안 지연된 버전을 보여줄 수 있습니다.
useEffect
#useEffect
는 외부 시스템과 컴포넌트를 동기화하는 Hook입니다.
jsx
useEffect(setup, dependencies)
useEffect
는 그 사용방법이 상당히 많습니다. 대표적인 예시를 알아보도록 하겠습니다.
컴포넌트를 외부 시스템과 연결하기
jsx
import { useEffect } from 'react'
import { createConnection } from './chat.js'
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234')
useEffect(() => {
const connection = createConnection(serverUrl, roomId)
connection.connect()
return () => {
connection.disconnect()
}
}, [serverUrl, roomId])
// ...
}
몇몇 컴포넌트들은 페이지에 표시되는 동안 네트워크나 브라우저 API, 또는 서드파티 라이브러리와 연결이 유지되어야 합니다.
React에 제어되지 않는 이러한 시스템들을 외부 시스템이라고 부르며, React 컴포넌트를 외부 시스템과 연결하려면 useEffect
를 사용해야합니다.
useEffect
는 2개의 인수를 받습니다.
- 외부 시스템과 컴포넌트를 연결하는 설정 코드가 포함된 설정 함수
- 외부 시스템과 연결을 해제하는 정리 코드가 포함된 정리 함수 또한 반환할 수 있습니다.
- 위 함수 내부에서 사용하는 컴포넌트에서 비롯된 반응형 값들을 포함하는 의존성 배열
React는 설정 함수와 정리 함수가 필요할 때마다 호출할 수 있으며, 이는 여러 번 호출될 수 있습니다.
- 컴포넌트가 화면에 추가되었을 때 설정 코드가 동작합니다.
- 의존성이 변경된 컴포넌트가 리렌더링 될 때마다 아래 동작을 수행합니다.
- 먼저, 정리 코드가 오래된 props, state와 함께 실행됩니다.
- 이후, 설정 코드가 새로운 props, state와 함께 실행됩니다.
커스텀 Hook을 Effect로 감싸기
Effect는 탈출구입니다. 'React 바깥으로 나가야 할 때'와 유즈케이스에 필요한 빌트인 솔루션이 없을 때 사용합니다.
만약 Effect를 자주 작성해야 한다면 컴포넌트가 의존하고 있는 공통적인 동작들을 커스텀 Hook으로 추출해야한다는 신호일 수 있습니다.
jsx
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId,
}
const connection = createConnection(options)
connection.connect()
return () => connection.disconnect()
}, [roomId, serverUrl])
}
위와 같이 작성하면 커스텀 hook은 effect의 로직을 조금 더 선언적인 API로 보일 수 있도록 숨겨줍니다.
React로 작성되지 않은 위젯 제어하기
가끔은 컴포넌트의 prop 또는 state를 외부 시스템과 동기화해야할 때가 있습니다.
위의 예시처럼 React로 작성된 third-party 위젯이더라도 useEffect
Hook을 통해 제어할 수 있습니다.
Effect를 이용한 데이터 패칭
컴포넌트에 데이터를 패칭할 때도 Effect를 사용할 수 있습니다. 만약 프레임워크를 사용하고 있다면 해당 프레임워크의 데이터 패칭 메커니즘을 사용하는 것이 Effect를 직접 작성하는 것보다 더 효율적일 것입니다.
하지만 직접 Effect를 작성하여 데이터를 패칭하고 싶다면, 코드는 다음과 같을 수 있습니다.
jsx
import { useState, useEffect } from 'react'
import { fetchBio } from './api.js'
export default function Page() {
const [person, setPerson] = useState('Alice')
const [bio, setBio] = useState(null)
useEffect(() => {
let ignore = false
setBio(null)
fetchBio(person).then((result) => {
if (!ignore) {
setBio(result)
}
})
return () => {
ignore = true
}
}, [person])
// ...
}
async
/ await
을 사용한다면 아래와 같이 작성할 수 있습니다.
jsx
import { useState, useEffect } from 'react'
import { fetchBio } from './api.js'
export default function Page() {
const [person, setPerson] = useState('Alice')
const [bio, setBio] = useState(null)
useEffect(() => {
async function startFetching() {
setBio(null)
const result = await fetchBio(person)
if (!ignore) {
setBio(result)
}
}
let ignore = false
startFetching()
return () => {
ignore = true
}
}, [person])
// ...
}
Effect에서 직접 데이터 페칭 로직을 작성하면 나중에 캐싱 기능이나 서버 렌더링과 같은 최적화를 추가하기 어려워집니다. 자체 제작된 커스텀 Hook이나 커뮤니티에 의해 유지보수되는 Hook을 사용하는 편이 더 간단합니다.
useEffect
를 쓸 때 다음을 주의하세요.
Effect의 의존성은 선택할 수 없습니다.
Effect 코드에서 사용하는 모든 반응형 값은 의존성으로 선언되어야 합니다. Effect의 의존성 배열은 코드에 의해 결정됩니다.
jsx
function ChatRoom({ roomId }) {
// 이것은 반응형 값입니다
const [serverUrl, setServerUrl] = useState('https://localhost:1234') // 이것도 반응형 값입니다
useEffect(() => {
const connection = createConnection(serverUrl, roomId) // 이 Effect는 이 반응형 값들을 읽습니다
connection.connect()
return () => connection.disconnect()
}, [serverUrl, roomId]) // ✅ 그래서 이 값들을 Effect의 의존성으로 지정해야 합니다
// ...
}
일반적으로 린터가 잘 설정되어 있다면 린터는 누락된 의존성이 있다면 이를 표시해줍니다. 의존성을 제거하려면 그것이 의존성이 되지 않아야 함을 린터에 증명해야합니다.
jsx
const serverUrl = 'https://localhost:1234' // 더 이상 반응형 값이 아님
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId)
connection.connect()
return () => connection.disconnect()
}, [roomId]) // ✅ 모든 의존성이 선언됨
// ...
}
위처럼 설정하면 serverUrl
은 반응형 값이 아니게 되었으므로 의존성에 추가할 필요가 없습니다.
Effect의 코드가 어떤 반응형 값도 사용하지 않는다면 그 의존성 목록은 비어있어야 합니다. []
이전 state를 기반으로 state를 업데이트 할 때는 변경함수를 사용하세요.
jsx
// bad
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1) // 초마다 카운터를 증가시키고 싶습니다...
}, 1000)
return () => clearInterval(intervalId)
}, [count]) // 🚩 ... 하지만 'count'를 의존성으로 명시하면 항상 인터벌이 초기화됩니다.
// ...
}
// good
import { useState, useEffect } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const intervalId = setInterval(() => {
setCount((c) => c + 1) // ✅ State 업데이터를 전달
}, 1000)
return () => clearInterval(intervalId)
}, []) // ✅ 이제 count는 의존성이 아닙니다
return <h1>{count}</h1>
}
bad
의 예시에서는 count
가 반응형 값이기 떄문에 의존성 배열에 추가해야 합니다. 그러나 count
가 변경되는 것은 Effect가 정리된 후 다시 설정되는 것을 야기하므로 count
는 계속 증가할 것입니다. 이상적이지 않은 방식입니다.
이러한 현상을 방지하기 위해 state 변경함수를 사용하세요, 이처럼 변경하면 Effect는 더 이상 count
에 의존하지 않게 됩니다.
불필요한 객체의 의존성은 제거하세요
Effect가 렌더링 중에 생성된 객체나 함수에 의존하게되면 너무 자주 실행될 수 있습니다.
jsx
const serverUrl = 'https://localhost:1234'
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('')
const options = {
// 🚩 이 객체는 재 렌더링 될 때마다 새로 생성됩니다
serverUrl: serverUrl,
roomId: roomId,
}
useEffect(() => {
const connection = createConnection(options) // 객체가 Effect 안에서 사용됩니다
connection.connect()
return () => connection.disconnect()
}, [options]) // 🚩 결과적으로, 의존성이 재 렌더링 때마다 다릅니다
// ...
}
jsx
import { useState, useEffect } from 'react'
import { createConnection } from './chat.js'
const serverUrl = 'https://localhost:1234'
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('')
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId,
}
const connection = createConnection(options)
connection.connect()
return () => connection.disconnect()
}, [roomId])
// ...
}
렌더링 중에 생성된 객체를 의존성으로 사용하지 말고 객체를 Effect 내부에서 생성하세요.
useId
#useId
는 접근성 어트리뷰트에 전달할 수 있는 고유 ID를 생성하기 위한 Hook입니다.
jsx
const id = useId()
그러나 해당 hook을 리스트의 key를 생성하기 위해 사용하지 마세요.
해당 hook은 다음과 같은 경우에 사용하면 유용합니다.
- 접근성 어트리뷰트를 위한 고유 ID를 생성할 때
- 여러 개의 연관된 엘리먼트의 ID를 생성할 때
- 생성된 모든 ID에 대해 공유 접두사를 지정하고 싶을 때
- 클라이언트와 서버에서 동일한 ID 접두사를 사용 할 때
useImperativeHandle
#jsx
useImperativeHandle(ref, createHandle, dependencies)
useImperativeHandle
은 ref로 노출되는 핸들을 사용자가 직접 정의할 수 있게 해주는 Hook입니다.
기본적으로 컴포넌트는 자식 컴포넌트의 DOM 노드를 부모 컴포넌트에 노출하지 않습니다. 예를 들어 MyInput
의 부모 컴포넌트가 <input>
DOM 노드에 접근하려면 forwardRef
를 사용해야합니다.
jsx
import { forwardRef } from 'react'
const MyInput = forwardRef(function MyInput(props, ref) {
return <input {...props} ref={ref} />
})
위에서 MyInput
에 대한 ref는 <input>
DOM 노드를 받게 됩니다. 하지만 useImperativeHandle
을 사용하면 사용자 지정 값을 노출할 수 있습니다.
jsx
import { forwardRef, useRef, useImperativeHandle } from 'react'
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null)
useImperativeHandle(
ref,
() => {
return {
focus() {
inputRef.current.focus()
},
scrollIntoView() {
inputRef.current.scrollIntoView()
},
}
},
[]
)
return <input {...props} ref={inputRef} />
})
이렇게 useImperativeHandle
을 사용하여 부모 컴포넌트에서 호출할 메서드만 있는 핸들을 노출할 수 있습니다.
또, imperative handle을 통해 노출하는 메서드는 DOM 메서드와 정확히 일치할 필요 없습니다.
useInsertionEffect
#useInsertionEffect
는 layout effects가 실행되기 전에 전체 요소를 DOM에 주입할 수 있습니다.
useInsertionEffect
는 CSS-in-JS 라이브러리 작성자를 위한 것입니다. CSS-in-JS 라이브러리 작업 중에 스타일을 주입할 위치가 필요한 것이 아니라면, useEffect
또는 useLayoutEffect
를 사용하세요.
jsx
useInsertionEffect(setup, dependencies)
css
.success {
color: green;
}
전통적으로는 plain CSS를 사용해 React 컴포넌트의 스타일링을 진행했으나, 최근에는 JS 코드에서 직접 스타일을 작성하는 경우도 생겼습니다. 이를 CSS-in-JS라고 합니다.
CSS-in-JS는 일반적으로 라이브러리 또는 도구를 사용해야 합니다. CSS-in-JS는 세 가지 일반적인 접근 방법이 있습니다.
- 컴파일러를 사용하여 CSS 파일로 정적 추출
- 인라인 스타일, 예:
<div style={{ opacity: 1}}>
- 런타임에
<style>
태그 주입할
CSS-in-JS를 사용하는 경우 일반적으로 앞의 두 가지 접근 방식을 사용하는게 좋고, 런타임에 style 태그를 주입하는 것은 다음과 같은 이유로 권장하지 않습니다.
- 런타임 주입은 브라우저에서 스타일을 훨씬 더 자주 다시 계산하도록 합니다.
- 런타임 주입이 React 생명주기 중에 잘못된 시점에 발생하면 속도가 매우 느려질 수 있습니다.
첫 번째 문제는 해결할 수 없지만 두 번째 문제는 useInsertionEffect
를 사용하여 해결할 수 있습니다.
jsx
// CSS-in-JS 라이브러리 안에서
let isInserted = new Set()
function useCSS(rule) {
useInsertionEffect(() => {
// 앞서 설명했듯이 <style> 태그의 런타임 주입은 권장하지 않습니다.
// 하지만 꼭 주입해야 한다면 useInsertionEffect에서 주입하는 것이 중요합니다.
if (!isInserted.has(rule)) {
isInserted.add(rule)
document.head.appendChild(getStyleForRule(rule))
}
})
return rule
}
function Button() {
const className = useCSS('...')
return <div className={className} />
}
useLayoutEffect
#useLayoutEffect
는 브라우저가 화면을 다시 그리기 전에 실행되는 useEffect
입니다.
다만, 해당 hook을 사용하면 성능이 저하될 수 있으므로 가능하면 useEffect
를 사용해주세요.
jsx
useLayoutEffect(setup, dependencies)
대부분의 컴포넌트는 렌더링을 위해 해당 컴포넌트의 화면상 위치와 크기를 알 필요가 없습니다. 컴포넌트가 JSX를 반환하면 브라우저가 컴포넌트의 레이아웃을 계산하고 화면을 다시 그리기 때문입니다.
하지만 이것만으로 부족한 경우가 있을 수 있습니다. 그 예시로 마우스 커서를 올리면 툴팁이 요소 옆에 나타나는 경우를 생각해보세요. 충분한 공간이 있다면 툴팁은 요소 위에 나타나겠지만, 공간이 부족하다면 아래에 나타나야 합니다. 결국 투립을 올바른 위치에 렌더링하려면 툴팁의 높이를 알아야 하는 것입니다.
이를 위해 두 번의 렌더링을 거쳐야 합니다.
- 툴팁을 아무 위치에 렌더링합니다. (잘못된 위치여도)
- 툴팁의 높이를 계산해서 위치를 결정합니다.
- 올바른 위치에 툴팁을 다시 렌더링합니다.
이 작업은 브라우저가 화면을 다시 그리기 전에 모두 이루어집니다.
useLayoutEffect
와 useEffect
간의 차이가 있다면 useLayoutEffect
의 경우 브라우저가 화면을 다시 그리기 전에 처리되는 것을 보장하기 떄문에 브라우저가 화면은 다시 그리는 것을 딜레이시키지만, useEffect
의 경우 그렇지 않습니다.
위의 예제는 useEffect
를 사용했습니다. 차이를 비교해보세요
useMemo
#useMemo
는 재렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 Hook입니다.
jsx
const cachedValue = useMemo(calculateValue, dependencies)
useMemo
를 사용하면 비용이 높은 로직의 재계산을 생략할 수 있습니다.
jsx
import { useMemo } from 'react'
function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab])
// ...
}
useMemo
에 두 가지를 전달하여 계산을 캐싱할 수 있습니다.
() =>
와 같이 인수를 받지 않고 계산하려는 값을 반환하는계산 함수
- 계산 내부에서 사용되는 컴포넌트 내의 모든 값을 포함하는
종속성 목록
useMemo
는 종속성이 변경되기 전까지 재렌더링 사이의 계산 결과를 캐싱합니다.
jsx
function TodoList({ todos, tab, theme }) {
const visibleTodos = filterTodos(todos, tab)
// ...
}
일반적으로 대부분의 계산은 매우 빠르기 때문에 문제가 되지 않으나, 큰 배열을 필터링 혹은 변환하는 경우 캐싱하는 것이 좋습니다.
컴포넌트 재렌더링 건너뛰기
경우에 따라 useMemo
는 하위 컴포넌트 재렌더링 성능을 최적화하는데 도움이 될 수도 있습니다.
jsx
export default function TodoList({ todos, tab, theme }) {
// ...
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
)
}
기본적으로 React는 컴포넌트가 다시 렌더링 될 때, 모든 자식 컴포넌트를 재귀적으로 다시 렌더링합니다. 위의 코드를 보면 TodoList
가 props나 state의 변경으로 다시 렌더링되면 하위 컴포넌트인 List
도 다시 렌더링됩니다.
이 때 List
를 다시 렌더링하는데 시간이 많이 걸린다면 memo
를 통해 props가 똑같을 때 다시 렌더링 하는 것을 생략할 수 있습니다.
jsx
import { memo } from 'react'
const List = memo(function List({ items }) {
// ...
})
memo
대신 useMemo
를 사용하여 캐싱할 수도 있습니다.
jsx
export default function TodoList({ todos, tab, theme }) {
// 테마가 변경될 때 마다 다른 배열이 표시됩니다.
const visibleTodos = filterTodos(todos, tab)
return (
<div className={theme}>
{/* ... List의 props는 동일하지 않으며 매번 다시 렌더링 됩니다. */}
<List items={visibleTodos} />
</div>
)
}
위의 예시에서 filterTodos
는 항상 다른 배열을 생성합니다. 이럴 때 filterTodos
를 useMemo
를 통해 캐싱한다면 연산을 최적화 할 수 있습니다.
jsx
export default function TodoList({ todos, tab, theme }) {
// 재렌더링 사이에 계산을 캐싱하도록 React에 지시합니다...
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // ...따라서 해당 종속성이 변경되지 않는 한...
)
return (
<div className={theme}>
{/* ...List에 동일한 props가 전달되어 재렌더링을 생략할 수 있습니다. */}
<List items={visibleTodos} />
</div>
)
}
다른 Hook의 종속성 메모화
컴포넌트 본문에서 직접 생성된 객체에 의존하는 연산이 있다고 가정하겠습니다.
jsx
function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text }
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions)
}, [allItems, searchOptions]) // 🚩 주의: 컴포넌트 본문에서 생성된 객체에 대한 종속성
// ...
}
이렇게 객체에 의존하는 것은 메모이제이션의 목적을 무색하게 합니다. useMemo
를 적절하게 사용하여 바꿔봅시다.
jsx
function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text }
}, [text]) // ✅ text가 변경될 때만 변경
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions)
}, [allItems, searchOptions]) // ✅ allItems이나 searchOptions이 변경될 때만 변경
// ...
}
이렇게 변경한다면 text
가 변경되었을 때만 searchOptions
가 변경될 것입니다.
이보다 더 좋은 방법은 애초에 searchOptions
를 useMemo
게산 함수의 내부에 선언하는 것입니다.
jsx
function Dropdown({ allItems, text }) {
const visibleItems = useMemo(() => {
const searchOptions = { matchMode: 'whole-word', text }
return searchItems(allItems, searchOptions)
}, [allItems, text]) // ✅ allItems이나 text가 변경될 때만 변경
// ...
}
이제 연산은 text
에 직접적으로 의존하게 됩니다.
useOptimistic
#useOptimistic
은 UI를 낙관적으로 업데이트할 수 있게 해주는 Hook입니다.
jsx
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn)
useOptimistic
은 비동기 작업이 진행 중일때 다른 상태를 보여줄 수 있게 해줍니다. 인자로 주어진 일부 상태를 받아, 네트워크 요청과 같은 비동기 작업 기간 동안 달라질 수 있는 그 상태의 복사본을 반환합니다.
현재 상태와 작업의 입력을 취하는 함수를 제공하고, 작업이 대기 중일 때 사용할 낙관적인 상태를 반환합니다.
낙관적
인 상태란, 실제로 작업을 완료하는 데 시간이 걸리더라도 사용자에게 즉시 작업의 결과를 표시하기 위해 일반적으로 사용됩니다.
jsx
import { useOptimistic } from 'react'
function AppContainer() {
const [optimisticState, addOptimistic] = useOptimistic(
state,
// updateFn
(currentState, optimisticValue) => {
// merge and return new state
// with optimistic value
}
)
}
useReducer
#useReducer
는 컴포넌트에 reducer를 추가하는 Hook입니다.
jsx
const [state, dispatch] = useReducer(reducer, initialArg, init)
useReducer
는 현재 state와 dispatch 함수. 2개의 엘리먼트로 구성된 배열을 반환합니다.
dispatch
함수는 state를 새로운 값으로 업데이트하고 리렌더링을 일으킵니다. dispatch
의 유일한 인수는 action입니다.
jsx
const [state, dispatch] = useReducer(reducer, { age: 42 })
function handleClick() {
dispatch({ type: 'incremented_age' })
// ...
}
React는 현재 state
와 dispatch
를 통해 전달된 action을 제공받아 호출된 reducer
의 반환값을 통해 다음 state값을 설정합니다.
React는 현재 state와 action을 reducer 함수
로 전달합니다. reducer
는 다음 state를 계산한 후 반환합니다.
reducer
는 다음과 같이 작성할 수 있습니다.
jsx
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1,
}
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age,
}
}
}
throw Error('Unknown action: ' + action.type)
}
보통은 컨변션에 따라 switch
문을 사용하며, switch
는 각 case
를 이용해 다음 state를 계산하고 반환합니다.
Actions는 다양한 형태가 될 수 있지만, 컨벤션에 따라 액션이 무엇인지 정의하는 type
프로퍼티를 포함한 객체로 선언하는 것이 일반적입니다. type
은 reducer가 다음 state를 계산하는데 필요한 최소한의 정보를 포합해야 합니다.
jsx
function Form() {
const [state, dispatch] = useReducer(reducer, { name: 'Taylor', age: 42 })
function handleButtonClick() {
dispatch({ type: 'incremented_age' })
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value,
})
}
// ...
}
useRef
#useref
는 렌더링에 필요하지 않은 값을 참조할 수 있는 Hook입니다.
jsx
const ref = useRef(initialValue)
useRef
는 처음에 제공한 초기값으로 설정된 단일 current 프로퍼티가 있는 ref객체를 반환합니다.
ref는 변경되어도 리렌더링을 촉발하지 않습니다. 즉, ref는 컴포넌트의 시각적 출력에 영향을 미치지 않는 정보를 저장하는 데 적합합니다.
ref 내부의 값을 업데이트하려면 current 프로퍼티를 수동으로 변경해야 합니다.
jsx
function handleStartClick() {
const intervalId = setInterval(() => {
// ...
}, 1000)
intervalRef.current = intervalId
}
ref를 사용하면 다음을 보장합니다.
- (렌더링할 때마다 재설정되는 일반 변수와 달리) 리렌더링 사이에 정보를 저장할 수 있습니다.
- (리렌더링을 촉발하는 state 변수와 달리) 변경해도 리렌더링을 촉발하지 않습니다.
- (정보가 공유되는 외부 변수와 달리) 각각의 컴포넌트에 로컬로 저장됩니다.
ref를 사용하면 다음과 같은 작업을 수행할 수 있습니다.
ref로 DOM 조작하기
jsx
import { useRef } from 'react'
function MyComponent() {
const inputRef = useRef(null)
// ...
return <input ref={inputRef} />
}
function handleClick() {
inputRef.current.focus()
}
ref를 사용하여 DOM을 조작하는 것은 특히 일반적입니다. useRef
를 통해 ref를 생성하고 이를 JSX에 전달하면 DOM 노드는 <input>
에 접근해 focus()
같은 메서드를 호출할 수 있습니다.
노드가 화면에서 제거되면 React는 ref의 current
프로퍼티를 null로 설정합니다.
ref로 콘텐츠 재생성 피하기
React는 초기에 ref 값을 한 번 저장하고, 다음 렌더링부터는 이를 무시합니다.
jsx
function Video() {
const playerRef = useRef(new VideoPlayer())
// ...
}
new VideoPlayer()
의 결과는 초기 렌더링에만 사용되지만, 호출 자체는 이후의 모든 렌더링에서도 여전히 계속 이뤄집니다. 비싼 객체를 생성하는 경우 이는 낭비가 됩니다.
jsx
function Video() {
const playerRef = useRef(null)
if (playerRef.current === null) {
playerRef.current = new VideoPlayer()
}
// ...
}
일반적으로 렌더링 중에 ref.current
를 쓰거나 읽는 것은 허용되지 않지만 이 경우에는 결과가 항상 동일하고 초기화 중에만 조건이 실행되므로 충분히 예측할 수 있어 괜찮습니다.
useState
#useState
는 컴포넌트에 state 변수를 추가할 수 있는 Hook입니다.
jsx
const [state, setState] = useState(initialState)
일반적으로 사용할 때 배열 구조분해를 사용하여 [something, setSomething]
과 같은 state 변수의 이름을 지정하는 것이 규칙입니다.
useState
는 현재 state
, state를 변경하고 리렌더링을 촉발하는 set함수
정확히 두 개의 값을 가진 배열을 반환합니다.
이러한 set
함수를 통해서 우리는 state를 다른 값으로 업데이트 하고 리렌더링을 촉발할 수 있습니다. 여기에는 다음 state를 직접 전달하거나, 이전 state로 부터 계산한 함수를 전달할 수도 있습니다.
jsx
const [name, setName] = useState('Edward')
function handleClick() {
setName('Taylor')
setAge((a) => a + 1)
// ...
}
state는 객체와 배열 또한 가능하지만. React에서 state는 읽기 전용으로 간주되므로, set
함수를 통해 기존 객체를 변경하는 것이 아닌 새로운 객체를 만들어 교체해야합니다.
jsx
// bad
// 🚩 state 안에 있는 객체를 다음과 같이 변경하지 마세요.
form.firstName = 'Taylor'
// good
// ✅ 새로운 객체로 state를 교체합니다.
setForm({
...form,
firstName: 'Taylor',
})
useSyncExternalStore
#useSyncExternalStore
는 외부 store를 구독할 수 있는 Hook입니다.
jsx
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
대부분의 React 컴포넌트는 props, state, context에서만 데이터를 읽습니다. 하지만 때로는 컴포넌트가 시간이 지남에 따라 변경되는 React 외부의 일부 저장소에서 데이터를 읽어야 하는 경우가 있습니다.
- React 외부에 state를 보관하는 서드파티 상태 관리 라이브러리
- 변경 가능한 값을 노출하는 브라우저 API와 그 변경 사항을 구독하는 이벤트
이 때 useSyncExternalStore
를 사용하면 외부 store를 구독할 수 있습니다.
jsx
import { useSyncExternalStore } from 'react'
import { todosStore } from './todoStore.js'
function TodosApp() {
const todos = useSyncExternalStore(
todosStore.subscribe,
todosStore.getSnapshot
)
// ...
}
store에 있는 데이터의 snapshot
을 반환합니다. 이 때 두 개의 함수를 인수로 전달해야 합니다.
subsribe
함수는 store에 구독하고 구독을 취소하는 함수를 반환해야 합니다.getSnapshot
함수는 store에서 데이터의 스냅샷을 읽어야 합니다.
React는 이 함수를 사용해 컴포넌트를 store에 구독한 상태로 유지하고 변경 사항이 있을 때 리렌더링합니다.
브라우저의 API를 구독하기 위해 useSyncExternalStore
를 사용할 수도 있습니다.
해당 예제에서는 브라우저의 navigator.online
이라는 속성을 구독하는 예제입니다.
useTransition
#useTransition
은 UI를 차단하지 않고 상태를 업데이트 할 수 있는 Hook입니다.
jsx
const [isPending, startTransition] = useTransition()
useTransition
은 어떠한 매개변수도 받지 않으며, 대기중인 transition의 여부
와 상태 업데이트를 transition으로 표시할 수 있게 해주는 함수
두 항목이 있는 배열을 반환합니다.
startTransition
을 통해 state 업데이트를 transition으로 표시할 수 있습니다.
jsx
function TabContainer() {
const [isPending, startTransition] = useTransition()
const [tab, setTab] = useState('about')
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab)
})
}
// ...
}
Transition을 사용하면 느린 디바이스에서도 사용자 인터페이스 업데이트의 반응성을 유지할 수 있습니다.
Transition을 사용하면 리렌더링 도중에도 UI가 반응성을 유지합니다. 예를 들어 사용자가 탭을 클릭했다가 마음이 바뀌어 다른 탭을 클릭하면 첫 번째 리렌더링이 완료될 때까지 기다릴 필요 없이 다른 탭을 클릭할 수 있습니다.
useTransition
호출에서도 부모 컴포넌트의 state를 업데이트 할 수 있습니다.
useTransition
이 반환하는 isPending
값을 사용하여 transition이 진행중인지 사용자에게 표시할 수 있습니다.
참고
#신현호
Frontend Developer
프론트엔드 개발자를 꿈꾸고 있는 대학생입니다. 끊임없이 배우고 성장하는 개발자가 되기 위해 노력하고 있습니다.
React Docs
총 5개의 포스트가 존재합니다.