리액트 함수 컴포넌트에서 가장 중요한 개념인 리액트 훅에 대해 알아봅시당.
3.1.1 useState
컴포넌트에서 상태를 관리하기 위해 사용하는 가장 기본적인 훅
- 리액트 컴포넌트 내부에서 상태를 선언하고 업데이트
- 컴포넌트에서 변하는 값을 리액트가 기억하고 관리할 수 있다
기본 사용법
const [state, setState] = useState(initialState)
state
- 현재 상태 값을 저장하는 변수
- setState를 사용하여 값을 변경
setState
- 상태를 업데이트하는 함수
- 호출하면 컴포넌트가 리렌더링 된다
initialState
- 상태의 초기값. 숫자, 문자열, 객체 등 어떤 값도 가능하다.
useState는 클로저를 활용해 컴포넌트 렌더링 이후에도 상태와 상태 업데이트 함수를 기억하고 관리한다,!
게으른 초기화
리액트에서 함수 컴포넌트를 사용하면 렌더링 될 때마다 함수가 다시 호출된다.
useState에서 초기값을 설정하는데 시간이 오래 걸리거나 많은 비용을 필요로 하는 작업이라면
렌더링마다 실행되는 데 있어 성능 문제가 발생할 수 있다.
이러한 불필요한 실행은 > 게으른 초기화를 통해 해결 가능하다
// 일반적인 useState 사용
const [count, setCount] = useState(
Number.parseInt(window.localStorage.getItem(cacheKey)),
)
// 게으른 초기화: 함수를 실행해 값을 반환하게 한다
const [count, setCount] = useState(
() => Number.parseInt(window.localStorage.getItem(cacheKey)),
)
게으른 초기화는 초기값 계산을 함수로 감싸 useState에 전달하는 방식이다.
이 함수는 컴포넌트가 처음 렌더링될 때만 실행되고, 이후에는 실행되지 않는다.
>> 렌더링 성능 개선, 불필요한 계산을 방지할 수 있다.!
일반 초기화 | 게으른 초기화 |
매번 렌더링 시 초기값 계산 함수 실행 | 처음 렌더링 시 한 번만 초기값 계산 함수 실행 |
계산 비용이 큰 작업에 비효율적 | 계산 비용이 큰 작업에 최적화된 방식 |
useState(initialValue) | useState(() => initialValue) |
3.1.2 useEffect
useEffect란?
컴포넌트가 렌더링 된 이후 실행되는 작업(API 호출, 이벤트 등록 등)을 수행하도록 설정하는 훅.
> 리액트 컴포넌트에서 부수 효과(side-effect)를 처리하는 도구.
기본 사용법
function Component() {
useEffect(() => {
// 여기에 실행할 작업을 작성
}, [의존성 배열]);
}
첫 번째 인수로는 실행할 부수 효과가 포함된 함수를, 두 번째 인수로는 의존성 배열을 전달한다.
의존성 배열
- 빈 배열 []
- 컴포넌트가 처음 렌더링 될 때 한 번만 실행된다
- [어떠한 값]
- 배열에 넣은 값이 변경될 때마다 useEffect가 실행된다.
- 생략
- 매 렌더링마다 실행된다.
클린업 함수의 목적
클린업 함수
- useEffect 내부에서 반환되는 함수
- 컴포넌트가 다음 렌더링 전이나 언마운트될 때 실행된다.
이벤트를 새롭게 추가하기 직전에, 이전에 등록했던 이벤트 핸들러를 삭제하여 특정 이벤트 핸들러가 무한으로 추가되는 것을 방지한다.
>> 이전 상태를 청소해 준다는 개념!!
useEffect를 사용할 때 주의할 점
- useEffect의 두 번째 인자로 빈 배열을 넘기기 전 이 작업이 컴포넌트 상태 변화와 무관하게 딱 한 번만 실행되는 게 맞는지, 꼭 이 위치에서 실행되어야 하는지 확인하기
- useEffect의 첫 번째 인수에 함수명을 부여하여 가독성을 좋게 하기
- 거대한 useEffect 만들지 않기
- 불필요한 외부 함수 만들지 않고 최대한 useEffect 내부에 구현하기
3.1.3 useMemo
useMemo는 비용이 큰 연산에 대한 결과를 저장(메모이제이션)해두고, 이 저장된 값을 반환하는 훅이다.
기본 사용법
const memoizedValue = useMemo(() => {
// 계산할 값
return computeExpensiveValue(a, b);
}, [a, b]);
첫 번째 매개변수로는 계산할 함수로 전달하고,
두 번째 매개변수로는 함수가 의존하는 배열의 값을 전달한다.
의존성 배열 안에 있는 값이 변경될 때만 계산 함수가 실행되고, 값이 변경되지 않으면 기억해 둔 해당 값을 반환한다.
>> 특정 값이 변경되지 않으면 이전 계산 결과를 재사용하기 때문에 성능 최적화에 사용된다.!
3.1.4 useCallback
useMemo는 값을 기억했다면 , useCallback은 인스로 넘겨받은 콜백 자체를 기억한다.
useCallback은 함수를 저장(메모이제이션)하는 훅이다.
기본 사용법
const memoizedCallback = useCallback(() => {
// 실행할 함수
}, [dependency1, dependency2]);
첫 번째 매개변수로는 메모이제이션 할 콜백 함수를 받고,
두 번째 매개변수로는 함수가 의존하는 배열의 값을 전달한다.
의존성 배열에 포함된 값이 변경될 때만 새로운 함수가 생성된다.
>> 의존성 배열의 값이 변경되지 않는 한 같은 함수 인스턴스를 재사용하여 불필요한 함수 재생성을 방지한다.!
3.1.5 useRef
useRef는 참조(reference) 객체를 생성하고, 이를 통해 컴포넌트의 특정 요소나 값을 직접 조작하거나 기억할 수 있게 해주는 훅이다.
기본 사용법
const ref = useRef(initialValue);
초기값(initialValue)을 설정하고, ref.current를 통해 해당 값에 접근하거나 수정할 수 있다.
useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다는 공통점이 있지만
useRef는 값이 변경되어도 컴포넌트를 다시 렌더링 하지 않는다.
useState | useRef |
상태 값 변경 시 컴포넌트를 다시 렌더링 | 값 변경 시 렌더링 x |
setState로 값을 업데이트 | ref.current를 직접 수정 |
UI 상태 관리에 주로 사용 | 렌더링 간 값 저장, DOM 조작에 주로 사용 |
useState는 값이 변경되면 화면이 업데이트되어야 할 때 사용하고,
useRef는 값이 변경되더라도 UI에 영향을 주지 않아야 할 때 쓰인다.
3.1.6 useContext
컨텍스트 (Context)
부모- 자식 간 여러 단계의 prop 전달을 생략하고 데이터를 공유할 수 있다.
useContext는 컴포넌트에서 컨텍스트를 사용할 수 있게 해주는 훅이다.
기본 사용법
import React, { createContext, useContext } from "react";
// 1. 컨텍스트 생성
const ThemeContext = createContext();
const App = () => {
// 2. 상위 컴포넌트에서 Provider로 값 "제공"
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
};
const Toolbar = () => {
return (
<div>
<h1>Toolbar 컴포넌트</h1>
<ThemeButton />
</div>
);
};
const ThemeButton = () => {
// 3. 하위 컴포넌트에서 useContext로 값 가져오기
const theme = useContext(ThemeContext);
return <button className={theme}>현재 테마: {theme}</button>;
};
export default App;
useContext 사용 시 주의할 점
- useContext가 있는 컴포넌트는 눈으로 보이지 않는 Provider와의 의존성을 갖게 된다. >> 재사용하기 어려운 컴포넌트
- 위의 상황을 방지하고 리소스의 낭비를 막으려면 useContext의 사용범위를 최대한 좁혀야 한다.
- useContext는 상태를 주입해 주는 API이지 상태 관리를 위한 리액트의 API가 아니다.
- 단순히 props 값을 하위로 전달해 줄 뿐, 렌더링 최적화는 이룰 수 없다.
3.1.7 useReducer
useReducer는 복잡한 상태 로직을 관리하기 위해 사용하는 훅이다.
useState의 심화버전으로 상태와 상태 변경 로직을 분리하여 상태 업데이트를 더 체계적이고 직관적으로 관리할 수 있게 한다.
기본 사용법
import React, { useReducer } from "react";
// 1. 초기 상태
const initialState = { count: 0 };
// 2. Reducer 함수
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function App() {
// 3. useReducer로 상태와 dispatch 생성
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
);
}
export default App;
reducer : 상태(state)와 액션(action)을 받아서 새로운 상태를 반환한다. dispatch가 호출될 때 실행된다.
initialState : 상태의 초기값
state : 현재 useReducer가 가지고 있는 값.
dispatch : state를 업데이트하는 함수. 액션(action) 객체를 reducer에 전달하여 상태 업데이트를 트리거한다.
상태 업데이트 하는 로직을 reducer 함수로 분리하여 코드가 더 깔끔하고 유지보수하기 쉽다.
액션 객체를 통해 여러 동작을 하나의 로직으로 관리하여 다양한 상태 업데이트를 쉽게 처리할 수 있다.
3.1.8 useImperativeHandle
forwardRef
부모 컴포넌트에서 자식 컴포넌트에게 props로 ref를 넘겨주고 싶을 때 사용된다.
>> 네이밍에 자유가 주어진 props보다 ref를 전달하는 데 있어서 일관성을 제공한다.
기본 사용법
// ref를 받고자 하는 컴포넌트를 forwardRef로 감싸고 두 번째 인수로 ref를 전달받는다
const ChildComponent = forwardRef((props, ref) => {
useEffect(() => {
// {current: undefined}
// {current: HTMLInputElement}
console.log(ref)
}, [ref]);
return <div>안녕!</div>;
}
function ParentComponent() {
const inputRef = useRef();
return (
<>
//자식 컴포넌트에게 ref를 전달
<input ref={inputRef} />
<ChildComponent ref={inputRef} />
</>
);
}
useImperativeHandle은 부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅이다.
기본 사용법
import React, { useImperativeHandle, forwardRef, useRef } from "react";
// 자식 컴포넌트
const ChildComponent = forwardRef((props, ref) => {
const [value, setValue] = React.useState("");
// useImperativeHandle을 사용하여 메서드 노출
useImperativeHandle(ref, () => ({
setInputValue: (newValue) => setValue(newValue), // 부모에서 호출할 메서드
clearInput: () => setValue(""), // 입력값 초기화
}));
return (
<div>
<input
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<p>현재 값: {value}</p>
</div>
);
});
// 부모 컴포넌트
const ParentComponent = () => {
const childRef = useRef();
const handleSetValue = () => {
childRef.current.setInputValue("Hello, World!"); // 자식 컴포넌트의 메서드에 접근
};
const handleClear = () => {
childRef.current.clearInput(); // 자식 컴포넌트의 메서드에 접근
};
return (
<div>
<ChildComponent ref={childRef} /> //ref 전달
<button onClick={handleSetValue}>값 설정</button>
<button onClick={handleClear}>초기화</button>
</div>
);
};
export default ParentComponent;
useImpertiveHandle을 사용하면 부모가 자식의 상태를 직접 변경하거나 메서드를 호출할 수 있다.!
3.1.9 useLayoutEffect
useLayoutEffect는 DOM이 업데이트된 직후, 브라우저가 화면을 그리기(render) 전에 실행되는 훅이다.
공식문서
> 이 함수의 시그니처는 useEffect와 동일하나, 모든 DOM의 변경 후에 동기적으로 발생한다.
ㄴ 두 훅의 형태나 사용 예제가 동일하다는 의미
실행 순서
- 리액트가 DOM을 업데이트
- useLayoutEffect를 실행
- 브라우저에 변경 사항을 반영
- useEffect를 실행
순서상으로는 useEffect가 먼저 선언돼 있지만 항상 useLayoutEffect가 먼저 실행된다.
>> useLayoutEffect가 브라우저에 변경 사항이 반영되기 전에 실행되고, useEffect는 브라우저에 변경사항이 반영된 이후 실행되기 때문.
useEffect | useLayoutEffect |
DOM 업데이트 후, 브라우저가 화면을 그린 뒤 실행 | DOM 업데이트 후 , 브라우저가 화면을 그리기 전에 실행 |
비동기 작업, 데이터 가져오기, DOM과 무관한 작업 | DOM을 조작하거나 동기 작업이 필요한 경우 |
브라우저가 화면을 빨리 그리도록 (우선순위 낮음) | UI를 조정하기 위해 레이아웃 변경을 우선처리 |
그럼 언제 useLayoutEffect를 사용하는 것이 좋을까?
DOM의 요소를 기반으로 한 애니메이션, 스크롤 위치를 제어하는 등 화면에 반영되기 전에 하고 싶은 작업에 useLayoutEffect를 사용한다면 useEffect를 사용했을 때보다 훨씬 더 자연스러운 사용자 경험을 제공할 수 있다.
3.1.10 useDebugValue
useDebugValue
리액트 앱 개발 과정에서 디버깅하고 싶은 정보를 이 훅에 사용하면 리액트 개발자 도구에서 확인 가능하다.
기본 사용법
useDebugValue(date, (date)=> `현재 시간 :${date.toISOString()}`)
// 첫번째 인수값이 변하지 않으면 두번째 함수가 실행되지 않는다.
사용 시 주의해야 할 사항
다른 훅 내부에서 사용해야 작동하고, 컴포넌트 단위에서 사용 시 제대로 작동하지 않는다.
3.1.11 훅의 규칙
- 최상위에서만 훅을 호출해야 한다.
- 반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다.
- 훅을 호출할 수 있는 것은 리액트 함수 컴포넌트, 사용자 정의 훅 두 가지 경우뿐으로, 일반 자바스크립트 함수에서는 훅을 사용할 수 없다.
3.2.1 사용자 정의 훅
사용자 정의 훅
- 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용되는 것.
- 리액트에서만 사용할 수 있는 방식
- 반드시 use로 시작하는 함수를 만들어야 한다.(해당 함수가 리액트 훅이라는 것을 인식할 수 있다)
사용자 정의 훅으로 분리하지 않는다면 fetch로 API를 호출해야 하는 모든 컴포넌트 내에서 각각 선언해서 구현해야 한다.
useReducer을 사용하더라도 useEffect가 필요하기 때문에 훅을 중복해서 사용해야 한다.
훅은 함수 컴포넌트 내부 혹은 사용자 정의 훅 내부에서만 사용할 수 있기 때문에 use로 시작하지 않거나 대문자로 시작하지 않는 함수 내부에서 훅을 호출한다면 에러가 발생한다.
3.2.2 고차 컴포넌트
컴포넌트 자체의 로직을 재사용하기 위한 방법이다.
리액트 훅, 사용자 정의 훅은 리액트에서만 사용할 수 있지만 고차 컴포넌트는 고차함수의 일종으로,
자바스크립트의 일급 객체, 함수의 특징을 이용하기 때문에 자바스크립트 환경에서도 널리 쓰일 수 있다.
리액트에서는 고차 컴포넌트 기법으로 다양한 최적화나 중복 로직 관리를 할 수 있다.
3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
사용자 정의 축과 고차 컴포넌트 모두 리액트 코드에서 어떠한 로직을 공통화해 별도로 관리할 수 있다는 특징이 있다.
애플리케이션 전반에 필요한 중복된 로직을 별도로 분리해 컴포넌트의 크기를 줄이고 가독성을 향상하는 데 도움을 준다.
사용자 정의 훅이 필요한 경우
- 공통 로직이 있는 경우: useEffect, useState 등 리액트 훅을 이용해 공통 로직을 쉽게 관리할 수 있다.
- 비즈니스 로직의 재사용: 앱 내에서 특정 기능이나 상태가 여러 컴포넌트에서 재사용될 때 사용자 정의 훅이 적합하다.
- 성능 측면에서 더 효율적: 고차 컴포넌트처럼 렌더링을 추가로 발생시키지 않고 로직만 제공하기 때문에 성능에 더 유리하다.
고차 컴포넌트를 사용해야 하는 경우
- 컴포넌트의 렌더링을 조건부로 제어: 예를 들어, 특정 사용자만 접근 가능한 컴포넌트를 만들 때 유용하다.
- 재사용 가능한 컴포넌트 확장: 고차 컴포넌트를 사용하면 컴포넌트의 UI와 관련된 로직을 추가하거나 변경할 수 있다.
- 에러 처리: 공통적으로 에러 처리 로직을 추가하거나, 상태에 따라 다른 컴포넌트를 보여줘야 할 때도 사용된다.
'모던 리액트 Deep Dive 스터디' 카테고리의 다른 글
크롬 개발자 도구를 활용한 애플리케이션 분석 (2) | 2024.11.23 |
---|---|
리액트 개발 도구로 디버깅하기 (0) | 2024.11.19 |
사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야할까? (1) | 2024.11.16 |
리액트 훅 깊게 살펴보기(useReducer, useRef) (7) | 2024.11.15 |
리액트 훅 깊게 살펴보기(useState, useEffect, useMemo, useCallback) (0) | 2024.11.12 |