React Hook
리액트 프레임워크가 제공하는 훅(hook)의 개념과 사용법
리액트에서는 컴포넌트의 생명주기(생성, 수정)를 제어할 수 있는 여러 가지 hook을 제공한다.
리액트와 리액트 네이티브의 컴포넌트의 속성은 불변이다. 한 번 렌더링된 컴포넌트의 상태는 변경할 수 없다. 컴포넌트의 데이터(상태)를 바꾸고 싶다면 hook을 사용해야 한다.
hook은 Spring에서의 bean을 등록하고 사용하는 것과 유사하다. 함수 또는 값을 리액트 캐시에 저장해놓고 필요할 때마다 사용하는 매커니즘이다.
useState
컴포넌트의 상태 변수와 상태를 업데이트 하는 콜백함수를 정의한다.
import React, {useState} from 'react'
const [상태_변수, set함수] = useState<상태의 타입>(초기값);
이렇게 획득한 set함수는 아래와 같이 사용하면 된다.
import {useState} from 'react'
const [avatars, setAvatars] = useState<Avatar>([]);
// 1. 새로운 상태를 직접 설정하기
setAvatars([...avatars, {/* 새로운 Avatar 객체 */}]);
// 2. 이전 상태를 기반으로 새로운 상태 설정
setAvatars((prevAvatars) => [...prevAvatars, {/* 새로운 Avatar 객체 */}]);
// 3. 초기화
setAvatars([]);
Closure와 Capture
클로저(closure)란 함수와 해당 함수가 선언된 환경(lexical environment)의 조합이다.
function createCounter() {
let count = 0;
return function() {
return count++;
};
}
const counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
useEffect
의존성 목록의 조건 중 어느 하나라도 만족하면 그때마다 콜백함수를 실행시킨다.
import React, {useEffect} from 'react'
useEffect(콜백, 의존성_목록)
콜백 = () => {}
의존성
리액트 프레임워크에서 의존성이란 캐시를 업데이트해야 하는 특정 상황을 의미한다. 리액트 프레임워크는 의존성 목록(배열) 중 하나라도 조건이 만족되면 캐시를 업데이트하고 해당 컴포넌트를 다시 랜더링하여 업데이트 내용을 화면에 반영한다.
한 번 저장된 캐시를 반환할 필요가 없다면 의존성으로 빈 배열을 전달하면 된다.
잘못된 의존성
의존성을 지정하지 않으면 컴포넌트의 상태가 바뀌어도 화면에 반영되지 않는다. 여기에서 상태란 useState
에 의해 관리되는 대상이다.
아래의 코드는 start
값이 false
에서 true
로 변해도 화면에 반영되지 않는다.(재랜더링 되지 않는다.)
export default function Timer(): ReactElement {
const [avatars, setAvatars] = useState<IdandAvatar>([]);
const [start, setStart] = useState(false);
const toggleStart = useCallback(() => setStart(start => !start), []);
const clearAvatars = useCallback(() => setAvatars(_ => []), []);
let intervalId: NodeJS.Timeout;
const addAvatar = () => {
intervalId = setInterval(() => {
if (start)
setAvatars(avatars => [
...avatars,
{id: D.randomId(), avatar: D.randomAvatarUrl()},
]);
}, 1000);
};
useEffect(() => {
addAvatar();
return () => clearInterval(intervalId);
}, []); // useEffect에 전달된 콜백의 의존성의 정의되지 않았다.
// 이하 생략
}
useEffect
에 전달된 콜백의 의존성이 정의되지 않았기 때문에 start
값이 바뀌어도 다시 랜더링되지 않는다.
Re-rendering trigger
모든 hook이 컴포넌트의 재랜더링을 trigger하지는 않는다. useEffect
는 직접적으로 컴포넌트의 렌더링을 트리거하지 않는다. 다만 useEffect
의 콜백 내부에서 useState
가 관리하는 상태를 변경하는 함수(상태 변경함수)를 호출하는 경우 컴포넌트의 재랜더링을 트리거한다.
그렇기 때문에 useEffect
의 의존성이 상태가 아니라 그냥 로컬 변수인 경우, 값이 변경되도 랜더링 되지 않는다.(화면에 반영되지 않는다.)
useMemo
와 useCallback
훅은 각각 값이나 콜백을 캐싱(memoization)한다. 의존성 배열에 명시된 값에 변화가 있을 때에만 새로운 값이나 콜백을 생성한다. 의존성이 객체일 경우 deep copy와 shallow copy에 주의해야 한다.
useMemo
useMemo
useCallback
useEffect
모두 의존성 배열을 인자로 받으며 의존성에 변화가 발생할 때마다 콜백을 호출하여 의존성을 반영한다. 그리고 해당 컴포넌트는 다시 랜더링된다.
import React, {useMemo} from 'react'
const 캐시_데이터 = useMemo(콜백, [의존성1, 의존성2, ...])
콜백 = () => 원본_데이터
useMemo
의 시그니쳐는 다음과 같다. useMemo
는 캐시에 저장할 데이터를 반환하는 함수와 의존성을 인자로 받는다.
useMemo<T>(() => T, [의존성1, 의존성2, ...]): T
아래의 Memo
컴포넌트는 처음 렌더링될 때 D.makeArray(5).map(D.createRandomPerson)
으로 생성한 데이터를 리액트 프레임워크가 관리하는 캐시에 저장한다.
import React, {ReactElement, useMemo} from 'react';
import * as D from '../data';
export default function Memo(): ReactElement {
const people = useMemo(() => D.makeArray(5).map(D.createRandomPerson), [])
return <></>;
}
useCallback
콜백 함수를 캐시한 뒤 필요할 때마다 꺼내서 사용하여 매번 콜백함수가 생성되는 것을 막아 성능을 향상시킨다.
const 캐시된_함수 = useCallback(캐시할_콜백, 의존성_목록)
useEffect vs useLayoutEffect
useEffect
- asynchronous: 콜백함수의 실행이 완료될때까지 DOM painting을 기다리지 않는다.
useLayoutEffect
- synchronous: 콜백함수의 실행이 완료될때까지 기다린 뒤 DOM을 화면에 출력(paint)한다.
리액트 공식문서에서는 useEffect 사용을 권장한다.
실습
아래의 코드들은 모두 결과가 똑같다. start를 터치하면 1초마다 랜덤으로 생성된 아바타가 추가된다.
addAvatar를 캐싱하지 않음
addAvatar
함수를 useCallback
hook을 통해 캐싱하지 않았기 때문에 컴포넌트가 다시 랜더링 될때마다 addAvatar
함수가 다시 정의된다.
import React, {ReactElement, useState, useCallback, useEffect} from 'react';
import {StyleSheet, View, Text, ScrollView} from 'react-native';
import {MD2Colors} from 'react-native-paper';
import {Avatar} from '../components';
import * as D from '../data';
const title = 'Timer';
type IdandAvatar = Pick<D.Person, 'id' | 'avatar'>;
export default function Timer(): ReactElement {
const [avatars, setAvatars] = useState<IdandAvatar>([]);
const [start, setStart] = useState(false);
const toggleStart = useCallback(() => setStart(start => !start), []);
const clearAvatars = useCallback(() => setAvatars(_ => []), []);
let intervalId: NodeJS.Timeout;
const addAvatar = () => {
intervalId = setInterval(() => {
if (start)
setAvatars(avatars => [
...avatars,
{id: D.randomId(), avatar: D.randomAvatarUrl()},
]);
}, 1000);
};
useEffect(() => {
addAvatar();
return () => clearInterval(intervalId);
}, [start]);
const children = avatars.map(({id, avatar}) => (
<Avatar
key={id}
uri={avatar}
size={70}
viewStyle={styles.avatarViewStyle}
/>
));
return (
<View style={[styles.view]}>
<View style={styles.topBar}>
<Text onPress={toggleStart} style={styles.topBarText}>
{start ? 'stop' : 'start'}
</Text>
<Text onPress={clearAvatars} style={styles.topBarText}>
clear avatars
</Text>
</View>
<Text style={styles.title}>{title}</Text>
<ScrollView contentContainerStyle={styles.contentContainerStyle}>
{children}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
view: {
flex: 1,
padding: 5,
backgroundColor: MD2Colors.lime300,
alignItems: 'center',
},
title: {fontSize: 30, fontWeight: '600'},
topBar: {
width: '100%',
flexDirection: 'row',
padding: 5,
justifyContent: 'space-between',
backgroundColor: MD2Colors.blue900,
},
topBarText: {fontSize: 20, color: 'white'},
contentContainerStyle: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
},
avatarViewStyle: {padding: 5},
});
addAvatar 함수를 캐싱
addAvatar
함수를 캐싱했기 때문에 컴포넌트가 다시 렌더링되어도 함수를 재정의 하지 않고 저장된 함수를 불러와 사용한다.
export default function Timer(): ReactElement {
const [avatars, setAvatars] = useState<IdandAvatar>([]);
const [start, setStart] = useState(false);
let intervalId: NodeJS.Timeout;
const addAvatar = useCallback(() => {
intervalId = setInterval(() => {
if (start)
setAvatars(avatars => [
...avatars,
{id: D.randomId(), avatar: D.randomAvatarUrl()},
]);
}, 1000);
}, [start]);
const toggleStart = useCallback(() => setStart(start => !start), []);
const clearAvatars = useCallback(() => setAvatars(_ => []), []);
useEffect(() => {
addAvatar();
return () => clearInterval(intervalId);
}, [start]);
const children = avatars.map(({id, avatar}) => (
<Avatar
key={id}
uri={avatar}
size={70}
viewStyle={styles.avatarViewStyle}
/>
));
return (
<View style={[styles.view]}>
<View style={styles.topBar}>
<Text onPress={toggleStart} style={styles.topBarText}>
{start ? 'stop' : 'start'}
</Text>
<Text onPress={clearAvatars} style={styles.topBarText}>
clear avatars
</Text>
</View>
<Text style={styles.title}>{title}</Text>
<ScrollView contentContainerStyle={styles.contentContainerStyle}>
{children}
</ScrollView>
</View>
);
}
Comments