리액트 16.8부터 훅(Hooks)이 도입되어 함수형 컴포넌트에서 상태와 생명주기 기능을 사용할 수 있게 되었다.
화면을 그릴 때마다 호출되는 훅의 숫자와 순서를 동일하게 하기 위하여 루프, 조건 분기, 콜백 함수 안에서는 훅을 호출할 수 없다. 이런 위치에서 훅을 호출하는 코드를 작성하면, 빌드 에러 또는 실행 시 에러가 발생한다. 따라서, 훅은 컴포넌트의 최상위 레벨에서만 호출되어야 한다. 또한, 리액트 훅은 반드시 함수 컴포넌트에서만 사용해야 한다.
용도
용도 | 훅 |
컴포넌트 데이터 관리 | useMemo |
useCallback | |
useState | |
useReducer | |
컴포넌트 생명 주기 대응 | useEffect |
useLayoutEffect | |
컴포넌트 메서드 호출 | useRef |
useImperativeHandle | |
컴포넌트 간의 정보 공유 | useContext |
특징
- 같은 리액트 훅을 여러 번 호출할 수 있다.
- 함수 몸통이 아닌 몸통 안 복합 실행문의 { } 안에서 호출할 수 없다.
- 비동기 함수를 콜백 함수로 사용할 수 없다.
리액트가 화면을 그리는 순서
1. 초기 렌더링(마운트)
- ReactDOM.render()나 컴포넌트의 처음 렌더링이 시작된다.
- 렌더링이 시작되면 해당 컴포넌트의 render() 함수가 호출된다.
- render() 함수는 Virtual DOM에 해당하는 React 엘리먼트를 생성한다.
- Virtual DOM에 있는 엘리먼트들을 실제 DOM에 반영하여 화면에 그린다.
2. 상태나 속성 변경에 따른 업데이트
- 컴포넌트의 상태나 속성이 변경되면 리액트는 해당 컴포넌트를 다시 렌더링 한다.
- 이때 변경된 상태나 속성을 기반으로 render() 함수가 다시 호출되고, Virtual DOM이 업데이트된다.
- 이후 Virtual DOM의 변경 내용을 기존의 실제 DOM과 비교하여 변경 사항을 찾는다.
- 변경된 부분만을 실제 DOM에 반영하여 화면을 업데이트한다.
3. 언마운트
- 컴포넌트가 화면에서 제거되면 해당 컴포넌트의 componentWillUnmount() 메서드가 호출된다.
- 이때 컴포넌트가 사용한 리소스를 해제하거나 정리 작업을 수행한다.
리액트가 화면을 다시 그리는 시점
1. Props나 내부 상태가 업데이트됐을 때
컴포넌트의 props나 내부 상태가 변경되면, 리액트는 해당 컴포넌트를 다시 렌더링 한다. 이때, 컴포넌트의 render 함수가 호출되어 가상 DOM이 다시 생성되고, 변경된 부분만 실제 DOM에 반영된다.
2. 컴포넌트 안에서 참조하는 Context 값이 업데이트됐을 때
Context를 사용하여 상태를 전역적으로 관리하는 경우, 해당 Context 값이 업데이트되면 그 값을 참조하는 모든 하위 컴포넌트들이 다시 렌더링 된다. 이는 Context의 Provider가 새로운 값을 제공할 때 발생한다.
3. 부모 컴포넌트가 다시 그려졌을 때
부모 컴포넌트가 다시 렌더링 되면, 그 부모에게서 상속받은 props나 상태가 변경되거나, 부모 컴포넌트 자체가 변경되었을 수 있다. 이 경우 부모 컴포넌트의 다시 그려짐에 따라 자식 컴포넌트도 다시 렌더링 된다.
의존성 배열(deps)
컴포넌트가 처음 렌더링될 때 훅이 실행되어 초기 상태 값을 설정한다. 이후 각 훅은 의존성 배열을 사용하여 훅이 언제 다시 실행될지를 결정한다.
사용법
파라미터 | 훅 실행 |
빈 배열 [] | 컴포넌트가 처음 마운트될 때만 실행 |
특정 값이 있는 배열 [a, b] | a 나 b가 변경될 때마다 실행 |
생략 | 컴포넌트가 렌더링될 때마다 실행 |
예제
useEffect(() => {
console.log('Effect executed');
return () => {
console.log('Cleanup');
};
}, [count]); // count가 변경될 때마다 함수가 실행된다.
useMemo & useCallback
메모이제이션용 훅이다. 값이나 함수를 유지하고, 불필요한 자식 요소의 렌더링이나 계산을 억제하기 위해 사용된다. 메모이제이션 컴포넌트는 부모 컴포넌트에서 화면 다시 그리기가 발생했을 때도, props나 context 값이 바뀌지 않은 경우에는 부모 컴포넌트에 의한 화면 다시 그리기가 발생하지 않는다.
useMemo
useMemo는 메모이제이션된 값을 반환한다. 복잡한 계산을 수행하는 경우, 결과 값을 메모이제이션하여 불필요한 재계산을 방지할 수 있다. 또한, useMemo로 생성된 값은 해당 값을 props로 받는 하위 컴포넌트들의 재랜더링도 방지한다.
import { useState, useMemo } from 'react';
const Example = () => {
const [a, setA] = useState(1);
const [b, setB] = useState(1);
const expensiveValue = useMemo(() => {
return a + b;
}, [a, b]);
return (
<div>
<button onClick={() => setA(a + 1)}>Increment A</button>
<button onClick={() => setB(b + 1)}>Increment B</button>
<div>{expensiveValue}</div>
</div>
);
};
export default Example;
useCallback
함수를 메모이제이션 하기 위한 훅이다. 함수가 불필요하게 다시 생성되는 것을 방지함으로써 불필요한 렌더링을 줄이고, 컴포넌트의 성능을 향상시킬 수 있다. 특히 컴포넌트가 다시 렌더링될 때 마다 함수가 재생성되는 경우, 해당 함수를 props로 받는 하위 컴포넌트들도 다시 렌더링 되는데, 이는 성능 저하를 초래할 수 있다.
import { useState, useCallback } from 'react';
const Example = () => {
const [count, setCount] = useState(0);
// useCallback을 사용하여 함수를 메모이제이션
const handleClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []); // 의존성 배열이 빈 배열이므로 함수는 컴포넌트가 마운트될 때 한 번만 생성됨
return (
<div>
<p>Count: {count}</p>
{/* handleClick 함수를 사용하는 버튼 */}
<button onClick={handleClick}>Increment</button>
</div>
);
};
export default Example;
useState & useReducer
컴포넌트에서 상태를 관리하기 위한 훅이다. 상태 변수를 선언하고, 그 변수를 업데이트하는 함수를 반환한다. 이 훅들을 사용하면 컴포넌트는 내부 상태를 가지며, 해당 상태의 변화에 따라 표시를 변경할 수 있다.
useState
import { useState } from 'react';
const Example = () => {
// count를 0으로 초기화한다.
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
// 버튼 클릭 시 setCount를 통해 count를 1씩 증가시킨다.
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export default Example;
- 버튼을 클릭하면 setCount(count + 1)가 호출된다.
- setCount는 count 상태를 업데이트하고, React는 상태 변경을 감지한다.
- 상태가 변경되었기 때문에, React는 Example 컴포넌트를 다시 렌더링 한다.
- 컴포넌트가 다시 렌더링 되면서, Example 함수가 다시 호출된다.
- 새로운 count 값이 <p>{count}</p>에 반영된다.
의존성 문제
const increment = useCallback(() => {
setCount(count + 1) // 캐시된 함수 내의 count는 항상 0
}, []) // 의존성 목록에 count를 등록하지 않음
위에 함수는 의존성 목록에 count를 등록하지 않음으로 useCallback에 캐시 된 함수는 항상 같다.
1. 첫 번째 방법
const increment = useCallback(() => {
setCount(count + 1) // 의존성 목록에 count 넣지 않으면 count는 항상 0
}, [count]) // 의존성 목록에 count를 넣어야 함
이 경우, count가 의존성 배열에 포함되어 있으므로 count 값이 변경될 때마다 increment 함수가 새로 생성된다. 따라서 최신 count 값을 참조할 수 있다.
2. 두 번째 방법
const increment = useCallback(() => {
setCount(count => count + 1) // 함수를 입력 변수로 세터 호출
}, []) // 의존성 목록에 count를 넣지 않아도됨
이 방법에서는 setCount에 함수형 업데이트를 사용한다. 함수형 업데이트는 이전 상태 값을 매개변수로 받아 새로운 상태를 반환한다. 이 방식은 의존성 배열에 count를 포함할 필요가 없으므로 increment 함수는 항상 동일한 참조를 유지하고, 최신 상태를 올바르게 업데이트할 수 있다.
useReducer
useState보다도 복잡한 용도에 적합하다. 배열이나 객체 등의 여러 데이터를 모은 것을 상태로 다루는 경우에 많이 사용한다. useReducer는 업데이트 함수(dispatch)에 action이라 불리는 데이터를 전달한다.
import { useReducer } from 'react';
// reducer 함수가 받을 수 있는 action 타입을 정의
type Action = "DECREMENT" | "INCREMENT" | "DOUBLE" | "RESET";
// reducer 함수를 정의한다. 현재 상태와 action을 받아 새로운 상태를 반환한다.
const reducer = (currentCount: number, action: Action) => {
switch(action){
case "INCREMENT":
return currentCount + 1;
case "DECREMENT":
return currentCount - 1;
case "DOUBLE":
return currentCount * 2;
case "RESET":
return 0;
default:
return currentCount;
}
}
// Counter 컴포넌트의 props 타입을 정의
type CounterProps = {
initialValue: number;
}
const Counter = ({ initialValue }: CounterProps) => {
// useReducer 훅을 사용하여 상태와 디스패치 함수를 가져온다
const [count, dispatch] = useReducer(reducer, initialValue);
return (
<div>
<p>Count : {count}</p>
{/* 각 버튼이 클릭될 때마다 해당 action을 디스패치 */}
<button onClick={() => dispatch("DECREMENT")}>-</button>
<button onClick={() => dispatch("INCREMENT")}>+</button>
<button onClick={() => dispatch("DOUBLE")}>x2</button>
<button onClick={() => dispatch("RESET")}>Reset</button>
</div>
);
};
export default Counter;
버튼 클릭 시 일어나는 일
- 클릭 시 핸들러에서는 dispatch(action)가 호출된다.
- 이렇게 전달된 액션은 reducer 함수에 전달된다.
- reducer 함수에서는 결과를 새로운 상태로 반환한다.
- 이렇게 반환된 새로운 상태는 useReducer 훅에서 반환된 count 상태로 갱신되어 화면에 반영된다.
useEffect & useLayoutEffect
컴포넌트가 렌더링 된 후에 부수 효과(side effects)를 수행하는 훅으로 컴포넌트 렌더링 이후 실행된다.
부가 작용이란 컴포넌트의 그리기와는 직접적인 관계가 없는 처리를 말한다. 예를 들어, DOM의 수동변경, 로그 출력, 타이머 설정, 데이터 취득, 구독 설정 등이 있다.
useEffect
useEffect는 화면 그리기 함수가 실행되고, DOM이 업데이트되고, 화면에 실제로 그려진 뒤에 실행된다.
import { useState, useEffect } from 'react';
const TimerComponent = () => {
const [timer, setTimer] = useState(0);
useEffect(() => {
// 타이머 설정
const interval = setInterval(() => {
setTimer((prevTimer) => prevTimer + 1);
}, 1000);
// 클린업 함수: 컴포넌트가 언마운트되거나 업데이트되기 직전에 실행됨
return () => {
clearInterval(interval); // 타이머 해제
console.log('Timer Cleared'); // 클린업 로그
};
}, []); // 빈 배열을 전달하여 컴포넌트가 마운트될 때 한 번만 실행
return (
<div>
<p>Timer: {timer}</p>
</div>
);
};
export default TimerComponent;
useLayoutEffect
useEffect 실행 시점이 다르다. useLayoutEffect는 DOM이 업데이트된 후, 화면에 실제로 그려지기 전에 실행된다.
주로 DOM 요소의 크기나 위치와 관련된 작업을 수행할 때 사용된다. 예를 들어, 요소의 크기가 변경되었을 때 적절한 조치를 취하거나, 렌더링 후에 DOM에 대한 측정이 필요한 경우에 사용된다.
useLayoutEffect의 작업이 브라우저의 렌더링을 차단할 수 있으므로, 최대한 사용을 줄이고 성능에 영향을 미칠 수 있는 작업을 지양해야 한다.
import { useLayoutEffect, useRef } from 'react';
const Example = () => {
const divRef = useRef(null);
useLayoutEffect(() => {
if (divRef.current) {
divRef.current.style.color = 'blue';
}
}, []);
return <div ref={divRef}>This text will be blue.</div>;
};
export default Example;
리액트 18에서의 useEffect/useLayoutEffect의 작동
리액트 18에서, <React.StrictMode> 아래의 컴포넌트 안에서 useEffect와 useLayoutEffect가 안전하지 않은 부가 작용을 발견하기 위해, 컴포넌트가 화면을 두 번 그리게 된다. 따라서 빈 배열을 전달했을 때, 마운트 시에 useEffect나 useLayoutEffect가 두 번 호출된다. 이러한 현상은 StrictMode에서만 발생하며, 개발 모드에서만 동작한다.
이는 useRef 등을 사용하여 앞에서 실행 여부를 저장하여 대처할 수 있다. 이를 통해 컴포넌트가 처음으로 렌더링 될 때 특정 부작용이 한 번만 발생하도록 보장할 수 있다.
useRef & useImperativeHandle
컴포넌트 간의 상태 관리와 통신에 사용되는 훅이다.
useRef
useRef는 치환 가능한 ref 객체를 반환한다. ref는 크게 2가지 방법으로 사용된다.
- 데이터 저장: ref 객체에 저장된 값은 업데이트되더라도 화면을 다시 그리지 않는다. 때문에 화면 그리기와 관계없는 데이터를 저장할 때 사용한다. 데이터는 ref.current에서 읽거나 치환한다.
- DOM 참조: ref는 컴포넌트에 전달하면, 이 요소가 마운트될 때, ref.current에 DOM 참조가 설정되어, DOM 함수등을 호출할 수 있다.
import { useRef } from 'react';
const SimpleRefExample = () => {
const inputRef = useRef<HTMLInputElement | null>(null);
const handleFocus = () => {
// input 요소에 포커스를 준다.
inputRef.current?.focus();
};
return (
<div>
{/* useRef로 생성한 ref 객체를 input 요소의 ref 속성에 할당한다. */}
<input type="text" ref={inputRef} />
{/* 버튼을 클릭하면 handleFocus함수가 호출되고 input 요소에 포커스를 준다. */}
<button onClick={handleFocus}>Focus Input</button>
</div>
);
};
export default SimpleRefExample;
useImperativeHandle
컴포넌트에 ref가 전달될 때, 부모의 ref에 대입될 값을 설정할 때 사용한다. useImperativeHandle를 사용함으로써, 자식 컴포넌트가 가진 데이터를 참조하거나, 자식 컴포넌트에 정의된 함수를 부모로부터 호출할 수 있다.
훅의 첫 번째 인수에는 부모로부터 전달된 ref, 두 번째 인수에는 객체가 반환하는 함수를 정의, 세 번째 인수에는 의존 배열을 전달할 수 있다.
import { useRef, useImperativeHandle, forwardRef } from 'react';
// 자식 컴포넌트, forwardRef를 사용해 ref를 전달받을 수 있도록 한다.
const ChildComponent = forwardRef((props, ref) => {
// 내부에서 사용할 input 요소에 대한 ref 생성
const inputRef = useRef<HTMLInputElement | null>(null);
// 부모에서 접근할 수 있도록 메서드를 정의한다.
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
}
}
}));
// input 요소에 inputRef를 연결
return <input ref={inputRef} type="text" />;
});
// 부모 컴포넌트
const ParentComponent = () => {
// 자식 컴포넌트의 메서드를 참조할 ref 객체 생성
const childRef = useRef<{ focus: () => void; clear: () => void }>(null);
return (
<div>
{/* ChildComponent에 ref를 전달 */}
<ChildComponent ref={childRef} />
{/* 버튼 클릭 시 자식 컴포넌트의 focus 메서드를 호출 */}
<button onClick={() => childRef.current?.focus()}>Focus Input</button>
{/* 버튼 클릭 시 자식 컴포넌트의 clear 메서드를 호출 */}
<button onClick={() => childRef.current?.clear()}>Clear Input</button>
</div>
);
};
export default ParentComponent;
forwardRef
React에서 부모 컴포넌트가 자식 컴포넌트의 내부 DOM 요소나 인스턴스 메서드에 접근할 수 있도록 ref를 전달하는데 사용되는 고차 함수이다. 보통 ref는 HTML 요소에 직접 연결하거나 클래스 컴포넌트에 연결할 때 사용되지만, 함수형 컴포넌트에서는 직접적으로 ref를 받을 수 없기 때문에 forwardRef 를 사용한다.
useContext
useContext는 Context로부터 값을 참조하기 위한 훅이다.
import { useContext } from 'react';
type User = {
id: number;
name: string;
}
// UserContext를 생성하고 초기값으로 null을 설정다.
const UserContext = React.createContext<User | null>(null);
// 자식 컴포넌트인 GrandChild는 UserContext에서 사용자 정보를 가져와 화면에 표시한다.
const GrandChild = () => {
const user = useContext(UserContext);
// 사용자 정보가 있으면 환영 메시지를 표시하고, 없으면 아무것도 표시하지 않는다.
return user !== null ? <p>Hello, {user.name}</p> : null;
}
// 자식 컴포넌트인 Child는 현재 날짜를 표시하고, 그 하위에 GrandChild를 렌더링한다.
const Child = () => {
const now = new Date();
return (
<div>
<p>Current: {now.toLocaleString()}</p>
<GrandChild />
</div>
);
}
// 부모 컴포넌트인 Parent는 사용자 정보를 생성하여 UserContext.Provider로 제공한다.
const Parent = () => {
// 가짜 사용자 정보를 생성한다.
const user: User = {
id: 1,
name: "Alice"
}
// UserContext.Provider로 하위 컴포넌트에 사용자 정보를 전달한다.
return (
<UserContext.Provider value={user}>
<Child />
</UserContext.Provider>
);
}
커스텀훅 & useDebugValue
커스텀훅
React의 훅(Hook) 기능을 사용하여 개발자가 자신의 요구에 맞게 재사용 가능한 상태 로직을 캡슐화하는 방법이다. 기본적으로, 여러 컴포넌트에서 공통으로 사용되는 로직을 하나의 함수로 추출하여, 해당 로직을 간단하게 재사용할 수 있도록 한다. 커스텀 훅은 일반적인 함수처럼 작동하지만, 내부적으로 React 훅을 사용할 수 있으며, use 접두사를 붙여서 명명한다.
useDebugValue
디버그 용도로 사용되는 훅이다. 이 훅은 REact Developer Tools라 불리는 브라우저 확장 기능을 사용한 리액트 애플리케이션 개발 지원 도구와 함께 사용된다. 훅이 실행될 때마다 인수의 데이터가 React Developer Tools에 전달되고, 그 데이터는 React Developer Tools의 Components 탭에서 확인할 수 있다.
import { useDebugValue, useState, useEffect } from 'react';
// friendID에 따른 친구의 온라인 상태를 추적하는 커스텀 훅
function useFriendStatus(friendID: number) {
// isOnline 상태를 정의, 초기값은 null
const [isOnline, setIsOnline] = useState<boolean | null>(null);
// 디버깅 목적으로 isOnline 상태값을 표시
// 값은 개발자 도구의 Components 탭에 표시된다.
useDebugValue(isOnline ? 'Online' : 'Offline');
useEffect(() => {
// 친구의 상태가 변경되었을 때 호출되는 콜백 함수
function handleStatusChange(status: { isOnline: boolean }) {
setIsOnline(status.isOnline);
}
// 가상의 ChatAPI를 사용하여 친구의 상태를 구독
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
// 컴포넌트 언마운트 시 구독 해제
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
}, [friendID]); // friendID가 변경될 때마다 effect를 재실행
// 친구의 온라인 상태를 반환
return isOnline;
}
export default useFriendStatus;
'Javascript > React' 카테고리의 다른 글
깊은 복사와 얕은 복사, 그리고 의존성 목록 (0) | 2024.07.13 |
---|---|
Form 다루기 (0) | 2024.07.13 |
[React] DetailedHTMLProps와 HTMLAttributes 이해하기 (0) | 2024.06.29 |
[React] PropsWithChildren 이해하기 (0) | 2024.06.19 |
styled-components에서 $ 접두사 사용하기 (0) | 2024.06.01 |