요약하자면 Hook은 함수형 컴포넌트가 클래스형 컴포넌트의 기능을 사용할 수 있도록 해주는 기능입니다.
hook을 사용해 함수형 컴포넌트에서도 state와 생명주기를 다룰 수 있기에 클래스형 컴포넌트에서만 가능하던 상태관리를 더 손쉽게 할 수 있어 필요합니다.
함수형 컴포넌트에서 상태를 관리하기 위해서 만들어졌습니다.
const [count, setCount] = useState<number>(0);
// 상태 변경 (re-rendering 발생)
const handlePress = () => {
setCount(1);
};
return (
<TouchableOpacity onPress={handlePress}>
<Text>{count}</Text>
</TouchableOpacity>
)
product.price
값의 변화를 감지하지 못하는 경우
const [product, setProduct] = useState({
name: '과자',
price: 2000
});
const handlePress = () => {
product.price = 3000;
};
return (
<TouchableOpacity onPress={handlePress}>
<Text>{product.price}</Text>
</TouchableOpacity>
)
React는 상태가 변경되면 얕은 비교 즉, 참조값 비교를 통해 이전 값과 현재 값이 같은 객체인지 체크합니다.
해당 경우가 리렌더링이 발생하지 않은 이유는 product 객체의 price라는 프로퍼티만 업데이트 했을뿐 price라는 참조값이 변경된건 아니기때문에 발생하지 않았습니다.
const Something = (props) => {
// ⚠️ createRows()는 모든 렌더링에서 호출됩니다
const [rows, setRows] = useState(createRows(props.count));
return (
...
)
};
export default Something;
const Something = (props) => {
// ✅createRows()는 한 번만 호출됩니다
const [rows, setRows] = useState(() => createRows(props.count));
return (
...
)
};
export default Something;
일반적인 사용처는 자식에게 명령적으로 접근하는 경우입니다.
다양한 사용처가 있으며, 그 사용처는 아래와 같습니다.
import React, { useRef } from 'react';
...
const inputRef = useRef<TextInput>(null);
...
useEffect(() => {
// 이런 형태로 Element에 접근이 가능합니다
inputRef.current.focus();
}, []);
return (
<>
<TextInput
ref={inputRef}
...
>
</>
)
import React, { useRef } from 'react';
...
const productInfoRef = useRef<ItemDto>();
...
// 상품 정보를 useRef에 담는다
const handleSaveProductInfo = (item: ItemDto) => {
productInfoRef.current = item;
}
return (
<>
...
<div onClick={() => handleSaveProductInfo(item)}>
상품 정보를 저장
</div>
</>
)
위 2가지 케이스에서 초기값 지정은 어떻게 해줘야 할까?
DOM을 직접 조작하기 위한 용도
// DOM을 직접 조작하기 위해 프로퍼티로 useRef 객체를 사용할 경우, RefObject<T>를 사용해야 하므로 초깃값으로 null을 넣어주자
const inputRef = useRef<HTMLInputElement>(null);
로컬 변수 용도
// 로컬 변수 용도로 useRef를 사용하는 경우, MutableRefObject<T>를 사용해야 하므로 제네릭 타입과 같은 타입의 초깃값을 넣어주자
const localVarRef = useRef<number>(0);
// A.tsx
...
const aRef = useRef
useEffect 함수는 렌더링 될 때 마다 특정작업을 실행할 수 있도록 하는 hook입니다.
이는 기존 클래스 형식에서 쓰던 생명주기 메소드를 함수형 컴포넌트에서도 사용할 수 있게 된 것입니다.
동작 순서는 useEffect
로 전달된 함수는 지연 이벤트 동안에 레이아웃 배치와 그리기를 완료한 후
발생합니다.
// 배열안을 생략하면 마운트될 때만 작동합니다
useEffect(() => {
console.log('마운트될 때 실행!')
}, [])
const [visible, setVisible] = useState(false);
// 마운트 할때, visible의 값이 바뀌기 직전에 호출
useEffect(() => {
console.log('visible값이 변경!');
// 이 부분이 dps
}, [visible]);
useEffect(() => {
console.log('마운트 때 작동');
// cleanup 함수 반환 (return 뒤에 나오며 useEffect 뒷정리 함수라고 합니다)
return () => {
// 언마운트는 벗어날 때를 뜻합니다
console.log('언마운트 때 작동');
}
}, []);
useLayoutEffect 는 컴포넌트들이 render된 후 실행되며, paint 되기 전에 실행됩니다. 이 작업은 동기적으로 실행됩니다. 또한, dom 을 조작하는 코드가 존재하더라도 사용자는 깜빡임을 경험하지 않습니다.
사용법은 useEffect와 동일합니다.
useLayoutEffect(() => {
console.log('동작');
}, []);
const Screen = () => {
const [value, setValue] = useState<number>(0);
// 이 동작은 paint 되기전 실행함으로 value가 바뀌면서 렌더링될때 깜빡이는 현상을 캐치하지 않습니다
useLayoutEffect(() => {
if (value === 0) {
setValue(10);
}
}, [value]);
// 반면 아래 동작은 value가 바뀌면서 깜빡임을 보이기때문에 사용자가 불편함을 느낄 수 있습니다
useEffect(() => {
if (value === 0) {
setValue(10);
}
}, [value]);
return (
<button onClick={() => setValue(0)}>
{value}
</button>
);
};
useLayoutEffect 는 동기적으로 실행되고 내부의 코드가 모두 실행된 후 painting 작업을 거칩니다. 따라서 로직이 복잡할 경우 사용자가 레이아웃을 보는데까지 시간이 오래걸린다는 단점이 있어, 기본적으로는 항상 useEffect 만을 사용하는 것을 권장합니다. 구체적인 예시로는
등의 작업은 항상 useEffect 를 사용하되, 화면이 깜빡거리는 상황일 때, 예를들어 위와같이(4-2의 코드 비교 케이스같이) state 이 존재하며, 해당 state 이 조건에 따라 첫 painting 시 다르게 렌더링 되어야 할 때는 useEffect 사용 시 처음에 0이 보여지고 이후에 re-rendering 되며 화면이 깜빡거려지기 때문에 useLayoutEffect 를 사용하는 것이 바람직 합니다.
useMemo는 메모이제이션된 값을 return하는 hook이며, 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화 하는 용도로 사용됩니다.
(*메모이제이션이란? 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술입니다)
import { useMemo, useEffect, useState } from 'react';
function App() {
const [number, setNumber] = useState(0);
const [isKorea, setIsKorea] = useState(true);
// const location = { country: isKorea ? '한국' : '일본' };
const location = useMemo(() => {
return {
country: isKorea ? '한국' : '일본'
}
}, [isKorea])
return (
<header className="App-header">
<h2>하루에 몇 끼 먹어요?</h2>
<input type="number" value={number} onChange={(e) => setNumber(e.target.value)}/>
<hr/>
<h2>어느 나라에 있어요?</h2>
<p>나라: {location.country}</p>
<button onClick={() => setIsKorea(!isKorea)}>Update</button>
</header>
);
}
export default App;
import React, { useState } from 'react';
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const getAverage = () => {
console.log('평균값 계산 중...');
if (list.length === 0) return 0;
const sum = list.reduce((a, b) => a + b);
return sum / list.length;
};
const onChange = (e) => {
setNumber(e.target.value);
};
const onInsert = () => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber('');
};
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {getAverage()}
</div>
</div>
);
};
export default Average;
결과는?
이유는?
위에서 언급했듯이 re-rendering 되는 조건들에 해당합니다.
state 변경이 있을 때 (setState에 의해서)
props가 업데이트 됐을 때 (전달받은 props가 업데이트 됐다면 렌더링)
부모 컴포넌트가 업데이트 될 때
useMemo를 사용했을 때의 경우
import React, { useState, useMemo } from 'react';
const Average = () => {
const [list, setList] = useState([]);
const [number, setNumber] = useState('');
const getAverage = useMemo(() => {
console.log('평균값 계산 중...');
if (list.length === 0) return 0;
const sum = list.reduce((a, b) => a + b);
return sum / list.length;
}, [list]); //list값이 업데이트 될때만 실행
const onChange = (e) => {
setNumber(e.target.value);
};
const onInsert = () => {
const nextList = list.concat(parseInt(number));
setList(nextList);
setNumber('');
};
return (
<div>
<input value={number} onChange={onChange} />
<button onClick={onInsert}>등록</button>
<ul>
{list.map((value, index) => (
<li key={index}>{value}</li>
))}
</ul>
<div>
<b>평균값:</b> {getAverage}
{/* useMemo는 값을 반환 */}
</div>
</div>
);
};
export default Average;
결과는?
useMemo는 메모이제이션된 함수를 return하는 hook이며, 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화 하는 용도로 사용됩니다.
(*메모이제이션이란? 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술입니다)
...
const onRemove = (id) => {
setUsers(users.filter(user => user.id !== id));
};
const onToggle = (id) => {
setUsers(
users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
)
);
};
return (
...
)
위 함수들은 컴포넌트가 리렌더링 될 때 마다 새로 만들어집니다.
함수를 선언하는 것 자체는 사실 메모리도, CPU 도 리소스를 많이 차지 하는 작업은 아니기 때문에 함수를 새로 선언한다고 해서 그 자체 만으로 큰 부하가 생길일은 없지만, 한번 만든 함수를 필요할때만 새로 만들고 재사용하는 것은 여전히 중요합니다.
그 이유는, 우리가 나중에 컴포넌트에서 props
가 바뀌지 않았으면 Virtual DOM 에 새로 렌더링하는 것 조차 하지 않고 컴포넌트의 결과물을 재사용 하는 최적화 작업을 할건데요, 이 작업을 하려면, 함수를 재사용하는것이 필수입니다.
...
const onRemove = useCallback((id) => {
setUsers(users.filter(user => user.id !== id));
}, [users]);
const onToggle = useCallback((id) => {
setUsers(users.map(user =>
user.id === id ? { ...user, active: !user.active } : user
));
}, [users]);
...
return (
...
)
useCallback은 useMemo 를 기반으로 만들어졌습니다. 다만, 함수를 위해서 사용 할 때 더욱 편하게 해준 것 뿐입니다. 이 사실을 이용해서 아래와 같은 식으로도 표현 할 수 있습니다.
const onToggle = useMemo(() => {
/* ... */
}, [users]);
import React, { useState, useCallback } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { useStore } from 'stores/StoreHelper';
import { observer } from 'mobx-react';
const T = observer(() => {
const [count, setCount] = useState<number>(0);
const { itemStore } = useStore();
const handleChangeCount = () => {
setCount((prev) => prev + 1);
};
/** 어딘가에서 가져온 컴포넌트라고 가정한다 */
const SomeComponent = useCallback(() => {
return (
<View>
<Text>somethings</Text>
{/* mobx에서 가져온 state 입니다 */}
<Text>{itemStore.item?.name}</Text>
</View>
);
}, [itemStore.item?.name]);
return (
<View>
<TouchableOpacity onPress={handleChangeCount}>
<Text>버튼</Text>
</TouchableOpacity>
<Text>{`현재 숫자는 ${count}`}</Text>
<SomeComponent />
</View>
);
});
export default T;
React.memo
는 Higher-Order Components(HOC)입니다.
(HOC란 컴포넌트를 인자로 받아서 새로운 컴포넌트를 return해주는 구조의 함수)
useMemo, useCallback과 마찬가지로 불필요한 렌더링을 방지하기 위해서 사용됩니다.
일반적으로 아래와 같은 형태로 사용됩니다.
const Welcome = ({ name }) => {
return <h1>Hello { name }</h1>;
};
export default React.memo(Welcome);
-----------------------혹은--------------
const MyComponent = (props) => {
/* 컴포넌트 렌더링 코드 */
};
const somethings(prevProps, nextProps) {
/*
만약 전달되는 nextProps가 prevProps와 같다면 true를 반환, 같지 않다면 false를 반환
*/
};
export default React.memo(MyComponent, somethings);
memo
는 HOC, useMemo
는 hookmemo
는 HOC이기 때문에 클래스형 컴포넌트, 함수형 컴포넌트 모두 사용 가능하지만, useMemo
는 hook이기 때문에 오직 함수형 컴포넌트 안에서만 사용 가능하다.useCallback & useMemo & memo 는 사용법을 제대로 알고 정확히 쓰지 않는다면 불필요한 연산만 추가되는 꼴이 되버릴 수 있으니 주의 혹은 아예 사용하지 않는 것을 고려해야할 것 같습니다.