리액트의 자주 사용하는 훅인 useRef 와 useEffect에 대해 정리해보았습니다. useRef는 리액트의 DOM 요소들을 직접 조작하고자 할 때 사용하며, useEffect의 경우 마운트, 업데이트, 언마운트와 같은 리액트의 라이프 사이클을 조절하기 위해 사용합니다.
useEffect의 경우 이번 시간에는 간단한 사용법까지만 알아보고, 다음 글에 이어서 라이프 사이클을 제어하는 방법을 상세히 알아보겠습니다.
useRef
리액트의 Ref(Reference)를 이용하면 DOM 요소들을 직접 조작할 수 있습니다. 마치 실제 DOM의 getElementById()와 비슷한 역할을 한다고 볼 수 있습니다.
사용법
useRef를 이용해 특정 요소에 focus를 주는 jsx 코드를 구현해보았습니다.
1) 먼저 useRef를 import 해준 후 생성해줍니다.
2) focusInput 함수 내부에 inputRef.current.focus() 를 이용해 함수 실행 시 inputRef.current(현재 inputRef가 가리키는 대상)에 focus를 주도록 설정합니다.
3) 포커스 주기 버튼 클릭(onClick) 시, focusInput 함수가 실행되도록 합니다.
import { useRef } from 'react';
function MyComponent() {
// 1. ref 생성
const inputRef = useRef(null);
// 2. 포커스를 주는 함수
const focusInput = () => {
// ref.current로 DOM 요소에 접근
inputRef.current.focus();
};
return (
<div>
{/* 3. ref 속성에 연결 */}
<input ref={inputRef} type="text" />
<button onClick={focusInput}>포커스 주기</button>
</div>
);
}
※ 주석 정리
React 코드 주석: 한줄 //, 여러줄 /**/
React JSX 코드 주석 : 여러줄 {/* */}
실제 사용 예시
아래 코드처럼 이미지 업로드 시, 파일 입력창을 숨기고, 별도로 배치한 버튼 클릭 시, 파일 입력창이 클릭되도록 할 때 적용할 수 있습니다.
function ImageUploader() {
const fileInputRef = useRef(null);
const handleUploadClick = () => {
// 숨겨진 파일 입력창 클릭 효과
fileInputRef.current.click();
};
return (
<div>
<input
type="file"
ref={fileInputRef}
style={{ display: 'none' }}
onChange={(e) => {
// 파일 처리 로직
}}
/>
<button onClick={handleUploadClick}>
이미지 업로드
</button>
</div>
);
}
ref 값은 state 처럼 변경되어도 컴포넌트가 재랜더링 되지 않기 때문에, ref는 꼭 필요한 경우에만 사용하고 렌더링에 필요한 값은 state를 사용하는 것이 좋습니다.
function WrongUsage() {
const inputRef = useRef(null);
// ❌ 잘못된 사용: 렌더링 중에 ref.current 사용
return <div>{inputRef.current.value}</div>;
// ✅ 올바른 사용: 이벤트 핸들러나 useEffect 안에서 사용
useEffect(() => {
console.log(inputRef.current.value);
});
}
※ 리액트 훅 (React Hook)이란?
리액트 훅은 2018년도에 처음 발표된 기능으로, 함수로 만든 리액트 컴포넌트에서 클래스로 만든 리액트 컴포넌트의 기능을 이용하도록 도와주는 함수들입니다. useState, useRef, useEffect 모두 리액트 훅으로, 리액트 훅은 항상 이름 앞에 use가 붙습니다.
State, Ref, Effect 모두 함수형 컴포넌트에서는 원래 사용할 수 없었던 기능이었지만, 훅 기능을 이용하면 사용할 수 있습니다.
이러한 훅이 없던 시절에는 대부분의 컴포넌트를 클래스로 만들어 사용했었는데, 작성해야할 코드가 너무 많고, 문법이 간결하지 못해 불편했다고 합니다. 이를 개선하고자 만든 것이 훅이고, 명칭 또한 낚아채듯 클래스로 만든 기능을 가져와 사용한다고 해서 붙여진 이름이라고 합니다.
▼ 리액트 개발팀이 처음으로 훅을 소개한 영상 ▼
React Today and Tomorrow and 90% Cleaner React With Hooks
▼ 더 많은 리액트 훅 보기 ▼
< useState 공부 시작 전에 알고 가기 >
리액트 컴포넌트의 라이프 사이클
리액트 컴포넌트의 태어나고, 사라지는 생애 주기를 라이프 사이클이라고 하고 3단계로 구분 합니다. 크게 마운트, 업데이트, 언마운트로 아래와 같습니다.
- Mount (마운트) : 컴포넌트를 페이지에 처음 렌더링 할 때
- Update (업데이트) : State 나 Props의 값이 바뀌거나 부모 컴포넌트가 리렌더해서 자신도 리렌더될 때
- Unmount (언마운트) : 더이상 페이지에 컴포넌트를 렌더링 하지 않을 때
이러한 라이프 사이클을 통해
처음 렌더링 시 특정 동작을 하도록 만들거나, 업데이트 시 적절한지 검사하거나, 페이지에서 사라질 때 메모리를 정리하는 등 여러 가지 유용한 작업을 할 수 있습니다. 이러한 작업들을 라이프 사이클 제어라고 하며, 리액트 훅 중 하나인 useEffect를 이용해 이 사이클을 제어할 수 있습니다.
useEffect
어떤 값이 변경될 때마다 특정 코드를 실행하는 리액트 훅으로, "특정 값을 검사한다."라고 표현합니다. useEffect를 이용하면 컴포넌트의 State 값이 바뀔 때마다 변경된 값을 콘솔에 출력하게 할 수 있습니다.
사용법
(1) 함수 useEffect를 사용하기 위해 react 라이브러리에서 import 합니다.
(2) useEffect를 호출하고, 첫 번째 인수로는 콜백 함수를, 두번째 인수로는 배열(의존성 배열, Dependency Array, deps)을 전달합니다.
useEffect(callback, [deps])
// 콜백 함수, 의존성 배열
=> 의존성 배열의 요소 값이 변경되면 첫 번째 인수로 전달된 콜백 함수가 실행됩니다.
단일 요소 및 여러 개의 요소 검사
해당 state 변수를 deps 배열에 넣으면 됩니다. 여러 개일 경우도 deps 배열 안에 해당 state 변수들을 나열하여 넣으면 됩니다.
import { useEffect, useState } from "react";
export default function App() {
const [ count, setCount ] = useState(0);
const [ text, setText ] = useState("");
// 중략 ...
// 아래 두 경우 중 하나 선택
// (1) 단일 요소만 검사할 경우
useEffect(() => {
console.log("count 업데이트: ", count);
}, [count]);
// (2) 여러 개의 요소를 검사할 경우
useEffect(() => {
console.log("count 업데이트: ", count);
console.log("text 업데이트: ", text);
}, [count, text]);
return (
// 중략 ...
);
}
잠깐!! useState와 useEffect 비교해보기
위에서 설명했듯 useEffect는 특정 State 변수가 변경될 때마다 그 변경을 감지하고 특정 함수를 실행되게 할 수 있습니다. 그런데 돌이켜보면 우리가 앞서 배웠던 useState 함수도 잘 이용하면 state 변수값의 변경에 따라 특정 동작을 수행할 수 있었는데요.
두 리액트 훅의 코드를 비교해보며 사용방법과 기능상의 차이를 알아보겠습니다.
아래 두 사진의 코드 모두 count라는 상태 변수의 변화를 감지하고 console.log 가 실행되도록 구현된 코드입니다. 아래 사진의 코드를 통해 간단히 사용 방법의 차이를 체크해보시고 아래 차이점을 비교해보시기 바랍니다.
차이점 1 : 실행 시점
useState 설정을 통해 우리는 특정 state 변수 값의 변경에 따라 컴포넌트가 리렌더링 되도록 할 수 있습니다. useEffect의 경우 해당 state 변수값의 변경으로 인한 리렌더링이 완료된 후에 실행됩니다.
function ExampleComponent() {
const [value, setValue] = useState(0);
// useState: 상태 변경 함수 호출 시점에 실행
const handleChange = (newValue) => {
setValue(newValue); // 상태 변경
performSideEffect(); // 즉시 실행
};
// useEffect: 렌더링 완료 후 실행
useEffect(() => {
performSideEffect(); // 렌더링 후 실행
}, [value]);
}
차이점 2 : 비동기 작업 처리
useState의 값의 set함수를 통해 해당 State 값을 변경하더라도, set함수 자체가 비동기적으로 실행되고, React의 상태 업데이트는 현재 함수 실행이 끝난 후 일괄처리 되기 때문에 State변수 값이 업데이트 되지 않고 나머지 코드가 실행됩니다. 따라서, 동시적으로 여러 작업이 진행되는 비동기 작업 처리가 어렵습니다.
function UserProfile() {
const [userId, setUserId] = useState(1);
const handleUserChange = async (newId) => {
setUserId(newId); // 1️⃣ 상태 업데이트
// 2️⃣ 이 시점에서 userId는 아직 이전 값
const userData = await fetchUserData(userId); // 문제: 이전 userId로 요청됨
console.log('현재 userId:', userId); // 이전 값이 출력됨
};
return (
<button onClick={() => handleUserChange(2)}>
유저 변경
</button>
);
}
반면 useEffect의 경우 데이터의 변경 모두 적용된 후 코드들이 실행되기 때문에 async를 적용한 비동기적 함수를 통한 비동기적 작업에도 적합합니다.
function UserProfile() {
const [userId, setUserId] = useState(1);
const [userData, setUserData] = useState(null);
console.log('1. 컴포넌트 렌더링 - userId:', userId);
useEffect(() => {
console.log('3. useEffect 실행 - userId:', userId);
const fetchUser = async () => {
console.log('4. 데이터 요청 시작 - userId:', userId);
const data = await fetchUserData(userId);
console.log('5. 데이터 수신 완료:', data);
setUserData(data);
};
fetchUser();
}, [userId]);
const handleUserChange = (newId) => {
console.log('2. 상태 업데이트 시작');
setUserId(newId);
console.log('userId 직후:', userId); // 아직 이전 값
};
return (
<div>
<button onClick={() => handleUserChange(2)}>
ID {userId}의 유저 데이터 불러오기
</button>
<div>
{userData ? (
<div>이름: {userData.name}</div>
) : (
<div>로딩 중...</div>
)}
</div>
</div>
);
}
차이점 3 : DOM
useEffect는 DOM 업데이트 후 실행되기 때문에 콜백함수 내에서 DOM 요소 내부의 변경이 업데이트 된 후, 접근이 가능합니다.
function TextInput() {
const [text, setText] = useState('');
// useState: DOM 업데이트 전에 실행되므로 부적절
const handleTextChange = (newText) => {
setText(newText);
// DOM 요소의 크기를 측정하려고 하면 이전 상태의 크기를 얻게 됨
};
// useEffect: DOM 업데이트 후 실행되므로 적절
useEffect(() => {
// 텍스트가 변경된 후 DOM 요소의 실제 크기를 얻을 수 있음
const element = document.querySelector('.text-input');
console.log('입력창 높이:', element.scrollHeight);
}, [text]);
}
다음 글에서는 useEffect를 이용해 리액트의 라이프 사이클을 제어하는 방법을 상세히 알아보겠습니다.