리액트의 훅은 16.8 버전부터 새로 추가된 기능입니다. Hook은 함수형 컴포넌트에서 React state와 생명주기 기능(lifecycle features)을 “연동(hook into)“할 수 있게 해주는 함수입니다. Hook은 class 안에서는 동작하지 않으며, 대신 class 없이 React를 사용할 수 있게 해줍니다.
리액트 Hook은 반복문, 조건문 혹은 중첩된 함수 내에서 호출하면 안되며반드시 최상위(at the Top Level)에서만 Hook을 호출해야 합니다. 또한 Hook은 렌더링 시 항상 동일한 순서로 호출 되어야 합니다.
Hook은React 함수 내에서만 호출 해야 합니다. 즉, 리액트 Hook은 함수형 컴포넌트(Functional Component) 에서 호출해야 하며, 추가적으로 custom hooks에서 또한 호출이 가능합니다
useEffect는 리액트 함수 컴포넌트 내에서 부수 효과를 수행할 수 있게 해주는 특별한 hook 입니다. 우리는 useEffect 가 호출되는 시점을 지정해줄 수 있습니다.
useEffect(() => {
console.log('mount!')
}, [])
위의 경우는 컴포넌트가 처음으로 mount 되었을 때 실행됩니다. 이 경우에만 실행되고 다른 경우에는 실행되지 않기에 초기에 한 번 실행할 작업들이 필요할 때, 위와 같이 사용하면 됩니다.
const [change, setChange] = useState(true);
useEffect(() => {
console.log('rendering!')
}, [change])
useEffect에는 [] 안에 값이 들어가거나 들어가지 않는 경우가 있습니다. 이를 dependency 라고 부르는데 dependency 파라미터 값에 의존하여 useEffect 함수가 실행될지 말지를 결정합니다.
위와같이 dependency값에 특정 값이 들어가 있는 경우에는 해당 변수의 값이 변경될 때, useEffect 함수가 실행됩니다.
useEffect(() => {
console.log('mount!');
return () => console.log('unmount!');
}, [])
useEffect에는 해당 컴포넌트가 언마운트될 때 실행되는 cleanup함수라는 기능이 있습니다. cleanup함수는 컴포넌트가 사라질 때 호출되는 부분으로 메모리누수를 방지하여 메모리 관리를 하거나 컴포넌트가 사라질 때, 수행할 작업들을 추가하기 위해 사용합니다.
cleanup 함수는 컴포넌트가 언마운트(unmount)될 때 호출됩니다. 이것은 컴포넌트가 사라질 때 컴포넌트가 생성했던 side effect를 정리하는 데 매우 중요합니다. 예를 들어, 컴포넌트가 WebSocket 연결을 설정하거나 setInterval을 사용하여 반복 작업을 설정한 경우, 컴포넌트가 언마운트되면 이들 작업을 정리하지 않으면 메모리 누수가 발생할 수 있기 때문입니다.
const [number, setNumber] = useState(0);
useState를 사용하면 함수형 컴포넌트 내에서 상태 변수를 선언하고 업데이트할 수 있습니다.
const [number, setNumber] = useState(0);
useEffect(() => {
setNumber(5);
console.log(number); // 0
}, [])
react를 하면서 많이 겪었을 문제입니다. dependency에 number를 넣어주어야만 console에 5가 찍히는걸 확인할 수 있습니다. 왜 setNumber 함수로 변수를 수정해 주었는데 값이 변하지 않고, console에는 0이 찍힐까요?
답변에 대한 정보는 해당 글을 참조하시기 바랍니다. 이 글을 본다면 useState는 어떻게 동작하고 closure와는 무슨 관계가 있는지, 왜 const 로 선언된 number가 변할 수 있는지 알 수 있습니다.
const [numbers, setNumbers] = useState([]);
const pressArr = () => {
numbers.push(1);
setNumbers(numbers);
console.log(numbers); // [1];
};
return (
<SafeAreaView>
<Pressable onPress={pressArr}>
<Text>push!</Text>
</Pressable>
{numbers.map((number) => (
<View>
<Text>{number}</Text>
</View>
))}
</SafeAreaView>
);
위 코드에서는 push를 통해 배열에 값을 추가하고, 그 다음에는 setNumbers를 사용하여 상태를 갱신하려고 합니다. 하지만 버튼을 눌러 값을 추가해도 화면에는 아무런 값도 나타나지 않습니다. 하지만 console로 찍으면 값이 추가되고 있긴 합니다.
React에서 상태(state)를 변경할 때는 항상 이전 상태를 변경하지 않고 새로운 상태를 반환해야 하기 때문입니다. 그렇지 않으면 React가 상태 변경을 감지하지 못하고 업데이트를 제대로 처리하지 못합니다. 위에서는 push 메서드는 원본 배열을 직접 수정하므로 React가 이를 감지하지 못한 것입니다.
const [numbers, setNumbers] = useState([]);
const pressArr = () => {
setNumbers((prev) => [...prev, numbers]);
}
올바른 방법은 spread 연산자를 사용하여 이전 상태의 배열을 복사한 후 새로운 값을 추가하여 새로운 배열을 생성하는 것입니다. 또한 원본 배열을 훼손하지 않는 map, filter, slice등이 사용될 수 있겠네요
import { useRef } from 'react'
import './App.css'
const App = () => {
const inputEl = useRef<HTMLInputElement>(null)
const onBtnClick = () => {
inputEl.current?.focus()
}
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onBtnClick}>input 초점</button>
</>
)
}
export default App
예를 들어 Input창이 아닌 곳을 눌렀을때, Input창이 포커싱 되게 하고 싶은 경우에 사용할 수 있습니다. 위에서는 버튼을 눌렀을때 Input에 focusing이 되도록 해주었네요.
위처럼 컴포넌트에서 특정 DOM 을 선택해야 할 때, ref 를 사용해야 한다고 배웠었습니다. 그리고, 함수형 컴포넌트에서 이를 설정 할 때 useRef 를 사용하여 설정한다고 배웠었습니다.
useRef Hook 은 DOM 을 선택하는 용도 외에도, 다른 용도가 한가지 더 있는데요, 바로, 컴포넌트 안에서 조회 및 수정 할 수 있는 변수를 관리하는 것 입니다.
useRef 로 관리하는 변수는 값이 바뀐다고 해서 컴포넌트가 리렌더링되지 않습니다. 리액트 컴포넌트에서의 상태는 상태를 바꾸는 함수를 호출하고 나서 그 다음 렌더링 이후로 업데이트 된 상태를 조회 할 수 있는 반면, useRef 로 관리하고 있는 변수는 설정 후 바로 조회 할 수 있습니다.
const temp = useRef(null);
const pressArr = () => {
temp.current.number += 1;
};
return (
<SafeAreaView>
<Pressable onPress={pressArr}>
<Text>push!</Text>
</Pressable>
</SafeAreaView>
);
한마디로 변수를 수정은 하지만 바뀐 변수를 화면에 보여줄 필요가 없는 작업에 useRef를 사용하면 됩니다. 상황에 맞게 useState와 useRef를 잘 사용하면 될 것 같습니다.
const square = (number: number) => {
console.log('제곱 계산중...');
return number * number;
};
const TestScreen = () => {
const [text, setText] = useState<string>('');
const [number, setNumber] = useState<number>(0);
const square_value = square(number);
return (
<SafeAreaView>
<View>
<Text>number * number= {square_value}</Text>
</View>
<TextInput value={text} onChangeText={(e) => setText(e)} />
</SafeAreaView>
);
};
숫자를 받아, 제곱을 계산해서 화면에 보여주는 square 함수가 있습니다. 그리고 이 계산 값을 sqaure_value 에 저장하여 화면에 보여주었습니다.
여기서 TextInput에 무언가를 입력하고 있으면 렌러링 될때마다 해당 함수가 실행되며 계속해서 sqaure_value 을 계산하고 있는 것을 확인하고 있습니다. number 의 값이 변하지 않았는데 계속해서 계산하는 것은 매우 비효율 적입니다.
const square = (number: number) => {
console.log('제곱 계산중...');
return number * number;
};
const TestScreen = () => {
const [text, setText] = useState<string>('');
const [number, setNumber] = useState<number>(0);
const square_value = useMemo(() => square(number), [number]);
return (
<SafeAreaView>
<View>
<Text>number * number= {square_value}</Text>
</View>
<TextInput value={text} onChangeText={(e) => setText(e)} />
</SafeAreaView>
);
};
이럴때 우리는 useMemo를 사용할 수 있습니다. useMemo 의 첫번째 파라미터에는 어떻게 연산할지 정의하는 함수를 넣어주면 되고 두번째 파라미터에는 deps 배열을 넣어주면 되는데, 이 배열 안에 넣은 내용이 바뀌면, 우리가 등록한 함수를 호출해서 값을 연산해주고, 만약에 내용이 바뀌지 않았다면 이전에 연산한 값을 재사용하게 됩니다.
useMemo 로 해당 함수를 감싼 이후에는 TextInput에 무언가를 입력하고 있어도 함수가 실행되지 않는 것을 확인할 수 있습니다.
useCallback 은 우리가 지난 시간에 배웠던 useMemo 와 비슷한 Hook 입니다. useMemo 는 특정 결과값을 재사용 할 때 사용하는 반면, useCallback 은 특정 함수를 새로 만들지 않고 재사용하고 싶을때 사용합니다.
이 함수들은 컴포넌트가 리렌더링 될 때 마다 새로 만들어집니다. 함수를 선언하는 것 자체는 사실 메모리도, CPU 도 리소스를 많이 차지 하는 작업은 아니기 때문에 함수를 새로 선언한다고 해서 그 자체 만으로 큰 부하가 생길일은 없지만, 한번 만든 함수를 필요할때만 새로 만들고 재사용하는 것은 여전히 중요합니다.
const TestScreen = () => {
const [text, setText] = useState<string>('');
const [toggle, setToggle] = useState<boolean>(false);
const toggling = () => {
setToggle(!toggle);
};
return (
<SafeAreaView>
<Pressable onPress={toggling}>
<Text>토글하기</Text>
</Pressable>
{toggle && (
<View>
<Text>toggle</Text>
</View>
)}
<TextInput value={text} onChangeText={(e) => setText(e)} />
</SafeAreaView>
);
};
react를 사용하며 어떤 버튼을 toggle하여 보여주고 다시 숨기는 함수를 많이 만들어 보았을 겁니다. 위의 예시처럼요 하지만 이렇게 된다면 textInput에 무언가를 입력할때마다 toggling 함수가 재생성 되게 됩니다.
const TestScreen = () => {
const [text, setText] = useState<string>('');
const [toggle, setToggle] = useState<boolean>(false);
const toggling = useCallback(() => {
setToggle(!toggle);
}, [toggle]);
return (
<SafeAreaView>
<Pressable onPress={toggling}>
<Text>토글하기</Text>
</Pressable>
{toggle && (
<View>
<Text>toggle</Text>
</View>
)}
<TextInput value={text} onChangeText={(e) => setText(e)} />
</SafeAreaView>
);
};
이렇게 useCallback을 사용하게 된다면 textInput에 무언가를 입력하는 것과 상관없이 toggle이 바뀔때만 함수가 재생성되게 됩니다. 하지만 함수가 재생성 되었는지, 원래 있던 것을 사용하였는지 우리는 console로 확인할 수 없기 때문에. useCallback 을 사용 함으로써, 바로 이뤄낼수 있는 눈에 띄는 최적화는 없습니다.
컴포넌트의 props 가 바뀌지 않았다면, 리렌더링을 방지하여 컴포넌트의 리렌더링 성능 최적화를 해줄 수 있는 React.memo 라는 함수에 대해서 알아보겠습니다. 이 함수를 사용한다면, 컴포넌트에서 리렌더링이 필요한 상황에서만 리렌더링을 하도록 설정해 줄 수 있게 됩니다.
import React, { useState } from 'react';
// 자식 컴포넌트
const ChildComponent = React.memo(({ count }) => {
console.log('자식 컴포넌트 렌더링');
return <div>카운트: {count}</div>;
});
// 부모 컴포넌트
const ParentComponent = () => {
const [text, setText] = useState<string>('');
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>증가</button>
<button onClick={() => setCount(count - 1)}>감소</button>
<ChildComponent count={count} />
<TextInput value={text} onChangeText={(e) => setText(e)} />
</div>
);
};
export default ParentComponent;
이렇게 되면 textInput에 무언가를 입력할때마다 ParentComponent는 re-rendering되고 그에따라 ParentComponent의 모든 자식들도 re-rendering 되어야 합니다. 하지만 React.memo를 활용하여 props가 바뀌지 않았을때는 re-rendering을 시켜주지 않을 수 있습니다. 버튼을 눌러 count가 바뀔때만 자식 component가 re-rendering이 되는 것이죠.
그렇다면 모든 함수에는 useCallback 모든 컴포넌트에는 React.memo 로 감싸는게 효율적일까요? 아닙니다. useCallback과 useMemo는 성능 최적화의 목적으로 사용되긴 하지만, 무분별하게 사용할 경우 오히려 성능 저하를 초래할 수 있습니다.
메모이제이션 자체의 비용: 이 두 Hook을 사용하면 함수와 계산 결과를 캐싱하기 위한 메모리 사용량이 늘어납니다. 게다가, 새롭게 계산되는 값이 일정 기간 동안 사용되지 않아도 메모리에 남아 있어야 하므로 메모리 관리의 측면에서 비효율적일 수 있습니다.
의존성 배열의 관리: useCallback과 useMemo는 의존성 배열이 필요한데, 이 배열에 들어간 값들이 변경될 때마다 메모이제이션 된 값을 무효화하고 새로 계산합니다. 이 과정에서 복잡성이 증가하며, 관리가 미흡한 경우 오히려 성능이 저하될 수 있습니다.
남용에 따른 실수: 무분별한 사용으로 인해 모든 함수나 결과값을 메모이제이션하려 할 때 실수가 발생할 가능성이 높아집니다. 이로 인해 성능 최적화를 기대하는 대신 버그나 성능 저하를 초래할 수 있는 상황이 생길 수 있습니다.
말 그대로 최적화에는 공짜가 없습니다. useCallback과 useMemo는 신중하게 사용되어야 하고, 필요한 경우에만 적용하여 성능 최적화를 추구하는 것이 좋습니다. 진짜 성능 이슈가 있는 곳에서만 해당 Hook 들을 사용하고, 대부분의 상황에서는 useCallback, useMemo를 사용하지 않는 편이 성능, 가독성 측면에 이점이 있을 것입니다 :)