useRef
는.current
프로퍼티로 전달된 인자로 초기화 된 변경 가능한 ref 객체를 반환한다. 반환된 객체는 컴포넌트의 전 생애주기를 통해 유지된다.
React 공식문서 - useRef
useRef()
는 저장 공간 또는 component가 return하는 jsx코드의 DOM 요소에 접근해서 사용할 수 있도록 한다.
(javascript의 getElementById()처럼 컴포넌트의 어떤 부분을 선택할 수 있게 해주는 방법이다.)
useRef()
가 반환하는 값은 항상 객체이고, current
라는 prop을 기본으로 가지고있다. current
prop에는 ref
로 연결된 html의 DOM node
값이 저장된다.선언 시에 초기값을 지정해둘 수 있다.
이때 초기값은 처음 렌더링될 때만 사용되고 그 후에는 무시된다. (state의 초기값과 동일하다!)
import React, { useRef } from 'react'; const AddUser = props => { // useRef로 미리 선언해두기 // 초기값을 생략해도 된다! const nameInputRef = useRef(); const ageInputRef = useRef(); ...생략 return ( ...생략 ); } export default AddUser;
1️⃣ 에서 선언한 useRef
와 html element
를 연결하려면, 연결하려는 html element
에 ref prop
을 넣어주면 된다.
보통은 <input>
과 연관되어 많이 사용되지만 모든 html element
는 ref값을 가질 수 있음을 기억하자.
const AddUser = props => { const nameInputRef = useRef(); const ageInputRef = useRef(); ...생략 return ( <form onSubmit={addUserHandler}> <input id="username" type='text' // ref를 prop으로 주어서 만들어둔 useRef() 연결하기! ref={nameInputRef} /> <input id="age" type='number' // ref를 prop으로 주어서 만들어둔 useRef() 연결하기! ref={ageInputRef} /> <Button type='submit'>Add User</Button> </form> ) }
useRef
는 current
property를 반환한다.
초기에는 ref
를 처음 만들 때 설정한 initial value
로 지정되어있지만, 그 후에 다른 값으로 직접 설정 가능하다.
// AddUser.js const AddUser = props => { // 1. useRef선언 const nameInputRef = useRef(); const ageInputRef = useRef(); // 3. 연결된 함수에서 ref값 이용하기 const addUserHandler = (event) => { event.preventDefault(); // ref가 가진 current prop을 이용해서 input의 value에 접근 const enteredName = nameInputRef.current.value; const enteredUserAge = ageInputRef.current.value; // ref로 DOM 요소 수정하기.. nameInputRef.current.value = ''; ageInputRef.current.value = ''; } return ( <!--3. 연결된 함수 --> <form onSubmit={addUserHandler}> <input id="username" type='text' // 2. ref 연결 ref={nameInputRef} /> <input id="age" type='number' // 2. ref 연결 ref={ageInputRef} /> <Button type='submit'>Add User</Button> </form> ) }
사실 DOM에 접근할 수 있는 hook이라고 한다면 .. 굳이 필요할까 ? DOM API를 사용하면 되는 거 아닌가?라는 생각이 들 수 있다. (는 내가 그랬다)
반면 리액트에서는 아래와 같은 이유로 DOM API를 이용한 컴포넌트 제어 방식을 권장하지 않는다.
- React를 이용한 웹 소프트웨어에서 데이터는 State로 조작되기에 DOM API와 혼합해서 데이터 및 조작을 할 경우 디버깅이 어려워지고, 유지보수가 어려운 코드가 된다.
- map 메소드를 이용해 렌더링 되는 리스트 형태의 Element는 같은 ID를 가지기에 특정 DOM 객체를 querySelector, getElementById로 판별하기 어렵다.
참고자료
useRef
로 만들어진 객체는 React가 만든 전역 저장소에 저장되기 때문에 함수를 재호출 하더라도 해당 컴포넌트의 생애주기 동안에는 계속 current값을 유지하고 있을 수 있다.
컴포넌트가 mount될 때 React는 current 프로퍼티에 DOM element를 대입하고,
컴포넌트가 unmount될 때 프로퍼티를 다시 null로 돌려놓는다.
useRef는 매번 렌더링을 할 때 동일한 ref 객체를 제공한다.
가장 기본적인 useRef 사용으로, 위의 사용방법에 작성된 코드와 같이 DOM 요소에 접근하고, 수정할 때 사용된다.
함수형 컴포넌트에서 부모 컴포넌트에서 자식 컴포넌트 안의 DOM element에 접근하는 방법!
자식 컴포넌트에서 부모 컴포넌트의 함수를 사용하려면 props를 이용하면 됐지만 .. 반대의 경우라면
forwardRef Hook
을 사용하면 된다 !
참고 자료
import { useRef } from 'react'
import Child from './Child'
const Parent = () => {
const compRef = useRef()
return (
<Child ref={compRef} />
)
}
export default Parent;
forwardRef()
의 두번째 매개변수인 ref
는 부모 컴포넌트가 props
로 넘긴 값이다. (compRef)useImperativeHandler()
의import { forwardRef, useImperativeHandle } from 'react'
const Child = forwardRef((props, ref) => {
// 부모 컴포넌트에서 사용할 함수 설정
useImperativeHandle(ref, () => ({
req1,
req2
}))
// 함수 1
const req1 = () => { }
// 함수 2
const req2 = () => { }
}
export default Child
import { useRef } from 'react'
import Child from './Child'
const Parent = () => {
const compRef = useRef()
const fnReqBtn1 = e => {
e.preventDefault();
compRef.current.req1();
}
const fnReqBtn2 = e => {
e.preventDefault();
compRef.current.req2();
}
return (
<>
<Child ref={compRef} />
<Link onClick={fnReqBtn1}>가입버튼1</Link>
<Link onClick={fnReqBtn2}>가입버튼2</Link>
</>
)
}
export default Parent;
DOM요소 접근 말고도 useRef를 사용할 수 있다 !
함수형 컴포넌트
에서는 렌더링 될 때마다 매번 새로운 변수를 스택에 할당해 값이 초기화되기도 하고, 불필요한 성능 낭비가 발생하기도 한다. 클래스 컴포넌트
에서는 인스턴스를 생성 후 렌더링 메소드만 재실행하는 구조였다면, 함수형 컴포넌트
는 매번 함수를 실행하기 때문이다.
리액트에서 기존에 변수를 선언하던 방식들과 useRef
를 통해 변수를 선언하는 방식들을 비교해보자면
1. useState, useContext로 선언하기
이렇게 선언된 변수들은 값이 바뀔 때마다 리랜더링을 유발하기 때문에 렌더링과 상관 없는 변수를 선언하기에 적당하지 않다.
2. 컴포넌트 내부에서 const, let으로 선언하기
렌더링될 때마다 값이 초기화되기 때문에 컴포넌트의 생애주기 동안 관리해야 하는 변수를 선언하기에 적당하지 않다.
3. 컴포넌트 외부에서 const, let으로 선언하기
불필요한 렌더링을 유발하거나, 렌더링될 때마다 값이 초기화되지도 않지만 컴포넌트를 재사용할 때 해당 변수들을 관리하기가 쉽지않다.
👍🏻 4. useRef로 선언하기
해당 변수는 리렌더링을 유발하지 않고, 리렌더링 되더라도 값이 초기화되는 것이 아닌 이전의 값을 기억하고 있으며, 컴포넌트마다 각각의 값을 가져 재사용에 용이하다 !
➡️ 때문에 경우에 따라서 값이 변경될 때마다 렌더링이 필요하다면
useState
로, 렌더링이 필요하지 않은 경우라면useRef
를 통해서 변수를 선언하여 사용하면 된다 !
re-rendering을 굳이 동반할 필요가 없는 작업 (보통 인터랙션 작업에 관련된 변수가 필요한 경우)을 할 때 useRef
가 사용된다.
- 포커스, 텍스트 선택 영역, 미디어 재생을 관리할 때
- 애니메이션을 직접적으로 실행시킬 때
(스크롤에 따라 이벤트가 발생해야 할 때 등 ..)- 서드 파티 DOM 라이브러리를 React와 같이 사용할 때
ref
의 값을 업데이트 하는 것은 side Effect이므로, 컴포넌트의 렌더링을 방해해선 안된다. 그러므로 반드시 컴포넌트가 마운트 되고 난 직후 (useEffect) 내에서 쓰거나 이벤트가 발생할 때 실행 (event handler) 안에서만 업데이트가 발생하도록 코드를 작성하여야 한다 !
즉 렌더링 중에 ref.current
를 쓰거나 읽으면 구성 요소의 동작을 예측할 수 없게 된다 ..
참고자료
메모이제이션된 함수를 반환하는 함수이다. 즉 특정 함수를 새로 만들지 않고 재사용하고 싶을 때 사용한다.
첫번째 인자
로 넘어온 함수
를, 두번째 인자
로 넘어온 배열
내의 값이 변경될 때까지 저장해놓고 재사용할 수 있게 해준다.
const add = useCallback(() =>
x + y
,[x, y]);
위 add
함수를 포함하고 있는 컴포넌트가 리랜더링 되더라도 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환한다.
즉 x
또는 y
의 값이 바뀌면 새로운 함수가 생성되어 add 변수
에 할당되고, x
와 y
의 값이 동일하다면 다음 랜더링 때 함수를 재사용한다.
사실 .. 함수를 새로 선언하는 것 자체로는 성능상 큰 문제가 되지 않는다. 그렇다면 성능 향상을 위해서 useCallback
을 사용한다는 것은 무슨 소리일까?
자바스크립트에서 함수는 객체로 취급이 되기 때문에 메모리 주소에 의한 참조 비교가 일어난다.
이러한 자바스크립트 특성은 React 컴포넌트 함수 내에서 어떤 함수를 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제로 이어질 수 있기 때문에 useCallback
을 통해서 함수가 변하지 않도록 해준다.
Reack hook들 중에는 불필요한 작업을 줄이기 위해 두 번째 인자
로 첫 번째 함수
가 의존해야하는 배열을 받는 경우가 많다.
이때, 위에서 말한 자바스크립트 함수 동등성
에 따라 의존 배열로 함수를 넘겼을 경우, 실제 함수에 변화가 없더라도 함수가 다른 메모리 주소에 올라가 있기 때문에 의존 배열의 함수가 변했다고 판단하고, 계속해서 리렌더링을 발생시키는 무한 루프가 발생할 수 있다.
import {useState, useEffect} from "react";
function Profile() {
const [user, setUser] = useState(null);
const fetchFn = () => {
...
}
// fetchFn가 실제로 변경이 됨의 여부와 관계없이 계속 변경되었다고 판단
// useEffect가 계속 호출되는 악순환..
useEffect(() => {
fetchFn().then(user => setUser(user));
},[fetchFn]);
...
}
import {useState, useEffect, useCallback} from "react";
function Profile() {
const [user, setUser] = useState(null);
// useCallback으로 감싸기
const fetchFn = useCallback(
() => {
...
}
)
// fetchFn이 실제로 변경되지 않는 한 useEffect 호출 안 됨
useEffect(() => {
fetchFn().then(user => setUser(user));
},[fetchFn]);
...
}
React.memo
에서는 부모 컴포넌트에서 함수를 Props로 받게 될 때 참조값
이기 때문에 재실행을 막을 수 없는 경우가 발생한다.
이때, useCallback
을 통해 해당 함수를 전달하게 되면 React.memo()
에서 해결할 수 없었던 부분들을 해결할 수 있게 된다!
따라서 useCallback
을 최적화를 위해 사용하고자 한다면, React.memo()
와 함께 써서 최적화를 만들어낼 수 있다.
이 부분은 실습을 통해 더 자세히 알아볼까요 ? 🤔
useCallback
은 크게1️⃣ mount시에 동작하는
mountCallback
2️⃣ update 또는 rerender시에 동작하는updateCallback
이 두 가지의 구현체로 동작한다
컴포넌트가 처음 마운트되어 useCallback
이 실행될 때 실행되는 함수이다.
1. mountCallback
함수는 hook
을 등록하고,
memoizedState
에 [콜백, 의존성 배열]
을 저장한 후 전달받은 콜백을 리턴한다.useCallback
은 실제로 전달받은 콜백을 그대로 리턴한다.hook 객체
를 받아온 다음 이전 의존성 배열과 새로운 의존성 배열을 비교한다.참고링크 ) 더 자세한 코드를 확인하고 싶다면 참고하세요!
useCallback
을 사용하면 함수를 다시 생성하지 않게 해준다? ❌무조건 렌더링마다 함수가 생성된다. 다만 ! 바로 위에서 살펴본 것과 같이 비교를 통해 새로 생성한 함수를 리턴하는지, 이전에 생성해서 저장해둔 함수를 리턴하는지의 차이가 있을 뿐이다.
역시나 최적화
에만 초점을 맞추고 useCallback
을 남발하면 안된다 !! useCallback
을 사용하기 위한 코드와 메모이제이션용 메모리가 추가로 필요하므로 잘못 사용하면 단순히 함수를 props로 만들어서 전달하는 것 보다 훨씬 더 많은 비용이 든다.
따라서 컴포넌트를 재실행할 때 드는 비용과, props만을 비교하는 비용 중 어떤 것이 더 효과적인지 생각해서 적용해야 한다!
참고 자료
예제코드까지 꼼꼼히 다뤄주신 아티클 잘 읽었습니다!
저는 리렌더링 최적화를 위해 useState를 useRef로 변환했던 경험이 있는데요, 이런 리팩토링 과정을 통해 useState는 값이 변할 때마다 매번 렌더링이 발생하지만 useRef의 current는 해당 값이 변하더라도 렌더링이 발생하지 않기 때문에 리렌더링 최적화에 도움이 된다는 사실을 알 수 있었어요! 다만, DOM 요소에 접근할 수 있는 훅이라는 사실은 몰랐는데 이번 아티클을 통해 알게 되었어요! 그러나 state와 DOM API를 혼합하여 사용하게 될 경우 가독성과 유지보수 면에서 복잡해지기 때문에 이는 추천하지 않는 방식이기때문에 DOM 요소에 접근할 때 useRef를 활용하는 것이 낫다는 사실도 알 수 있었어요..! 또, 서진님이 언급해주신 것처럼 ref 값을 업데이트 하는 것은 side effect인 만큼, 컴포넌트의 렌더링을 방해해선 안되기 때문에 반드시 렌더링 중에 ref의 current 값을 변경하여 구성요소의 동작을 예측할 수 없는 상황이 발생하지 않도록 주의해야겠다고 느꼈습니다!
개인적으로 useCallback은 제대로 사용해본 경험이 없어서 어떤 느낌인지 정확히는 이해하지 못했었는데, 이번 아티클을 통해 잘 알아갈 수 있었어요! 무조건 렌더링마다 함수는 생성이 되지만, 비교를 통해 새로 생성한 함수를 리턴하는지, 이전에 생성해서 저장해둔 함수를 리턴하는지의 차이가 있을 뿐이다.
라는 말이 가장 잘 와닿았어요! 그러나 최적화에만 초점을 맞추고 해당 훅을 사용하기 보다는, 구현하고자 하는 목적과 비용 등 다양한 부분을 잘 고려하여 사용해야겠다는 생각이 들었습니다!
좋은 아티클 감사합니다 넘 수고 많으셨어요 :-) 🖤
useRef의 사용중에 side effect가 있다는것을 간과하고 사용한 적이 많았는데 이번 아티클을 통해 알아가는 시간이 있어서 좋았습니다. 특히 useRef가 필요한 경우를 정확히 짚어주셔서 왜, 언제 사용하는지를 알아갈 수 있어서 너무 좋은 아티클이라고 생각합니다.
저는 ref객체가 무엇인지 부터를 잘 모르고 있어서 이것을 알아보았는데 ref는 DOM객체를 바인딩 하기 위해 사용하는 객체이며 리렌더링을 촉발시키지 않는다고 하네요! 또 이는 Mutable하며 말씀하신 것 처럼 렌더링중에 읽고 쓰는 것이 안된다고 합니다. mutable하기 때문에 side effect가 발생하는 거였군요!
구체적인 사용예시는 input에 값이 변경될 때마다 리렌더링시키지 않기 위해서 사용된다고 합니다!
와아 오늘 좀 머리 아픈 (=== 어려운) 주제였던 것 같아요.. useRef, useCallback 둘 다 많이 들어봤지만 사실 간단한 프로젝트 만들 때는 잘 안쓰게 되는 hook들이라서 정말 친하지 않았던 친구들인데,, 어려운 주제를 쉽고 차근차근 풀어주셔서 이해에 많은 도움이 되었습니다!!
아티클을 읽고 useRef에 대해서 좀 더 알아보다가, useRef가 일종의 변수처럼 쓰일 수 있게 된 이유를 자바스크립트 언어 관점에서 이해할 수 있게 되었어요! 첫째로 useRef()는 일반적인 자바스크립트 객체이기 때문에 heap
에 저장되는데요. 일반적인 함수가 stack
에 할당되어 종료되면 메모리 할당이 해제되는 것과 다르게, heap
에 저장되는 useRef()는 어플리케이션이 종료되거나 가비지 컬렉팅이 될 때까지 항상 같은 메모리 주소에 저장이 되어있고, 따라서 언제나 변경사항이 없는 것으로 감지가 되어 리렌더링이 일어나지 않는 것이라고 합니다.
또 주의해야할 점 중 하나로, 리렌더링
과 새로고침
은 전혀 다른 개념이기 때문에, 화면이 새로고침 될 경우에는 ref값도 전부 초기렌더링이 다시 수행되므로 값이 초기화된다는 점도 염두에 두어야겠네요!
정리를 잘 해주셔서 해당 개념들을 이해하는데 많은 도움이 되었습니다!
react memo와 usecallback을 같이 이용하는 부분에 대한 실습도 너무 궁금한데 듣지 못하게 되어 너무나도 아쉬운 거 같습니다
usecallback을 사용해도 렌더링마다 함수가 새로 생성된다는 건 몰랐던 사실인데 굉장히 흥미로웠습니다
그냥 const에 변수 할당하면 되는데 useRef를 꼭 사용해야 하나에 대한 궁금증이 생겨 찾아봤는데 변수를 선언하면 재렌더링이 일어날 때마다 새롭게 초기화가 진행이 되는데 useRef는 매번 같은 ref 객체를 반환해줘서 새롭게 초기화가 되지 않는다고 합니다
찾아보고 나니 뭔가 당연한 이유 같아서 조금 머쓱했습니다,,ㅎ
아티클 작성하시느라 고생 많으셨습니다!
사용법을 예시코드와 함께 제공해주어 더욱더 도움이 됐던 글인거 같아요 !
평소 다른 깃허브 속 코드를 참고할때 useRef를 많이 보진 않았던 거 같은데, 내가 아직 많은 코드를 못 봐서 그런가? 라고 생각했지만 "useRef가 굳이 필요한가 ..?"부분을 읽어보니 이런 이유 때문에 그럴 수 있겠구나 라는 생각이 들었던 거 같아요.
평소 많이 보지 못했던 hooks에 대하여 알고 지나가게 되어 정말 좋은 기회가 아니었나 생각합니다.
useCallback 파트도 인상깊게 봤는데, 계속해서 리랜더링 되는 useEffect 코드를 useCallback으로 리펙토링 시켜봐야겠다는 생각이 많이 드는 글이었던 거 같습니다!!
제 코드를 다시 한번 더 되돌아 보게 되는군요 ㅎㅎㅎ
도움 되는 아티클 정말 감사합니다 !!!