React는 UI를 조작하는 선언적인 방법을 제공합니다. UI의 개별 부분을 직접 조작하는 대신 컴포넌트가 가질 수 있는 다양한 상태를 설정하고 사용자 입력에 응답하여 상태 간에 전환합니다. 이는 디자이너가 UI에 대해 생각하는 방식과 유사합니다.
UI 상호 작용을 디자인할 때, 사용자 액션에 따라 UI가 어떻게 변경되는지 생각해 볼 수 있습니다. 사용자가 답변을 제출할 수 있는 form을 고려해보세요.
명령형 프로그래밍에서 위의 내용은 상호 작용을 구현하는 방법과 직접적으로 일치합니다. 방금 일어난 일에 따라 UI를 조작하려면 정확한 지침을 작성해야 합니다. 이에 대해 생각하는 또 다른 방법은 다음과 같습니다. 차 안에 누군가 옆에 타서 어디로 가야 하는지 차례대로 말해 준다고 상상해 보세요.
그들은 당신이 어디로 가고 싶은지 모르고 단지 당신의 명령을 따를 뿐입니다. (그리고 방향이 틀리면 결국 잘못된 위치에 있게 됩니다!) 스피너에서 버튼까지 각 요소에 "command(명령)"을 내려 컴퓨터에 UI 업데이트 방법을 알려야 하기 때문에 필수라고 합니다.
아래 명령형 UI 프로그래밍 예에서는 form이 React 없이 구축되었습니다. 브라우저 DOM만 사용합니다.
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;
UI를 명령적으로 조작하는 것은 격리된 예에서는 충분히 잘 작동하지만 더 복잡한 시스템에서는 관리하기가 기하급수적으로 더 어려워집니다. 이와 같은 다양한 form으로 가득 찬 페이지를 업데이트한다고 상상해 보십시오. 새로운 UI 요소나 새로운 상호 작용을 추가하려면 모든 기존 코드를 주의 깊게 확인하여 버그가 발생하지 않았는지 확인해야 합니다(예: 표시하거나 숨기는 것을 잊어버린 경우).
React는 이 문제를 해결하기 위해 만들어졌습니다.
React에서는 UI를 직접 조작하지 않습니다. 즉, 컴포넌트를 직접 활성화, 비활성화, 표시 또는 숨기지 않습니다. 대신, 보여주고 싶은 것을 선언하면 React는 UI를 업데이트하는 방법을 알아냅니다. 택시를 타고 운전사에게 방향을 정확히 알려주는 대신 가고 싶은 곳을 알려주는 것을 생각해 보세요. 당신을 거기까지 데려다주는 것이 운전기사의 임무이며, 심지어 당신이 고려하지 않은 몇 가지 지름길을 알고 있을 수도 있습니다!
위에서 명령적으로 form을 구현하는 방법을 살펴보았습니다. React에서 생각하는 방법을 더 잘 이해하기 위해 아래에서 React에서 이 UI를 다시 구현하는 과정을 살펴보겠습니다.
컴퓨터 과학에서는 여러 "states" 중 하나에 있는 "state machine"에 대해 들을 수 있습니다. 디자이너와 함께 작업한다면 다양한 "visual states"에 대한 모형을 본 적이 있을 것입니다. React는 디자인과 컴퓨터 과학의 교차점에 있기 때문에 이 두 가지 아이디어 모두 영감의 원천입니다.
먼저, 사용자가 볼 수 있는 UI의 다양한 "상태"를 모두 시각화해야 합니다.
디자이너와 마찬가지로 논리를 추가하기 전에 다양한 상태에 대해 "mock up"하거나 "mocks"를 만들고 싶을 것입니다. 예를 들어, 다음은 form의 시각적 부분에 대한 mock입니다. 이 mock는 기본값이 'empty'
인 status
라는 prop에 의해 제어됩니다.
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'으로 편집해 보세요. Mocking를 사용하면 로직을 연결하기 전에 UI를 빠르게 반복할 수 있습니다. 다음은 status prop에 의해 여전히 "controlled"되는 동일한 컴포넌트의 보다 구체화된 프로토타입입니다.
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>
</>
);
}
구성 요소에 시각적 상태가 많은 경우 모든 상태를 한 페이지에 표시하는 것이 편리할 수 있습니다.
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>
))}
</>
);
}
이와 같은 페이지는 종종 “living styleguides” 또는 “storybooks”이라고 불립니다. 스토리북이 여기서 나오는군...
두 종류의 입력에 대한 응답으로 상태 업데이트를 트리거할 수 있습니다.
두 경우 모두 UI를 업데이트하려면 상태 변수를 설정해야 합니다. 개발 중인 form의 경우 몇 가지 다른 입력에 응답하여 상태를 변경해야 합니다.
Note. 인간의 입력에는 이벤트 핸들러가 필요한 경우가 많습니다.
이 흐름을 시각화하는 데 도움이 되도록 종이에 각 상태를 레이블이 지정된 원으로 그리고 두 상태 간의 각 변경 사항을 화살표로 그려보세요. 이런 방식으로 많은 흐름을 스케치하고 구현하기 오래 전에 버그를 분류할 수 있습니다. (오.. 흥미롭네요)
다음으로 useState
를 사용하여 컴포넌트의 시각적 상태를 메모리에 표시해야 합니다. 단순함이 핵심입니다. 상태의 각 조각은 "moving piece"이며 가능한 한 적은 "moving pieces"을 원합니다. 복잡할수록 버그도 많아집니다!
반드시 존재해야 하는 상태부터 시작하세요. 예를 들어 입력에 대한 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개만 해당됩니다. 'impossible' 상태를 제거하려면 'typing'
, 'submitting'
또는 'success'
이라는 세 가지 값 중 하나여야 하는 상태로 이를 결합할 수 있습니다.isEmpty
와 isTyping
은 동시에 참일 수 없습니다. 별도의 상태 변수로 만들면 동기화되지 않고 버그가 발생할 위험이 있습니다. 다행히 isEmpty
를 제거하고 대신 Answer.length === 0
을 확인할 수 있습니다.error !== null
을 확인할 수 있으므로 isError
는 필요하지 않습니다.이 정리 후에는 필수 상태 변수가 3개(7개에서 줄음!) 남습니다.
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'
이 세 가지 변수는 이 form의 상태를 충분히 잘 표현합니다. 그러나 여전히 완전히 이해되지 않는 일부 중간 상태가 있습니다. 예를 들어 상태가 'success'인 경우 null이 아닌 error는 의미가 없습니다. 상태를 보다 정확하게 모델링하려면 이를 reducer로 추출하면 됩니다. reducer를 사용하면 여러 상태 변수를 단일 객체로 통합하고 관련된 모든 로직을 통합할 수 있습니다!
마지막으로 상태를 업데이트하는 이벤트 핸들러를 만듭니다. 다음은 모든 이벤트 핸들러가 연결된 최종 form입니다.
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);
});
}
이 코드는 원래 명령형 예제보다 길지만 훨씬 덜 취약합니다. 모든 상호 작용을 상태 변경으로 표현하면 나중에 기존 상태를 손상시키지 않고 새로운 시각적 상태를 도입할 수 있습니다. 또한 상호 작용 자체의 논리를 변경하지 않고도 각 상태에 표시되어야 하는 내용을 변경할 수 있습니다.
리액트가 지향하는 방향이 무엇인지 확실히 알 수 있었던 시간인 거 같네요. 일일히 조작해야 하는 명령형 프로그래밍과는 다르게 리액트는 UI를 직접 조작하지 않습니다. 저희는 상태 변수를 관리하고 조작하면 됩니다. 나머지는 리액트가 알아서 해줍니다.
리액트를 왜 사용해야 되는지 잘 모르는 분들에게 리액트를 왜 사용하면 좋은지 설명하려면 이 글을 읽고 설명하면 되겠네요.