해결방법 final 2. useRef 사용
import { useEffect, useRef } from "react";
import { useState } from "react";
export default function StateTestT() {
const [memberId, setMemberId] = useState(1);
const memberIdRef = useRef();
memberIdRef.current = memberId;
const updateMemberId = () => {
memberIdRef.current = memberIdRef.current + 1;
console.log(memberIdRef.current);
memberIdRef.current = memberIdRef.current + 1;
console.log(memberIdRef.current);
memberIdRef.current = memberIdRef.current + 1;
console.log(memberIdRef.current);
validateMemberId();
};
const validateMemberId = () => {
memberIdRef.current = memberIdRef.current + 1;
console.log(memberIdRef.current);
memberIdRef.current = memberIdRef.current + 1;
console.log(memberIdRef.current);
if (memberId > 3) {
console.log("5 이상");
return 0;
}
};
return (
<>
<div onClick={updateMemberId}>test</div>
</>
);
}
>> 2
>> 3
>> 4
>> 5
>> 6
import { useRef, useState } from "react";
export default function StateTest() {
const [memberId, setMemberId] = useState("");
const memberIdRef = useRef();
const updateMemberId = e => {
console.log(e.target.value);
memberIdRef.current = e.target.value;
setMemberId(e.target.value);
console.log(memberId);
console.log(memberIdRef.current);
validateMemberId();
};
const validateMemberId = () => {
console.log(memberId);
console.log(memberIdRef.current);
};
return (
<>
<input
onChange={e => {
updateMemberId(e);
}}
/>
</>
);
}
>>a
>>
>>a
>>
>>a
- 코드를 보면 이런 생각이 들 것이다.
- ‘이건 useState를 쓰는게 아니잖아요?’
- 맞다.
- 이 방법은 결국
- 값을
state
, useRef()
이 두 곳에 저장한다는것이다.
- 비동기로 인해 문제가 발생하는 지점에서는
state
대신 useRef()
에 저장해놓은 값을 사용해서 이 문제를 해결할 수 있다.
- 고전적인 방법들 중에서는 가장 훌륭하지만… 이걸 정리했던 사람은 뭔가 아쉽다고 한다..
- 2번째 예시는 더 바꿀 수도 있다.
import { useRef, useState } from "react";
export default function StateTest() {
const [memberId, setMemberId] = useState("");
const memberIdRef = useRef();
const updateMemberId = e => {
console.log(e.target.value);
setMemberId(e.target.value);
console.log(memberId);
console.log(memberIdRef.current.value);
validateMemberId();
};
const validateMemberId = () => {
console.log(memberId);
console.log(memberIdRef.current.value);
};
return (
<>
<input
ref={memberIdRef}
onChange={e => {
updateMemberId(e);
}}
/>
</>
);
}
>>a
>>
>>a
>>
>>a
- 물론 결과는 같다.
- 내가 생각했을 때 제일 깔끔한 방법은 이 방법이다.
- 그럼 이제 이 useRef에 대해 정리하고 어떻게 이런 결과를 낼 수 있는지 알아보자
useRef 사용법(렌더링과 관련이 없는 변수를 관리)
상태 변경 -> 컴포넌트 재 랜더링
- React 컴포넌트는 기본적으로 내부 상태(state)가 변할 때 마다 다시 랜더링(rendering)이 된다.
- 예를 들어, 아래
<Counter/>
컴포넌트의 버튼을 5번 클릭하면 count
상태값은 5번 바뀌게 된다.
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
console.log(`랜더링... count: ${count}`);
return (
<>
<p>{count}번 클릭하셨습니다.</p>
<button onClick={() => setCount(count + 1)}>클릭</button>
</>
);
}
- 브라우저 콘솔을 확인해보면, 5번의 로그가 찍히는 것을 볼 수 있는데 이를 통해,
<Counter/>
컴포넌트 함수는 count
상태가 바뀔 때 마다 호출되는 것을 알 수 있다.
랜더링... count: 1
랜더링... count: 2
랜더링... count: 3
랜더링... count: 4
랜더링... count: 5
- 컴포넌트 함수가 다시 호출이 된다는 것은 함수 내부의 변수들이 모두 다시 초기화가 되고 함수의 모든 로직이 다시 실행된다는 것을 의미한다.
다시 랜더링 되어도 동일한 참조값을 유지하려면?
- 우리는 대부분의 경우, 위와 같이 상태가 변할 때 마다 React 컴포넌트 함수가 호출되어 화면이 갱신되기를 바란다.
- 하지만 그에 따른 부작용으로 함수 내부의 변수들이 기존에 저장하고 있는 값들을 잃어버리고 초기화되는데
- 간혹 다시 랜더링이 되더라도 기존에 참조하고 있던 컴포넌트 함수 내의 값이 그대로 보존되야 하는 경우가 있다.
- 예를 들어, 카운팅이 자동으로 되도록
useEffect
훅(hook) 함수를 이용하여 위의 컴포넌트를 수정해보자.
import React, { useState, useEffect } from "react";
function AutoCounter() {
const [count, setCount] = useState(0);
console.log(`랜더링... count: ${count}`);
useEffect(() => {
const intervalId = setInterval(() => setCount((count) => count + 1), 1000);
return () => clearInterval(intervalId);
}, []);
return <p>자동 카운트: {count}</p>;
}
- 자, 여기서 만약에 카운트를 자동으로 시작하지 않고 버튼을 이용하여 시작하고 정지하고 싶다면 어떻게 해야 할까?
import React, { useState, useEffect } 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>
</>
);
}
- 여기서 가장 큰 걸림돌은 안에서 선언된
intervalId
변수를 startCounter()
함수와 stopCounter()
함수가 공유할 수 있도록 해줘야 한다는 것이다.
- 그럴려면
intervalId
변수를 두 함수 밖에서 선언해야하는데 그럴 경우, count
상태값이 바뀔 때 마다 컴포넌트 함수가 호출되어 intervalId
도 매번 새로운 값으로 바뀔 것이다.
- 따라서, 브라우저 메모리에는 미처 정리되지 못한
intervalId
들이 1초에 하나식 쌓여나갈 것이다. 💥
- 클래스를 이용해서 React 컴포넌트를 작성할 시절에는, 이와 같은 문제를 해결하는 가장 명료한 방법은 인스턴스(instance) 변수에 이러한 값들을 저장하는 것이었다.
- 하지만 최근처럼 대부분 함수를 이용해서 React 컴포넌트를 작성할 때는 일반적으로
useRef
훅(hook) 함수를 사용해서 이러한 문제를 해결한다.
useRef 사용하기
useRef
함수는 current
속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current
속성에 할당한다.
- 이
current
속성은 값을 변경해도 상태를 변경할 때 처럼 React 컴포넌트가 다시 랜더링되지 않는다.
- React 컴포넌트가 다시 랜더링될 때도 마찬가지로 이
current
속성의 값이 유실되지 않는다.
useRef
훅 함수가 반환하는 객체의 이러한 독특한 성질을 이용하여 startCounter()
와 stopCounter()
함수를 구현해보자.
import React, { useState, useRef } from "react";
function ManualCounter() {
const [count, setCount] = useState(0);
const intervalId = useRef(null);
console.log(`랜더링... count: ${count}`);
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>
</>
);
}
- 위 컴포넌트를 실행하면 콘솔에 아래와 같은 로그가 찍히는데
- 시작 버튼을 누르면 새로운
intervalId
가 생성되고, 정지 버튼을 누르면 기존 intervalId
가 정리되는 것을 확인할 수 있다.
랜더링... count: 0
시작... intervalId: 17
랜더링... count: 1
랜더링... count: 2
랜더링... count: 3
랜더링... count: 4
랜더링... count: 5
정지... intervalId: 17
시작... intervalId: 32
랜더링... count: 6
랜더링... count: 7
랜더링... count: 8
정지... intervalId: 32
useRef
훅 함수가 다른 React Hooks에 비해서 사용 빈도가 떨어지는 이유는 아무래도 살펴본 것처럼 용도가 극히 제한적이기 때문일 것이다.
useRef
훅 함수를 사용하는 또 다른 경우가 있는데, DOM 노드나 React 엘리먼트에 직접 접근하기 위해서이다.
useRef 사용법(DOM선택하기)
- HTML과 JS를 사용할때, DOM에 접근하거나 선택할 일이 있으면,
[getElementById](https://developer.mozilla.org/ko/docs/Web/API/Document/getElementById)
나 [querySelector](https://developer.mozilla.org/ko/docs/Web/API/Document/querySelector)
등의 DOM selector 함수를 사용한다.
- React를 사용하는 경우에서도 특정 DOM에 접근할 일이 있다.
- 특정 엘리먼트의 크기/위치를 알아낼 때,
- 스크롤 바의 위치를 가져오거나 설정할 때,
- focus를 설정해야할 때,
- Video.js(비디오 관련 라이브러리)등 HTML5 비디오 관련 라이브러리를 사용할 때,
- D3, Chart.js와 같은 그래프 관련 라이브러리를 사용하게 될 때> 특정 DOM에 라이브러리 설정해야 함
- 이럴때, React에서는
ref
라는 것을 사용하고,함수형 컴포넌트에서는 useRef
라는 훅 함수를 사용한다.
- 클래스형 컴포넌트에서는
React.createRef()
라는 함수를 사용.
- 여기선 함수형 컴포넌트일때 사용하는
useRef
함수만 알아본다.
예시
- '초기화' 버튼을 누르면 '이름' input 태그로 포커스가 이동하게 변경해보자.
- 이렇게 바꾸기 위해서는 React 자체적인 기술로는 뭔가 할 수 있는게 없어, 직접 DOM에 접근을 해야 한다.
- DOM에 직접 접근하기 위해서는 먼저
useRef
함수를 불러온다.
import React, { useState, useRef } from 'react';
const nameInput = useRef();
- 라고 nameInput 객체를 선언.
- 그리고 만들어진 nameInput 객체를 우리가 선택해주고 싶은 DOM에 설정해준다.
<input
name="name"
placeholder="이름"
onChange={onChange}
value={name}
ref={nameInput}
/>
- 여기까지 하고 나면 우리가 원하는 DOM에 직접 접근할 수 있는데,
const onReset = () => {
setInputs({
name: '',
nickname: '',
});
nameInput.current.focus();
};
- 위와 같이 쓸 수 있다.
nameInput.current.focus();
이 문장을 해석해보면,
nameInput.current
까지가 해당 DOM을 가리키게 되고,
- 그 다음 DOM API 중 하나인
focus()
함수를 사용하여 우리가 원하는 기능을 구현할 수 있다.
다시 돌아와서
- 첫번째 예시 코드로 쓴 게 렌더링과 관련이 없는 변수를 관리하는 useRef의 첫번째 기능을 쓴 것이고
- 두번째 예시 코드로 쓴 게 DOM을 선택하는 useRef의 두번째 기능을 쓴 것이다.
- 무엇하나 중요하지 않은 내용이 없었으며 이 useState의 비동기문제의 원인과 해결을 잘 이해한다면 react의 기초와 핵심에 한 발자국 더 가까이 다가갔다고 봐도 되지 않을까 싶다.(아직은 많이 부족하지만..)