전에는 useState, useEffect로 거의 99프로의 코딩을 했었지만, 이제는 조금 달라져야하지 않을까 하여 useRef, useMemo, useLayoutEffect 등의 훅들을 정리하는 포스트를 작성하게 되었다.
useRef
함수는 current 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current 속성에 할당한다. 이 current 속성은 값을 변경해도 상태를 변경할 때처럼 React 컴포넌트가 다시 랜더링되지 않는다.
const num = useRef(0);
<button onClick={() => {
num3.current += 1;
console.log(num.current);
}}>
</button>
import React, { useState } from "react";
function ManualCounter() {
const [count, setCount] = useState(0);
let intervalId;
const startCounter = () => {
intervalId = setInterval(() => setCount((count) => count + 1), 1000);
};
const stopCounter = () => {
clearInterval(intervalId);
};
return (
<>
<p>자동 카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
}
export default ManualCounter;
-> 이 코드의 문제점은 안에서 선언된 intervalId 변수를 startCounter() 함수와 stopCounter() 함수가 공유할 수 있도록 해줘야 한다는 것이다. 그럴려면 intervalId 변수를 두 함수 밖에서 선언해야하는데 그럴 경우, count 상태값이 바뀔 때 마다 리렌더링 되어 intervalId도 매번 새로 선언된다. 따라서, 브라우저 메모리에는 미처 정리되지 못한 intervalId 들이 1초에 하나씩 쌓여나갈 것이다.
import React, { useState, useRef } from "react";
function ManualCounter() {
const [count, setCount] = useState(0);
const intervalId = useRef(null);
const startCounter = () => {
intervalId.current = setInterval(
() => setCount((count) => count + 1),
1000
);
console.log(`시작... intervalId: ${intervalId.current}`);
};
const stopCounter = () => {
clearInterval(intervalId.current);
console.log(`정지... intervalId: ${intervalId.current}`);
};
return (
<>
<p>자동 카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
}
export default ManualCounter;
import React, { useState } from "react";
function ManualCounter() {
const [count, setCount] = useState(0);
const [intervalId, setIntervalid] = useState(null);
const startCounter = () => {
setIntervalid(setInterval(() => setCount((count) => count + 1), 1000));
};
const stopCounter = () => {
clearInterval(intervalId);
};
return (
<>
<p>자동 카운트: {count}</p>
<button onClick={startCounter}>시작</button>
<button onClick={stopCounter}>정지</button>
</>
);
}
export default ManualCounter;
-> 이런 식으로 state로 선언하여, 문제를 해결할 수 있지만, 이 예제의 경우 useRef를 사용하는 것이 좋은 이유는 intervalid는 화면에 나타나지 않기 때문이다. 화면에 나타나지 않아 intervalid가 바뀌었을 때 리렌더링이 필요없다. 화면에 나타나는 경우는 useState, 화면에 나타나지 않는 경우는 useRef를 쓰면 좋을 것 같다는 개인적인 생각을 덧붙여본다.
querySelector
나 getElementById
를 사용하지만 React에서는 useRef
를 사용한다.import React, { useState, useRef } from "react";
const TryUseRefDom = () => {
const [emailValue, setEmailValue] = useState(""); // email state 값
const emailInput = useRef(null); // email input에 대한 useRef
const inputCheck = (e) => {
e.preventDefault();
if (emailInput.current.value === "") {
alert("이메일을 입력해주세요");
emailInput.current.focus();
return;
}
setEmailValue(emailInput.current.value);
};
return (
<form>
<label>
이메일 : <input type="email" ref={emailInput} />
</label>
<button type="submit" style={{ width: "100px" }} onClick={inputCheck}>
로그인
</button>
<br />
<span>입력한 이메일 : {emailValue}</span>
</form>
);
};
export default TryUseRefDom;
명령형 프로그래밍은 컴퓨터가 수행할 명령들을 순서대로 써 놓은 것이고, 선언형 프로그래밍은 프로그램이 어떤 방법으로 해야 하는지를 나타내기보다 무엇과 같은지를 설명한다.
import React, { useState, useRef } from "react";
const TryUseRefDom = () => {
const [emailValue, setEmailValue] = useState(""); // email state 값
const inputCheck = (e) => {
e.preventDefault();
if (emailValue === "") {
alert("이메일을 입력해주세요");
e.target[0].focus();
return;
}
alert("로그인 완료");
};
const handleChange = (e) => {
setEmailValue(e.target.value);
};
return (
<form onSubmit={inputCheck}>
<label>
이메일 :
<input type="email" value={emailValue} onChange={handleChange} />
</label>
<button type="submit" style={{ width: "100px" }}>
로그인
</button>
<br />
<span>입력한 이메일 : {emailValue}</span>
</form>
);
};
export default TryUseRefDom;
-> a의 코드는 비제어 컴포넌트, b의 코드는 제어컴포넌트 (React: 제어 컴포넌트와 비제어 컴포넌트의 차이점)
+) 신뢰가능한 단일 출처: 입력한 데이터 상태와 저장된 상태가 같은 것을 말하고 사용자가 입력한 값과 저장되는 값이 실시간으로 동기화 되는 것을 말함.