상호작용 추가하기
화면의 일부 요소는 사용자의 입력에 따라 업데이트됩니다. 예를 들어 이미지 갤러리에서 특정 이미지를 클릭하면 해당 이미지가 활성 상태가 됩니다. React에서는 시간에 따라 변화하는 데이터를 state라고 합니다. state는 어떠한 컴포넌트에든 추가할 수 있으며 필요에 따라 업데이트할 수도 있습니다. 이번 장에서는 상호작용을 다루는 컴포넌트를 작성하고 state를 업데이트하며, 시간에 따라 화면을 갱신하는 방법에 대해서 알아보겠습니다.
이 장에서는
이벤트에 대한 응답
React에서는 JSX에 이벤트 핸들러를 추가할 수 있습니다. 이벤트 핸들러는 클릭, 마우스 호버, 폼 인풋 포커스 등 사용자 상호작용에 따라 유발되는 사용자 정의 함수입니다.
<button>
과 같은 내장 컴포넌트는 onClick
과 같은 내장 브라우저 이벤트만 지원합니다. 반면 사용자 정의 컴포넌트를 생성하는 경우, 컴포넌트 이벤트 핸들러 props의 역할에 맞는 원하는 이름을 사용할 수 있습니다.
export default function App() { return ( <Toolbar onPlayMovie={() => alert('Playing!')} onUploadImage={() => alert('Uploading!')} /> ); } function Toolbar({ onPlayMovie, onUploadImage }) { return ( <div> <Button onClick={onPlayMovie}> Play Movie </Button> <Button onClick={onUploadImage}> Upload Image </Button> </div> ); } function Button({ onClick, children }) { return ( <button onClick={onClick}> {children} </button> ); }
State: 컴포넌트의 메모리
상호작용에 따라 컴포넌트는 종종 화면에 표시되는 내용을 변경해야 합니다. 폼에 타이핑하면 입력 필드를 업데이트해야 하고, 이미지 캐러셀에서 “다음”을 클릭하면 표시되는 이미지가 변경되어야 하며, “구매”를 클릭하면 제품이 장바구니에 추가되어야 합니다. 컴포넌트는 현재 입력값, 현재 이미지, 장바구니에 담긴 상품 등을 “기억”해야 합니다. React에서는 이러한 컴포넌트별 메모리를 state라고 부릅니다.
useState
Hook을 사용하면 컴포넌트에 state를 추가할 수 있습니다. Hooks는 컴포넌트가 React의 주요 기능(state는 그중 하나입니다.)을 사용할 수 있도록 해주는 특별한 함수입니다. useState
Hook을 사용하면 state 변수를 선언할 수 있습니다. useState
는 초기 state를 인자로 받으며, 현재 상태와 상태를 업데이트할 수 있는 상태 설정 함수를 배열에 담아 반환합니다.
const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);
다음은 이미지 갤러리가 클릭 이벤트에 따라 state를 사용하고 업데이트하는 방법입니다.
import { useState } from 'react'; import { sculptureList } from './data.js'; export default function Gallery() { const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); const hasNext = index < sculptureList.length - 1; function handleNextClick() { if (hasNext) { setIndex(index + 1); } else { setIndex(0); } } function handleMoreClick() { setShowMore(!showMore); } let sculpture = sculptureList[index]; return ( <> <button onClick={handleNextClick}> Next </button> <h2> <i>{sculpture.name} </i> by {sculpture.artist} </h2> <h3> ({index + 1} of {sculptureList.length}) </h3> <button onClick={handleMoreClick}> {showMore ? 'Hide' : 'Show'} details </button> {showMore && <p>{sculpture.description}</p>} <img src={sculpture.url} alt={sculpture.alt} /> </> ); }
렌더링과 반영
컴포넌트는 화면에 표시되기 전에 React에 의해 렌더링 되어야 합니다. 이 과정을 이해하면 코드가 어떻게 실행되는지 파악하고 코드의 동작을 설명하는 데 도움이 될 것입니다.
컴포넌트가 주방에서 재료들을 사용해 맛있는 요리를 조리하는 요리사라고 생각해 봅시다. 이 시나리오에서 React는 손님들로부터 주문받고 요리사에게 주문을 가져다주는 웨이터입니다. 이때, UI를 주문하고 서빙하는 과정은 세 단계로 이루어집니다.
- 렌더링 유발 (주방에 식사 주문을 전달하기)
- 컴포넌트 렌더링 (주방에서 주문을 준비하기)
- DOM에 반영 (주문을 테이블에 서빙하기)
Illustrated by Rachel Lee Nabors
snapshot으로서의 state
일반적인 JavaScript 변수와 달리, React의 state는 snapshot과 유사하게 동작합니다. 상태를 갱신하면 이미 있는 state 변수 자체를 변경하는 것이 아니라, 리렌더링을 유발합니다. 이는 처음에는 놀라울 수 있습니다!
console.log(count); // 0
setCount(count + 1); // Request a re-render with 1
console.log(count); // Still 0!
이 동작은 미묘한 버그를 피하는 데 도움이 됩니다. 간단한 채팅 앱을 예시로 들겠습니다. “Send”를 먼저 누른 다음 수신자를 Bob으로 변경하면 어떻게 될지 추측해 보세요. 5초 후에 alert
에 어떤 이름이 나타날까요?
import { useState } from 'react'; export default function Form() { const [to, setTo] = useState('Alice'); const [message, setMessage] = useState('Hello'); function handleSubmit(e) { e.preventDefault(); setTimeout(() => { alert(`You said ${message} to ${to}`); }, 5000); } return ( <form onSubmit={handleSubmit}> <label> To:{' '} <select value={to} onChange={e => setTo(e.target.value)}> <option value="Alice">Alice</option> <option value="Bob">Bob</option> </select> </label> <textarea placeholder="Message" value={message} onChange={e => setMessage(e.target.value)} /> <button type="submit">Send</button> </form> ); }
이 주제를 배울 준비가 되셨나요?
이벤트 핸들러 내에서 state가 “고정되어” 변하지 않는 것처럼 보이는 이유에 대하여 배우려면 state가 변경된 후 바로 업데이트되지 않는 이유 를 읽어보세요.
더 보기state 업데이트를 연속으로 대기열에 추가하기
이 컴포넌트는 버그가 있습니다. “+3”을 클릭하면 점수가 한 번만 증가합니다.
import { useState } from 'react'; export default function Counter() { const [score, setScore] = useState(0); function increment() { setScore(score + 1); } return ( <> <button onClick={() => increment()}>+1</button> <button onClick={() => { increment(); increment(); increment(); }}>+3</button> <h1>Score: {score}</h1> </> ) }
state가 변경된 후 바로 업데이트되지 않는 이유는 이런 현상이 발생하는 이유를 설명해 줍니다. state를 설정하면 리렌더링이 유발되지만, 이미 실행 중인 코드에서는 변경되지 않습니다. 따라서 setScore(score + 1)
를 호출한 직후에도 score
는 여전히 0
으로 유지됩니다.
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
이 문제는 state를 설정할 때 updater function을 전달하는 방식을 통해 해결할 수 있습니다. setScore(score + 1)
를 setScore(s => s + 1)
로 대체함으로써 “+3” 버튼이 수정되는 것을 확인할 수 있습니다. 이러한 방법으로 여러 개의 state 업데이트를 대기열에 추가할 수 있습니다.
import { useState } from 'react'; export default function Counter() { const [score, setScore] = useState(0); function increment() { setScore(s => s + 1); } return ( <> <button onClick={() => increment()}>+1</button> <button onClick={() => { increment(); increment(); increment(); }}>+3</button> <h1>Score: {score}</h1> </> ) }
state 내 객체 업데이트
State는 객체를 포함하여 모든 종류의 JavaScript 타입을 관리할 수 있습니다. 그러나 React state에 있는 개체와 배열을 직접 변경해서는 안 됩니다. 대신 객체나 배열을 업데이트할 때는 새로운 객체를 생성하거나 기존 객체의 복사본을 만들어서 상태를 업데이트해야 합니다.
일반적으로 변경하려는 객체나 배열을 복사하기 위해 ...
전개 구문을 사용합니다. 예를 들어 중첩된 객체의 업데이트는 다음과 같이 처리할 수 있습니다.
import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { setPerson({ ...person, name: e.target.value }); } function handleTitleChange(e) { setPerson({ ...person, artwork: { ...person.artwork, title: e.target.value } }); } function handleCityChange(e) { setPerson({ ...person, artwork: { ...person.artwork, city: e.target.value } }); } function handleImageChange(e) { setPerson({ ...person, artwork: { ...person.artwork, image: e.target.value } }); } return ( <> <label> Name: <input value={person.name} onChange={handleNameChange} /> </label> <label> Title: <input value={person.artwork.title} onChange={handleTitleChange} /> </label> <label> City: <input value={person.artwork.city} onChange={handleCityChange} /> </label> <label> Image: <input value={person.artwork.image} onChange={handleImageChange} /> </label> <p> <i>{person.artwork.title}</i> {' by '} {person.name} <br /> (located in {person.artwork.city}) </p> <img src={person.artwork.image} alt={person.artwork.title} /> </> ); }
객체를 복사하는 작업이 번거롭다면 Immer와 같은 라이브러리를 사용하여 반복적인 코드를 줄일 수 있습니다.
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
state 내 배열 업데이트
배열 또한 state에 저장될 때 읽기 전용으로 다루어야 하는 가변 JavaScript 객체입니다. 객체와 마찬가지로 상태에 저장된 배열을 업데이트하려면 새로운 배열을 생성하거나 기존 배열의 복사본을 만들어서 상태를 업데이트한 후, 새로운 배열을 상태에 설정해야 합니다.
import { useState } from 'react'; const initialList = [ { id: 0, title: 'Big Bellies', seen: false }, { id: 1, title: 'Lunar Landscape', seen: false }, { id: 2, title: 'Terracotta Army', seen: true }, ]; export default function BucketList() { const [list, setList] = useState( initialList ); function handleToggle(artworkId, nextSeen) { setList(list.map(artwork => { if (artwork.id === artworkId) { return { ...artwork, seen: nextSeen }; } else { return artwork; } })); } return ( <> <h1>Art Bucket List</h1> <h2>My list of art to see:</h2> <ItemList artworks={list} onToggle={handleToggle} /> </> ); } function ItemList({ artworks, onToggle }) { return ( <ul> {artworks.map(artwork => ( <li key={artwork.id}> <label> <input type="checkbox" checked={artwork.seen} onChange={e => { onToggle( artwork.id, e.target.checked ); }} /> {artwork.title} </label> </li> ))} </ul> ); }
배열을 복사하는 작업이 번거롭다면 Immer와 같은 라이브러리를 사용하여 반복적인 코드를 줄일 수 있습니다.
{ "dependencies": { "immer": "1.7.3", "react": "latest", "react-dom": "latest", "react-scripts": "latest", "use-immer": "0.5.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "devDependencies": {} }
What’s next?
이 장을 페이지별로 읽으려면 사용자 이벤트를 처리하는 방법으로 이동하세요!
이미 이러한 주제에 익숙하시다면 State 다루기에 대해 읽어보시는 것도 좋습니다.