이번 편의 결과물: (코드 없음) 카운터 예제로 명령형 DOM 조작과 선언적 UI의 차이를 비교해 이해합니다. · 다루는 개념: 명령형 DOM 조작의 한계, 선언적 UI, 컴포넌트란 무엇인가, React의 위치(SPA·프레임워크 맥락)
이 편에서 만드는 파일
이 편은 이론 편이라 로컬에 파일을 만들지 않습니다. bookshelf 프로젝트 생성은 03편에서 시작합니다. 아래 예제는 브라우저 안에서 바로 실행되는 코드로만 확인합니다.
개념 정리
명령형 DOM 조작
지금까지 브라우저에서 화면을 바꾸는 방식은 대부분 명령형(imperative)이었습니다. “어떤 상태가 되어야 하는가”가 아니라 “어떤 순서로 DOM을 조작해야 하는가”를 하나씩 지시합니다.
| 단계 | 명령형 코드가 하는 일 |
|---|---|
| 1 | document.querySelector로 바꿀 요소를 찾는다 |
| 2 | 현재 값을 변수나 요소 속성에서 읽는다 |
| 3 | 계산한 새 값으로 textContent, className 등을 직접 갱신한다 |
| 4 | 관련된 다른 요소가 있다면 그 요소도 각각 찾아서 똑같이 갱신한다 |
문제는 3~4번입니다. 화면에 같은 값을 보여주는 요소가 여러 개면, 하나를 빠뜨리고 갱신하지 않는 실수가 쉽게 생깁니다. 상태(값)와 화면(DOM)이 코드 곳곳에서 수동으로 동기화되기 때문입니다.
선언적 UI
선언적(declarative) 방식은 “지금 상태가 이렇다면 화면은 이런 모습이어야 한다”는 규칙만 적습니다. 상태가 바뀌면 그 규칙에 따라 화면을 다시 계산하는 일은 라이브러리(React)가 맡습니다. 개발자는 DOM을 직접 찾아가서 갱신하는 코드를 쓰지 않습니다.
| 구분 | 명령형 | 선언적(React) |
|---|---|---|
| 화면 갱신 방법 | 바뀐 부분을 직접 찾아 수정 | 상태 → 화면 매핑 함수를 다시 실행 |
| 동기화 실수 | 갱신 누락 가능 | 상태 하나만 바꾸면 관련 화면 전체 갱신 |
| 코드가 답하는 질문 | ”어떻게 바꿀까" | "지금 상태에서 무엇을 보여줄까” |
컴포넌트란 무엇인가
컴포넌트는 입력(props)을 받아 화면 구조(JSX)를 반환하는 함수입니다. 버튼 하나, 카드 하나처럼 작은 단위를 함수로 만들어 두면 여러 곳에서 재사용할 수 있습니다. bookshelf 앱에서 책 한 권을 보여주는 카드가 좋은 예입니다. 책이 여러 권이어도 카드 컴포넌트 하나만 만들고 데이터만 다르게 넣어 재사용합니다.
React의 위치
React는 UI를 만드는 라이브러리입니다. 프레임워크와 달리 라우팅·상태 관리·데이터 요청 같은 기능을 기본 제공하지 않고, 필요한 부분만 다른 패키지(React Router, TanStack Query 등)로 조합합니다. 이 과목에서 만드는 bookshelf는 페이지 이동 없이 한 화면 안에서 모든 상호작용이 일어나는 SPA(Single Page Application) 구조입니다.
| 용어 | 의미 |
|---|---|
| 라이브러리 | 필요한 기능을 골라 가져다 쓴다(React) |
| 프레임워크 | 정해진 구조 안에서 코드를 채운다(Next.js 등) |
| SPA | 페이지 전체를 다시 불러오지 않고 한 문서 안에서 화면을 갱신하는 앱 구조 |
실습
브라우저에서 바로 실행되는 예제로 명령형 방식의 한계를 확인합니다.
count를 표시하는 곳이 countLabel, countStatus 두 곳입니다. render 함수 안에서 두 곳을 모두 갱신하도록 직접 챙겨야 합니다. 셋째 표시 위치가 생기면 render 함수를 또 고쳐야 합니다.
React라면 같은 화면을 아래처럼 “상태에 따라 무엇을 보여줄지”만 적습니다(아직 실행 환경이 없으므로 코드만 읽어봅니다. 실제 실행은 03편부터입니다).
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>현재 값: {count}</p>
<p>상태 요약: {count === 0 ? '시작' : `${count}번 눌렀습니다`}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}count 값이 바뀌면 Counter 함수가 다시 실행되어 반환하는 화면 전체가 새로 계산됩니다. countLabel, countStatus를 따로 찾아가서 갱신하는 코드가 없습니다. useState는 08편에서 다룹니다.
직접 해보기
- 위 명령형 카운터 예제에 “-1” 버튼을 추가해 값을 감소시켜 봅니다. 단,
count가 음수로 내려가지 않게 합니다. - 감소 버튼을 눌러
count가 0이 되면countStatus가 다시 “시작”으로 보이는지 확인합니다.
정답 보기
<!-- index.html -->
<div id="app">
<p>현재 값: <span id="count-label">0</span></p>
<p>상태 요약: <span id="count-status">시작</span></p>
<button id="increment">+1</button>
<button id="decrement">-1</button>
</div>// index.js
let count = 0;
const countLabel = document.querySelector('#count-label');
const countStatus = document.querySelector('#count-status');
const incrementButton = document.querySelector('#increment');
const decrementButton = document.querySelector('#decrement');
function render() {
countLabel.textContent = count;
countStatus.textContent = count === 0 ? '시작' : count + '번 눌렀습니다';
}
incrementButton.addEventListener('click', () => {
count = count + 1;
render();
});
decrementButton.addEventListener('click', () => {
count = Math.max(0, count - 1);
render();
});
render();자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 값은 바뀌었는데 화면 일부만 갱신됨 | 표시 위치 중 하나를 갱신 코드에서 빠뜨림 | 상태를 표시하는 모든 요소를 한 render 함수 안에 모은다 |
| React를 “무조건 빠른 라이브러리”로 오해 | 선언적 방식의 장점을 성능으로만 이해 | 핵심은 속도가 아니라 “상태-화면 동기화를 대신 처리”하는 점이다 |
| React를 프레임워크로 부름 | 라우팅·상태 관리가 기본 포함된 도구로 착각 | React는 UI 라이브러리이며 나머지는 별도 패키지로 조합한다 |