하위 태스크 1

Counter 컴포넌트 생성

useState 기반 카운터 구현

Vite + React 프로젝트를 생성한다. 다음 명령어를 실행하면 mission-05-03 폴더에 React 애플리케이션이 준비된다.

npm create vite@latest mission-05-07 -- --template react

애플리케이션의 의존성을 일괄 설치한다.

npm install

Counter.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.jsxuseEffect를 사용해서 부수 효과를 추가한다. 의존성 배열의 요소가 없으므로 해당 부수 효과는 컴포넌트 렌더링 시 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);
		};
	}, []);
 
// ...