목차
리액트 훅의 규칙
리액트에서 훅은 함수 컴포넌트에서 상태와 라이프사이클 기능을 사용할 수 있게 해주는 함수다. 하지만 올바르게 사용하기 위해 반드시 지켜야 할 규칙이 있다.
함수 컴포넌트 안에서만 사용
리액트 훅 함수는 함수 컴포넌트의 본문 안에서만 사용할 수 있다. 여러 훅 함수를 조합해 만드는 함수인 커스텀 훅 내부에서도 사용할 수 있지만, 클래스 컴포넌트나 일반 함수, 컴포넌트 외부에서는 사용할 수 없다.
import { useState, useEffect } from 'react'
export default function App() {
const [count, setCount] = useState(0) // ✅
useEffect(() => { document.title = count }, [count]) // ✅
return (...)
}
항상 같은 순서로 호출
렌더링될 때마다 항상 같은 수, 같은 순서로 훅 함수가 실행되어야 한다. 따라서 훅을 조건문 또는 반복문 안에 넣으면 안 된다. 훅은 함수 컴포넌트 본문 최상단에 배치해야 안전하다.
import { useState, useEffect } from 'react'
function FunctionalComponent() {
useState('...') // ✅
useEffect(() => { ... }, []) // ✅
return (...)
}
조건부 로직은 훅 내부에서 처리
어떤 조건에 의해 실행되는 코드를 만들 때 훅 자체를 조건문에 넣으면 안 된다. 훅 함수 내부에 조건문을 작성해야 한다.
useEffect(() => {
// ✅
if (count <= 3) {
document.title = count
}
}, [count])
만약 훅의 규칙을 어기면 문제가 발생한다. 훅이 조건부로 실행되면, 렌더링될 때마다 훅의 실행 순서와 개수가 달라진다. 훅의 실행 순서와 개수가 달라지면, 리액트가 내부적으로 상태 관리에 혼란이 발생한다. 그러면 콘솔 패널에 오류가 출력되고 렌더링이 깨질 수 있다.
값 참조(Ref)
useRef 훅 함수는 리액트에서 렌더링 사이 값을 저장할 수 있게 해주는 훅이다. 리액트의 반응성 데이터(state
, props)와 다르게 이 값이 바뀌어도 컴포넌트를 다시 렌더링하지 않는다.
useRef의 주요 특징으로는 다음 두 가지가 있다.
- 렌더 트리거 없음: ref 객체는 { current: 값 } 형태로 ref.current를 통해 값을 읽거나 쓸 수 있다. ref 객체의 현재(current) 값을 바꿔도 컴포넌트가 다시 렌더링 되지 않는다.
- 컴포넌트마다 독립적: 컴포넌트마다 자신만의 ref 객체를 갖기 때문에 전역 변수와 달리 여러 컴포넌트가 ref 객체를 공유하지 않는다. 마치 클래스 컴포넌트의 인스턴스 멤버처럼 작동한다.
주요 사용 사례
useRef 훅 함수를 사용한 값 참조는 이전 값을 기억하거나, 타이머 ID 등을 저장하는 데 사용될 수 있다.
이전 값 기억
이전 상태 값을 어딘가 기억해두고 싶을 때 컴포넌트 외부에 상태 값을 기억하려 하면 안 된다. 이전 상태 값을 기억하려면 렌더링 이후 실행되는 이펙트 함수를 사용해 값을 참조해야 한다.
useEffect(() => {
// 렌더링될 때마다 이전 count 값을 저장
prevCountRef.current = count
}, [count])
타이머 ID
setInterval 함수가 반환한 타이머 ID 값은 렌더링과 무관하게 어딘가에 기억해야 한다. 타이머 ID 값을 전역 변수로 기억하려는 경우가 많은데 이것은 초보자가 자주 하는 실수다. 타이머 ID 값을 기억하려면 참조(ref) 객체를 생성한 후 현재 값으로 기억해야 한다.
const handleStartTimer = () => {
;(function recursion() {
clearTimeout(timerIdRef.current)
timerIdRef.current = setTimeout(() => {
console.log('타이머 작동 중...')
setTimer(new Date())
recursion()
}, 1000)
})()
}
const handleStopTimer = () => {
console.log('타이머 중지!')
clearTimeout(timerIdRef.current)
setTimer(null)
}
DOM 참조(Ref)
렌더링은 대부분 리액트에 의해 제어되지만, 언제나 리액트로 제어하는 것은 불가능하다. 예를 들어, 실제 DOM 노드에 접근 및 조작하는 것은 리액트의 렌더링과 무관한 작업이다.
실제 DOM 노드에 접근해야 할 때 useRef 훅 함수가 생성한 참조(ref) 객체를 사용한다. 실제 DOM 노드는 렌더링 이후 생성되기 때문에 반드시 이펙트 함수 안에서 접근해야 한다.
import { useRef, useEffect } from 'react'
export default function App() {
const sectionRef = useRef(null)
useEffect(() => {
console.log(sectionRef.current) // <section> 요소
}, [])
return (
<section className="app" ref={sectionRef}>
{/* ... */}
</section>
)
}
아래 코드는 <section> 요소에 tabindex 속성을 -1로 설정하고 초점을 이동시키는 일을 수행한다. 즉, 리액트 앱에서 렌더링 이후 실제 DOM 노드에 접근해 조작하는 과정을 보여준다.
import { useRef, useEffect } from 'react'
export default function App() {
const sectionRef = useRef(null)
useEffect(() => {
const sectionElement = sectionRef.current
if (sectionElement) {
// tabindex 속성 추가 및 초점 이동
sectionElement.setAttribute('tabindex', '-1')
sectionElement.focus()
}
}, [])
return (
<section className="app" ref={sectionRef}>
{/* ... */}
</section>
)
}
오늘 하루를 돌아보며
오늘 오전 시간에는 어제 하던 틱택토 게임 구현 실습을 마저 진행했다. 리액트 문서에서도 틱택토 게임을 리액트로 구현하는 자습서를 제공한다. 하지만 우리는 리액트 문서에 있는 내용과는 다르게 접근성을 고려하여 누구나 할 수 있는 게임을 만들었다.
리액트 공식 문서에서 제공하는 자습서(틱택토 게임)는 아래 링크에서 확인할 수 있다.
자습서: 틱택토 게임 – React
The library for web and native user interfaces
ko.react.dev
접근성을 위해 각 칸에 적절한 레이블을 추가해서 스크린리더가 상태를 안내할 수 있도록 했고, 각 칸에 키보드 접근이 가능하도록 구성했다. 화살표 키로 상하좌우로 포커스를 이동시킬 수 있도록 하는 것이 재밌었다. 열 인덱스와 행 인덱스를 받아서 1씩 증가하거나 감소하게 하고, 벽을 만나면 이동하지 않도록 설정한다. 그리고 esc 키를 누르면 밖으로 빠져나올 수 있도록 설정한다.
특히 esc 키로 빠져나올 수 있도록 처리하는 것이 중요한데, 네비게이션 메뉴 같은 데에서 요소가 엄청 많은데 일일이 포커스를 이동해서 빠져나오는 번거로움을 해소할 수 있기 때문이다. 하다 못해 esc 키를 누르면 닫기 버튼으로 포커스를 이동시키는 것이 타당해 보인다.