코로 넘어져도 헤딩만 하면 그만
리액트의 훅: useState, useEffect, useRef 본문
리액트에는 훅Hook이 참 많습니다. 필연적으로 쓰는 useState, useEffect, useRef 외에도 useContext, useMemo... useReducer...
문제는 하나씩 익힐 때는 괜찮았는데, 여러 훅이 섞이기 시작하니 어디에 적절하게 써야할지 감이 잘 오지 않는다는 점이었습니다. 따라서 이번 시간에는 리액트와 훅에 대해서 좀더 공부하고 정리해보려 합니다.
리액트와 훅에 대해 논하기 전에, 왜 훅이 나올 수밖에 없었는지부터 짧게 알아보겠습니다.
🚩리액트 훅의 등장, 함수형 컴포넌트 때문에...
훅은 리액트 16.8부터 새로 추가된 기능입니다.
기존까지 즐겨 쓰던 클래스형 컴포넌트에 쓰이던 기능들을 함수형 컴포넌트에서 구현하기 위해 훅이 필요해진 것이죠. 함수형 컴포넌트는 클래스형 컴포넌트에 비해 쓰기 간편하지만, 컴포넌트 생애 주기가 없어 컴포넌트가 생성되거나 업데이트 되는 시점에 관여하기 곤란했습니다. 또 클래스형 컴포넌트와 달리 변화를 관찰할 수 있는 '상태'가 없었습니다.
그렇기에 기존에 쓰던 이 기능들을 대체할 만한 것이 등장합니다. 그것이 바로 Hook입니다.
일종의 걸어 쓰는 갈고리(hook)처럼 import하여 사용하는데 용도에 따라 사용할 수 있는 훅이 다릅니다. 필요할 때마다 원하는대로 훅을 하나씩 걸어 사용한다고 보시면 될 것 같습니다.
리액트의 훅을 사용할 때는 다음과 같은 부분을 주의해야 합니다.
1) 최상위에서만 훅이 호출되어야 합니다. 즉, 반복문이나 조건문, 혹은 중첩된 함수 내부에서는 훅이 호출되면 안 됩니다. 리액트는 훅이 호출되는 순서에 의존하는데 조건문 내부에서 사용할 시 순서가 밀리며 예기치 않은 오류가 발생할 수 있습니다.
2) 훅은 반드시 함수 컴포넌트 내부에서 호출해야 합니다. 그렇지 않으면 에러가 발생합니다.
3) 훅을 만들 때에는 앞에 use를 붙여야 합니다.
4) 리액트는 훅이 호출되는 순서에 의존합니다. 여러 개의 훅이 사용되는 경우 위에서부터 아래로 동작합니다.
훅에는 여러 종류와 기능이 있는데, 필요할 때마다 최상단에서 import 하여 사용합니다. 그중 리액트에서 빼놓을 수 없는 훅인 useState, useEffect, useRef에 대해서 더 알아보도록 합시다.
🚩useState
useStates는 상태를 관리하기 위한 훅입니다. 함수형 컴포넌트 내부에 추가하여 사용합니다.
기본적으로 다음과 같은 구조를 취합니다.
const [state변수, 변수를 갱신하는 함수] = useState(초기값);
우선 useState()의 인자로 넘겨받는 것은 상태의 초기값입니다. 또한 상태를 나타내는 배열을 반환해주는데요. 첫번째 요소는 state변수, 두번째 요소는 state변수를 최신 상태로 갱신할 수 있는 Setter 함수로 한 쌍을 이룹니다. 뒤에 오는 인자값은 다른 이름을 지어주어도 되지만 관용적으로 set 첫번째 인자값 형태로 사용합니다. [count, setCount]처럼요.
변수는 아래와 같이 {} 내부에 직접 사용할 수도 있습니다.
<p>You clicked {state변수} times</p>
만약 state변수를 갱신하고 싶다면 어떻게 할까요?
변수를 갱신하는 함수를 활용할 수 있습니다. 이때 Setter 함수의 인자에 변경할 값을 넣어주면 됩니다. 그러면 이 컴포넌트는 화면에 다시 렌더링이 되고, state가 변경될 때마다 화면이 업데이트가 됩니다.
예를 들어 아래와 같이 수를 세는 useState를 만들어봅시다. count는 변수고, setCount는 변수를 바꾸어 갱신할 수 있는 함수입니다. 0는 초기값이 되겠네요. count를 버튼을 누를 때마다 +1씩 올려주고 싶습니다.
이때 setCount를 사용하여 count를 변경해주고 있는 것을 볼 수 있습니다.
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>
Click me
</button>
그런데 왜 count 변수를 직접 변경하지 않을까요? 그 이유는, 리액트는 기존 상태와 신규 상태를 비교한 뒤 두 값이 다를 때만 새로 렌더링이 일어나도록 짜여있기 때문입니다. 상태의 변화가 중요합니다. 단순히 변수만 바꿔준다면 참조하는 가상 공간의 위치는 동일하기 때문에 결국 변화가 일어나지 않다고 리액트가 인지합니다(참조 자료Refference data와 원리가 같음). 그러니 렌더링도 일어나지 않고, 우리가 원하는 대로 값도 바뀌지 않는 것이죠.
또다른 예시로 useState를 사용할 때 배열의 값을 변경해야 하는 경우가 있습니다. 이때 spread 문법을 사용해가며 값을 복사해주는 이유도 위와 같습니다. 원 배열을 변경하면 상태가 같다고 인식되기 때문에 상태의 업데이트가 리액트에 의해 감지되지 않기 때문이죠.
결론적으로 상태변경 함수를 사용해서 상태를 바꿔주어야지만 리렌더링이 일어나고 값이 잘 바뀝니다.
useState를 사용해야 하는 이유를 확실히 알 수 있겠네요.
🚩useEffect
useEffect는 리액트가 돔을 업데이트하고 난 뒤, 어떤 값의 변화를 감지하면 추가로 작업을 실행하기 위해 쓰는 훅입니다. 컴포넌트의 effect(부수 효과)를 담당하고 있다고도 하는데요. 원하는 값이 변경되거나 리렌더링이 일어날 때 어떤 동작을 해야 한다면 useEffect를 쓰게 됩니다.
아래와 같이 기본적으로 콜백 함수를 가지며 Dependency에 따라 조건부로 실행 됩니다.
useEffect(콜백함수, [Dependency])
또한 크게 세 가지 Dependency 의 조건에 따라 이 함수가 실행이 언제 될지가 결정됩니다.
1. 배열이 없는 경우: 모든 리렌더링마다 실행됩니다. 실제로는 자주 쓰지 않는데, 불필요하게 많이 실행되어 성능을 저하시키기 때문입니다.
2. 빈배열이 들어간 경우: 빈 배열을 넣으면 렌더링 직후에 무조건 단 한번만 실행됩니다.
3. 특정 변수를 넣은 경우: 배열 안에 변수를 넣어주면 그 변수가 변경될 때마다 실행됩니다.
결론적으로 useEffect는 Dependency가 존재하건 존재하지 않건 렌더링 뒤 꼭 한번은 실행되는 것을 볼 수 있습니다.
그런데 여기서 '정리'라는 개념이 등장합니다. 정리란, 이전 이펙트로 인한 결과를 정리하여 확실하게 메모리 누수가 발생하지 않도록 조치를 취해주는 것입니다. 돔 수동 조작, 네트워크 API, 로깅 등은 실행한 직후 바로 잊어버려도 되기 때문에 특별한 정리가 필요 없습니다. 하지만 외부 데이터를 구독하거나 현재 스크롤의 좌표를 출력하는 등의 경우에서는 정리를 따로 해줘야 합니다.
흠... 그러면 만약 정리Clean up가 필요한 경우라면 어떻게 대처해야 할까요?
기존 클래스형 컴포넌트에서는 이를 componentWillUnmount로 해결했습니다. 하지만 우리는 함수형 컴포넌트를 선호하고 있습니다. 따라서 useEffect 내에서 특별히 반환하는 함수, 즉 clean-up 함수를 사용해서 이 문제를 해결할 수 있습니다. 정리가 필요한 경우에는 이 함수를 반환하고, 정리가 필요 없는 경우엔 아무것도 반환하지 않습니다.
이 clean-up 함수에 대한 설명은 좀더 길어질 것 같아 추후 다른 포스팅으로 나누어 기재하겠습니다.(아래)
https://hj97codeart.tistory.com/93
useEffect의 clean-up 함수
일전에 블로깅을 위해 useEffect 훅에 대해 추가 복습을 하다 보니 문득 clean-up 함수 라는 단어가 눈에 들어왔습니다. 사실 전에 useEffect의 사용성에 대해 배울 때는 이 부분에 대해 아예 건너뛰었거
hj97codeart.tistory.com
아참. 그리고 useEffect는 비동기적으로 동작하는 훅입니다. 대부분의 effect는 비동기적으로 써도 무방하지만, 만에 하나 동기적으로 꼭 써야 하는 상황이라면 useLayoutEffect라는 훅을 사용할 수 있습니다. useEffect와 기능은 동일하지만 실행 시점이 다르다고 하네요.
🚩useRef
리액트는 상태가 변할 때마다 렌더링을 하는 특징을 갖습니다. 하지만 이것이 문제가 되기도 하는데요.
렌더링이 될 때마다 컴포넌트 내부 변수들은 기존 값을 다 잃어버리고 초기화 되기 때문에, 렌더링 뒤에도 이 값을 유지하고 싶은 경우 난감해집니다. 클래스형 컴포넌트를 사용할 시절에는 instance라고 하는 변수를 사용하여 이곳에 값을 저장해두었습니다. 하지만 함수형 컴포넌트에서는 이 기능을 어떻게 구현해야 할까요?
이때 쓸 수 있는 것이 바로 useRef 훅입니다.
const refContainer = useRef(initialValue);
useRef 함수는 current 속성을 가지는 객체를 반환하며 인자로 넘어온 초기값은 current 속성에 할당합니다. 이때 current 속성은 값을 변경해도 다시 렌더링 되지 않고 컴포넌트 렌더링 뒤에도 값을 초기화 시키지 않습니다. 즉 컴포넌트 업데이트에 따른 side effect를 받지 않아야 하는 값을 useRef로 사용하는 편이 좋습니다. 만약 컴포넌트 렌더링 횟수를 구하고 싶다면 useRef를 사용해서 구해볼 수 있겠네요.
useRef의 또 다른 독특한 특징은 직접 돔의 노드에 접근할 수 있다는 것입니다. 물론 리액트는 직접 돔을 변경하는 일을 지양합니다. 하지만, useRef를 사용해야 하는 경우가 있습니다. 입력창에 자동으로 포커스가 맞춰지도록 하는 경우 useRef를 사용하면 훨씬 편리합니다.(바닐라 JS에서 Document.querySelector()와 같은 기능)
이처럼 useRef 훅은 다른 광범위한 훅들에 비해 용도가 제한되어 있지만, 그만큼 특징이 두드러지기 때문에 가볍게라도 알아두는 편이 좋습니다.
이렇게 리액트의 대표적인 세 가지 훅에 대해서 알아보았습니다. 필요에 따라 다른 훅들도 찾아 짬짬이 공부하는 것이 좋겠네요. 리액트의 함수형 컴포넌트는 많은 부분을 훅에 의존하고 있습니다. 사용하는 훅이 많아 복잡하더라도, 잘 구분해서 유연하게 다룰 줄 아는 능력이 필요할 것 같습니다.
참고: https://ko.legacy.reactjs.org/docs/hooks-state.html
https://junhyunny.github.io/javascript/react/jest/how-to-test-clean-up/
'CODE STATES 44' 카테고리의 다른 글
useEffect의 clean-up 함수 (0) | 2023.04.26 |
---|---|
웹 표준과 시멘틱함에 대하여 (0) | 2023.04.26 |
리액트를 편리하게 쓰는 라이브러리, '리덕스' (0) | 2023.04.24 |
41일~45일차 스터디: 프론트도 피그마는 알아야지. (0) | 2023.04.20 |
Styled-Components (0) | 2023.04.18 |