React는 렌더 출력과 일치하도록 DOM을 자동으로 업데이트해요. 그래서 컴포넌트가 직접 DOM을 조작할 필요가 거의 없어요. 하지만 가끔 React가 관리하는 DOM 요소에 접근해야 할 때가 있어요—예를 들어 노드에 포커스를 주거나, 스크롤하거나, 크기와 위치를 측정할 때요. React에는 이런 작업을 하는 내장된 방법이 없기 때문에, DOM 노드에 대한 ref가 필요해요.
ref 속성으로 React가 관리하는 DOM 노드에 접근하는 방법ref JSX 속성이 useRef Hook과 어떻게 연결되는지React가 관리하는 DOM 노드에 접근하려면, 먼저 useRef Hook을 import하세요:
import { useRef } from 'react';
그다음, 컴포넌트 안에서 ref를 선언해요:
const myRef = useRef(null);
마지막으로, DOM 노드를 얻고 싶은 JSX 태그에 ref 속성으로 전달하세요:
<div ref={myRef}>
useRef Hook은 current라는 단일 프로퍼티를 가진 객체를 반환해요. 처음에 myRef.current는 null이에요. React가 이 <div>에 대한 DOM 노드를 생성하면, React는 이 노드에 대한 참조를 myRef.current에 넣어요. 그러면 이벤트 핸들러에서 이 DOM 노드에 접근하고 거기에 정의된 내장 브라우저 API들을 사용할 수 있어요.
// 예를 들어, 어떤 브라우저 API든 사용할 수 있어요:
myRef.current.scrollIntoView();
이 예제에서는 버튼을 클릭하면 input에 포커스가 들어가요:
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를 선언해요.<input ref={inputRef}>로 전달해요. 이렇게 하면 React에게 이 <input>의 DOM 노드를 inputRef.current에 넣으라고 알려주는 거예요.handleClick 함수에서 inputRef.current로부터 input DOM 노드를 읽고 inputRef.current.focus()로 focus()를 호출해요.<button>에 onClick으로 handleClick 이벤트 핸들러를 전달해요.DOM 조작이 ref의 가장 일반적인 사용 사례이지만, useRef Hook은 타이머 ID처럼 React 외부의 다른 것들을 저장하는 데도 사용할 수 있어요. state와 마찬가지로 ref는 렌더링 사이에 유지돼요. Ref는 설정해도 리렌더링을 트리거하지 않는 state 변수 같은 거예요. ref에 대해 더 알고 싶다면 Ref로 값 참조하기를 읽어보세요.
한 컴포넌트에 여러 개의 ref를 가질 수 있어요. 이 예제에서는 세 개의 이미지로 이루어진 캐러셀이 있어요. 각 버튼은 해당 DOM 노드에서 브라우저의 scrollIntoView() 메서드를 호출해서 이미지를 중앙에 위치시켜요:
import { useRef } from 'react';
export default function CatFriends() {
const firstCatRef = useRef(null);
const secondCatRef = useRef(null);
const thirdCatRef = useRef(null);
function handleScrollToFirstCat() {
firstCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
function handleScrollToSecondCat() {
secondCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
function handleScrollToThirdCat() {
thirdCatRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
return (
<>
<nav>
<button onClick={handleScrollToFirstCat}>
Neo
</button>
<button onClick={handleScrollToSecondCat}>
Millie
</button>
<button onClick={handleScrollToThirdCat}>
Bella
</button>
</nav>
<div>
<ul>
<li>
<img
src="https://placecats.com/neo/300/200"
alt="Neo"
ref={firstCatRef}
/>
</li>
<li>
<img
src="https://placecats.com/millie/200/200"
alt="Millie"
ref={secondCatRef}
/>
</li>
<li>
<img
src="https://placecats.com/bella/199/200"
alt="Bella"
ref={thirdCatRef}
/>
</li>
</ul>
</div>
</>
);
}
div {
width: 100%;
overflow: hidden;
}
nav {
text-align: center;
}
button {
margin: .25rem;
}
ul,
li {
list-style: none;
white-space: nowrap;
}
li {
display: inline;
padding: 0.5rem;
}
위 예제들에서는 미리 정해진 개수의 ref가 있었어요. 하지만 가끔 목록의 각 항목에 ref가 필요한데, 몇 개가 있을지 모르는 경우가 있어요. 이런 코드는 작동하지 않아요:
<ul>
{items.map((item) => {
// 작동 안 해요!
const ref = useRef(null);
return <li ref={ref} />;
})}
</ul>
Hook은 컴포넌트의 최상위 레벨에서만 호출해야 하기 때문이에요. 반복문, 조건문, 또는 map() 호출 안에서 useRef를 호출할 수 없어요.
이 문제를 해결하는 한 가지 방법은 부모 요소에 대한 단일 ref를 얻은 다음, querySelectorAll 같은 DOM 조작 메서드를 사용해서 개별 자식 노드들을 "찾는" 거예요. 하지만 이건 취약하고 DOM 구조가 바뀌면 깨질 수 있어요.
또 다른 해결책은 ref 속성에 함수를 전달하는 거예요. 이걸 ref 콜백이라고 해요. React는 ref를 설정할 때 DOM 노드와 함께 ref 콜백을 호출하고, ref를 지울 때 콜백에서 반환된 정리 함수를 호출해요. 이렇게 하면 자체 배열이나 Map을 유지하고, 인덱스나 어떤 종류의 ID로 어떤 ref든 접근할 수 있어요.
이 예제는 긴 목록에서 임의의 노드로 스크롤하기 위해 이 접근 방식을 사용하는 방법을 보여줘요:
import { useRef, useState } from "react";
export default function CatFriends() {
const itemsRef = useRef(null);
const [catList, setCatList] = useState(setupCatList);
function scrollToCat(cat) {
const map = getMap();
const node = map.get(cat);
node.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
function getMap() {
if (!itemsRef.current) {
// Initialize the Map on first usage.
itemsRef.current = new Map();
}
return itemsRef.current;
}
return (
<>
<nav>
<button onClick={() => scrollToCat(catList[0])}>Neo</button>
<button onClick={() => scrollToCat(catList[5])}>Millie</button>
<button onClick={() => scrollToCat(catList[8])}>Bella</button>
</nav>
<div>
<ul>
{catList.map((cat) => (
<li
key={cat.id}
ref={(node) => {
const map = getMap();
map.set(cat, node);
return () => {
map.delete(cat);
};
}}
>
<img src={cat.imageUrl} />
</li>
))}
</ul>
</div>
</>
);
}
function setupCatList() {
const catCount = 10;
const catList = new Array(catCount)
for (let i = 0; i < catCount; i++) {
let imageUrl = '';
if (i < 5) {
imageUrl = "https://placecats.com/neo/320/240";
} else if (i < 8) {
imageUrl = "https://placecats.com/millie/320/240";
} else {
imageUrl = "https://placecats.com/bella/320/240";
}
catList[i] = {
id: i,
imageUrl,
};
}
return catList;
}
div {
width: 100%;
overflow: hidden;
}
nav {
text-align: center;
}
button {
margin: .25rem;
}
ul,
li {
list-style: none;
white-space: nowrap;
}
li {
display: inline;
padding: 0.5rem;
}
이 예제에서 itemsRef는 단일 DOM 노드를 보관하지 않아요. 대신 항목 ID에서 DOM 노드로의 Map을 보관해요. (Ref는 어떤 값이든 보관할 수 있어요!) 모든 목록 항목의 ref 콜백이 Map을 업데이트하는 역할을 해요:
<li
key={cat.id}
ref={node => {
const map = getMap();
// Map에 추가
map.set(cat, node);
return () => {
// Map에서 제거
map.delete(cat);
};
}}
>
이렇게 하면 나중에 Map에서 개별 DOM 노드를 읽을 수 있어요.
Strict Mode가 활성화되면, 개발 환경에서 ref 콜백이 두 번 실행돼요.
이게 콜백 ref에서 버그를 찾는 데 어떻게 도움이 되는지 더 읽어보세요.
부모 컴포넌트에서 자식 컴포넌트로 다른 prop처럼 ref를 전달할 수 있어요.
import { useRef } from 'react';
function MyInput({ ref }) {
return <input ref={ref} />;
}
function MyForm() {
const inputRef = useRef(null);
return <MyInput ref={inputRef} />
}
위 예제에서 부모 컴포넌트 MyForm에서 ref가 생성되고 자식 컴포넌트 MyInput에 전달돼요. MyInput은 그 ref를 <input>에 전달해요. <input>은 내장 컴포넌트이기 때문에 React는 ref의 .current 프로퍼티를 <input> DOM 요소로 설정해요.
MyForm에서 생성된 inputRef는 이제 MyInput이 반환하는 <input> DOM 요소를 가리켜요. MyForm에서 생성된 클릭 핸들러가 inputRef에 접근해서 focus()를 호출하여 <input>에 포커스를 설정할 수 있어요.
import { useRef } from 'react';
function MyInput({ ref }) {
return <input ref={ref} />;
}
export default function MyForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
위 예제에서 MyInput에 전달된 ref는 원래 DOM input 요소로 전달돼요. 이렇게 하면 부모 컴포넌트가 거기에 focus()를 호출할 수 있어요. 하지만 이건 부모 컴포넌트가 다른 것도 할 수 있게 해요—예를 들어 CSS 스타일을 변경하는 것처럼요. 드문 경우지만, 노출되는 기능을 제한하고 싶을 수 있어요. useImperativeHandle로 그렇게 할 수 있어요:
import { useRef, useImperativeHandle } from "react";
function MyInput({ ref }) {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose focus and nothing else
focus() {
realInputRef.current.focus();
},
}));
return <input ref={realInputRef} />;
};
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>Focus the input</button>
</>
);
}
여기서 MyInput 안의 realInputRef가 실제 input DOM 노드를 보관해요. 하지만 useImperativeHandle은 React에게 부모 컴포넌트에 대한 ref의 값으로 여러분만의 특별한 객체를 제공하라고 지시해요. 그래서 Form 컴포넌트 안의 inputRef.current는 focus 메서드만 가지게 돼요. 이 경우 ref "핸들"은 DOM 노드가 아니라 useImperativeHandle 호출 안에서 생성한 커스텀 객체예요.
React에서 모든 업데이트는 두 단계로 나뉘어요:
일반적으로 렌더링 중에 ref에 접근하는 건 원치 않을 거예요. DOM 노드를 보관하는 ref도 마찬가지예요. 첫 번째 렌더링 동안에는 DOM 노드가 아직 생성되지 않았기 때문에 ref.current는 null이에요. 그리고 업데이트 렌더링 중에는 DOM 노드가 아직 업데이트되지 않았어요. 그래서 읽기에 너무 이른 거예요.
React는 커밋 중에 ref.current를 설정해요. DOM을 업데이트하기 전에 React는 영향받는 ref.current 값들을 null로 설정해요. DOM을 업데이트한 후에 React는 즉시 해당 DOM 노드로 설정해요.
보통 이벤트 핸들러에서 ref에 접근할 거예요. ref로 무언가를 하고 싶지만 그걸 할 특정 이벤트가 없다면, Effect가 필요할 수 있어요. 다음 페이지들에서 Effect에 대해 논의할 거예요.
새 할 일을 추가하고 목록의 마지막 자식으로 화면을 스크롤하는 이런 코드를 생각해 보세요. 어떤 이유에서인지 항상 마지막으로 추가된 것 바로 전 할 일로 스크롤되는 것에 주목하세요:
import { useState, useRef } from 'react';
export default function TodoList() {
const listRef = useRef(null);
const [text, setText] = useState('');
const [todos, setTodos] = useState(
initialTodos
);
function handleAdd() {
const newTodo = { id: nextId++, text: text };
setText('');
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
return (
<>
<button onClick={handleAdd}>
Add
</button>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<ul ref={listRef}>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</>
);
}
let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {
initialTodos.push({
id: nextId++,
text: 'Todo #' + (i + 1)
});
}
문제는 이 두 줄에 있어요:
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
React에서 state 업데이트는 큐에 들어가요. 보통은 이게 원하는 동작이에요. 하지만 여기서는 setTodos가 DOM을 즉시 업데이트하지 않기 때문에 문제가 생겨요. 그래서 목록을 마지막 요소로 스크롤할 때 할 일이 아직 추가되지 않은 거예요. 이래서 스크롤이 항상 한 항목씩 "뒤처지는" 거예요.
이 문제를 해결하려면 React가 DOM을 동기적으로 업데이트("플러시")하도록 강제할 수 있어요. 그러려면 react-dom에서 flushSync를 import하고 state 업데이트를 flushSync 호출로 감싸면 돼요:
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
이렇게 하면 flushSync로 감싼 코드가 실행된 직후에 React가 DOM을 동기적으로 업데이트하라고 지시해요. 결과적으로 스크롤하려고 할 때 마지막 할 일이 이미 DOM에 있을 거예요:
import { useState, useRef } from 'react';
import { flushSync } from 'react-dom';
export default function TodoList() {
const listRef = useRef(null);
const [text, setText] = useState('');
const [todos, setTodos] = useState(
initialTodos
);
function handleAdd() {
const newTodo = { id: nextId++, text: text };
flushSync(() => {
setText('');
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
return (
<>
<button onClick={handleAdd}>
Add
</button>
<input
value={text}
onChange={e => setText(e.target.value)}
/>
<ul ref={listRef}>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</>
);
}
let nextId = 0;
let initialTodos = [];
for (let i = 0; i < 20; i++) {
initialTodos.push({
id: nextId++,
text: 'Todo #' + (i + 1)
});
}
Ref는 탈출구예요. "React 외부로 나가야" 할 때만 사용해야 해요. 일반적인 예로는 포커스 관리, 스크롤 위치, 또는 React가 노출하지 않는 브라우저 API 호출이 있어요.
포커스나 스크롤 같은 비파괴적인 동작을 고수한다면 문제가 발생하지 않을 거예요. 하지만 DOM을 수동으로 수정하려고 하면 React가 만드는 변경 사항과 충돌할 위험이 있어요.
이 문제를 설명하기 위해 이 예제에는 환영 메시지와 두 개의 버튼이 포함되어 있어요. 첫 번째 버튼은 React에서 일반적으로 하듯이 조건부 렌더링과 state를 사용해서 존재 여부를 토글해요. 두 번째 버튼은 remove() DOM API를 사용해서 React의 제어 밖에서 DOM에서 강제로 제거해요.
"Toggle with setState"를 몇 번 눌러보세요. 메시지가 사라졌다 다시 나타나야 해요. 그다음 "Remove from the DOM"을 누르세요. 이건 강제로 제거할 거예요. 마지막으로 "Toggle with setState"를 누르세요:
import { useState, useRef } from 'react';
export default function Counter() {
const [show, setShow] = useState(true);
const ref = useRef(null);
return (
<div>
<button
onClick={() => {
setShow(!show);
}}>
Toggle with setState
</button>
<button
onClick={() => {
ref.current.remove();
}}>
Remove from the DOM
</button>
{show && <p ref={ref}>Hello world</p>}
</div>
);
}
p,
button {
display: block;
margin: 10px;
}
DOM 요소를 수동으로 제거한 후에 setState를 사용해서 다시 보여주려고 하면 충돌이 발생해요. 이건 DOM을 변경했는데 React가 그걸 올바르게 계속 관리하는 방법을 모르기 때문이에요.
React가 관리하는 DOM 노드를 변경하지 마세요. React가 관리하는 요소를 수정하거나, 자식을 추가하거나, 자식을 제거하면 위처럼 일관성 없는 시각적 결과나 충돌이 발생할 수 있어요.
하지만 이게 전혀 할 수 없다는 뜻은 아니에요. 주의가 필요해요. React가 업데이트할 이유가 없는 DOM 부분은 안전하게 수정할 수 있어요. 예를 들어 어떤 <div>가 JSX에서 항상 비어 있다면, React는 그 자식 목록을 건드릴 이유가 없어요. 따라서 거기에 수동으로 요소를 추가하거나 제거하는 건 안전해요.
<div ref={myRef}>를 전달해서 React에게 DOM 노드를 myRef.current에 넣으라고 지시해요.ref prop을 사용해서 DOM 노드 노출을 선택할 수 있어요.이 예제에서 버튼은 재생과 일시정지 상태를 전환하기 위해 state 변수를 토글해요. 하지만 실제로 비디오를 재생하거나 일시정지하려면 state 토글만으로는 충분하지 않아요. <video>의 DOM 요소에서 play()와 pause()를 호출해야 해요. ref를 추가하고 버튼이 작동하게 만드세요.
import { useState, useRef } from 'react';
export default function VideoPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
function handleClick() {
const nextIsPlaying = !isPlaying;
setIsPlaying(nextIsPlaying);
}
return (
<>
<button onClick={handleClick}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<video width="250">
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
type="video/mp4"
/>
</video>
</>
)
}
button { display: block; margin-bottom: 20px; }
추가 도전으로, 사용자가 비디오를 마우스 오른쪽 클릭하고 내장 브라우저 미디어 컨트롤을 사용해서 재생해도 "Play" 버튼이 비디오 재생 여부와 동기화되도록 유지하세요. 그러려면 비디오에서 onPlay와 onPause를 수신해야 할 수 있어요.
ref를 선언하고 <video> 요소에 넣으세요. 그다음 이벤트 핸들러에서 다음 state에 따라 ref.current.play()와 ref.current.pause()를 호출하세요.
import { useState, useRef } from 'react';
export default function VideoPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
const ref = useRef(null);
function handleClick() {
const nextIsPlaying = !isPlaying;
setIsPlaying(nextIsPlaying);
if (nextIsPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}
return (
<>
<button onClick={handleClick}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<video
width="250"
ref={ref}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
>
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
type="video/mp4"
/>
</video>
</>
)
}
button { display: block; margin-bottom: 20px; }
내장 브라우저 컨트롤을 처리하려면 <video> 요소에 onPlay와 onPause 핸들러를 추가하고 거기서 setIsPlaying을 호출하면 돼요. 이렇게 하면 사용자가 브라우저 컨트롤을 사용해서 비디오를 재생해도 state가 그에 맞게 조정돼요.
"Search" 버튼을 클릭하면 필드에 포커스가 들어가도록 만드세요.
export default function Page() {
return (
<>
<nav>
<button>Search</button>
</nav>
<input
placeholder="Looking for something?"
/>
</>
);
}
button { display: block; margin-bottom: 10px; }
input에 ref를 추가하고 DOM 노드에서 focus()를 호출해서 포커스를 주세요:
import { useRef } from 'react';
export default function Page() {
const inputRef = useRef(null);
return (
<>
<nav>
<button onClick={() => {
inputRef.current.focus();
}}>
Search
</button>
</nav>
<input
ref={inputRef}
placeholder="Looking for something?"
/>
</>
);
}
button { display: block; margin-bottom: 10px; }
이 이미지 캐러셀에는 활성 이미지를 전환하는 "Next" 버튼이 있어요. 클릭하면 갤러리가 활성 이미지로 가로 스크롤되도록 만드세요. 활성 이미지의 DOM 노드에서 scrollIntoView()를 호출하면 돼요:
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
이 연습에서는 모든 이미지에 ref가 필요하지 않아요. 현재 활성 이미지나 목록 자체에 대한 ref만 있으면 충분해요. 스크롤하기 전에 DOM이 업데이트되도록 flushSync를 사용하세요.
import { useState } from 'react';
export default function CatFriends() {
const [index, setIndex] = useState(0);
return (
<>
<nav>
<button onClick={() => {
if (index < catList.length - 1) {
setIndex(index + 1);
} else {
setIndex(0);
}
}}>
Next
</button>
</nav>
<div>
<ul>
{catList.map((cat, i) => (
<li key={cat.id}>
<img
className={
index === i ?
'active' :
''
}
src={cat.imageUrl}
alt={'Cat #' + cat.id}
/>
</li>
))}
</ul>
</div>
</>
);
}
const catCount = 10;
const catList = new Array(catCount);
for (let i = 0; i < catCount; i++) {
const bucket = Math.floor(Math.random() * catCount) % 2;
let imageUrl = '';
switch (bucket) {
case 0: {
imageUrl = "https://placecats.com/neo/250/200";
break;
}
case 1: {
imageUrl = "https://placecats.com/millie/250/200";
break;
}
case 2:
default: {
imageUrl = "https://placecats.com/bella/250/200";
break;
}
}
catList[i] = {
id: i,
imageUrl,
};
}
div {
width: 100%;
overflow: hidden;
}
nav {
text-align: center;
}
button {
margin: .25rem;
}
ul,
li {
list-style: none;
white-space: nowrap;
}
li {
display: inline;
padding: 0.5rem;
}
img {
padding: 10px;
margin: -10px;
transition: background 0.2s linear;
}
.active {
background: rgba(0, 100, 150, 0.4);
}
selectedRef를 선언하고 현재 이미지에만 조건부로 전달할 수 있어요:
<li ref={index === i ? selectedRef : null}>
index === i일 때, 즉 이미지가 선택된 것일 때 <li>가 selectedRef를 받아요. React는 selectedRef.current가 항상 올바른 DOM 노드를 가리키도록 해요.
flushSync 호출은 스크롤 전에 React가 DOM을 업데이트하도록 강제하는 데 필요해요. 그렇지 않으면 selectedRef.current가 항상 이전에 선택된 항목을 가리킬 거예요.
import { useRef, useState } from 'react';
import { flushSync } from 'react-dom';
export default function CatFriends() {
const selectedRef = useRef(null);
const [index, setIndex] = useState(0);
return (
<>
<nav>
<button onClick={() => {
flushSync(() => {
if (index < catList.length - 1) {
setIndex(index + 1);
} else {
setIndex(0);
}
});
selectedRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}}>
Next
</button>
</nav>
<div>
<ul>
{catList.map((cat, i) => (
<li
key={cat.id}
ref={index === i ?
selectedRef :
null
}
>
<img
className={
index === i ?
'active'
: ''
}
src={cat.imageUrl}
alt={'Cat #' + cat.id}
/>
</li>
))}
</ul>
</div>
</>
);
}
const catCount = 10;
const catList = new Array(catCount);
for (let i = 0; i < catCount; i++) {
const bucket = Math.floor(Math.random() * catCount) % 2;
let imageUrl = '';
switch (bucket) {
case 0: {
imageUrl = "https://placecats.com/neo/250/200";
break;
}
case 1: {
imageUrl = "https://placecats.com/millie/250/200";
break;
}
case 2:
default: {
imageUrl = "https://placecats.com/bella/250/200";
break;
}
}
catList[i] = {
id: i,
imageUrl,
};
}
div {
width: 100%;
overflow: hidden;
}
nav {
text-align: center;
}
button {
margin: .25rem;
}
ul,
li {
list-style: none;
white-space: nowrap;
}
li {
display: inline;
padding: 0.5rem;
}
img {
padding: 10px;
margin: -10px;
transition: background 0.2s linear;
}
.active {
background: rgba(0, 100, 150, 0.4);
}
"Search" 버튼을 클릭하면 필드에 포커스가 들어가도록 만드세요. 각 컴포넌트는 별도의 파일에 정의되어 있고 거기서 옮기면 안 돼요. 어떻게 연결할 수 있을까요?
SearchInput 같은 자체 컴포넌트에서 DOM 노드를 노출하려면 ref를 prop으로 전달해야 해요.
// src/App.js
import SearchButton from './SearchButton.js';
import SearchInput from './SearchInput.js';
export default function Page() {
return (
<>
<nav>
<SearchButton />
</nav>
<SearchInput />
</>
);
}
// src/SearchButton.js
export default function SearchButton() {
return (
<button>
Search
</button>
);
}
// src/SearchInput.js
export default function SearchInput() {
return (
<input
placeholder="Looking for something?"
/>
);
}
button { display: block; margin-bottom: 10px; }
SearchButton에 onClick prop을 추가하고 SearchButton이 그걸 브라우저 <button>에 전달하도록 해요. 또한 <SearchInput>에 ref를 전달하고, 그게 실제 <input>에 전달해서 채워지도록 해요. 마지막으로 클릭 핸들러에서 그 ref 안에 저장된 DOM 노드에서 focus를 호출해요.
// src/App.js
import { useRef } from 'react';
import SearchButton from './SearchButton.js';
import SearchInput from './SearchInput.js';
export default function Page() {
const inputRef = useRef(null);
return (
<>
<nav>
<SearchButton onClick={() => {
inputRef.current.focus();
}} />
</nav>
<SearchInput ref={inputRef} />
</>
);
}
// src/SearchButton.js
export default function SearchButton({ onClick }) {
return (
<button onClick={onClick}>
Search
</button>
);
}
// src/SearchInput.js
export default function SearchInput({ ref }) {
return (
<input
ref={ref}
placeholder="Looking for something?"
/>
);
}
button { display: block; margin-bottom: 10px; }