- 훅은 함수 컴포넌트에 클래스와 생명주기 메서드를 작성할 필요 없이 리액트 기능을 사용하기 위해 도입됐다. 대규모 애플리케이션에서는 전역 state를 관리하기 위해 주로 리덕스, 리코일(Recoil), Mobx 같은 서드파티 라이브러리를 사용한다. 하지만, useContext나 useReducer 같은 리액트 훅을 함께 사용하면 외부 state를 관리하는 더 나은 방법이 될 수 있다. 혹은 복잡한 외부 라이브러리를 사용하는 것보다 애플리케이션 전체에 걸쳐 반복적으로 생성되는 파일과 폴더, 코드의 양을 줄이는 데 도움이 되고, 다양한 내장 훅을 제공하므로 다양한 경우에 유용하다. 특정 사용 사례에 대한 내장 룩이 없는 경우 비즈니스 요구 사항을 충족시키기 위해 사용자 정의 훅을 만들 수 있다. 훅의 일반적인 사용 사례(또는 횡단 관심사(cross-cutting concerns))로는 인증, 로깅, 캐싱, 데이터 불러오기, 오류 처리 등이 있다.
- 리액트 훅을 소개하고 훅의 이점, 함수 컴포넌트에 리액트 기능을 추가할 수 있는 다양한 내장 훅에 대해 알아본다. 예제를 통해 내장 훅을 자세히 설명하므로 리액트 애플리케이션에서 내장 훅의 사용법을 이해하는 데 도움이 될 것이다. 또한 서드파티 훅, 사용자 정의 훅 생성, 훅 관련 문제 해결과 관련된 질문에 답할 수 있는 지식과 자신감을 얻게 될 것이다.
- 이번 장에서 다룰 주제는 다음과 같다.
- 훅 소개와 목적
- 훅을 이용한 지역 state 관리
- 훅을 이용한 전역 state 관리
- 리액트 애플리케이션에서 부수 효과 실행
- ref 훅을 이용한 DOM 노드 접근
- 애플리케이션 성능 최적화
- 인기 있는 서드파티 훅
- 사용자 정의 훅 구축
- 훅 문제 해결 및 디버깅
훅 소개와 목적
- 초기에는 리액트가 주로 클래스 컴포넌트와 함께 사용됐지만 시간이 흐르면서 리액트 컴포넌트는 컴포넌트 로직을 재사용하기 위한 다양한 패턴을 사용해 복잡해졌다. 이후에 렌더 props(render props), 고차 컴포넌트(HOC) 같은 패턴을 작성하지 않고도 코드를 단순화하기 위해 훅이 도입됐다. 최근에는 훅이 리액트 애플리케이션을 구축하는 데 중요한 역할을 하기 때문에 리액트 면접에서 훅에 관한 질문을 받을 것이라고 예상할 수 있다. 이번 절에서는 훅이 무엇이며, 그 목적은 무엇인지 자세히 알아보겠다.
훅이란 무엇인가?
- 훅은 컴포넌트가 지역 state를 사용하고 부수 효과(또는 횡단 관심사) 및 다른 리액트 기능을 실행할 수 있게 하는 단순한 자바스크립트 함수다. 훅 API는 리액트 16.8 버전에서 도입되어 컴포넌트에서 state 관련 로직을 격리하는 역할을 한다. 간단히 말해서 훅이란 함수 컴포넌트가 리액트 생명주기와 state를 연결하는(hook into) 수단이다.
훅의 도입 배경은 무엇인가
- 훅은 다양한 문제를 해결할 수 있다. 몇 가지 예시는 다음과 같다.
- 컴포넌트 간 상태 로직 재사용의 어려움 : 리액트는 기본적으로 컴포넌트 로직을 재사용 하는 방법을 제공하지 않는다. 이 문제를 해결하기 위해 렌더 props나 고차 함수 같은 프로그래밍 패턴이 사용됐다. 하지만 이런 패턴은 컴포넌트 계층 구조를 수정해야 하기 때문에 애플리케이션이 여러 레이어 래퍼로 복잡해지고 코드를 어렵게 만들었다.
- 복잡한 컴포넌트를 이해하기 어려움 : 애플리케이션이 성장할수록 컴포넌트는 state 로직과 부수 효과가 많이 포함된 훨씬 더 복잡한 로직이 된다. 생명주기 메서드는 이 생명주기 메서드 한 곳에서 데이터 불러오기, 이벤트 리스너 추가 또는 제거와 같은 서로 관련 없는 로직들이 복잡하게 얽혀 있다. 예를 들어, componentDidMount 생명주기 메서드는 컴포넌트를 위한 데이터를 가져올 수 있으면서, 이벤트 리스너를 추가할 수도 있다. 동시에, 초기화와 같은 연관된 이벤트 리스너 로직이 componentWillUnmount 생명주기 메서드에 추가돼야 한다. 결론적으로, 큰 컴포넌트를 작은 컴포넌트로 나누기 어려워지는 동시에 테스트하기도 까다로워진다.
- 클래스에 의한 혼란 : 클래스는 리액트에서만 쓸 수 있는 것이 아니라 자바스크립트에 속한 것이다. 클래스 컴포넌트를 사용하려면 먼저 다른 언어의 것이 아닌 자바스크립트의 this 키워드가 동작하는 방식을 명확하게 이해해야 한다. 또한 ES2022 퍼블릭 클래스 필드(public class fields) 구문을 사용하는 데 익숙하지 않다면 생성자에서 이벤트 리스너를 바인딩 하는 방법을 알아둬야 한다. 이러한 모든 개념은 개발자들이 적절한 사용법을 파악하는 데 혼란을 야기했다.
- 훅은 클래스를 작성하지 않고도 리액트의 다양한 기능을 구현해서 개발자 커뮤니티 내의 혼란을 피하는 데 도움이 된다.
훅 사용 규칙을 설명할 수 있는가?
- 훅을 사용할 때 가장 중요한 두 가지 규칙이 있다.
- 최상단에서만 훅을 호출한다 : 반복문, 조건문 또는 리액트 컴포넌트 로직의 일부인 복잡한 함수에서는 혹을 호출해서는 안 된다. 대신, 다른 조기 반환(early return) 구문보다 리액트 함수 컴포넌트의 최상단에서 먼저 호출하는 것을 적극 권장한다.
이 가이드라인은 컴포넌트가 랜더링 될 때마다 동일한 순서로 훅이 호출되는 것을 보장한다. 다시 말해, 이는 여러개의 useState와 useEffect 훅 사이의 state르 유지한다. - 리액트 함수에서만 훅을 호출한다 : 훅을 단순 자바스크립트 함수에서 호출해서는 안 된다. 대신, 리액트 함수 컴포넌트 또는 사용자 정의 훅에서 호출해야 한다.
- 최상단에서만 훅을 호출한다 : 반복문, 조건문 또는 리액트 컴포넌트 로직의 일부인 복잡한 함수에서는 혹을 호출해서는 안 된다. 대신, 다른 조기 반환(early return) 구문보다 리액트 함수 컴포넌트의 최상단에서 먼저 호출하는 것을 적극 권장한다.
♣ 참고 : eslint-plugin-react-hooks(https://www.npmjs.com/package/eslint-plugin-react-hooks)라는 eslint 플러그인을 사용하면 '훅 사용 규칙을 설명할 수 있는가?' 절에 나온 두 규칙을 강제할 수 있다.
클래서 컴포넌트 내부에서 훅을 사용할 수 있는가?
- 훅은 클래스 컴포넌트에서 작성할 수 없다. 즉, 훅은 오직 함수 컴포넌트만을 위해 만들어졌다. 하지만 클래스 컴포넌트와 훅을 사용하는 함수 컴포넌트를 하나의 컴포넌트 트리에서 혼용해도 문제가 발생하지 않는다.
- 리액트 컴포넌트는 데이터를 보유하고 state라고 불리는 업데이트 업데이트 가능한 구조를 사용해 데이터 변경을 추적한다. 현실 세계의 애플리케이션에서 대부분의 컴포넌트는 UI에서 데이터를 처리하고 표시하기 위해 state를 사용한다. 다음 절에서는 state 관리와 관련된 질문과 답변을 다루겠다.
훅을 이용한 지역 state 관리
- 지역 state 관리를 위해 리액트 애플리케이션 내에서 두 가지 훅을 사용할 수 있다. 첫 번째 훅인 useState는 간단한 state 변환에 사용되며, 또 다른 룩인 useReducer는 복잡한 state 로직에 사용된다. 기본적으로 useState는 내부적으로 useReducer를 사용한다. 이는 전체 컴포넌트 state를 useReducer 훅을 통해 관리할 수 있다는 것을 의미한다. state는 리액트 컴포넌트의 핵심 구성 요소이므로 모든 개발자는 훅을 사용해 state를 관리하는 것을 명확하게 이해해야 한다.
useState 훅이란 무엇인가?
- useState 훅은 함수 컴포넌트에 state를 추가하는 데 사용된다. useState 훅은 리액트에서 가장 많이 사용되는 내장 훅 중 하나다. 이 훅은 초기 state를 인수로 사용되며, 동일한 초기 state는 값이나 함수 타입(즉, 초기화 함수)이 될 수 있다. 초기 state가 비용이 많이 드는 계산을 통해 도출되는 경우 초기 렌더링 시에만 실행되는 초기화 함수를 사용하는 것이 좋다. useState 훅은 state 변수와 state를 업데이트하는 세터 함수를 포함하는 배열을 반환한다.
- useState의 작성 방법은 다음과 같다
const [state, setState] = useState(initialState);
- useState 훅을 사용해 카운터의 state를 유지하는 카운터 컴포넌트 예제를 살펴보자. 세터 함수는 count state 변수를 업데이트 하고 변경 사항에 대해 UI를 리렌더링 한다.
// /Chapter03/useStateDemo.jsx
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</>
)
}
- 카운터 버튼을 클릭할 때마다 count state 변수가 1씩 증가하고, UI는 최신 state 변수 값으로 업데이트 된다.
♣ 참고 : state 세터 함수는 이미 실행 중인 코드에서 현재 state를 업데이트 하지 않는다. 이는 다음 렌더링에서만 사용 가능하다.
updater 함수를 사용하는 것이 항상 좋은가?
- 개발자 커뮤니티에서 새로운 state가 이전 state에서 계산된 경우에는 updater 함수를 사용하라는 조언을 받아 본 적이 있을 것이다. 이 규칙은 state 계산 로직 이후에 예측할 수 없는 state를 피하는 데 도움이 된다. 이 규칙을 따르는 것에 문제가 있는 것은 아니지만 항상 그래야 하는 것은 아니다. 대부분의 경우 리액트는 다음 이벤트가 발생하기 전에 state 변수를 업데이트 한다. 즉, 이벤트 핸들러의 시작 부분에서 state가 불일치 데이터일 위험이 없다.
- 그러나 동일한 이벤트 핸들러 내에서 여러 state 업데이트를 수행하는 경우 예상하는 데이터 결과를 받기 위해 updater 함수를 사용하는 것이 좋다. 이벤트 핸들러 내에서 updater 함수를 사용하는 방법은 다음과 같다.
// /Chapter03/updaterFunction.jsx
function handleClick() {
setCounter((a) => a + 1)
setCounter((a) => a + 1)
setCounter((a) => a + 1)
}
- 이 코드에서, (a) =>a+1은 updater 함수다. 리액트는 updater 함수를 큐에 넣고, 그 결과로 동일한 state 변수에 대한 업데이트가 일괄 처리된다. 다음 렌더링 중에 리액트는 동일한 순서로 함수를 호출한다.
useReducer 훅이란 무엇인가? 어떻게 사용하는가?
- useReducer 훅은 useState 훅의 대체재다. useReducer 훅은 사용자 정의 state 로직(예를 들어, state의 항목 추가, 업데이트, 삭제)을 랜더링 로직에서 분리하기 위해 사용된다. 다시말해 컴포넌트에서 state 관리를 추출하는 데 도움이 된다.
- useReducer 훅은 세 개의 인수를 받는다. 첫 번째 인수는 state를 어떻게 업데이트 할지를 지정하는 리듀서 함수이고 ,두 번째 인수는 초기 state 이며, 세 번째 함수는 초기 state를 결정하는 초기화 함수로서 선택적인 인수다. 이와 달리, useState는 초기 state 만을 받는다.
- useReducer 훅은 현재 state와 state를 수정하고 리렌더링을 호출하는 디스패치 함수로 두 개의 값으로 구성된 배열을 변환한다.
- 이 훅의 사용 사례를 카운터 예시로 이해해보자. 카운터 state 값을 리듀서 함수를 사용해 증가, 감소, 초기화 할 수 있다.
// /Chapter03/useReducerDemo.jsx
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return { count: action.payload }
default:
throw new Error()
}
}
function init(initialCount) {
return { count: initialCount }
}
function Counter() {
const initialCount = 0
const [state, dispatch] = useReducer(reducer, initialCount, init)
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({ type: 'reset', payload: initialCount })}
>
Reset
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>decrement</button>
<button onClick={() => dispatch({ type: 'increment' })}>increment</button>
</>
)
}
- 리액트는 초기 state를 저장하고 다음 렌더링에서 이를 무시할 것이다. 따라서 state를 함수 호출을 통해 파생하고 있다면 각 렌더링마다 초기 state를 재생성하지 않아야 한다. 대신, 리듀서 함수의 세 번째 인수로 초기화 함수를 사용할 수 있다.
- 앞서 언급한 코드의 세번째 인수에서 init 함수를 사용해 훅 내에서 두 번째 인수로 지정된 기본값을 토대로 초기 state를 처리했다.
- 앞서 설명한 코드의 중요한 단계는 다음과 같이 설명할 수 있다.
- 버튼 클릭 이벤트 중 하나가 트리거되면 해당 이벤트 핸들러가 리듀서 함수에 대한 액션과 함께 디스패치 된다.
- 그 후, 리듀서 함수는 요구에 따라 state를 새 state로 업데이트 한다.
- state 업데이트는 UI를 업데이트 하기 위해 컴포넌트를 리렌더링 되도록 트리거 한다.
♣ 참고 : useReducer 훅은 이전 state과 비교해서 state에 변화가 없다면 자식을 리렌더링하지 않을 것이다.
언제 useState 훅 대신 useReducer 훅을 사용해야 하는가?
- useReducer와 useState 훅 모두 애플리케이션 state를 관리하는 데 도움이 되지만 useReducer 훅은 다음과 같은 이유에서 더 state에 대한 정교한 제어와 강력한 관리가 가능하다.
- useReducer 훅은 더 복잡한 state 로직을 관리할 필요가 있을 때 useState 훅보다 낫다. 예를 들어, state가 여러 개의 복잡한 값으로 구성되거나 다음 state가 이전 state의 영향을 받을 때 useReducer 훅이 더 나은 선택 이다.
- useState 훅을 사용하면 각 액션을 위해 별개의 함수를 생성해야 하는 데 반해 useReducer 훅은 하나의 함수로 여러개의 액션을 다룰 수 있다.
- useReducer 혹은 깊거나 중첩된 업데이트를 트리거하는 컴포넌트의 성능을 최적화하는 데 도움이 된다. 왜냐하면 useReducer 훅의 디스패치 함수를 컨텍스트를 통해 어떤 중첩된 수준에서든 전달할 수 있으므로 각 수준의 컴포넌트 트리에 콜백을 전달할 필요가 없기 때문이다. 다시 말해 이는 2장에서 언급한 프롭드릴링 문제를 방지하는 데 도움이 된다. 게다가 디스패치 함수는 리렌더링 사이에 변경되지 않는다.
♣ 참고 : useState 훅이 useReducer에서 파생됐기 때문에 useReducer로 모든 state 사용 사례를 처리할 수 있다.
- 컴포넌트의 state를 더 깊은 수준의 여러 하위 컴포넌트와 공유해야 하는 경우 리액트에 내장된 컨텍스트 훅을 사용하는 것이 좋다. 이 훅은 주로 전역 state 관리로 알려진 애플리케이션 전체 데이터를 유지하는 데 사용된다.
'FE > 리액트 인터뷰 가이드' 카테고리의 다른 글
| 6. 리덕스: 최고의 상태 관리 솔루션 - 3 (0) | 2025.10.04 |
|---|---|
| 6. 리덕스: 최고의 상태 관리 솔루션 - 2 (1) | 2025.10.04 |
| 6. 리덕스: 최고의 상태 관리 솔루션 - 1 (1) | 2025.10.04 |
| 3. 훅: 함수 컴포넌트에 state와 다른 기능 추가하기 - 3 (0) | 2025.09.24 |
| 3. 훅: 함수 컴포넌트에 state와 다른 기능 추가하기 - 2 (1) | 2025.09.23 |