한 입 크기로 잘라먹는 React 개정판 리뷰

Etc
2026-04-03

들어가며

'유행을 타는 책이 아니라, 오래 꺼내보는 책을 만들자'는 2년 전 작가의 말이 단순한 다짐에 그치지 않고, 프론트엔드 변화의 흐름을 반영한 개정판으로 이어졌다는 점이 특히 인상 깊었습니다. 이번 개정판에서 주요하게 변화된 내용으로는

  1. 자바스크립트 실습 환경 최신화 (코드샌드박스 웹 에디터 -> 비주얼 스튜디오 코드)
  2. 리액트 앱 생성 도구 최신화 (CRA -> Vite) 및 그에 따른 실습 내용 변화
  3. 서비스 배포 플랫폼 변경 (Firebase -> Vercel) 및 그에 따른 실습 내용 변화

책 소개

책의 구성과 학습 흐름

해당 책은 총 11개의 장과 3개의 프로젝트로 구성되어 있다.

1장 자바스크립트 기초
2장 자바스크립트 실전
3장 Node.js
4장 리액트 시작하기
5장 리액트의 기본 기능 다루기 - project 1 [카운터] 앱 만들기
6장 라이프 사이클과 리액트 개발자 도구 - project 2 [할 일 관리] 앱 만들기
7장 useReducer와 상태 관리
8장 최적화
9장 컴포넌트 트리에 데이터 공급하기 - project 3 [감정 일기장] 만들기
10장 웹 스토리지 이용하기
11장 [감정 일기장] 배포

책의 특징

해당 교재는 감정일기장 앱 만들기 프로젝트를 중심으로 전개되어, 실습과 함께 리액트를 배울 수 있다는 장점이 있다. 또한 4장 이전까지는 상수와 변수, 반복문, 단락 평가, 관련 메서드 등 실전에 들어가기 전에 필요한 기초 지식을 탄탄하게 다룬다.

1 ~ 3장을 읽으며 나 역시 기본 개념을 다시 정리해보고자, 아래에 주요 내용을 정리했다.

✨ 책 읽으며 개념 정리

  1. let, var, const의 차이? p,16, 66 참고
    let은 var와 달리 중복 선언이 불가능하며, let과 var는 변수지만 const는 상수이다. 변수는 선언과 동시에 할당하는 초기화 과정이 필요치 않지만 상수는 필요하다. var는 함수 스코프, let과 const는 블록 스코프이다.

  2. 함수 Number와 parseInt의 차이? p.29 참고
    Number는 문자열을 숫자로 변환해서 반환한다.

// 숫자가 아닌 문자를 포함한 문자열은 NaN을 반환함
let strA = '10';
let strB = '10개';
 
let numA = Number(strA);
let numB = Number(strB);
 
console.log(numA); // 10
console.log(numB); // NaN

parseInt는 괄호 안에 두 개의 값을 콤마로 구분해서 전달하는데, 첫 번째 값은 변환하려는 문자열이고 두 번째 값은 진수이다.

// 문자열이 숫자가 아닌 문자로 시작하면 NaN을 반환함
let str = '화이팅 2026';
let num = parseInt(str, 10); 
 
console.log(num); // NaN
  1. 비교연산자 == 과 === 의 차이? p.36 참고
    == 는 값만 비교할 뿐 자료형은 비교하지 않는다.
let numA = 2;
let numB = 2;
let numC = '2';
 
console.log(numA === numB); // true
console.log(numB === numC); // false
console.log(numA == numC); // true
  1. 인수와 매개변수의 차이? p.53 참고
    인수는 함수를 호출하면서 넘겨주는 값이고 매개변수는 함수에서 넘겨받은 인수를 저장하는 변수이다. 즉, 인수는 '값', 매개변수는 그 값을 저장할 '변수'로 생각하면 이해가 쉽다.
function getArea(width, height) {
let area = width * height;
console.log(area);
}
 
getArea(10, 20); // 에서 10과 20이 인수, width와 height가 매개변수
  1. 호이스팅은 선언이 끌어올려지는 것이다? p.55 참고
    X. 끌어올려지는 것처럼 보이는 현상이다.
    호이스팅: 변수나 함수를 호출하거나 접근하는 코드가 함수 선언보다 위에 있음에도 불구하고, 마치 선언 코드가 위에 있는 것처럼 동작하는 방식이다.
    자바스크립트는 코드를 실행하기 전 준비 단계에서 중첩 함수가 아닌 함수들을 모두 찾아 미리 생성해두기 때문에 함수 선언 코드를 호출보다 늦게 작성해도 자연스럽게 호출할 수 있다.
func();
 
function func() {
console.log('Hello'); // Hello
}
// 함수 func에 대한 선언이 호출코드보다 아래에 있지만 호출보다 먼저 함수를 선언한 것처럼 동작한다.
  1. truthy falsy 단락평가(Short-circuit Evaluation) p.81 참고
    falsy한 값이란 boolean 자료형의 false값은 아니지만, 거짓과 같은 의미로 쓰이며 조건식에서 거짓(false)로 평가되는 것을 말한다. falsy 값은 다음 7가지이다.
  • undefined
  • null
  • 0, -0
  • NaN
  • "" (빈 문자열)
  • 0n (BigInt)

단락평가 부분을 읽으며, 이전 프로젝트에서 얕은 비교와 단락평가를 함께 정리해보면 좋겠다는 리뷰를 받았던 기억이 떠올랐다. 이 기회에 해당 개념을 다시 한 번 짚어보고자 한다.

단락평가는 논리 연산에서 첫 번째 피연산자의 값만으로 결과가 확정될 경우, 두 번째 피연산자는 평가하지 않는 것을 의미한다. 예를 들어 true || false에서는 첫 번째 값이 true이므로, 뒤의 값과 상관없이 결과는 true가 된다. 반대로 false && true와 같은 경우에는 첫 번째 값이 false이기 때문에, 두 번째 값을 확인하지 않아도 결과가 false로 결정된다.

또한 논리 연산자 &&는 단순히 boolean 값을 반환하는 것이 아니라, 조건에 따라 특정 값을 그대로 반환한다는 특징이 있다.

프로젝트에서의 문제 상황

다음과 같은 코드를 사용하고 있었다.

{isSoldOut && <SalesBadge isSoldOut={isSoldOut} />}

이 코드는 isSoldOut이 truthy일 때만 컴포넌트를 렌더링한다. 하지만 isSoldOut이 undefined인 경우, 기대와 다르게 컴포넌트가 렌더링되지 않았다.

왜 이런 일이 발생했을까?!

isSoldOut && <SalesBadge />
 
- isSoldOut이 true<SalesBadge /> 반환
- isSoldOut이 undefinedundefined 반환

undefined는 falsy이기 때문에 단락 평가에 의해 undefined가 그대로 반환된다. React는 undefined를 렌더링하지 않기 때문에 결과적으로 아무것도 보이지 않게 된다.

해결 방법
상황에 따라 ?? 연산자를 사용할 수 있다.

{isSoldOut ?? <SalesBadge isSoldOut={isSoldOut} />}

?? 연산자는 다음과 같이 동작한다.

  • null 또는 undefined → 오른쪽 값 반환
  • 그 외 → 왼쪽 값 반환
undefined ?? <SalesBadge /><SalesBadge /> // <SalesBadge />

따라서 isSoldOut이 undefined일 때는 정상적으로 컴포넌트가 렌더링된다.

그렇다면 && vs ?? 차이를 정리해보자

연산자조건반환값
&&falsy면왼쪽 값
&&truthy면오른쪽 값
??null/undefined면오른쪽 값
??그 외왼쪽 값

그동안 &&를 단순한 조건문처럼 사용했지만, 실제로는 “값을 반환하는 연산자”라는 점을 놓치고 있었다.
특히 undefined가 그대로 반환되면서 React에서 아무것도 렌더링되지 않는다는 점을 이번에 명확히 이해하게 되었다.

이 문제는 실제 프로젝트에서 겪었던 상황과 맞닿아 있어 더 인상 깊게 다가왔다.
PR 리뷰를 통해 단락 평가뿐만 아니라 얕은 비교(shallow compare)에 대해서도 다시 고민해볼 수 있었고, 단순히 동작하는 코드를 넘어 값의 흐름을 이해하는 것이 얼마나 중요한지 느낄 수 있었다.

  1. 메서드 정리 p.102 참고
    pop() : 배열의 맨 끝 요소를 제거하고 제거한 요소를 반환하는 메서드로, 빈 배열에서 pop 메서드를 사용하면 제거할 요소가 없기 때문에 undefined를 반환한다.
    push() : 배열 맨 끝에 요소를 추가하고 새로운 길이를 반환하는 메서드
    shift() : 배열의 맨 앞 요소를 제거하고 제거한 요소를 반환하는 메서드(pop 메서드와 반대)
    unshift() : 배열 맨 앞에 요소를 추가하고 새 배열의 길이를 반환하는 메서드(push 메서드와 반대)
    slice(start, end) : 기존 배열에서 특정 범위를 잘라 새로운 배열을 반환한다. 이 때 원본 배열은 수정되지 않는다!
const arr = [1, 2, 3];
const sliced = arr.slice(0, 2);
 
console.log(arr); // [1, 2, 3]
console.log(sliced); // [1, 2]

concat() : 서로 다른 배열을 이어 붙여 새 배열을 반환한다

let arrA = [1, 2];
let arrB = [3, 4];
let arrC = arrA.concat(arrB);
 
console.log(arrC); // [1, 2, 3, 4]

여기서부터는 4장 이후의 내용이다.

  1. 리액트의 장점
  • 컴포넌트 기반의 유연한 구조 리액트는 컴포넌트 단위로 코드를 모듈화하여 중복을 줄일 수 있다. 여러 페이지에서 공통으로 사용하는 UI를 컴포넌트로 만들어두고, 필요한 곳에서 재사용할 수 있다는 점이 큰 장점이다.
  • 쉽고 간단한 업데이트 일반적으로 웹 페이지를 업데이트하려면 DOM 객체를 직접 조작해야 한다. DOM은 트리 구조로 이루어져 있어 구조가 복잡해질수록 원하는 요소를 정확히 찾아 수정하기 어려워진다.
    반면 리액트는 변경이 필요한 부분을 기준으로 새로운 요소를 생성하고, 기존 요소를 교체하는 방식으로 업데이트를 수행한다. 이는 자동차가 고장났을 때 특정 부품만 수리하기보다 새 부품으로 교체하는 것과 비슷한 개념이다. 이러한 방식 덕분에 보다 효율적인 업데이트가 가능하다.
  1. 브라우저의 렌더링 과정 p.171
1. HTML을 해석하여 DOM으로 변환하고, CSS를 해석하여 스타일 규칙을 생성한다.
2. DOM과 스타일 규칙을 결합하여 렌더 트리를 생성한다.
3. 렌더 트리를 기반으로 각 요소의 위치와 크기를 계산하는 레이아웃 과정을 거친다.
4. 계산된 정보를 바탕으로 실제 화면에 그리는 페인팅 과정을 수행한다.

예제와 프로젝트를 진행하면서 리액트 앱의 동작 원리가 궁금해진다면, 다시 4장으로 돌아와 읽어보는 것도 도움이 될 것이다🎈

  1. onClick onChange 차이 이 부분은 학습하면서 헷갈렸던 개념이라 따로 정리해보았다.
    onClick은 사용자의 ‘클릭’이라는 행동이 발생했을 때 실행되는 이벤트이고, onChange는 입력 요소의 ‘값이 변경되었을 때’ 실행되는 이벤트이다.
    | 상황 | 어떤 이벤트? | | --------------- | -------- | | 버튼 눌렀다 | onClick | | input에 글자 입력했다 | onChange | | 체크박스 클릭해서 상태 바뀜 | onChange | | “제출하기” 버튼 눌렀다 | onClick |

이때, 체크박스를 '클릭'했는데 왜 onChange지? onClick 아닌가? 하는 생각이 들었다. 사실 체크박스를 클릭하면 실제로 일어나는 일은 checked 값이 true/false로 변경되는 것이므로 checked 라는 '값'이 변경된 것이기 때문에 onChange다.

  1. 여러 개의 사용자 입력 관리하기 p.239
    state를 이용해 컴포넌트에서 사용자의 입력을 처리하는 방법을 다루는 부분에서, 회원가입과 같이 입력 필드가 많은 경우 여러 개의 state로 값을 관리하는 방식을 소개하고 있다.

예를 들어 다음과 같이 각 입력값마다 state를 따로 선언해 관리할 수 있다.

const [name, setName] = useState('');
const [gender, setGender] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [birth, setBirth] = useState('');

이 방식은 직관적이지만, 입력 필드가 많아질수록 state와 이벤트 핸들러가 함께 증가해 코드가 빠르게 복잡해진다. 이에 대해 책에서는 여러 입력값을 하나의 state 객체로 관리하는 방법을 함께 제시한다.

const [form, setForm] = useState({
  name: '',
  gender: '',
  phoneNumber: '',
  birth: '',
});

이 경우 하나의 state로 여러 값을 관리할 수 있어 구조를 단순화할 수 있지만, 여전히 입력값 변경 시마다 객체를 복사하고 특정 필드를 업데이트해야 하는 번거로움이 존재한다.

이 부분을 읽으며, 계약직 인턴십 당시 배송지 입력 폼을 개선했던 경험이 떠올랐다. 배송지 입력 역시 이름, 전화번호, 주소, 국가 등 다양한 사용자 입력을 다루고 있었고, 국가별로 서로 다른 입력 조건까지 고려해야 하는 복잡한 구조였다. 예를 들어 일본 배송지의 상세 주소 입력은 다음과 같이 구성되어 있었다.

<AddressInput
  name="detailAddress"
  required
  placeholder="Apt, Suite, etc"
  control={control}
  maxLength={44}
  pattern={/^[\u3040-\u30FF\u4E00-\u9FAF\uFF00-\uFFEF\u0020-\u007E0-9]+$/}
/>

해당 입력 필드는 단순한 텍스트 입력을 넘어 최대 길이 제한, 특정 문자만 허용하는 정규식 검증, 필수 입력 여부 등 다양한 조건을 동시에 만족해야 했다. 만약 이를 useState 기반으로 구현했다면 각 입력마다 state와 onChange 핸들러를 따로 관리하고, 그 안에서 유효성 검증 로직까지 직접 처리해야 했을 것이다.

이러한 문제를 해결하기 위해 react-hook-form을 활용해 폼 상태를 한 곳에서 관리하도록 개선했다. 각 input은 useController를 통해 form과 연결되며, 별도의 state 없이도 값과 유효성 검사를 일관된 방식으로 처리할 수 있었다. 이를 통해 단순히 코드 양을 줄이는 것을 넘어, 입력값 관리와 검증 로직을 하나의 흐름으로 통합할 수 있었고, 복잡한 폼일수록 구조적으로 관리하는 것이 중요하다는 점을 체감할 수 있었다.

  1. 리액트의 라이프 사이클 p.270
    마운트 : 컴포넌트가 페이지에 처음 렌더링될 때 업데이트 : state나 props의 값이 바뀌거나 부모 컴포넌트가 리렌더해 자신도 리렌더될 때 언마운트 : 더이상 페이지에 컴포넌트가 렌더링되지 않을 때

  2. 빈 입력 방지하기 p.329, 249
    사용자가 특정 폼에 내용을 입력하지 않거나, 입력 값이 조건을 만족하지 않을 경우 해당 input에 포커스를 주어 추가 입력을 유도할 수 있다. 이때 Ref 객체를 활용해 특정 DOM 요소에 직접 접근할 수 있다.

const [content, setContent] = useState('');
const inputRef = useRef(); // 할 일 입력폼을 제어할 객체 inputRef 생성
 
const onSubmit = () => {
 // 현재 content 값이 빈 문자열이면 inputRef가 현재값(current)으로 저장한 요소에 포커스하고 종료
 if (!content) {
   inputRef.current.focus();
   return;
 }
 onCreate(content);
}
 
 // 할 일 입력 폼의 ref에 inputRef 설정
 <input
   ref={inputRef}
   value={content}
   onChange={handleOnChangeContent}
   placeholder="새로운 할 일"
 />
  <button onClick={onSubmit}>추가</button>

이처럼 ref를 통해 특정 input 요소에 접근한 뒤 focus()를 호출하면, 사용자가 입력하지 않은 필드로 자연스럽게 이동시킬 수 있다. 단순히 입력값을 검증하는 것을 넘어, 사용자의 다음 행동을 유도하는 방식이라는 점에서 사용자 경험 측면에서도 의미 있는 처리라고 느꼈다.

  1. 리액트 라우터로 동적 경로 라우팅하기 p.427
    동적 경로를 표현하는 방법에는 URL 파라미터와 쿼리스트링 두 가지가 있다.

URL 파라미터는 URL 경로에 유동적인 값을 포함하는 방식으로, 라우트 정의 시 :id와 같은 형태로 작성한다.

<Route path="/diary/:id" element={<Diary />} />
// http://localhost:5173/diary/1

쿼리 스트링은 물음표(?) 뒤에 key=value 형태로 URL에 유동적인 값을 포함하는 방식이다.

// http://localhost:5173?sort=latest
 
여러 값을 전달할 경우 & 구분한다.
// http://localhost:5173?sort=latest&page=1

언제 무엇을 사용할까?

구분사용 목적예시
URL 파라미터특정 리소스를 식별할 때게시글 id, 사용자 id
쿼리 스트링정렬, 필터, 페이지네이션 등 부가적인 옵션 전달 시sort=latest, page=1
  1. 로컬 스토리지와 세션 스토리지 p.557
    로컬 스토리지와 세션 스토리지는 모두 별도의 라이브러리 없이 window 객체를 통해 사용할 수 있는 웹 스토리지이다.

로컬 스토리지(localStorage)는 window.localStorage를 통해 접근하며, 저장된 데이터는 브라우저를 종료하더라도 유지된다. 사용자가 직접 삭제하지 않는 이상 데이터는 반영구적으로 보관된다.

반면 세션 스토리지(sessionStorage)는 window.sessionStorage를 통해 사용하며, 탭 단위로 데이터를 관리한다. 따라서 브라우저를 종료하거나 해당 탭을 닫으면 데이터는 함께 삭제된다. 다만 새로고침이 발생하더라도 탭이 유지되는 동안에는 데이터가 유지된다. 주로 로그인 유지, 사용자 설정 저장과 같이 장기 보관이 필요한 경우에는 localStorage를, 일시적인 상태 관리에는 sessionStorage를 사용하는 것이 적절하다!

마치며

한 입 크기로 잘라먹는 리액트는 자바스크립트의 기초 문법부터 리액트를 활용한 프로젝트 구현, 그리고 Vercel을 이용한 배포까지 전반적인 흐름을 한 번에 경험할 수 있는 책이다.

개념을 학습하는 데서 끝나지 않고, 직접 구현해보는 과정 속에서 자연스럽게 이해를 돕는 구성이 처음 리액트를 접하는 사람이나 기초를 다시 정리하고 싶은 사람뿐만 아니라 부족한 자바스크립트 기초 지식으로 인해 어려움을 겪는 사람들에게도 추천하고 싶다. 나 또한 학습하면서 이전에 경험했던 프로젝트들을 다시 돌아보고, 개념과 실무를 연결해볼 수 있었던 점이 인상 깊었다. 이미 알고 있던 내용도 왜 그렇게 동작하는지까지 생각해보는 계기가 되었고, 이해의 깊이를 한 단계 더 넓힐 수 있었다.
서평할 기회를 주신 이정환 님께 감사드립니다 :)

이 글은 도서를 제공받아 작성한 솔직 리뷰입니다.