[React 19] 공식문서 톺아보기 - React Hook (1)
useActionState
폼 액션의 결과를 기반으로 State를 업데이트할 수 있도록 도와주는 Hook
const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);
const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);
폼제출에 특화되어있다
서버액션fn
과 초기값initialState
을 넣게 되면,
- 상태변경을 감지할수 있는 폼 액션
formAction
을 리턴 받는다. - 폼 액션
formAction
을 사용하여 제출하게 될경우state
와isPending
이 자동으로 업데이트된다.
tanstack-query의 useMutation 과 비슷한 기능을 제공한다.
서버액션 함수를 사용해야 한다
fn
은 두가지를 인자로 받게 되는데prevState
,formData
이는 폼제출을 더 용이하게 해준다.prevState
: 이전 상태값을 가진다. 아무런 입력이 없었을때는initialState
를 나타낸다.formData
: 현재 폼 데이터를 나타낸다.<input name="email">
→ formData.get('email')로 접근할 수 있다.
점진적 향상 지원
- 자바스크립트가 꺼진상태나 아직 로드중일때도 제출이 가능하다.
// 서버가 생성하는 HTML (순수 HTML), js가 로드되기전 동작 <form action="/_next/action" method="POST"> <input name="email"> <button type="submit">가입</button> </form>
// 서버가 생성하는 HTML (순수 HTML), js가 로드되기전 동작 <form action="/_next/action" method="POST"> <input name="email"> <button type="submit">가입</button> </form>
// React가 DOM을 인수(하이드레이션)하면서 변환 <form action={serverAction}> {/* 프로그래매틱 폼 처리 */} <input name="email"> <button type="submit">가입</button> </form>
// React가 DOM을 인수(하이드레이션)하면서 변환 <form action={serverAction}> {/* 프로그래매틱 폼 처리 */} <input name="email"> <button type="submit">가입</button> </form>
// 내부적으로 이런 로직이 동작 if (navigator.onLine && isJSLoaded) { event.preventDefault(); fetchAction(formData); } else { // 기본 폼 제출 유지 }
// 내부적으로 이런 로직이 동작 if (navigator.onLine && isJSLoaded) { event.preventDefault(); fetchAction(formData); } else { // 기본 폼 제출 유지 }
하이드레이션하면서 DOM의 속성이 바뀌게 되는데 이때 리렌더링이 발생할까?
속성만 변경된 경우는 리렌더링이 발생하지 않는다.
"같은 타입의 두 React DOM 엘리먼트를 비교할 때, React는 두 엘리먼트의 속성을 확인하여, 동일한 내역은 유지하고 변경된 속성들만 갱신합니다."
리액트 공식문서 재조정 에 대한 설명
permalink
- 폼액션 결과가 url에 반영되어야할 경우 (예를들면 검색결과 /search?q=야구) permalink에 바뀔 url을 넣어서 새로고침없이 반영할 수 있다.
- 상태와 url 동기화
- 브라우저 히스토리 스택에 상태 저장
폼 액션 vs 폼 제출
// 일반적인 폼 제출
<form action="/api/submit" method="POST">
<input name="email">
<button>제출</button>
</form>
// 일반적인 폼 제출
<form action="/api/submit" method="POST">
<input name="email">
<button>제출</button>
</form>
- 기본 HTML 동작
- 페이지 새로고침 발생
// 폼 액션
<form action={serverAction}>
<input name="email">
<button>제출</button>
</form>
// 폼 액션
<form action={serverAction}>
<input name="email">
<button>제출</button>
</form>
- React/Next.js의 고급 기능
- 현재 페이지 유지하면서 상태만 업데이트
useCallback
useCallback
은 리렌더링 간에 함수 정의를 캐싱해 주는 Hook
const cachedFn = useCallback(fn, dependencies);
const cachedFn = useCallback(fn, dependencies);
fn
: 캐싱할 함숫값- React는 첫 렌더링에서 이 함수를 반환한다. (호출하는 것이 아니다.)
- 다음 렌더링에서
dependencies
값이 이전과 같다면 React는 같은 함수를 다시 반환한다.
dependencies
:fn
내에서 참조되는 모든 반응형 값의 목록- 반응형 값은 props와 state, 그리고 컴포넌트 안에서 직접 선언된 모든 변수와 함수를 포함한다.
React 공식 문서에서 말하는 useCallback를 써야하는 기준
- 페이지내에서 미세한 상호작용을 하는 경우
- 대부분의 상호작용이 (페이지 전체나 전체 부문을 교체하는 것처럼) 굵직한 경우, 보통 memoization이 필요하지 않다. 반면에 앱이 (도형을 이동하는 것과 같이) 미세한 상호작용을 하는 그림 편집기 같은 경우, memoization이 매우 유용할 수 있다.
- 의존성 배열이 자주 바뀌지 않는 함수 (의존성 배열이 자주바뀌어 그때마다 재선언되면 의미가 없어진다.)
- 렌더링 시간 기준 10ms 이상 소요될 경우
- 반복 실행 빈도가 높을 경우
사용예시1 - memo로 감싼 컴포넌트에 props로 함수를 넘길때
function ProductPage({ productId, referrer, theme }) {
// theme이 바뀔때마다 리렌더링이 되어 재선언된다.
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ShippingForm의 props는 theme와 관계가 없음에도 매번 리렌더링이 된다. */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
function ProductPage({ productId, referrer, theme }) {
// theme이 바뀔때마다 리렌더링이 되어 재선언된다.
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ShippingForm의 props는 theme와 관계가 없음에도 매번 리렌더링이 된다. */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
theme
를 바뀔 경우ProductPage
이 리렌더링되면서handleSubmit
이 재선언되고ShippingForm
도 리렌더링 된다.- 이때
ShippingForm
는theme
를 props로 가지고 있지 않기에 최적화가 가능하다.ShippingForm
를memo
로 감싸고handleSubmit
를useCallback
으로 감싸면theme
가 바뀌어도ShippingForm
은 리렌더링 되지 않는다.
function ProductPage({ productId, referrer, theme }) {
// useCallback를 통해 리렌더링 간에 함수를 캐싱한다.
const handleSubmit = useCallback(
(orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
},
[productId, referrer], // 이 의존성이 변경되지 않는 한 다시 계산하지 않는다.
);
return (
<div className={theme}>
{/* ShippingForm은 같은 props를 받게 되고 theme값이 변하더라도 리렌더링을 건너뛸 수 있다. */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
function ProductPage({ productId, referrer, theme }) {
// useCallback를 통해 리렌더링 간에 함수를 캐싱한다.
const handleSubmit = useCallback(
(orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
},
[productId, referrer], // 이 의존성이 변경되지 않는 한 다시 계산하지 않는다.
);
return (
<div className={theme}>
{/* ShippingForm은 같은 props를 받게 되고 theme값이 변하더라도 리렌더링을 건너뛸 수 있다. */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
useEffect안에서만 함수를 사용할때 내부에 선언하는게 좋을까? 아니면 외부에 useCallback을 이용해서 선언하는게 좋을까?
useEffect
내부에 선언하는게 더 좋다.
useEffect
가 실행될때마다 함수가 재선언되지만, 함수 선언은 무거운 연산이 아니다.- 외부 의존성이 필요없고 해당
useEffect
에서만 사용된다는 것을 바로 알 수 있다.만약 다른곳에서 사용되거나 함수 로직이 길어지게 될 경우는 외부에 선언하는게 더 좋다.
사용예시2 - 커스텀 훅에서 함수를 반환하는 경우
function Parent() {
const { increment } = useCounter(); // 매번 새 함수 생성
return <Child onClick={increment} />;
}
function Parent() {
const { increment } = useCounter(); // 매번 새 함수 생성
return <Child onClick={increment} />;
}
-
훅 사용자가 별도 최적화 없이도 안정적인 함수 참조 보장한다.
- 만약
increment
를useCallback
을 이용해서 선언하지 않았다면memo
로Child
를 감쌌다고 하더라도, Parent가 리렌더링될때마다 매번increment
가 새로 생성되므로 함수 객체의 참조가 변경되어Child
는 props가 바뀌었다고 인식한다. 따라서 같은 함수임에도 리렌더링이 발생한다. - 이러한 최적화에 대한 처리를 사용하는 쪽에서 매번 신경쓰지 않기 위해 훅안에서
useCallback
을 사용하면 좋다.
- 만약
꼭 useCallback을 쓰지 않아도 됩니다
function Report({ item }) {
const handleClick = () => {
sendReport(item);
};
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
function Report({ item }) {
const handleClick = () => {
sendReport(item);
};
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
-
위 컴포넌트를 최적화 할때
memo
vshandleClick
를useCallback
vs 그냥두기- 세가지 모두
item
이 바뀌게 되면 리렌더링이 발생한다. - 그러나 부모컴포넌트에서
item
이 아닌 다른 이유로 리렌더링이 발생하게 된다면memo
로 감싸면 리렌더링을 막을 수 있다.
- 세가지 모두
번외) children 과 리렌더링
// React.memo를 사용할 때
const MemoA = React.memo(A);
// 방식1: MemoA가 리렌더링되면 B도 무조건 리렌더링
function Parent({item}) {
return (
<MemoA item={item} /> // A 안에 <B />
);
}
// 방식2: MemoA가 children을 prop으로 받으면
function Parent({item}) {
return (
<MemoA>
<B item={item} /> {/* B는 MemoA의 렌더링 영향에서 자유로워질 수 있음 */}
</MemoA>
);
}
// React.memo를 사용할 때
const MemoA = React.memo(A);
// 방식1: MemoA가 리렌더링되면 B도 무조건 리렌더링
function Parent({item}) {
return (
<MemoA item={item} /> // A 안에 <B />
);
}
// 방식2: MemoA가 children을 prop으로 받으면
function Parent({item}) {
return (
<MemoA>
<B item={item} /> {/* B는 MemoA의 렌더링 영향에서 자유로워질 수 있음 */}
</MemoA>
);
}
- A에서는 item이 쓰이지 않고 자식 컴포넌트인 B에서만 사용된다면, A에서 B를 children으로 분리하고 A를 memo로 최적화하자.
- item값 변화에 대해서 A는 자유로워진다.
- B는 A의 렌더링에서 자유로워진다.
useContext
useContext
는 컴포넌트에서 Context를 읽고 구독할 수 있는 Hook
// 정의할때
const ThemeContext = createContext(defaultValue);
// 정의할때
const ThemeContext = createContext(defaultValue);
// 값을 사용할때
const value = useContext(ThemeContext);
// 값을 사용할때
const value = useContext(ThemeContext);
ThemeContext
:createContext
로 생성한 Context이다. Context 자체는 정보를 담고 있지 않으며, 컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타낸다.value
: 트리에서 호출하는 컴포넌트 상위의 가장 가까운ThemeContext.Provider
에 전달된 값으로 결정된다.- Provider가 없으면 반환된 값은 해당 Context에 대해
createContext
에 전달한defaultValue
가 된다. - 반환된 값은 항상 최신 상태로 값이 변경되면 즉시 반영된다.
- Provider가 없으면 반환된 값은 해당 Context에 대해
중첩 provider 구조
- 컴포넌트 내의
useContext()
호출은 동일한 컴포넌트에서 반환된 Provider에 영향을 받지 않는다. 해당하는<Context.Provider>
는useContext()
호출을 하는 컴포넌트 상위에 배치되어야 전달하는 값을 제대로 반영한다.
const value = useContext(ThemeContext)
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
)
const value = useContext(ThemeContext)
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
)
- 즉, 위와 같이 컨텍스트가 중첩으로 감싸는 구조일때
value
값은 "dark"가 아니라 상위 컴포넌트면서 가장가까운 Provider에 영향을 받는다.
<ThemeContext.Provider value='dark'>
...
<ThemeContext.Provider value='light'>
<Footer />
</ThemeContext.Provider>
...
</ThemeContext.Provider>
<ThemeContext.Provider value='dark'>
...
<ThemeContext.Provider value='light'>
<Footer />
</ThemeContext.Provider>
...
</ThemeContext.Provider>
- 가장 가까운 Provider가 제공하는 값을 참조한다는 점에서 동일한 값에 대해 정반대 값을 쓴다거나 영향을 받을때 오버라이딩을 하는 식으로 중첩구조를 활용할 수 있을것 같다.
- 이러한 경우를 제외하고 다른 값을 사용하고 싶을때는 중첩되게 Provider를 사용하여 Context 값의 범위를 혼란스럽게 만드는것보다는 여러개의 Context로 분리해서 사용하는것이 더 알기쉬운 구조를 만드는것 같다.
Context 와 memo
- memo로 감싸더라도 내부에서 context를 호출하고 있고 값이 변경되었다면 리렌더링이 발생한다.
memo
가 방지하는 것: 부모 컴포넌트의 리렌더링 전파memo
가 막지 못하는 것: Context 값 변화로 인한 리렌더링
- Context는 주로 "자주 변경되지 않는 값"에 사용하면 좋다. 자주 바뀌는 값에 대해서 Context를 사용하면 어디서 리렌더링을 유발하는지 예측하기 어렵고 이를 구독하고 있는 모든 컴포넌트가 리렌더링이 된다.
사용방법1 - 싱글톤으로 선언
// 여러 파일에서 createContext()를 호출하면 다른 Context 인스턴스 생성
// FileA.js
const ThemeContext = createContext();
// FileB.js
const ThemeContext = createContext(); // 다른 인스턴스!
// 여러 파일에서 createContext()를 호출하면 다른 Context 인스턴스 생성
// FileA.js
const ThemeContext = createContext();
// FileB.js
const ThemeContext = createContext(); // 다른 인스턴스!
// contexts/ThemeContext.js
const ThemeContext = createContext('light'); // 단일 인스턴스 생성
export default ThemeContext;
// App.js
import ThemeContext from './contexts/ThemeContext';
// contexts/ThemeContext.js
const ThemeContext = createContext('light'); // 단일 인스턴스 생성
export default ThemeContext;
// App.js
import ThemeContext from './contexts/ThemeContext';
- Context를 사용하는 쪽에서
createContext
를 선언하지 않고 따로 선언하고 불러와서 사용하는 방식으로 관리해야 중복선언을 막아 동일한 인스턴스 내에서 값을 공유할 수 있다.
// .eslintrc.js
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/!(contexts)/*.js'],
importNames: ['createContext'],
message: 'Context는 contexts/ 디렉토리에서만 생성하세요.'
}
]
}
]
}
// .eslintrc.js
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/!(contexts)/*.js'],
importNames: ['createContext'],
message: 'Context는 contexts/ 디렉토리에서만 생성하세요.'
}
]
}
]
}
- 린트 룰로 contexts 폴더아래가 아닌곳에서
createContext
를 호출하려고할때 에러를 줄수도 있다.
사용방법2 - 커스텀 훅과 결합
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('ThemeProvider 없음!');
return context;
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error('ThemeProvider 없음!');
return context;
}
- 사용할때마다
useContext
로 값을 불러오는것보다 위와같은 선언을 통해 훅으로 값을 불러오고, Provider로 감싼 범위 밖에서 사용할때 에러를 반환하는것이 좋다.
객체와 함수를 전달할때 렌더링 최적화
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
function login(response) {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}
return (
<AuthContext.Provider value={{ currentUser, login }}>
<Page />
</AuthContext.Provider>
);
}
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
function login(response) {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}
return (
<AuthContext.Provider value={{ currentUser, login }}>
<Page />
</AuthContext.Provider>
);
}
- 위의 경우 MyApp이 리렌더링 될때마다
login
은 재선언되고 이로 인해서useContext(AuthContext)
를 호출하는 모든 컴포넌트도 같이 리렌더링이 된다.
import { useCallback, useMemo } from 'react';
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
const login = useCallback((response) => {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}, []);
const contextValue = useMemo(
() => ({
currentUser,
login,
}),
[currentUser, login],
);
return (
<AuthContext.Provider value={contextValue}>
<Page />
</AuthContext.Provider>
);
}
import { useCallback, useMemo } from 'react';
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
const login = useCallback((response) => {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}, []);
const contextValue = useMemo(
() => ({
currentUser,
login,
}),
[currentUser, login],
);
return (
<AuthContext.Provider value={contextValue}>
<Page />
</AuthContext.Provider>
);
}
-
따라서
useCallback
을 통해 login 함수가 재선언되지 않도록 최적화한다. -
value
에 전달하는 객체도 매 렌더링마다 새로 생성되지 않도록 전달하는 객체를useMemo
로 감싼다.- _여러 값을 Context로 공유할때는
useMemo
로 감싸고 객체로 전달하거나 각각을 Context로 분할해야한다.
- _여러 값을 Context로 공유할때는
context 사용시 유의사항
- value가 없는 Provider가 있는지 확인
- 싱글톤을 적용했는지 체크
- 호출하는 곳과 Provider가 같은 컴포넌트내에 있는지 체크 (Provider가 더 상위에 존재)
- value로 전달하는 객체가 렌더링 최적화가 되어있는지
- 자주 바뀌지 않는 값으로 설정
useDebugValue
useDebugValue
는 React DevTools에서 커스텀 훅에 라벨을 추가할 수 있게 해주는 Hook
useDebugValue(value, format?)
useDebugValue(value, format?)
value
: React DevTools에 표시하고 싶은 값format
: 포맷팅 함수 (예시 :value ⇒ 값 : ${value}
)
사용예시
import { useDebugValue } from 'react';
function useOnlineStatus() {
// ...
useDebugValue(isOnline ? 'Online' : 'Offline');
// ...
}
import { useDebugValue } from 'react';
function useOnlineStatus() {
// ...
useDebugValue(isOnline ? 'Online' : 'Offline');
// ...
}
- devTools에서는 OnlineStatus : 'Online' 와 같이 나타난다.
- OnlineStatus는 훅에서 use가 빠진 이름 (위 예시에서 훅의 이름이 useOnlineStatus 이므로 OnlineStatus 로 표기)
useDeferredValue
useDeferredValue
는 일부 UI 업데이트를 지연시킬 수 있는 Hook
const deferredValue = useDeferredValue(value, initialValue?)
const deferredValue = useDeferredValue(value, initialValue?)
value
: 지연시키려는 값- 렌더링마다 선언되는 값이 아니어야 한다.
initialValue
: 컴포넌트 초기 렌더링 시 사용할 값- 생략하면 초기 렌더링 동안
useDeferredValue
는 값을 지연시키지 않는다. 이는 대신 렌더링할value
의 이전 버전이 없기 때문이다.
- 생략하면 초기 렌더링 동안
deferredValue
: 첫 렌더링에서 ‘지연된 값’은initialValue
이고 업데이트가 발생하면 React는 먼저 이전 값으로 리렌더링을 시도하고 그 다음 백그라운드에서 다시 새 값으로 리렌더링을 시도한다.
지연되는 과정
- 초기 렌더링:
deferredValue = 초기값
→ 화면 표시 - 새 값 입력:
setValue('새값')
호출 - React 동작:
deferredValue
는 이전 값(초기값
)을 유지한 채로- 백그라운드에서 새 값(
'새값'
) 계산 준비 - 계산 완료후 React가 새 값으로 컴포넌트를 자연스럽게 교체
- 만약
value
와deferredValue
를 하나의 컴포넌트에서 동시에 사용하게 된다면 하나의 상태 변경이 두 가지 다른 타이밍의 렌더링을 유발할 수 있다. (이러한 과정은 우선순위에 따른 ui업데이트를 하는 동시성 기능과 연관되어있다.)
콘텐츠가 오래되었음을 표시하기
const deferredValue = useDeferredValue(value, initialValue?)
const deferredValue = useDeferredValue(value, initialValue?)
value
와deferredValue
의 값이 다르다는 것을 통해 현재 백그라운드에서 계산중이라는 것을 인지할 수 있다.
주의 사항
-
deferredValue
는 백그라운드에서 ui와 로직을 계산하고 완료되었을때 값이 바뀐다.- 즉, 값이 바뀌는 과정을 보여주고 싶지 않을때 사용한다.
- 리렌더링을 방지하는 목적이 아니다. 실제로
value
의 값이 변했기에 리렌더링은 발생하고deferredValue
를 props로 받는 컴포넌트에 대해서만 렌더링을 지연시킨다. - useEffect 의존성 배열에 포함되어있다면 Effect의 재실행 기준은 value이 아니라
deferredValue
기준으로 실행된다.
-
useDeferredValue
의 목적은 렌더링 우선순위 조정을 하는 것이다.- 긴급 업데이트(입력)는 즉시 반영
- 비긴급 업데이트(결과 표시)는 나중에 처리
-
ui 업데이트만 지연시킬뿐 네트워크 요청에 대한 최적화가 적용되는건 아니다.
- 검색어 입력에 따른 api 요청이 있다고 할때 ‘나무’를 입력하게 되면 초기값에서 중간 입력에 대한 결과를 생략하고 '나무'에 대한 결과를 보여주지만 실제로는 ‘ㄴ’, ‘나’, ‘남’, ‘나무’ 에 대한 네가지 요청을 모두 하게 된다.
- 단, 새로운 값 변경이 되면 그전 백그라운드 계산은 중단되고 새값으로 다시 계산한다.
- 네트워크 요청도 최적화하는 방법
- 디바운스와 결합하여 요청한다.
useTransition
와useDeferredValue
를 함께 적용한다.
- 검색어 입력에 따른 api 요청이 있다고 할때 ‘나무’를 입력하게 되면 초기값에서 중간 입력에 대한 결과를 생략하고 '나무'에 대한 결과를 보여주지만 실제로는 ‘ㄴ’, ‘나’, ‘남’, ‘나무’ 에 대한 네가지 요청을 모두 하게 된다.
useEffect
useEffect
는 외부 시스템과 컴포넌트를 동기화하는 Hook
useEffect(setup, dependencies?)
useEffect(setup, dependencies?)
setup
: React는 컴포넌트가 DOM에 추가된 이후에 설정 함수를 실행한다. 의존성 변화에 따라 리렌더링이 될경우, 클린업 함수를 실행하고 새로운 값으로 설정함수를 실행한다. 컴포넌트가 DOM에서 제거되었을때도 클린업 함수를 실행한다.dependencies
: 설정 함수의 코드 내부에서 참조되는 모든 반응형 값들이 포함된 배열로 구성된다. 반응형 값에는 props와 state, 모든 변수 및 컴포넌트 body에 직접적으로 선언된 함수들이 포함된다. 의존성을 생략할 경우, Effect는 컴포넌트가 리렌더링될 때마다 실행된다.
실행순서
- 컴포넌트가 마운트 될때 설정 함수가 실행된다.
- 의존성이 변경된 컴포넌트가 리렌더링 될 때마다 아래 동작을 수행한다.
- 먼저 정리 함수가 오래된 props와 state와 함께 실행된다.
- 이후, 설정 함수가 새로운 props와 state와 함께 실행된다.
- 컴포넌트가 언마운트 되고나서 정리 함수가 마지막으로 실행된다.
엄격모드와 정리함수
- React는 첫 번째 설정 함수가 실행되기 이전에 개발 모드에만 한정하여 한 번의 추가적인 설정 + 정리 사이클을 실행한다.
- 이는 정리 로직이 설정 로직을 완벽히 “반영”하고 설정 로직이 수행하는 작업을 중단하거나 취소할 수 있는지를 확인하는 테스트이다.
- 설정 함수가 한 번 호출될 때와 [설정 → 정리 → 설정] 순서로 호출될 때의 차이가 없어야 한다.
- 정리 함수는 설정 함수가 여러번 실행되었을때 문제가 발생하는 경우에만 필요하다.
- 예를들면 구독이벤트, 애니메이션 초기설정 등이 있다.
- 멱등성을 가지고 있는 경우에는 정리 함수가 없어도 된다.
의존성 배열 최적화하기
-
Effect에서 사용하는 모든 반응형 값은 의존성으로 선언되어야 한다.
- 값이 변할때 렌더링에 영향을 주지 않는
ref
라고해도 부모 컴포넌트로부터 전달받은 값인 경우, 자식 컴포넌트에서는 해당 값이 변경될지 안될지 알 수 없으므로 의존성 배열에 추가해야한다.
- 값이 변할때 렌더링에 영향을 주지 않는
-
상수를 사용하는 경우
- 컴포넌트 밖에 선언함으로써 리렌더링이 될때 변경되지 않음을 증명하여 의존성에서 제거할 수 있다.
-
기존 state 값을 이용해서 업데이트 시키는 경우
- 업데이터 함수를 통해 의존성에서 제거할 수 있다.
setCount(count + 1)
대신setCount(prev ⇒ prev + 1)
으로 사용한다.
-
객체를 선언하고 Effect내에서 사용하는 경우
useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId, }; const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]);
useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId, }; const connection = createConnection(options); connection.connect(); return () => connection.disconnect(); }, [roomId]);
- 리렌더링 될때마다 새로 생성되기 때문에 객체 선언은 Effect 내에서 선언하거나
useMemo
로 감싸서 객체를 의존성으로 사용하는 것을 피할 수 있다.
- 리렌더링 될때마다 새로 생성되기 때문에 객체 선언은 Effect 내에서 선언하거나
-
컴포넌트 내에 선언된 함수를 Effect내에서 사용하는 경우
- 리렌더링 될때마다 함수가 새로선언되므로 Effect내에 선언하거나,
useCallback
으로 감싸서 최적화를 해야합니다.
- 리렌더링 될때마다 함수가 새로선언되므로 Effect내에 선언하거나,
실험기능) useEffectEvent를 이용하여 원하는 반응형값을 의존성에서 제거하기
const onVisit = useEffectEvent((visitedUrl) => {
logVisit(visitedUrl, shoppingCart.length);
});
useEffect(() => {
onVisit(url);
}, [url]);
const onVisit = useEffectEvent((visitedUrl) => {
logVisit(visitedUrl, shoppingCart.length);
});
useEffect(() => {
onVisit(url);
}, [url]);
- 위와 같이
useEffectEvent
로 감싼 로직에서 사용되는 반응형 값은 의존성에 추가하지 않아도 된다. 그 외의 반응형값이 Effect에서 사용됨에도 의존성에 없는 경우는 린트가 이를 감지한다. // eslint-ignore-next-line react-hooks/exhaustive-deps
주석을 추가하여 의존성을 제거하는 방법은 지양해야한다.- 주석의 경우 코드가 수정되어 다른 반응형 값이 추가되어도 감지를 못하는 반면,
useEffectEvent
는 해당 부분에서 사용하는 반응형 값만 린트에서 벗어나기 때문에 다른 반응형 값이 추가되어도 린트의 도움을 받을 수 있다.
- 주석의 경우 코드가 수정되어 다른 반응형 값이 추가되어도 감지를 못하는 반면,
useId
useId
는 접근성 어트리뷰트에 전달할 수 있는 고유 ID를 생성하기 위한 Hook
const id = useId();
const id = useId();
- useId는 컴포넌트 인스턴스의 수명주기와 연관된 고유ID를 생성하기 위해 만들어졌다.
- 이 값을 key에 지정하면 안된다. 매 렌더링마다 새로운 값을 반환하여 매번 전체 리스트를 리렌더링 한다.
- 직접 id값을 쓰지않고, 중복되지 않는 값이 필요할때 쓴다.
사용예시 1 - 접근성 속성연결
function InputField() {
const id = useId();
return (
<>
<label htmlFor={id}>이메일</label>
<input id={id} type='email' />
</>
);
}
function InputField() {
const id = useId();
return (
<>
<label htmlFor={id}>이메일</label>
<input id={id} type='email' />
</>
);
}
사용예시 2 - 중복되지 않는 DOM ID 생성
function CheckboxList() {
const id = useId();
return (
<div>
{items.map((item) => (
<div key={item.id}>
<input id={`${id}-${item.id}`} type='checkbox' />
<label htmlFor={`${id}-${item.id}`}>{item.label}</label>
</div>
))}
</div>
);
}
// ---
<>
<CheckboxList />
<CheckboxList />
</>;
function CheckboxList() {
const id = useId();
return (
<div>
{items.map((item) => (
<div key={item.id}>
<input id={`${id}-${item.id}`} type='checkbox' />
<label htmlFor={`${id}-${item.id}`}>{item.label}</label>
</div>
))}
</div>
);
}
// ---
<>
<CheckboxList />
<CheckboxList />
</>;
- 같은 컴포넌트가 여러 번 렌더링될 때 ID 충돌을 방지한다. (id는 고유해야하므로)
사용예시 3 - 서버/클라이언트 일관성 보장
export default function Page() {
return (
<div>
{/* 항상 같은 위치에 렌더링 */}
<CheckboxList /> {/* ":r1:1" (서버/클라이언트 일치) */}
</div>
);
}
export default function Page() {
return (
<div>
{/* 항상 같은 위치에 렌더링 */}
<CheckboxList /> {/* ":r1:1" (서버/클라이언트 일치) */}
</div>
);
}
useId()
가 생성하는 고유 ID는 컴포넌트의 트리 구조와 렌더링 순서에 따라 결정되므로 서버와 클라이언트 간 동일한 트리 구조가 유지되면 같은 ID가 생성된다.- 다른 트리 구조면 다른 id가 리턴된다.
- 중복을 피하기 위해 단지 랜덤한 값을 생성하는 다른 것을 이용하면 서버와 클라이언트에서 다른 값이 나와 하이드레이션 오류가 발생한다.
useImperativeHandle
useImperativeHandle
은 ref로 노출되는 핸들을 사용자가 직접 정의할 수 있게 해주는 Hook
useImperativeHandle(ref, createHandle, dependencies?)
useImperativeHandle(ref, createHandle, dependencies?)
ref
: 부모 컴포넌트의 prop으로 받은ref
createHandle
: 인수가 없고 노출하려는 ref 핸들을 반환하는 함수이다.dependencies
:createHandle
코드 내에서 참조하는 모든 반응형 값을 나열한 목록이다.- 리렌더링으로 인해 일부 의존성이 변경되거나 이 인수를 생략한 경우
createHandle
함수가 다시 실행되고 새로 생성된 핸들이 ref에 할당된다.
- 리렌더링으로 인해 일부 의존성이 변경되거나 이 인수를 생략한 경우
사용예시 - 노출하고자하는 메서드만 노출한다.
function MyInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input ref={inputRef} />;
}
function MyInput({ ref }) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input ref={inputRef} />;
}
- 부모 컴포넌트가
focus
및scrollAndFocusAddComment
메서드를 호출할 수 있다. 그 외 DOM 노드의 전체 엑세스 권한은 없다. - 메서드의 이름을 반드시 DOM 메서드와 일치하게 선언하지 않아도 된다.