푸딩캠프 이야기
리액트 테스트 코드 작성하며 캘린더 구현하기 특강
2024년 12월 3일 화요일, 리액트 테스트 코드를 작성하며 캘린더를 구현하는 특강이 열립니다! 예상보다 많은 분이 참가 신청해주셔서 무리해서 참가 슬롯을 늘렸는데, 이 슬롯도 어느 덧 2자리만 남게 되었습니다. 관심 있으신 분은 서두르세요!
새로운 캠프 예고
12월 중에 캠프 두 가지를 선보입니다.
- 스터디 모임 2기
- 글쓰기 모임 1기 (NEW)
푸딩캠프 스터디 모임의 학습 효과에 대해서는 지난 1기에서 호평을 받았습니다.
이번 2기에서는 조금 더 빠르게 진행하는 새로운 학습 체계를 도입합니다. 기대해주세요.
글쓰기 모임은 이번에 새롭게 선보이는데, 4주 동안 다양한 프로그램으로 진행됩니다. 직군 상관없이 커리어 계발에 필수 요소인 글쓰기 능력을 키울 수 있게 효과적인 체계를 준비했습니다. 글쓰기 모임 1기도 기대해주세요!
리액트 useEffect로 부수작용 다루기
이번 편에서는 React의 또 다른 핵심 Hook 중 하나인 useEffect를 알아보겠습니다.
부수작용(Side effect)이란?
리액트 컴포넌트의 주요 역할은 UI를 렌더링하는 것입니다. 앞선 useState로 상태 관리하기 편에서 멱등성을 다뤘는데, 사실 우리가 만드는 리액트 컴포넌트는 멱등성이 보장되지 않는 경우에 많이 노출됩니다. 리액트 컴포넌트라는 개체 상태가 외부 요인으로 변할 수 있지요. 대표적인 것 몇 가지를 나열해자면,
- API 호출을 하여 데이터 가져오기
- DOM을 직접 조작하는 작업
- 브라우저 API(localStorage, setInterval 등) 사용
- 이벤트를 구독하고 해제하기
이러한 작업들로 발생하는 작용이나 동작을 부수작용 또는 부수효과(Side Effect)이라고 합니다. 이런 부수작용 동작은 대개 컴포넌트 렌더링에 직접적인 영향을 주진 않지만, 우리가 기능 동작엔 필수요소입니다. useEffect 훅은 바로 부수작용을 다루는 데 사용합니다.
실은 부수작용을 다루는 건 까다롭습니다. 리액트 컴포넌트 구현은 상태 관리와 부수작용 통제의 싸움이나 마찬가지입니다. UI 프로그래밍은 어렵다고 말하게 만드는 요인이죠.
useEffect 기본 사용 방법
useEffect를 사용하는 방법은 useState와 비슷하지만, 중요한 차이가 있습니다.
`useEffect()`함수의 첫 번째 인자는 화살표 함수가 전달되는데, 의존성 배열에 변화가 생기면 실행되는 콜백(callback) 함수입니다. 두 번째 인자는 첫 번째 인자인 ㅜ함수를 언제 실행시킬지 결정하는 요소로, 의존성 배열이라고 합니다. 의존성 배열에 나열된 객체에 변동이 생기면 콜백 함수가 실행됩니다. useEffect 없이 직접 부수 작용을 일으키는 구현을 해도 되지만, 의존하는 객체에 변동이 생길 때 실행되도록 하는 건 useEffect의 특별한 속성이지요.
오랜만에 실습을 해볼까요? 컴포넌트가 렌더링 된 후 5초 동안 E-mail 주소를 입력하지 않으면 입력란으로 초점을 옮기는 기능을 구현해보겠습니다.
코드가 좀 추가됐습니다. `useEffect()` 부분만 따로 보겠습니다.
먼저 `useEffect()`가 관찰할 또는 `useEffect()`의 콜백 함수를 실행시킬 의존성은 빈 배열입니다. 이것은 아무것도 의존하지 않는다는 뜻인데, 해당 컴포넌트가 렌더링 되고나서 실행하는 용도로 씁니다. `useEffect()`로 전달한 콜백 함수는 간단한 동작을 합니다. email 상태값이 있으면 아무것도 수행하지 않고, 상태값이 없으면(falsy) 뭔가의 `focus()` 메서드를 호출합니다. 이건 `<input>` 태그에 대해 `focus()`를 호출해 포커스를 맞추는 동작으로써, 이후 편에서 `useRef()`에 대해 다룰 예정입니다. 여기에선 `<input>` 컴포넌트를 DOM 제어하듯이 다루는 게 아니라 `useRef()` 훅으로 다루게 됐다고만 이해하셔도 충분합니다. 아무튼 이러한 동작을 5초 마다 반복합니다.
그다지 좋은 사용자 경험(UX)은 아니죠. 콜백 함수는 호출자에게, 즉 `useEffect`에게 아무것도 반환하지 않거나 함수를 반환합니다. 함수를 반환하면 콜백 함수가 종료될 때 호출되는데, 콜백 함수 안에서 일으킨 부수효과을 정리하는 용도로 주로 씁니다. 이름도 정리(Clean up) 함수라고 하는데, 사용한 자원을 정리해 해제해 메모리 누수를 막는다는 의미도 있지만 부수효과를 정리해서 클린업 함수라고 이름 지은 게 아닐까 생각합니다.
의존성 배열 사용하기
의존성 배열을 조금만 더 살펴보겠습니다. 의존성 배열은 크게 네 가지 유형입니다.
의존성 배열은 useEffect가 언제 실행될지를 결정하는 중요한 요소입니다. 크게 세 가지 경우가 있습니다:
위 네 경우 중 2번을 우리가 살펴본 것입니다. 먼저 의존성을 아예 전달하지 않는 경우는 `useEffect()`가 속한 컴포넌트가 렌더링될 때마다 실행됩니다. `useEffect()` 없이 뭔가를(함수) 호출하는 코드가 그대로 노출된 것과 사실상 같습니다.
세 번째와 네 번째는 같은데요. 의존성으로 전달된 값이 변하면 콜백 함수가 실행됩니다. 그럼 네 번째는 어떤 경우일까요? 콜백 함수 안에서 다루는 값은 의존성 배열에 담습니다. `count`의 값이 변했으니 변한 값을 다루는 콜백 함수를 호출하는 것이기 때문입니다. 만약 콜백 함수 안에서 다루는 상태 값인데 의존성 배열에 넣지 않으면 우리가 기대하는 동작이 이뤄지지 않을 수 있습니다. 그래서 리액트를 지원하는 코드 에디터는 의존성 배열을 검사해 개발자에게 경고해줍니다.
useEffect의 실행 시점
useEffect는 꽤 다루기 까다롭습니다. 용도 자체가 부수작용을 일으키는 동작을 하여 예상 못한 부분에서 엉뚱한 동작을 일으키기도 하는데, 콜백 함수가 실행되는 건 의존하는 다른 대상의 상태를 따르기 때문이죠. 그래서 리액트로 개발하다보면 심심치 않게 무한 리렌더링 상황에 부딪히곤 합니다.
이런 상황을 최소화하거나 문제를 해결할 때 도움이 되는 것은 useEffect가 동작하는 순서를 아는 것입니다.
1. 컴포넌트 렌더링
2. 화면에 반영(Paint)
3. useEffect **함수** 실행
4. (리렌더링 시) 이전 부수작용 클린업
5. 새로운 부수작용 실행
주요 활용 예
이런 까다로운 useEffect는 언제 사용할까요? 서두에 설명드린대로 컴포넌트를 벗어난 영역에서 일어나는 동작, 즉 부수작용을 다룰 때 사용합니다. 크게 네 가지 경우입니다.
네트워크로 데이터 가져오기(fetching)
사용자 정보는 서버에 저장되어 있지 컴포넌트 자체엔 그런 정보가 없습니다. 따라서 어디선가는 데이터를 가져와야(fetching) 하는데, 리액트에서는 useEffect 안에서 데이터를 가져오는 경우가 많습니다. 다음은 예시입니다.
물론 최근에는 SWR이나 Tanstack Query 등에서 제공하는 훅을 이용하여 useEffect 밖에서 부수작용을 상태 관리하듯이 다룹니다만, Tanstack Query도, 그리고 SWR도 내부에선 `useEffect()`를 사용합니다.
DOM 조작
리액트 DOM이 아닌 HTML DOM을 다루는 것은 리액트 입장에선 컴포넌트 외부 요소를 다루는 부수작용 대상입니다. 마땅히 useEffect로 다루는데, 흔한 예시가 위에서 우리가 실습한 input 태그를 다루는 것입니다.
구독과 해제
컴포넌트 외부의 자원을 사용하여 useEffect를 쓰는데, 사용한 자원을 해제도 해야 하는 경우가 있습니다. 흔한 상황 중 하나는 웹소켓을 사용하는 것입니다. 다음은 예시 코드인데, 코드를 이해하실 필요는 없습니다. `new WebSocket()`으로 객체를 생성하며 자원을 할당하고, 컴포넌트가 해제(release)될 때 해당 컴포넌트가 사용한 웹 소켓 자원도 해제하기 위해 소켓을 닫는 동작을 하는 흐름만 보면 됩니다.
또 다른 예는 DOM 조작입니다. 사용자의 화면 스크롤을 관찰하는 컴포넌트를 예시로 살펴보겠습니다.
이번 코드는 좀 간결하죠? 스크롤 이벤트를 구독(listening)하다가 컴포넌트가 해제(release)될 때 스크롤 이벤트에 대한 구독도 해지한 것입니다. 그렇지 않으면 메모리 누수가 일어나게 됩니다.
타이머 관리
마지막으로 예시도 우리가 실습한 경우입니다. `setTimeout()`은 실행된 후 종료된다지만, `setInterval()`은 지정한 시간마다 계속 반복되므로 상태가 유지됩니다. 따라서 타이머를 정리(`clearInterval()`)해주는 게 좋습니다.
댓글
의견을 남겨주세요