Ref: 외부변수 -> 비제어 컴포넌트
비제어 컴포넌트: 리액트가 인지하지 못한 변수이기에 Ref 내부 값이 변경 시 리렌더링 X
리액트 리렌더링을 발생시키지 않고 변경 가능한 값을 담는 참조(reference) 생성
- Ref(외부 변수)를 리액트가 인지할 수 없기에 Ref값의 변동과 리렌더링은 전혀 상관이 없다.라는 의미에서 Ref를 통해 값을 관리하는 컴포넌트를 비제어 컴포넌트라고 부른다.
Reference(useRef로 생성한)를 HTML 요소에 연결
Reference(useRef로 생성한)는 값을 저장하는 용도로도 사용될 수 있지만 Reference는 기본적으로 리액트에서 DOM 조작을 위해 사용
- `reference.current` 출력: DOM
- `document.getElementById` 출력 = DOM
function App() {
// 1. Javascript in <head>
const reference = useRef(null)
console.log(reference.current) //null
console.log(reference.current?.id) //undefined
console.log(document.getElementById('for-reference')) //null
console.log(document.getElementById('for-reference')?.id) //undefined
useEffect(() => {
// 3. Javascript in end of <body>
console.log(reference.current) //<input id='for-reference' type='number'>
console.log(reference.current?.id) //for-reference
console.log(document.getElementById('for-reference')) //<input id='for-reference' type='number'>
console.log(document.getElementById('for-reference')?.id) //for-reference
}, [])
// 2. HTML (DOM) < 이때 reference 에 DOM 할당
return (
<>
<input id='for-reference' ref={reference} type='number' />
</>
)
}
왜 첫번째 4개 로그는 null 혹은 undefined가 찍히고, 그 다음 4개 로그는 제대로 찍히는가?
1. 가장 먼저 useRef 통한 reference 생성(null) -> typescript: useRef 통한 reference생성시 타입니다 null로 초기화 해야함
2. 그 다음 HTML (DOM)모두 파싱 및 로드 <- 이때 reference에 DOM할당
3. 마지막으로 reference 로 DOM조작 가능(reference에 DOM이 연결되어 있으니)
Ref는 언제 활용하는가
✅ 1. 리렌더링 감소
많은 값을 가진 큰 객체를 다양한 컴포넌트에서 업데이트하는 경우 리렌더가 사정없이 발생
- 값 하나만 바뀌더라도 그걸 사용해서 컴포넌트 전체에서 리렌더가 발생
- 이를 방지하기 위해, 입력 폼으로 어차피 값을 표기되니 실시간 갱신을 하지 말자
- 제출 버튼을 클릭했을 때만 Ref에 있는 값을 사용
실습
실습 코드를 통해 이해해 보자
[useState만 사용했을 때]
import { useState } from 'react'
import '@/App.css'
const FEE_ADULT = 20000
const FEE_NON_ADULT = 10000
function App() {
const [age, setAge] = useState(0)
const [valid, setValid] = useState(false)
const [entrance, setEntrance] = useState(FEE_NON_ADULT)
console.log('-rerendering')
return (
<>
<input
type='number'
value={age}
onChange={(event) => {
const changed = Number(event.currentTarget.value)
setAge(changed)
setValid(changed >= 19)
// 성년이 되어도 15000원이 되지 않습니다. 여러분들이 각자 한번 풀어보세요.
setEntrance(changed >= 19 ? FEE_ADULT : FEE_NON_ADULT)
}}
/>
{valid ? (
<div>성년입니다.</div>
) : (
<div style={{ color: 'red' }}>미성년입니다.</div>
)}
<div>{`${entrance}원`}</div>
</>
)
}
export default App
input의 값이 바뀔 때마다 리렌더링이 된다. 코드 복붙한 후 콘솔을 확인해보자
[useRef를 사용했을 때]
import { useRef, useState } from 'react'
import '@/App.css'
function App() {
const [valid, setValid] = useState(false)
const ageReference = useRef()
console.log('- rerendered')
return (
<>
<input
ref={ageReference} //ref 돔에 연결
type='number'
onChange={(e) => setValid(Number(e.currentTarget.value) >= 19)}
/>
{valid ? <div>성년입니다</div> : <div style={{ color: 'red' }}>미성년입니다</div>}
</>
)
}
export default App
`e.currentTarget.value` 가 19 이상일 때만 리렌더링 발생하고 input 태그에 값이 바뀔 때마다 리렌더링 되지 않는다.
✅ 2. 순수 HTML 요소 간접 조작
`<input/>` 이런 것들은 React componet가 아니라 내부 상태가 존재하지 않는 순수 HTML요소이다.
DOM 객체(reference)를 통해 간접적으로 조작(리렌더링 없이, 가장 이상적인 용례)
- DOM 객체(reference) = ref.current 프로퍼티
기존에는 순수 HTML 요소 (DOM) 선택을 위해 아래 방법 사용
- 일반 JS에서는 `document.findElementById()`같은걸 사용 (DOM 객체 반환)
- jQuery에서는 `$` 사용 (jQuery 객체 중 첫번째 요소를 불러야 -> DOM 객체 접근 / 조작 가능)
실습
1. useRef 통해 원하는 DOM(HTML 요소)과 연결
- `useEffect` 통해 DOM 연결된 이후 시점에서의 referece 조회
import '@/App.css'
import { useEffect, useRef } from 'react'
function App() {
const reference = useRef(null)
console.log('렌더링 전 : ')
console.log(reference.current) //null
console.log(reference.current?.innerText) //undefined
useEffect(() => {
console.log('렌더링 후 : ')
console.log(reference.current) //<div>apple</div>
console.log(reference.current.innerText) //apple
}, [])
return (
<>
<div ref={reference}>apple</div>
</>
)
}
export default App
2. useRef 통해 리렌더 없이 DOM(HTML 요소) 조작
- `.current.style.color` / `.current.className` 과 같은 속성들 리렌더 없이 조작 가능
- useRef 는 값 자체 저장으로도 사용 `useRef("mango")` / `.current = "orange"`
import '@/App.css'
import { useRef } from 'react'
function App() {
const reference = useRef(null)
console.log('- rerendered')
return (
<>
<div ref={reference}>apple</div>
<button onClick={(e) => (reference.current.style.color = 'red')}>변경</button>
</>
)
}
export default App
3. useRef통한 <video> 태그 DOM(HTML 요소) 영상 소스 변경
import '@/App.css'
import { useRef } from 'react'
function App() {
const reference = useRef(null)
const sources = [
'https://vjs.zencdn.net/v/oceans.mp4',
'https://lamberta.github.io/html5-animation/examples/ch04/assets/movieclip.mp4',
]
console.log('- rerendered')
return (
<>
<video ref={reference} autoPlay controls width={500} />
<div>
<button onClick={() => (reference.current.src = sources[0])}>전환 1</button>
<button onClick={() => (reference.current.src = sources[1])}>전환 2</button>
</div>
</>
)
}
export default App
리액트에서 이런 `ref`를 통해 HTML에 직접 접근해서 무엇을 할 수 있나?
1. 특정 요소에 포커스
import { useRef } from 'react'
function Field() {
const inputRef = useRef();
function handleFocus() {
inputRef.current.focus();
}
return (
<>
<input type="text" ref={inputRef} />
<button onClick={handleFocus}>입력란 포커스</button>
</>
)
}
특정 요소에 Utteran
2. 특정 요소에 Utterances 를 붙일 때
const Comments = () => {
const commentRef = useRef(null);
useEffect(() => {
// script element 생성
const utterances = document.createElement('script');
// attribute를 전체를 객체로 만들기
const utterancesConfig = {
src: 'https://utteranc.es/client.js',
repo: 'user/repo',
theme: '선택한 테마',
'issue-term': '포스트 페이지 매핑 방법',
async: true,
crossorigin: 'anonymous',
};
// 객체 전체를 setAttribute로 붙이기
Object.entries(utterancesConfig).forEach(([key, value]) => {
utterances.setAttribute(key, value);
});
// 만든 script를 ref 항목에 appendChild로 붙이기
commentRef.current.appendChild(utterances);
}, []);
return <div ref={commentRef} />;
};
export default Comments;
3. 특정 요소에 Observer붙일 때 (스크롤에 따라 상단에 제목을 표기할지 말지..)
import { useRef, useEffect } from 'react'
function SpyExample() {
const spy = useRef()
useEffect(() => {
// 1. Define
const observer = new window.IntersectionObserver(([entry]) => {
if (!entry.intersectionRatio) {
document.getElementById('header-title').classList.add('scrolled-a-bit')
} else {
document.getElementById('header-title').classList.remove('scrolled-a-bit')
}
})
// 2. Attach
observer.observe(spy.current)
// 3. Detach
return () => {
observer.disconnect()
}
}, [])
return (
<div ref={spy} />
)
}
useRef와 setState들이 복잡하게 얽혀있을 때 어떻게 리렌더 영향을 주나?
모든 컴포넌트 그리고 부모-자식에서의 리렌더 기준은 무조건 state(model)가 어디있는지에 의존
- 부모-자식 간 리렌더는 아래 세가지 중 가장 마지막인 state(model)에 의존한다.
- useRef로 생성된 ref가 어디있는지?
- useState로 생성된 setState가 어디서 호출됐는지?
- useState로 생성된 state가 속한 컴포넌트를 기준으로 (부모로) 모든 자식 컴포넌트 리렌더 -> 이것을 기준으로 리렌더함
모든 View의 리렌더의 기준은 model 변경 여부에 의존한다. 이런 특성에 따라 아래와 같은 문제 발생
- props drilling: state(model)에 의존하는 view가 State 전달받기 위해 props여행
- state의 변경은 state가 속한 부모의 자식 컴포넌트 모두를 리렌더링(나비효과)
MVC패턴은 수직구조로 변경되는 단점이 있다.
forwardRef 써야 ref값을 넘길 수 있다는건 인지했는데, 함수명을 매번 바꿔줘야하나?
yes, 익명 함수 컴포넌트보다 기명 함수 컴포넌트를 forwardRef로 감싸주는 것이 좋음
익명 함수 컴포넌트를 forwardRef로 감싼 경우 디버깅이 어렵다.
익명 함수 컴포넌트를 forwardRef로 감싼 경우
const CustomInput = forwardRef(function ({/* value, onChange */}, ref) {
if (true) { throw Error('익명함수에러') }
return <input type='number' ref={ref} /* value={value} onChange={onChange} */ />
})
기명 함수 컴포넌트를 forwardRef로 감싼 경우
const CustomInput = forwardRef(function WrappedCustomInput({/* value, onChange */}, ref) {
if (true) { throw Error('익명함수에러') }
return <input type='number' ref={ref} /* value={value} onChange={onChange} */ />
})
리액트19 버전부터 함수형 컴포넌트에서 Props에 기본으로 ref가 들어감
우리는 실습할 때 forwardRef를 사용하지 않고 파라미터로 ref를 넘겨받아서 사용함
'ASAC 웹 풀스택' 카테고리의 다른 글
Java 기본 문법 및 JVM 구성(1) - Java동작 원리 (0) | 2024.09.25 |
---|---|
React 의 특장점, 렌더 라이프사이클 및 Hook(7) - immer(리렌더링 이슈 해결) (1) | 2024.09.22 |
React 의 특장점, 렌더 라이프사이클 및 Hook(4) - State (1) | 2024.09.22 |
React 의 특장점, 렌더 라이프사이클 및 Hook(5) - Props (0) | 2024.09.22 |
React: setState에 대해 더 자세히 알아보자 (0) | 2024.09.21 |