하.......................
발표라고 3시간 넘게 쓰고 있던 제 아티클이 날아갔네요........ 막 눈싸움도 하고 싶다고 쓰고 신나는 얘기가 많았는데......
급해서 이름도 안 적었네여 웹파트 YB 한수정입니다 ~! 지금은 좀 정신이 회복돼서 다시 읽다가 발견했습니다😆
이번엔 딴 얘기 없이 바로 아티클 시작하겠습니다.
6주차는 Ref로 값 참조하기, Ref로 DOM조작하기입니다 !!
컴포넌트에 ref를 추가하기
// React에서 useRef Hook을 가져와 컴포넌트에 ref를 추가할 수 있습니다.
import { useRef } from 'react';
// 컴포넌트 내에서 useRef Hook을 호출하고 참조할 초깃값을 유일한 인자로 전달합니다.
// 아래는 값 0에 대한 ref 입니다.
const ref = useRef(0);
// useRef 는 다음과 같은 객체를 반환합니다.
{
current: 0 // useRef에 전달한 값
}
ref.current 프로퍼티를 통해 해당 ref의 current 값에 접근할 수 있습니다. ref.current는 의도적으로 변경할 수 있으므로 읽고 쓸 수 있습니다. React가 추적하지 않는 구성 요소의 비밀 주머니라 할 수 있습니다🤔?
React가 추적하지 않는 구성 요소의 비밀 주머니란?
React는 상태(useState)나 props가 변경되면 자동으로 컴포넌트를 리렌더링합니다. 그러나 useRef는 React의 리렌더링 사이클과 연결되지 않습니다. 따라서 useRef에 저장된 값은 React의 감시 대상에서 벗어나 있습니다.
- React는 useRef의 값을 추적하거나 렌더링에 반영하지 않습니다.
- useRef는 UI를 업데이트하지 않습니다. 대신, 리렌더링 간에 값을 유지하고 변경할 수 있는 비밀 주머니로 활용됩니다.
useRef로 클릭수 확인하기
import { useRef } from 'react';
export default function Counter() {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert('You clicked ' + ref.current + ' times!');
}
return (
<button onClick={handleClick}>
Click me!
</button>
);
}
useState로 클릭수 확인하기
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // 상태를 업데이트하고 리렌더링 트리거
}
return (
<div>
<p>You clicked {count} times!</p>
<button onClick={handleClick}>
Click me!
</button>
</div>
);
}
useState와 useRef로 스톱워치 만들기
import { useState, useRef } from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>
Start
</button>
<button onClick={handleStop}>
Stop
</button>
</>
);
}
- useState
- startTime과 now는 현재 시간과 시작 시간을 저장하는 상태입니다.
- 이 값이 변경되면 컴포넌트가 리렌더링됩니다.
- setStartTime(Date.now()), setNow(Date.now())는 상태를 변경하고, 상태가 변경되면 React는 UI를 다시 그려주므로 secondsPassed가 계속 업데이트 됩니다.
- useRef
- intervalRef는 setInterval의 ID를 저장하는 데 사용됩니다. useRef는 값이 변경되어도 리렌더링을 발생시키지 않기 때문에, intervalRef.current를 통해 setInterval의 ID를 관리하고, 이를 clearInterval로 정리하는 데 유용합니다.
- intervalRef의 값은 렌더링 사이에 계속 유지되며 리렌더링을 발생시키지 않기 때문에, setInterval을 사용하여 시간을 업데이트하면서도 컴포넌트가 불필요하게 리렌더링되지 않도록 합니다.
ref와 state의 차이
ref | state |
useRef(initialValue) 는 { current: initialValue } 을 반환합니다. | useState(initialValue) 은 state 변수의 현재 값과 setter 함수 [value, setValue] 를 반환합니다. |
state를 바꿔도 리렌더링 되지 않습니다. | state를 바꾸면 리렌더링 됩니다. |
"Mutable" 변경 가능! 렌더링 프로세스 외부에서 current 값을 수정 및 업데이트할 수 있습니다. |
"Immutable" 불변! state 를 수정하기 위해서는 state 설정 함수를 반드시 사용하여 리렌더 대기열에 넣어야 합니다. |
렌더링 중에는 current 값을 읽거나 쓰면 안 됩니다. | 언제든지 state를 읽을 수 있습니다. 그러나 각 렌더마다 변경되지 않는 자체적인 state의 snapshot이 있습니다. |
"렌더링 중에는 current 값을 읽거나 쓰면 안 됩니다."
- ref는 React의 렌더링 사이클과 연결되지 않으며, 그 값의 변화를 추적하지 않습니다.
- ref.current 값을 렌더링 중에 수정하면 React의 렌더링에 영향을 주지 않기 때문에 예기치 않은 결과가 발생할 수 있습니다.
"언제든지 state를 읽을 수 있습니다. 그러나 각 렌더마다 변경되지 않는 자체적인 state의 snapshot이 있습니다."
- state는 React가 렌더링 중에 추적하는 값입니다. state를 변경하면 React가 컴포넌트를 리렌더링하고, 새로운 상태 값을 UI에 반영합니다.
- 각 렌더마다 state의 변경되지 않는 스냅샷을 유지하며, 이전 렌더의 상태와 비교해 UI를 업데이트합니다.
useRef로 화면에 카운트 값 반영하기
import { useRef } from 'react';
export default function Counter() {
let countRef = useRef(0);
function handleClick() {
// 이것은 컴포넌트의 리렌더를 일으키지 않습니다!
countRef.current = countRef.current + 1;
}
return (
<button onClick={handleClick}>
You clicked {countRef.current} times
</button>
);
}
아무리 버튼을 눌러도 카운트 수가 올라가지 않습니다.
이전에 alert를 통해 ref.current 값을 보여주던 것과는 조금 다릅니다.
그 코드에서는 리렌더링과 관계없이 ref.current의 값이 업데이트됩니다. 왜냐하면 useRef는 렌더링 간에 값을 유지하는 데 사용되지만, React가 이 값을 기준으로 컴포넌트의 리렌더링을 트리거하지는 않기 때문입니다. 따라서 alert은 리렌더링과 무관하게 현재의 ref.current 값을 보여줍니다.
반면, 위의 코드는 버튼 내부의 {countRef.current}를 통해 ref.current 값을 렌더링하려고 합니다. 하지만, ref.current 값이 업데이트되더라도 리렌더링이 발생하지 않기 때문에 버튼 안의 값은 변경되지 않습니다 !!
따라서 버튼 내부의 숫자를 업데이트 하기 위해서는 useState를 사용해야 합니다.
DEEP DIVE
useRef가 내부적으로 동작하는 방법
- 초기화: useRef는 처음 호출될 때 initialValue를 받습니다. 이 값은 current라는 속성을 가진 객체로 저장됩니다. 예를 들어, useRef(0)을 호출하면, { current: 0 } 형태의 객체가 반환됩니다.
- 값 유지: useRef는 렌더링 간에 동일한 객체를 유지합니다. 즉, useRef는 새로운 렌더링이 발생해도 객체를 변경하지 않고 계속 같은 객체를 반환합니다. 이를 통해 useRef는 상태를 추적할 수 있지만, 그 값이 변경되더라도 UI가 리렌더링되지 않게 합니다.
- 렌더링에 영향 없음: useRef로 관리되는 값(current)은 렌더링을 트리거하지 않습니다. 상태 값이 바뀌더라도 컴포넌트가 리렌더링되지 않죠. 이 점이 useState와 다른 점입니다. useState는 상태를 변경하면 렌더링을 트리거하지만, useRef는 그렇지 않습니다.
React 내부 구현 예시
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
- useRef는 useState와 비슷한 방식으로 동작하는데, 차이점은 상태값을 변경하는 setter 함수가 필요 없다는 것입니다.
- useState는 상태 값과 setter 함수 두 가지를 반환하지만, useRef는 단지 current 속성을 가진 객체만 반환합니다.
- 이 current 객체는 렌더 간에 동일하게 유지됩니다.
const myRef = useRef(0); // { current: 0 }
myRef.current = 5; // 변경: { current: 5 }
// UI는 리렌더링되지 않음
- useRef는 렌더링 사이에서 값을 유지하는 객체를 반환합니다.
- 이 객체는 렌더링을 트리거하지 않으며, current 속성을 통해 값을 추적할 수 있습니다.
- useRef는 값 변경 시 UI에 영향을 주지 않기 때문에 리렌더링이 발생하지 않습니다.
따라서, useRef는 주로 렌더링에 영향을 미치지 않는 값을 추적하거나 DOM 엘리먼트에 직접 접근할 때 사용됩니다.
refs를 사용할 시기
- Refs는 React 컴포넌트가 외부 시스템이나 브라우저 API와 상호작용해야 할 때 주로 사용됩니다.
- timeout IDs를 저장할 때 사용됩니다.
- DOM 엘리먼트 저장 및 조작할 때 사용됩니다.
- JSX를 계산하는 데 필요하지 않은 다른 객체 저장 시, 즉 렌더링과 무관한 데이터를 ref에 저장할 때 사용됩니다.
refs의 좋은 예시
- Refs를 탈출구로 사용
- 주로 브라우저 API나 외부 시스템과 통신 시 사용합니다.
- 애플리케이션 로직의 대부분이 refs에 의존하지 않도록 설계해야 합니다.
- ref는 자주 필요하지 않은 “탈출구”입니다.
- 렌더링 중에 ref.current를 읽거나 쓰지 않기
- ref.current가 언제 변하는지 React는 모르기 때문에 렌더링할 때 읽어도 컴포넌트의 동작을 예측하기 어렵습니다. 따라서 필요하다면 state를 대신 사용합니다.
- React의 제한을 받지 않음
- Ref는 자바스크립트 객체처럼 동작하며, 값을 즉시 변경 가능합니다.
- Ref에 저장된 값은 렌더링과 무관하므로, 상태 업데이트처럼 동기화가 필요하지 않습니다.
DOM과 Refs
Refs는 DOM 엘리먼트와 상호작용할 때 가장 자주 사용됩니다.
- JSX에서 ref 속성을 통해 DOM에 연결합니다. (e.g., <div ref={myRef}>).
- React는 해당 DOM 엘리먼트를 ref.current에 저장합니다.
- DOM 엘리먼트가 사라지면 React가 ref.current를 null로 업데이트합니다.
근데 refs는 뭔가 다른 것을 의미하나 싶어서 찾아봤더니, 여러 개의 ref를 나타내 단순히 복수형 명칭이라고 합니다.
Ref로 DOM 조작하기
React는 보통 DOM을 자동으로 관리하지만, 간혹 직접 DOM 요소에 접근해야 할 때가 있습니다. 예를 들어, 특정 요소에 포커스를 주거나, 스크롤 위치를 변경하거나, 요소의 크기를 측정하는 경우입니다. 이런 상황에서는 React의 ref를 사용해 DOM 요소에 접근할 수 있습니다.
ref로 노드 가져오기
// React가 관리하는 DOM 노드에 접근하기 위해 useRef Hook을 가져옵니다.
import { useRef } from 'react';
// 컴포넌트 안에서 ref를 선언하기 위해 방금 가져온 Hook을 사용합니다.
const myRef = useRef(null);
// 마지막으로 ref를 DOM 노드를 가져와야하는 JSX tag 에 ref 어트리뷰트로 전달합니다.
<div ref={myRef}>
// 아래와 같이 브라우저 API를 사용할 수 있습니다
myRef.current.scrollIntoView();
useRef Hook은 current라는 단일 속성을 가진 객체를 반환합니다. 초기에는 ‘myRef.current’가 ‘null’이 됩니다. React가 이 <div>에 대한 DOM 노드를 생성할 때, React는 이 노드에 대한 참조를 myRef.current에 넣습니다. 그리고 이 DOM 노드를 이벤트 핸들러에서 접근하거나 노드에 정의된 내장 브라우저 API를 사용할 수 있습니다.
useRef로 텍스트 입력칸에 포커스 넣기
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
- useRef Hook을 사용하여 inputRef를 선언합니다.
- 선언한 inputRef를 <input ref={inputRef}>처럼 전달합니다. 이 행위는 React에 이 <input>의 DOM 노드를 inputRef.current에 넣어줘 라고 하는 것입니다.
- handleClick 함수에서 inputRef.current에서 input DOM 노드를 읽고 inputRef.current.focus()로 focus()를 호출합니다.
- <button>의 onClick으로 handleClick 이벤트 핸들러를 전달합니다.
다른 컴포넌트의 DOM 노드 접근하기 - useRef로 텍스트 입력칸에 포커스 넣기
React는 기본적으로 다른 컴포넌트의 DOM 노드에 접근하는 것을 허용하지 않습니다. Ref는 자제해서 사용해야 하는 탈출구이고 직접 다른 컴포넌트의 DOM 노드를 조작하는 것은 코드가 쉽게 깨지게 만듭니다.
대신 특정 컴포넌트에서 소유한 DOM 노드를 선택적으로 노출할 수 있습니다. 컴포넌트는 자식 중 하나에 ref를 전달하도록 지정할 수 있습니다.
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
<input> 태그가 아닌 <MyInput> 컴포넌트에 넣기 위한 방법을 알아보겠습니다.
MyInput 컴포넌트를 자세히 보도록 하겠습니다.
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
- forwardRef 함수 : 컴포넌트가 전달받은 ref를 내부 DOM 요소에 전달할 수 있게 합니다.
- ref: 부모 컴포넌트에서 전달한 ref입니다. React가 DOM 요소와 연결되도록 처리합니다.
- forwardRef를 사용하면 컴포넌트가 두 번째 인수로 ref를 받게 됩니다.
- <input> : 실제 DOM 요소입니다. MyInput 컴포넌트는 이 <input> 태그를 반환합니다.
- {...props} : 부모 컴포넌트에서 전달받은 모든 속성을 <input>에 전달합니다. 예를 들어, type="text"가 전달되면 <input type="text" />로 렌더링됩니다.
- ref={ref} : 부모 컴포넌트에서 전달된 ref를 <input>에 연결합니다. React는 이 ref를 사용해 <input> DOM 요소를 참조할 수 있습니다.
DEEP DIVE
명령형 처리방식으로 하위 API 노출하기
문제상황 : DOM 노드가 과도하게 노출되는 경우
- 기본적으로 ref를 통해 부모 컴포넌트는 하위 컴포넌트의 DOM 노드를 완전히 제어할 수 있습니다.
- 하지만 이는 의도치 않은 사용(CSS 직접 수정)으로 컴포넌트의 안정성을 해칠 수 있습니다.
해결방법 : 필요한 기능만 노출하기
- useImperativeHandle을 사용하면 부모가 ref를 통해 사용할 수 있는 기능을 선택적으로 노출할 수 있습니다.
MyInput 컴포넌트 자세히 보기
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null); // 실제 DOM 노드를 저장
useImperativeHandle(ref, () => ({
// focus 기능만 노출
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
- realInputRef : 실제 <input> DOM 노드를 참조합니다.
- useImperativeHandle : 부모 컴포넌트가 사용할 수 있는 기능을 정의합니다.
- 여기서는 focus() 메서드만 노출하여 부모가 다른 작업을 하지 못하게 합니다.
부모 컴포넌트 Form 자세히 보기
export default function Form() {
const inputRef = useRef(null); // MyInput의 ref
function handleClick() {
inputRef.current.focus(); // focus 기능 호출
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
- inputRef.current.focus() : 부모는 focus()만 호출할 수 있습니다.
- 실제 DOM 노드에 접근하지 못하므로 안정성이 향상됩니다.
Ref로 DOM을 조작할 때 알아야 할 점
1. Ref는 탈출구입니다.
Ref는 React의 선언적 방식이 아닌 명령형 방식으로 DOM을 직접 조작해야 할 때만 사용해야 합니다.
- 사용 예시: 포커스 이동, 스크롤 위치 조정, 특정 브라우저 API 호출.
2. DOM을 직접 조작할 때의 문제점
React는 DOM을 자체적으로 관리합니다. 하지만 Ref로 직접 DOM을 조작하면 React의 상태 관리와 충돌이 발생할 수 있습니다.
- 예시: React가 관리하는 노드를 remove()로 강제로 삭제하면 React는 해당 노드가 사라졌다는 사실을 모르기 때문에 충돌이 발생합니다.
3. 안전한 Ref 사용법
- 비파괴적인 작업: 포커스 이동, 스크롤 관리처럼 DOM 구조를 변경하지 않는 작업은 안전합니다.
// 포커스를 다른 입력 필드로 이동하는 경우
const inputRef = useRef(null);
function focusInput() {
inputRef.current.focus();
}
return <input ref={inputRef} />;
- DOM 변경 금지: React가 관리하는 노드(추가/삭제)를 직접 수정하면 충돌 위험이 있습니다.
// remove()를 사용하여 React 관리 외부에서 DOM 요소를 삭제하는 경우
const ref = useRef(null);
function removeNode() {
ref.current.remove(); // React의 상태와 충돌할 수 있음
}
return <div ref={ref}>Hello World</div>;
- 예외적인 경우: React가 DOM 일부를 건드리지 않는다는 확신이 있다면 해당 부분을 조작할 수 있습니다.
// 항상 빈 <div>로 JSX에서 div를 선언하고, 내부에서만 동적으로 자식 요소를 추가하는 경우
function App() {
const divRef = useRef(null);
function addChild() {
const newDiv = document.createElement('div');
newDiv.textContent = 'New Child';
divRef.current.appendChild(newDiv); // React가 관리하지 않는 부분에만 추가
}
return (
<div ref={divRef}>
<button onClick={addChild}>Add Child</button>
</div>
);
}
4. 결론 : React의 선언적 패턴을 우선하세요
Ref는 필요할 때만 사용하는 최후의 수단입니다. React가 제공하는 선언적 상태 관리 방식을 최대한 활용하고, Ref로 DOM을 조작해야 할 때는 신중하고 제한적으로 접근해야 합니다.
챌린지 도전하기 3. 이미지 캐러셀 스크롤링
selectedRef를 선언하고 조건적으로 현재 활성화된 이미지에 전달할 수 있습니다.
<li ref={index === i ? selectedRef : null}>
index === i 조건이 만족할 때 이 이미지가 선택된 이미지라는 뜻이고 그 <li>은 selectedRef를 받을 것입니다. React는 selectedRef.current가 현재 선택된 올바른 DOM 노드를 올바르게 가리키도록 합니다.
스크롤 하기 전에 React가 DOM 변경을 끝내기 위해 flushSync 호출이 필요하다는 것을 주의해야 합니다.
아래와 같이 챌린징을 해볼 수 있습니다.
import { useRef, useState } from 'react';
import { flushSync } from 'react-dom';
const CatFriends = () => {
const selectedRef = useRef(null);
const [index, setIndex] = useState(0);
const handleNextClick = () => {
flushSync(() => {
setIndex((prevIndex) => (prevIndex < catList.length - 1 ? prevIndex + 1 : 0));
});
selectedRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
});
};
return (
<>
<nav>
<button onClick={handleNextClick}>Next</button>
</nav>
<div>
<ul style={{ display: 'flex', overflowX: 'auto', padding: '10px' }}>
{catList.map((cat, i) => (
<li
key={cat.id}
ref={index === i ? selectedRef : null}
style={{ flexShrink: 0, margin: '0 10px', display: 'inline-block' }}
>
<div
className={index === i ? 'active' : ''}
style={{
position: 'relative',
borderRadius: '10px',
overflow: 'hidden',
width: 'auto',
height: '200px',
boxShadow: index === i ? '0 0 15px rgba(0, 0, 0, 0.8)' : 'none',
}}
>
<img
src={cat.imageUrl}
style={{
height: '100%',
width: '100%',
objectFit: 'cover',
}}
alt={`Cat #${cat.id}`}
/>
</div>
</li>
))}
</ul>
</div>
</>
);
};
const catList = Array.from({ length: 9 }, (_, i) => ({
id: i + 1,
imageUrl: `/images/cat${i + 1}.jpg`,
}));
export default CatFriends;
저는 이번에 하면서 ref를 제대로 알고 써볼 수 있었던 것 같습니다!
또한, ref를 사용해본 적이 기억이 잘 나지 않고 마지막 챌린징에서 본 flushSync도 처음 보는 낯선 개념인데, 나리스분들은 ref 개념을 언제 사용해보셨는지 여러분의 경험이 궁금합니다...!
하.. 중간에 날려먹어서 너무 급하게 적은 것 같아요.. 다신 이런 실수를 하지 않도록 조심해야겠습니다.
'나야, 리액트 스터디' 카테고리의 다른 글
[week7] useEffect, customHook (2) | 2024.12.08 |
---|---|
[Week 6] Ref (1) | 2024.12.02 |
[week 6] useRef 뿌시기 (3) | 2024.12.01 |
[week6] Ref (1) | 2024.12.01 |
[week 6] Ref로 값 참조하기, Ref로 DOM조작하기 (3) | 2024.12.01 |