React는 UI를 조작하는 선언적인 방법을 제공합니다. UI의 개별 부분을 직접 조작하는 대신 컴포넌트가 가질 수 있는 다양한 상태를 설명하고 사용자 입력에 응답하여 상태 간에 전환합니다. 이는 디자이너가 UI에 대해 생각하는 방식과 유사합니다.
UI 상호 작용을 디자인할 때 사용자 작업에 따라 UI가 어떻게 변경되는지 생각해 볼 수 있습니다. 사용자가 답변을 제출할 수 있는 양식을 고려할 수 있습니다.
- 양식에 내용을 입력하면 “제출”버튼이 활성화됩니다.
- “제출”을 누르면 양식과 버튼이 모두 비활성화되고 스피너가 나타납니다.
- 네트워크 요청이 성공하면 양식이 숨겨지고 메시지가 나타납니다.
- 네트워크 요청이 실패하면 오류 메시지가 나타나고 양식이 다시 활성화됩니다.

그들은 당신이 어디로 가고 싶은지 모르고 단지 당신의 명령을 따를 뿐입니다. (그리고 방향이 틀리면 결국 잘못된 위치에 있게 됩니다) 스피너에서 버튼까지 각 요소에 “명령”을 내려 컴퓨터에 UI 업데이트 방법을 알려주어야 하기 때문에 필수라고 합니다.
이 명령형 UI 프로그래밍 예에서는 양식이 React 없이 구축되었습니다. 브라우저 DOM 만 사용합니다.
// index.js
async function handleFormSubmit(e) {
e.preventDefault();
disable(textarea);
disable(button);
show(loadingMessage);
hide(errorMessage);
try {
await submitForm(textarea.value);
show(successMessage);
hide(form);
} catch (err) {
show(errorMessage);
errorMessage.textContent = err.message;
} finally {
hide(loadingMessage);
enable(textarea);
enable(button);
}
}
function handleTextareaChange() {
if (textarea.value.length === 0) {
disable(button);
} else {
enable(button);
}
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
function enable(el) {
el.disabled = false;
}
function disable(el) {
el.disabled = true;
}
function submitForm(answer) {
// Pretend it's hitting the network.
return new Promise((resolve, reject) => {
setTimeout(() => {
if (answer.toLowerCase() === 'istanbul') {
resolve();
} else {
reject(new Error('Good guess but a wrong answer. Try again!'));
}
}, 1500);
});
}
let form = document.getElementById('form');
let textarea = document.getElementById('textarea');
let button = document.getElementById('button');
let loadingMessage = document.getElementById('loading');
let errorMessage = document.getElementById('error');
let successMessage = document.getElementById('success');
form.onsubmit = handleFormSubmit;
textarea.oninput = handleTextareaChange;
// index.html
<form id="form">
<h2>City quiz</h2>
<p>
What city is located on two continents?
</p>
<textarea id="textarea"></textarea>
<br />
<button id="button" disabled>Submit</button>
<p id="loading" style="display: none">Loading...</p>
<p id="error" style="display: none; color: red;"></p>
</form>
<h1 id="success" style="display: none">That's right!</h1>
<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
</style>
UI를 명령적으로 조작하는 것은 격리된 예에서는 충분히 잘 작동하지만 더 복잡한 시스템에서는 관리하기가 기하급수적으로 더 어려워집니다. 이와 같은 다양한 양식으로 가득 찬 페이지를 업데이트한다고 상상상해 보십시오. 새 UI 요소나 새 상호 작용을 추가하려면 모든 기존 코드를 주의 깊게 확인하여 버그가 발생하지 않았는지 확인해야 합니다.
React는 이러한 문제를 해결하기 위해 만들어졌습니다.
React에서는 UI를 직접 조작하지 않습니다. 즉, 컴포넌트를 직접 활성화, 비활성화, 표시 또는 숨기지 않습니다. 대신, 보여주고 싶은 것을 선언하면 React는 UI를 업데이트하는 방법을 알아냅니다.
위에서 명령적으로 양식을 구현하는 방법을 살펴보았습니다. React에서 생각하는 방법을 더 잘 이해하기 위해 아래에서 React가 이 UI를 다시 구현하는 과정을 살펴보겠습니다.
컴퓨터 과학에서는 여러 “상태” 중 하나에 있는 “state machine” 에 대해 들을 수 있습니다. 디자이너와 함께 작업한다면 다양한 “시각적 상태”에 대한 모형을 본 적이 있을 것입니다. React는 디자인과 컴퓨터 과학의 교차점에 있기 때문에 이 두 가지 아이디어 모두 영감의 원천입니다.
먼저, 사용자가 볼 수 있는 UI의 다양한 “상태”를 모두 시각화해야 합니다.
- 비어 있음 : 양식에 “제출” 버튼이 비활성화되어 있습니다.
- 입력 : 양식에 “제출” 버튼이 활성화되어 있습니다.
- 제출 중 : 양식이 완전히 비활성화되었습니다. 스피너가 표시됩니다.
- 성공 : 양식 대신 “감사합니다” 메시지가 표시됩니다.
- 오류 : 입력 상태와 동일하지만 추가 오류 메시지가 있습니다.
디자이너와 마찬가지로 논리를 추가하기 전에 다양한 상태에 대한 “모의”를 만들거나 “모의”를 만들고 싶을 것입니다. 예를 들어, 다음은 양식의 시각적 부분에 대한 모의입니다. 이 모의는 empty 라는 기본 값을 가진 status 라는 props에 의해 제어됩니다.
export default function Form({
status = 'empty'
}) {
if (status === 'success') {
return <h1>That's right!</h1>
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form>
<textarea />
<br />
<button>
Submit
</button>
</form>
</>
)
}
prop의 이름은 중요하지 않습니다. 성공 메시지가 나타나는지 보려면 status = 'empty' 를 status = 'success' 로 편집해 보세요. 모의를 사용하면 로직을 연결하기 전에 UI를 빠르게 반복할 수 있습니다. 다음은 여전히 status prop에 의해 제어되는 동일한 컴포넌트의 좀 더 구체화된 프로토타입니다.
export default function Form({
// Try 'submitting', 'error', 'success':
status = 'empty'
}) {
if (status === 'success') {
return <h1>That's right!</h1>
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form>
<textarea disabled={
status === 'submitting'
} />
<br />
<button disabled={
status === 'empty' ||
status === 'submitting'
}>
Submit
</button>
{status === 'error' &&
<p className="Error">
Good guess but a wrong answer. Try again!
</p>
}
</form>
</>
);
}
컴포넌트에 시각적 상태가 많은 경우 모든 상태를 한 페이지에 표시하는 것이 편리할 수 있습니다.
// App.js
import Form from './Form.js';
let statuses = [
'empty',
'typing',
'submitting',
'success',
'error',
];
export default function App() {
return (
<>
{statuses.map(status => (
<section key={status}>
<h4>Form ({status}):</h4>
<Form status={status} />
</section>
))}
</>
);
}
// Form.js
export default function Form({ status }) {
if (status === 'success') {
return <h1>That's right!</h1>
}
return (
<form>
<textarea disabled={
status === 'submitting'
} />
<br />
<button disabled={
status === 'empty' ||
status === 'submitting'
}>
Submit
</button>
{status === 'error' &&
<p className="Error">
Good guess but a wrong answer. Try again!
</p>
}
</form>
);
}
이와 같은 페이지를 종종 “living guidebook” 혹은 “storybooks”라고 부릅니다.
두 종류의 입력에 대한 응답으로 상태 업데이트를 트리거할 수 잇습니다.
- 버튼 클릭, 필드 입력, 링크 탐색 등의 인간 입력
- 네트워크 응답 도착, 시간 초과 완료, 이미지 로드와 같은 컴퓨터 입력
두 경우 모두 UI를 업데이트하려면 상태 변수를 설정해야 합니다. 개발 중인 양식의 경우 몇 가지 다른 입력에 대한 응답으로 상태를 변경해야 합니다.
- 텍스트 입력(사람)을 변경하면 텍스트 상자가 비어 있는지 여부에 따라 빈 상태에서 입력 중 상태로 또는 다시 전환되어야 합니다.
- 제출 버튼(사람)을 클릭하면 제출 중 상태로 전환되어야 합니다.
- 성공적인 네트워크 응답(컴퓨터)은 성공 상태로 전환되어야 합니다.
- 실패한 네트워크 응답(컴퓨터)은 일차하는 오류 메시지와 함께 오류 상태로 전환되어야 합니다.
이 흐름을 시각화하는 데 도움이 되도록 종이에 각 상태를 레이블이 지정된 원으로 그리고 두 상태 간의 각 변경 사항을 화살표로 그려보세요. 이런 방식으로 많은 흐름을 스케치하고 구현하기 오래 전에 버그로 분류할 수 있습니다.

useState다음으로 useState 를 사용하여 메모리에 컴포넌트의 시각적 상태를 표시해야 합니다. 단순함이 핵심입니다. 상태의 각 부분은 “움직이는 부분”이며 가능한 한 적은 수의 “움직이는 부분”을 원합니다. 복잡할수록 버그도 많아집니다.
반드시 존재해야 하는 상태부터 시작하세요. 예를 들어, answer 에 대한 입력을 저정하고 마지막 오류를 저장하려면 error 를 저장해야 합니다.
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
그런 다음 표시하려는 시각적 상태 중 하나를 나타내는 상태 변수가 필요합니다. 일반적으로 이를 메모리에 표현하는 방법은 여러 가지가 있으므로 실험해 봐야 합니다.
최선을 방법을 즉시 생각하는 데 어려움을 겪고 있다면 가능한 모든 시각적 상태가 포함된다고 확실히 확신할 수 있을 만큼 충분한 사태를 추가하는 것부터 시작하세요.
const [isEmpty, setIsEmpty] = useState(true);
const [isTyping, setIsTyping] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [isError, setIsError] = useState(false);
상태 콘텐츠의 중복을 방지하여 필수적인 내용만 추적하려고 합니다. 상태 구조를 리팩토링하는데 시간을 투자하면 컴포넌트를 더 쉽게 이해하고 중복을 줄이며 의도하지 않은 의미를 피할 수 있습니다. 목표는 메모리의 상태가 사용자에게 표시하려는 유효한 UI를 나타내지 않는 경우를 방지하는 것입니다.
다음은 상태 변수에 대하여 물어볼 수 있는 몇 가지 질문입니다.
- 이 상태가 역설을 일으키는가? 예를 들어
isTyping및isSubmitting은 둘 다true일 수는 없습니다. 역설은 일반적으로 상태가 충분히 제한되지 않음을 의미합니다. 두 개의 부울 값에는 네가지 가능한 조합이 있지만 유효한 상태는 3개만 해당됩니다. “불가능” 상태를 제거하려면 이러한 상태를'typing','submitting','success'세 가지 값 중 하나여아 하는 status로 결합할 수 있습니다.- 이미 다른 상태 변수에서 동일한 정보를 사용할 수 있습니까? 또 다른 역설 :
isEmpty와isTyping은 동시에true일 수 없습니다. 별도의 상태 변수로 만들면 동기화 되지 않고 버그가 발생할 위험이 있습니다. 다행히isEmpty을 제거하고answer.length === 0로 대신 확인할 수 있습니다.- 다른 상태 변수의 역으로 동일한 정보를 얻을 수 있습니까?
isError대신error !== null로 확인할 수 있습니다.
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
이 세가지 변수는 이 양식의 상태를 충분히 잘 표현합니다. 그러나 여전히 완전히 이해되지 않는 일부 중간 상태가 있습니다. 예를 들어, non-null error는 status 가 'success' 일 때 존재하지 않습니다. 상태를 보다 정확하기 모델링하려면 이를 리듀서로 추출하면 됩니다. 리듀서를 사용하면 여러 상태 변수를 단일 객체로 통합하고 관련된 모든 로직을 통합할 수 있습니다.
마지막으로 상태를 업데이트하는 이벤트 핸들러를 만듭니다. 다음은 모든 이벤트 핸들러가 연결된 최종 양식입니다.
import { useState } from 'react';
export default function Form() {
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing');
if (status === 'success') {
return <h1>That's right!</h1>
}
async function handleSubmit(e) {
e.preventDefault();
setStatus('submitting');
try {
await submitForm(answer);
setStatus('success');
} catch (err) {
setStatus('typing');
setError(err);
}
}
function handleTextareaChange(e) {
setAnswer(e.target.value);
}
return (
<>
<h2>City quiz</h2>
<p>
In which city is there a billboard that turns air into drinkable water?
</p>
<form onSubmit={handleSubmit}>
<textarea
value={answer}
onChange={handleTextareaChange}
disabled={status === 'submitting'}
/>
<br />
<button disabled={
answer.length === 0 ||
status === 'submitting'
}>
Submit
</button>
{error !== null &&
<p className="Error">
{error.message}
</p>
}
</form>
</>
);
}
function submitForm(answer) {
// Pretend it's hitting the network.
return new Promise((resolve, reject) => {
setTimeout(() => {
let shouldError = answer.toLowerCase() !== 'lima'
if (shouldError) {
reject(new Error('Good guess but a wrong answer. Try again!'));
} else {
resolve();
}
}, 1500);
});
}
이 코드는 원래 명령형 예제보다 길지만 훨씬 덜 취약합니다. 모든 상호 작용을 상태 변경을 표현하면 나중에 기존 상태를 손상시키지 않고 새로운 시각적 상태를 도입할 수 있습니다. 또한, 상호 작용 자체의 논리를 변경하지 않고도 각 상태에 표시되어야 하는 내용을 변경할 수 있습니다.