하위 태스크 1
Counter 컴포넌트 생성
useState 기반 카운터 구현
Vite + React 프로젝트를 생성한다. 다음 명령어를 실행하면 mission-05-03 폴더에 React 애플리케이션이 준비된다.
npm create vite@latest mission-05-07 -- --template react애플리케이션의 의존성을 일괄 설치한다.
npm installCounter.jsx를 작성한다. useState 훅으로 숫자 상태를 관리한다.
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
function handleCountUp() {
setCount((c) => c + 1);
}
function handleCountDown() {
setCount((c) => c - 1);
}
return (
<div>
<div style={{ display: "flex" }}>
<input type="number" value={count} readOnly />
<button type="button" onClick={handleCountDown}>-</button>
<button type="button" onClick={handleCountUp}>+</button>
</div>
</div>
);
}App.jsx에서 Counter 컴포넌트를 import 하여 사용한다.
import Counter from "./Counter";
export default function App() {
return (
<main>
<Counter />
</main>
);
}웹브라우저에서의 결과는 다음과 같다.

하위 태스크 2
상태 업데이트 로그 출력
상태 변경 시 콘솔 로그 남기기
Counter 컴포넌트에 상태가 변경되면 로그를 남기는 부수 효과를 추가한다.
// ...
useEffect(() => {
console.log(count);
}, [count]);
// ...개발자 도구에서 로그를 확인한 결과는 다음과 같다.

하위 태스크 3
함수형 업데이트 비교
함수형 vs 일반 업데이트 패턴 비교
setCounter(n + 1)에서 매개 변수에 전달된 n은 컴포넌트 렌더링 시점의 n으로 고정된다. n이 0이라면 setCounter(n + 1)을 수차례 호출해도 counter State는 1이 된다.
// n이 0이라면
setCounter(n + 1);
setCounter(n + 1);
setCounter(n + 1);
// counter의 값은 1이다.setCounter((n) => n + 1)은 setCounter Setter 함수의 호출에 따른 n의 변경을 추적한다. 따라서 setCounter 함수를 호출한 만큼 n이 증가한다.
// n이 0이라면
setCounter((n) => n + 1);
setCounter((n) => n + 1);
setCounter((n) => n + 1);
// counter의 값은 3이다.하위 태스크 4
Controller/Viewer 분리
입력과 표시 컴포넌트를 분리
Controller.jsx를 작성한다.
export default function Controller({ score, journal, handleScoreChange, handleJournalChange }) {
return (
<form>
<fieldset>
<legend>일지</legend>
<label style={{ display: "block" }}>
감정 점수:
<input type="number" value={score} onChange={(event) => {
handleScoreChange(event.target.value);
}} />
</label>
<label style={{ display: "block" }}>
오늘의 한 줄:
<input type="text" value={journal} onChange={(event) => {
handleJournalChange(event.target.value);
}} />
</label>
</fieldset>
</form>
);
}Viewer.jsx를 작성한다.
export default function Viewer({ score, journal }) {
return (
<div>
<p>감정 점수: {score}</p>
<figure>
<figcaption>오늘의 한 줄</figcaption>
</figure>
<blockquote>{journal}</blockquote>
</div>
);
}하위 태스크 5
App에서 상태 통합 관리
상위 컴포넌트에서 상태를 관리하고 Props로 전달
App.jsx에서 Controller 컴포넌트와 Viewer를 import 하고, App 컴포넌트에 포함시킨다.
import { useState } from "react";
import Viewer from "./Viewer";
import Controller from "./Controller";
export default function App() {
const [score, setScore] = useState(0);
const [journal, setJournal] = useState("");
return (
<main>
<Controller
score={score}
journal={journal}
handleScoreChange={setScore}
handleJournalChange={setJournal}
/>
<Viewer score={score} journal={journal} />
</main>
);
}웹브라우저에서 확인한 결과는 다음과 같다.

하위 태스크 6
객체 상태 관리
여러 필드를 하나의 객체로 관리
App 컴포넌트가 지녔던 score State와 journal State를 journal State로 통합한다.
import { useState } from "react";
import Viewer from "./Viewer";
import Controller from "./Controller";
export default function App() {
const [journal, setJournal] = useState({ score: 0, text: "" });
return (
<main>
<Controller journal={journal} handleJournalChange={setJournal} />
<Viewer journal={journal} />
</main>
);
}바뀐 State 구조에 대응하기 위해 Controller 컴포넌트와 Viewer 컴포넌트의 구현을 수정한다.
export default function Controller({ journal, handleJournalChange }) {
return (
<form>
<fieldset>
<legend>일지</legend>
<label style={{ display: "block" }}>
감정 점수:
<input type="number" value={journal.score} onChange={(event) => {
handleJournalChange({ ...journal, score: event.target.value });
}} />
</label>
<label style={{ display: "block" }}>
오늘의 한 줄:
<input type="text" value={journal.text} onChange={(event) => {
handleJournalChange({ ...journal, text: event.target.value });
}} />
</label>
</fieldset>
</form>
);
}export default function Viewer({ journal }) {
return (
<div>
<p>감정 점수: {journal.score}</p>
<figure>
<figcaption>오늘의 한 줄</figcaption>
</figure>
<blockquote>{journal.text}</blockquote>
</div>
);
}웹브라우저에서 확인한 결과는 다음과 같다.

하위 태스크 7 ~ 9
useEffect 마운트 로그
컴포넌트 초기 마운트 시 로그 출력
상태 변경 감시 useEffect
특정 상태 변경 시 추가 동작 실행
의존성 배열 실험
빈 배열, 상태 포함, 없음 등 다양한 경우 테스트
App.jsx에 useEffect를 사용해서 부수 효과를 추가한다. 의존성 배열의 요소가 없으므로 해당 부수 효과는 컴포넌트 렌더링 시 1회만 발생한다.
import { useEffect, useState } from "react";
// ...
useEffect(() => {
console.log("앱이 시작되었습니다.");
}, []);
// ...웹브라우저 관리자 도구에서 확인한 결과는 다음과 같다.

App 컴포넌트에 감정 상태 또는 오늘의 한 줄이 변경될 때 로그를 남기는 부수 효과를 추가한다.
// ...
useEffect(() => {
console.log(`감정 상태가 변경되었습니다: ${journal.score}`);
}, [journal.score]);
useEffect(() => {
console.log(`오늘의 한 줄이 변경되었습니다: ${journal.text}`);
}, [journal.text]);
// ...웹브라우저의 개발자 도구에서 확인한 결과는 다음과 같다.

하위 태스크 10
useRef로 포커스 제어
입력창에 자동 포커스 구현
Controller 컴포넌트에서 useRef를 사용해 자동 포커스를 구현할 수 있다.
// ...
const ref = useRef(null);
useEffect(() => {
ref.current.focus();
}, []);
// ...
<input ref={ref} type="text" value={journal.text} onChange={(event) => {
// ...하위 태스크 11
클린업 함수 구현
언마운트 시 정리 작업 추가
App 컴포넌트에 setInterval을 호출하는 부수 효과를 추가한다. setInterval은 클린업 함수를 정의하지 않으면 컴포넌트가 사라져도 Interval은 계속 실행된다.
// ...
useEffect(() => {
const id = setInterval(() => {
console.log("똑딱");
}, 1000);
return () => {
clearInterval(id);
};
}, []);
// ...