안녕하세요! 프론트엔드 개발자라는 멋진 목표를 향해 달려가시는 과정에 함께하게 되어 기쁩니다. React의 핵심 개념을 공부하고 계시네요! 지금 배우시는 '상태(State)를 통한 선언형 UI' 개념은 앞으로 만드실 여러 웹 애플리케이션이나, 나아가 Next.js 같은 프레임워크를 다루실 때 아주 든든한 뼈대가 되어줄 거예요.
공식 문서의 내용을 하나도 빠짐없이, 이해하기 쉬운 구어체로 번역해 보았습니다. 중간중간 이해를 돕기 위해 제가 [👩🏫 강사의 보충 설명]을 덧붙여 두었으니 천천히 읽어보세요. 화이팅입니다!
React는 UI를 조작하는 선언적인(declarative) 방법을 제공합니다. UI의 개별적인 조각들을 하나하나 직접 조작하는 대신, 여러분의 컴포넌트가 가질 수 있는 다양한 상태(states)들을 묘사해 두고, 사용자의 입력에 반응하여 그 상태들 사이를 전환하게 됩니다. 이 방식은 디자이너분들이 UI를 생각하고 구상하는 방식과 아주 비슷하답니다.
UI 인터랙션을 디자인할 때, 아마도 여러분은 사용자의 행동에 반응해서 UI가 어떻게 변하는지를 먼저 떠올리실 거예요. 사용자가 답변을 제출할 수 있는 폼(form)을 예로 들어볼게요:
명령형 프로그래밍(imperative programming)에서는, 방금 설명한 과정들이 여러분이 인터랙션을 구현하는 방식과 정확히 일치합니다. 방금 일어난 일에 따라 UI를 조작하기 위한 정확한 지시사항들을 일일이 작성해야만 하죠. 이렇게 생각해 볼 수 있어요. 자동차 조수석에 앉아서 운전자에게 언제 어디로 꺾어야 할지 턴바이턴으로 하나하나 지시하는 상황을 상상해 보세요.

[👩🏫 강사의 보충 설명]
운전자는 여러분이 최종적으로 어디로 가고 싶은지 몰라요. 그저 여러분의 명령을 그대로 따를 뿐이죠. (만약 여러분이 길 안내를 잘못한다면, 전혀 엉뚱한 곳에 도착하고 말 거예요!) 이 방식을 명령형(imperative)이라고 부르는 이유는 스피너부터 버튼까지 각각의 요소들에게 UI를 어떻게 업데이트해야 하는지 컴퓨터에게 하나하나 "명령"해야 하기 때문입니다. 즉, "A를 숨기고 B를 보여주고 C의 색깔을 바꿔라"라는 식으로 절차를 모두 지시하는 거죠.
이러한 명령형 UI 프로그래밍의 예시로, React를 사용하지 않고 순수하게 브라우저의 DOM만을 이용해서 폼을 만들어본 코드를 확인해 볼까요?
// src/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;
// sandbox.config.json
{
"hardReloadOnChange": true
}
<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를 직접 조작하지 않습니다. 즉, 컴포넌트를 직접 활성화하거나 비활성화하고, 보여주거나 숨기는 코드를 작성하지 않는다는 뜻이죠. 대신, 무엇을 보여주고 싶은지 선언(declare)하기만 하면 React가 알아서 UI를 어떻게 업데이트할지 결정합니다. 택시에 타서 기사님께 정확히 어디서 어떻게 꺾어야 할지 지시하는 대신, 그냥 목적지만 말하는 것과 같아요. 그곳까지 모셔다드리는 건 기사님의 역할이고, 어쩌면 기사님은 여러분이 몰랐던 더 빠른 지름길을 알고 계실지도 모르죠!

위에서 명령형으로 폼을 어떻게 구현하는지 보셨을 거예요. 이제 React답게 생각하는 방법을 더 잘 이해하기 위해, 아래에서는 이 UI를 React로 다시 구현하는 과정을 단계별로 살펴보겠습니다.
useState를 사용해 메모리 상에 상태를 표현(Represent)하기컴퓨터 과학에서는 여러 "상태(states)" 중 하나에 머무를 수 있는 "상태 기계(state machine)"라는 개념을 들어보셨을 수도 있어요. 만약 디자이너와 함께 일하신다면, 다양한 "시각적 상태"에 대한 목업(mockup) 디자인을 보신 적이 있을 테고요. React는 디자인과 컴퓨터 과학의 교차점에 서 있기 때문에, 이 두 가지 아이디어 모두 훌륭한 영감의 원천이 됩니다.
가장 먼저, 사용자가 보게 될 UI의 모든 다양한 "상태"들을 시각화해 볼 필요가 있습니다:
[👩🏫 강사의 보충 설명]
개발을 시작하기 전에 이렇게 상태를 미리 쭉 적어보는 습관은 아주 중요해요! 복잡한 컴포넌트를 만들 때 길을 잃지 않게 도와주는 훌륭한 나침반이 되거든요.
디자이너처럼, 여러분도 로직을 추가하기 전에 다양한 상태에 대한 "목업(mocks)"을 만들고 싶으실 거예요. 예를 들어, 여기 폼의 시각적인 부분만 구현한 목업이 있습니다. 이 목업은 기본값이 'empty'인 status라는 이름의 prop에 의해 제어되고 있어요.
// App.js
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에 의해 "제어"되지만, 조금 더 구체화된 똑같은 컴포넌트의 프로토타입입니다.
// App.js
export default function Form({
// '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>
</>
);
}
/* styles.css */
.Error { color: red; }
만약 컴포넌트가 아주 많은 시각적 상태를 가지고 있다면, 그것들을 한 페이지에 한꺼번에 보여주는 것도 좋은 방법입니다.
// src/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>
))}
</>
);
}
// src/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>
);
}
/* styles.css */
section { border-bottom: 1px solid #aaa; padding: 20px; }
h4 { color: #222; }
body { margin: 0; }
.Error { color: red; }
이런 페이지들은 흔히 "리빙 스타일가이드(living styleguides)"나 "스토리북(storybooks)"이라고 불린답니다.
여러분은 다음 두 가지 종류의 입력에 반응해서 상태 업데이트를 유발(trigger)할 수 있어요:

(사람의 입력)

(컴퓨터의 입력)
두 경우 모두, UI를 업데이트하려면 상태 변수(state variables)를 설정해야만 합니다. 지금 개발하고 있는 이 폼의 경우, 몇 가지 각기 다른 입력에 반응해서 상태를 변경해야 해요:
참고로, 사람의 입력은 보통 이벤트 핸들러(event handlers)를 필요로 한답니다!
이 흐름을 더 쉽게 시각화하려면, 종이에 각각의 상태를 동그라미로 그리고, 두 상태 사이의 변화를 화살표로 그려보는 걸 추천해요. 이 방식을 사용하면 코드를 작성하기 훨씬 전부터 다양한 흐름을 스케치해 보고 버그를 미리 잡아낼 수 있습니다.
[👩🏫 강사의 보충 설명]
머릿속으로만 생각하면 놓치는 케이스가 반드시 생기기 마련입니다. 특히 상태(State)가 3개, 4개로 늘어날수록 화살표를 그려보는 플로우차트 방식이 아주 큰 도움이 됩니다. 면접에서 라이브 코딩을 할 때도 바로 키보드부터 잡기보단 이런 흐름을 먼저 주석으로 적거나 그려보는 모습을 보여주시면 긍정적인 인상을 줄 수 있어요!
useState를 사용해 메모리 상에 상태 표현하기 {/step-3-represent-the-state-in-memory-with-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);
물론 처음 떠올린 이 아이디어가 최선이 아닐 확률이 높습니다. 하지만 괜찮아요! 상태를 리팩터링(refactoring)하는 것도 아주 자연스러운 개발 과정의 일부니까요.
상태 구조에서 중복을 피해서, 정말 필수적인 내용만 추적하도록 만들고 싶으실 거예요. 상태 구조를 리팩터링 하는 데 약간의 시간을 투자하면 컴포넌트를 이해하기 훨씬 쉬워지고, 중복을 줄일 수 있으며, 의도치 않은 논리적 오류를 피할 수 있습니다. 여기서 우리의 목표는 메모리 상의 상태가, 사용자가 보게 될 올바른 UI를 제대로 반영하지 못하는 '불가능한 경우'를 방지하는 것입니다. (예를 들어 에러 메시지를 보여주면서 동시에 입력창을 비활성화해서는 안 되겠죠. 그러면 사용자가 에러를 수정할 수 없으니까요!)
상태 변수들을 점검할 때 다음과 같은 질문들을 스스로에게 던져보세요:
isTyping과 isSubmitting은 절대로 둘 다 동시에 true가 될 수 없습니다. 이런 모순은 대개 상태가 충분히 제한적이지 않다는 것을 의미합니다. 2개의 boolean 변수를 조합하면 4가지 경우의 수가 나오지만, 유효한 상태는 3가지뿐이거든요. 이 "불가능한" 상태를 제거하기 위해, 이 변수들을 합쳐서 무조건 'typing', 'submitting', 'success' 셋 중 하나의 값만 가져야 하는 status라는 변수로 만들 수 있습니다.isEmpty와 isTyping 역시 동시에 true일 수 없습니다. 이 둘을 서로 다른 상태 변수로 놔두면 두 값이 일치하지 않게 되어 버그를 유발할 위험이 있습니다. 다행히도, isEmpty 변수를 지워버리고 대신 answer.length === 0인지 체크하는 방식으로 대체할 수 있습니다.isError 변수는 굳이 필요하지 않습니다. 대신 error !== null을 체크하면 되니까요.[👩🏫 강사의 보충 설명]
위에서 언급한 과정이 React 상태 관리의 핵심 중의 핵심입니다! 여러 개의 boolean(true/false) 값으로 상태를 쪼개놓으면, 개발자가 의도치 않게 서로 충돌하는 상태(예: 로딩 중인데 에러도 떠 있음)를 만들 위험이 커져요. 하나로 묶을 수 있는 건 묶고, 계산해서 얻을 수 있는 값은 파생 상태(derived state)로 빼는 연습을 꾸준히 해보세요.
이렇게 정리하고 나면 7개였던 상태 변수가 딱 3개의 필수적인 변수로 깔끔하게 줄어듭니다!
const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', 또는 'success'
이 세 개 중 하나라도 지우면 기능이 망가지기 때문에, 이들이 필수적이라는 것을 확신할 수 있습니다.
이 3개의 변수만으로도 폼의 상태를 나타내는 데는 꽤 훌륭합니다. 하지만, 여전히 완전히 논리적이지 않은 중간 상태들이 존재하긴 해요. 예를 들어 status가 'success'인데 error 값이 null이 아니라는 것은 말이 안 되죠. 상태를 더욱 정교하게 모델링하기 위해서, 상태 관리 로직을 reducer로 추출(extract it into a reducer)할 수 있습니다. Reducer를 사용하면 여러 상태 변수를 하나의 단일 객체로 통합하고, 관련된 모든 로직을 한 곳에 모을 수 있답니다!
마지막으로, 상태를 업데이트해 줄 이벤트 핸들러들을 만듭니다. 아래는 모든 이벤트 핸들러가 제대로 연결된 완성된 폼의 모습입니다.
// App.js
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);
});
}
/* styles.css */
.Error { color: red; }
이 코드가 원본의 명령형 예제 코드보다는 길이가 더 길지만, 구조는 훨씬 튼튼합니다(less fragile). 모든 인터랙션을 상태 변화로 표현하게 되면, 나중에 기존 상태를 망가뜨리지 않고도 새로운 시각적 상태를 쉽게 추가할 수 있어요. 또한, 인터랙션 로직 자체를 건드리지 않고도 각각의 상태에서 어떤 UI를 보여줄지 손쉽게 바꿀 수 있답니다.
useState를 사용해 메모리 상에 상태를 모델링하세요.사진을 클릭하면 바깥쪽 <div>에서 background--active CSS 클래스를 제거하고, 반대로 <img>에는 picture--active 클래스를 추가하도록 만들어보세요. 배경을 다시 클릭하면 원래의 CSS 클래스 상태로 되돌아와야 합니다.
시각적으로 설명하자면, 사진을 클릭하면 보라색 배경이 사라지고 사진 테두리가 강조되어야 해요. 사진 바깥의 배경을 클릭하면 배경이 다시 보라색으로 강조되고 사진 테두리 강조는 사라져야 합니다.
// App.js
export default function Picture() {
return (
<div className="background background--active">
<img
className="picture"
alt="Rainbow houses in Kampung Pelangi, Indonesia"
src="[https://i.imgur.com/5qwVYb1.jpeg](https://i.imgur.com/5qwVYb1.jpeg)"
/>
</div>
);
}
/* styles.css */
body { margin: 0; padding: 0; height: 250px; }
.background {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #eee;
}
.background--active {
background: #a6b5ff;
}
.picture {
width: 200px;
height: 200px;
border-radius: 10px;
border: 5px solid transparent;
}
.picture--active {
border: 5px solid #a6b5ff;
}
이 컴포넌트는 두 가지 시각적 상태를 가집니다. 이미지가 활성화되었을 때, 그리고 비활성화되었을 때입니다:
background 와 picture picture--active 입니다.background background--active 와 picture 입니다.이미지가 활성화되었는지를 기억하기 위해서는 단일 boolean 상태 변수 하나면 충분합니다. 원래의 목표는 CSS 클래스를 지우고 추가하는 것이었죠. 하지만 React에서는 UI 요소들을 직접 조작하는 대신, 무엇을 보고 싶은지 묘사(describe)해야 합니다. 따라서 현재 상태를 기반으로 두 요소의 CSS 클래스를 계산해서 넣어주어야 해요. 또한 이미지를 클릭했을 때 그 클릭 이벤트가 배경으로까지 전달(register)되지 않도록 이벤트 전파를 멈춰주어야(stop the propagation) 합니다.
이미지를 클릭해 보고, 그 밖의 공간을 클릭해 보며 이 버전이 제대로 작동하는지 확인해 보세요:
// App.js
import { useState } from 'react';
export default function Picture() {
const [isActive, setIsActive] = useState(false);
let backgroundClassName = 'background';
let pictureClassName = 'picture';
if (isActive) {
pictureClassName += ' picture--active';
} else {
backgroundClassName += ' background--active';
}
return (
<div
className={backgroundClassName}
onClick={() => setIsActive(false)}
>
<img
onClick={e => {
e.stopPropagation();
setIsActive(true);
}}
className={pictureClassName}
alt="Rainbow houses in Kampung Pelangi, Indonesia"
src="[https://i.imgur.com/5qwVYb1.jpeg](https://i.imgur.com/5qwVYb1.jpeg)"
/>
</div>
);
}
/* styles.css */
body { margin: 0; padding: 0; height: 250px; }
.background {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #eee;
}
.background--active {
background: #a6b5ff;
}
.picture {
width: 200px;
height: 200px;
border-radius: 10px;
border: 5px solid transparent;
}
.picture--active {
border: 5px solid #a6b5ff;
}
다른 방법으로는 두 개의 분리된 JSX 덩어리를 반환하도록 작성할 수도 있습니다:
// App.js
import { useState } from 'react';
export default function Picture() {
const [isActive, setIsActive] = useState(false);
if (isActive) {
return (
<div
className="background"
onClick={() => setIsActive(false)}
>
<img
className="picture picture--active"
alt="Rainbow houses in Kampung Pelangi, Indonesia"
src="[https://i.imgur.com/5qwVYb1.jpeg](https://i.imgur.com/5qwVYb1.jpeg)"
onClick={e => e.stopPropagation()}
/>
</div>
);
}
return (
<div className="background background--active">
<img
className="picture"
alt="Rainbow houses in Kampung Pelangi, Indonesia"
src="[https://i.imgur.com/5qwVYb1.jpeg](https://i.imgur.com/5qwVYb1.jpeg)"
onClick={() => setIsActive(true)}
/>
</div>
);
}
/* styles.css */
body { margin: 0; padding: 0; height: 250px; }
.background {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #eee;
}
.background--active {
background: #a6b5ff;
}
.picture {
width: 200px;
height: 200px;
border-radius: 10px;
border: 5px solid transparent;
}
.picture--active {
border: 5px solid #a6b5ff;
}
기억해 두어야 할 점은, 만약 두 개의 서로 다른 JSX 덩어리가 동일한 트리 구조를 묘사한다면, 그들의 중첩 구조(첫 번째 <div> → 그 안의 첫 번째 <img>)가 정확히 맞아떨어져야 한다는 것입니다. 그렇지 않으면 isActive 값이 바뀔 때마다 하위의 전체 트리를 다시 생성하게 되어 그 요소들의 상태가 리셋(reset its state)되어 버립니다. 그렇기 때문에, 두 경우 모두 비슷한 구조의 JSX 트리를 반환한다면 하나의 JSX 덩어리 안에서 작성하는 것이 훨씬 좋습니다.
순수 자바스크립트와 DOM만으로 구현된 작은 폼입니다. 어떻게 동작하는지 한 번 만져보세요:
// src/index.js
function handleFormSubmit(e) {
e.preventDefault();
if (editButton.textContent === 'Edit Profile') {
editButton.textContent = 'Save Profile';
hide(firstNameText);
hide(lastNameText);
show(firstNameInput);
show(lastNameInput);
} else {
editButton.textContent = 'Edit Profile';
hide(firstNameInput);
hide(lastNameInput);
show(firstNameText);
show(lastNameText);
}
}
function handleFirstNameChange() {
firstNameText.textContent = firstNameInput.value;
helloText.textContent = (
'Hello ' +
firstNameInput.value + ' ' +
lastNameInput.value + '!'
);
}
function handleLastNameChange() {
lastNameText.textContent = lastNameInput.value;
helloText.textContent = (
'Hello ' +
firstNameInput.value + ' ' +
lastNameInput.value + '!'
);
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
// sandbox.config.json
{
"hardReloadOnChange": true
}
<form id="form">
<label>
First name:
<b id="firstNameText">Jane</b>
<input
id="firstNameInput"
value="Jane"
style="display: none">
</label>
<label>
Last name:
<b id="lastNameText">Jacobs</b>
<input
id="lastNameInput"
value="Jacobs"
style="display: none">
</label>
<button type="submit" id="editButton">Edit Profile</button>
<p><i id="helloText">Hello, Jane Jacobs!</i></p>
</form>
<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
label { display: block; margin-bottom: 20px; }
</style>
이 폼은 두 가지 모드를 전환합니다. '편집(editing) 모드'에서는 입력창들을 볼 수 있고, '보기(viewing) 모드'에서는 오직 결과 텍스트만 볼 수 있습니다. 현재 어떤 모드인지에 따라 버튼 라벨도 "Edit"과 "Save"로 바뀝니다. 입력창의 내용을 변경하면 맨 아래에 있는 환영 메시지가 실시간으로 업데이트됩니다.
여러분의 과제는 아래 샌드박스에서 이 폼을 React로 다시 구현하는 것입니다. 편의를 위해 마크업 구조는 이미 JSX로 변환해 두었지만, 원본 코드처럼 입력창이 보이고 숨겨지도록 로직을 추가해야 합니다.
맨 아래의 환영 메시지 텍스트도 제대로 업데이트되는지 꼭 확인하세요!
// App.js
export default function EditProfile() {
return (
<form>
<label>
First name:{' '}
<b>Jane</b>
<input />
</label>
<label>
Last name:{' '}
<b>Jacobs</b>
<input />
</label>
<button type="submit">
Edit Profile
</button>
<p><i>Hello, Jane Jacobs!</i></p>
</form>
);
}
/* styles.css */
label { display: block; margin-bottom: 20px; }
입력값들을 보관하기 위해 firstName과 lastName 두 개의 상태 변수가 필요할 겁니다. 입력창들을 보여줄지 말지를 결정하는 isEditing 이라는 상태 변수도 필요하겠죠. 반면에 fullName 상태 변수는 굳이 만들 필요가 없습니다. 전체 이름(fullName)은 언제나 firstName과 lastName을 조합해서 알아낼 수 있으니까요.
마지막으로, 조건부 렌더링(conditional rendering)을 사용해서 isEditing 값에 따라 입력창을 보여주거나 숨기면 됩니다.
// App.js
import { useState } from 'react';
export default function EditProfile() {
const [isEditing, setIsEditing] = useState(false);
const [firstName, setFirstName] = useState('Jane');
const [lastName, setLastName] = useState('Jacobs');
return (
<form onSubmit={e => {
e.preventDefault();
setIsEditing(!isEditing);
}}>
<label>
First name:{' '}
{isEditing ? (
<input
value={firstName}
onChange={e => {
setFirstName(e.target.value)
}}
/>
) : (
<b>{firstName}</b>
)}
</label>
<label>
Last name:{' '}
{isEditing ? (
<input
value={lastName}
onChange={e => {
setLastName(e.target.value)
}}
/>
) : (
<b>{lastName}</b>
)}
</label>
<button type="submit">
{isEditing ? 'Save' : 'Edit'} Profile
</button>
<p><i>Hello, {firstName} {lastName}!</i></p>
</form>
);
}
/* styles.css */
label { display: block; margin-bottom: 20px; }
이 해결책과 원본의 명령형 코드를 한 번 비교해 보세요. 어떻게 다른가요?
이전 과제에서 봤던, React 없이 명령형으로 작성된 원본 샌드박스입니다:
// src/index.js
function handleFormSubmit(e) {
e.preventDefault();
if (editButton.textContent === 'Edit Profile') {
editButton.textContent = 'Save Profile';
hide(firstNameText);
hide(lastNameText);
show(firstNameInput);
show(lastNameInput);
} else {
editButton.textContent = 'Edit Profile';
hide(firstNameInput);
hide(lastNameInput);
show(firstNameText);
show(lastNameText);
}
}
function handleFirstNameChange() {
firstNameText.textContent = firstNameInput.value;
helloText.textContent = (
'Hello ' +
firstNameInput.value + ' ' +
lastNameInput.value + '!'
);
}
function handleLastNameChange() {
lastNameText.textContent = lastNameInput.value;
helloText.textContent = (
'Hello ' +
firstNameInput.value + ' ' +
lastNameInput.value + '!'
);
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
// sandbox.config.json
{
"hardReloadOnChange": true
}
<form id="form">
<label>
First name:
<b id="firstNameText">Jane</b>
<input
id="firstNameInput"
value="Jane"
style="display: none">
</label>
<label>
Last name:
<b id="lastNameText">Jacobs</b>
<input
id="lastNameInput"
value="Jacobs"
style="display: none">
</label>
<button type="submit" id="editButton">Edit Profile</button>
<p><i id="helloText">Hello, Jane Jacobs!</i></p>
</form>
<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
label { display: block; margin-bottom: 20px; }
</style>
만약 세상에 React가 없다고 상상해 보세요. 어떻게 하면 이 코드를 좀 덜 부서지기 쉽게 만들고 조금 더 React 버전과 비슷하게 리팩터링 할 수 있을까요? 마치 React에서처럼 상태(state)가 명시적으로 관리된다면 어떤 모습일까요?
어디서부터 손대야 할지 막막하시다면, 아래 제공된 기본 뼈대 코드를 보세요. 큰 구조는 이미 잡혀 있습니다. 여기서부터 시작해서, updateDOM 함수 안에 빠져 있는 로직을 채워 넣어 보세요. (필요하다면 원본 코드를 참고하셔도 좋습니다.)
// src/index.js
let firstName = 'Jane';
let lastName = 'Jacobs';
let isEditing = false;
function handleFormSubmit(e) {
e.preventDefault();
setIsEditing(!isEditing);
}
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
function setFirstName(value) {
firstName = value;
updateDOM();
}
function setLastName(value) {
lastName = value;
updateDOM();
}
function setIsEditing(value) {
isEditing = value;
updateDOM();
}
function updateDOM() {
if (isEditing) {
editButton.textContent = 'Save Profile';
// TODO: show inputs, hide content
} else {
editButton.textContent = 'Edit Profile';
// TODO: hide inputs, show content
}
// TODO: update text labels
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
// sandbox.config.json
{
"hardReloadOnChange": true
}
<form id="form">
<label>
First name:
<b id="firstNameText">Jane</b>
<input
id="firstNameInput"
value="Jane"
style="display: none">
</label>
<label>
Last name:
<b id="lastNameText">Jacobs</b>
<input
id="lastNameInput"
value="Jacobs"
style="display: none">
</label>
<button type="submit" id="editButton">Edit Profile</button>
<p><i id="helloText">Hello, Jane Jacobs!</i></p>
</form>
<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
label { display: block; margin-bottom: 20px; }
</style>
빠져있던 로직은 입력창과 결과 텍스트를 숨기거나 보여주는 부분을 전환하고, 라벨 텍스트들을 업데이트하는 것이었죠:
// src/index.js
let firstName = 'Jane';
let lastName = 'Jacobs';
let isEditing = false;
function handleFormSubmit(e) {
e.preventDefault();
setIsEditing(!isEditing);
}
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
function setFirstName(value) {
firstName = value;
updateDOM();
}
function setLastName(value) {
lastName = value;
updateDOM();
}
function setIsEditing(value) {
isEditing = value;
updateDOM();
}
function updateDOM() {
if (isEditing) {
editButton.textContent = 'Save Profile';
hide(firstNameText);
hide(lastNameText);
show(firstNameInput);
show(lastNameInput);
} else {
editButton.textContent = 'Edit Profile';
hide(firstNameInput);
hide(lastNameInput);
show(firstNameText);
show(lastNameText);
}
firstNameText.textContent = firstName;
lastNameText.textContent = lastName;
helloText.textContent = (
'Hello ' +
firstName + ' ' +
lastName + '!'
);
}
function hide(el) {
el.style.display = 'none';
}
function show(el) {
el.style.display = '';
}
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let firstNameInput = document.getElementById('firstNameInput');
let firstNameText = document.getElementById('firstNameText');
let lastNameInput = document.getElementById('lastNameInput');
let lastNameText = document.getElementById('lastNameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
firstNameInput.oninput = handleFirstNameChange;
lastNameInput.oninput = handleLastNameChange;
// sandbox.config.json
{
"hardReloadOnChange": true
}
<form id="form">
<label>
First name:
<b id="firstNameText">Jane</b>
<input
id="firstNameInput"
value="Jane"
style="display: none">
</label>
<label>
Last name:
<b id="lastNameText">Jacobs</b>
<input
id="lastNameInput"
value="Jacobs"
style="display: none">
</label>
<button type="submit" id="editButton">Edit Profile</button>
<p><i id="helloText">Hello, Jane Jacobs!</i></p>
</form>
<style>
* { box-sizing: border-box; }
body { font-family: sans-serif; margin: 20px; padding: 0; }
label { display: block; margin-bottom: 20px; }
</style>
방금 여러분이 직접 작성해 보신 이 updateDOM 함수가 보여주는 원리가, 바로 여러분이 상태(State)를 설정할 때 React가 내부적(under the hood)으로 수행하는 작업입니다. (물론 React는 여기서 한발 더 나아가서, 지난번에 값이 변경된 이후로 수정되지 않은 속성에 대해서는 아예 DOM을 건드리지 않는 최적화 작업까지 알아서 수행해 준답니다.)