본문으로 건너뛰기
SoulLog

React 학습 1주차 — 설치부터 상호작용성까지

15분 읽기

Vue.js를 주로 사용하다가 React를 처음 학습하면서 정리한 내용입니다. Vue와 비교되는 지점 위주로 기록했습니다.


설치

  • Vue는 공식 문서만 따라가면 설치 방법이 심플한 편인데, React는 새 프로젝트를 시작할 때 선택지가 많습니다.
# Vue3
npm create vue@latest
 
# Vue2
npm install vue@^2        # Webpack으로 설치 시
vue create my-project     # Vue CLI로 설치 시
  • React 공식 문서는 Next.js, Remix, Gatsby, 네이티브 앱까지 4가지 방향을 안내합니다.
npx create-next-app@latest  # Next.js
npx create-remix            # Remix
npx create-gatsby           # Gatsby
npx create-expo-app         # 네이티브 앱
  • 위의 선택지는 모두 프레임워크입니다. 순수한 React 프로젝트를 만들려면 아래와 같이 생성하면 됩니다.
npx create-react-app {프로젝트명}

UI 표현하기

첫 번째 컴포넌트

  1. Vue는 <template> 안에 HTML을 작성하지만, React는 export default 접두사 형태로 JavaScript 안에서 마크업을 내보냅니다.
  2. React는 대문자로 시작하면 컴포넌트, 소문자로 시작하면 HTML 태그로 인식합니다. 따라서 컴포넌트 이름은 반드시 대문자로 시작해야 합니다.
  3. 마크업은 JSX 구문으로 작성합니다. img와 같은 단일 태그도 닫는 슬래시가 필요합니다. Vue는 HTML5 형태로 작성해도 되지만, React는 좀 더 엄격합니다.

JSX란? JavaScript를 확장한 문법으로, JavaScript 안에서 HTML과 비슷하게 마크업을 작성할 수 있도록 해 줍니다.

  1. 컴포넌트 안에 또 다른 컴포넌트를 정의하는 것은 지양해야 합니다.

컴포넌트 import 및 export 하기

  1. Next.js처럼 파일 기반으로 라우팅하는 프레임워크는 페이지별로 root 컴포넌트가 다릅니다. React와 Vue 모두 기본 root 컴포넌트는 App.js입니다.
  2. 컴포넌트를 import/export 하는 방식은 Vue의 <script>에서 export default {}를 활용하는 방식과 크게 다르지 않습니다.
  3. 파일 확장자 없이 import 할 수도 있지만, 빌드 단계에서 문제가 생기는 경우가 있습니다. 명확하게 .js까지 붙이는 편이 안전합니다.
  4. export 방식에는 named exportdefault export 두 가지가 있습니다.
    • export default는 개체가 하나임을 의미합니다. import 시 이름을 자유롭게 바꿔서 사용할 수 있습니다.
    • 복수의 개체를 내보낼 때는 named export를 주로 사용합니다.

JSX로 마크업 작성하기

  1. 여러 엘리먼트를 반환하려면 하나의 부모 태그로 감싸야 합니다.
    • <div> 같은 HTML 태그로 감싸도 되고, 불필요한 마크업을 추가하고 싶지 않다면 <></> (Fragment)로 감싸면 됩니다.
    • key를 사용해야 하는 경우(예: 반복문)에는 React에서 Fragment를 import해서 사용해야 합니다.
    • Vue2에서는 Fragment가 공식 지원이 아니라 서드파티 패키지를 쓰다가 렌더링 오류가 난 경험이 있었는데, React는 공식 지원이라 안심이 됩니다.
  2. 기존 HTML 방식의 속성명 습관은 버리고 camelCase로 작성해야 합니다.
    • classclassName, background-imagebackgroundImage
    • 단, aria-*data-* 속성은 HTML과 동일하게 작성합니다.

중괄호가 있는 JSX 안에서 JavaScript 사용하기

React는 변수를 받을 때 중괄호 {}를 사용합니다. Vue의 :(v-bind)와 {{ }}에 대응합니다.

const test = '안녕안녕'
 
// Vue.js
<img src="" :alt="test" />
<h1>{{ test }}</h1>
 
// React
<img src="" alt={test} />
<h1>{test}</h1>

React에서 중괄호를 2개({{ }}) 사용하면 객체를 의미합니다. Vue의 보간법({{ }})과 헷갈리지 않도록 주의해야 합니다.

컴포넌트에 props 전달하기

React에서 props를 자식 컴포넌트 내부에서 읽으려면 매개변수에 구조분해 할당 형태로 받습니다. Vue보다 훨씬 심플합니다.

// Vue
props: {
  person: { type: String },
  size: { type: Number },
}
 
// React
function Avatar({ person, size }) {}

기본값 지정도 마찬가지로 매개변수 기본값 문법을 그대로 사용합니다.

// Vue
props: {
  size: { type: Number, default: 100 },
}
 
// React
function Avatar({ person, size = 100 }) {}
  • props를 자식 요소에 전달할 때 spread 문법({...props})을 사용할 수도 있습니다.
  • 콘텐츠를 중첩해서 사용하고 싶다면 children props를 활용합니다. Vue의 <slot>에 해당합니다. 단, children은 하나이므로 named slot처럼 여러 개를 활용하는 패턴은 별도로 구현해야 합니다.
  • props는 읽기 전용으로 취급합니다. 상호작용이 필요하다면 state를 사용해야 합니다.

조건부 렌더링

  • 조건부로 아무것도 렌더링하지 않을 때는 null을 return 합니다.
  • 삼항 연산자(? :)와 논리 연산자(&&)를 활용하는 방식은 Vue와 동일합니다.

리스트 렌더링

  • 리스트 렌더링 시 고유한 key를 사용해야 합니다.
  • key에 index를 사용하면 미묘한 버그가 생길 수 있습니다. (Vue도 마찬가지입니다.)
  • map()filter()를 적극 활용합니다.

컴포넌트를 순수하게 유지하기

  • props, state, context는 읽기 전용으로 취급해야 합니다.
  • 렌더링은 언제든 발생할 수 있기 때문에, 컴포넌트는 서로의 렌더링 순서에 의존하지 않아야 합니다.
  • 화면을 업데이트하려면 state를 설정해서 업데이트해야 합니다.

트리로서의 UI

  • 렌더 트리: React 컴포넌트 간의 중첩 관계를 나타냅니다.
  • 모듈 의존성 트리: React 앱의 모듈 의존성을 나타냅니다. 컴포넌트뿐만 아니라 일반 모듈도 포함됩니다.
  • 실용적으로는 렌더 트리 개념만 잘 이해해도 충분합니다.

상호작용성 더하기

이벤트에 응답하기

  1. Vue에서는 $emit으로 이벤트를 핸들링했지만, React에서는 함수를 props로 전달합니다. 함수 활용이 훨씬 적극적입니다.
  2. 화살표 함수를 사용하는 것을 권장합니다.
  3. 이벤트 핸들러에는 적절한 HTML 태그를 사용해야 합니다. div 태그에 클릭 이벤트를 붙이는 방식은 지양합니다.
  4. React에서는 onScroll을 제외한 모든 이벤트가 전파됩니다.
    • e.preventDefault()e.stopPropagation()으로 필요 시 전파를 막아야 합니다.
    • Vue에서는 @click.stop.prevent로 간단하게 처리했던 부분을 직접 코드로 작성해야 해서 다소 번거롭습니다.

State: 컴포넌트의 기억 저장소

React에서 컴포넌트별 메모리를 state라고 부릅니다. Vue의 data와 유사한 개념입니다.

import { useState } from 'react';
 
const [index, setIndex] = useState(0); // 네이밍은 되도록 동일하게 유지
  • use로 시작하는 함수들을 Hook이라고 부릅니다.
  • state 변수명과 setter 함수명은 동일한 패턴([value, setValue])으로 유지하는 것을 권장합니다.
  • state는 여러 개 선언할 수 있습니다.

렌더링 그리고 커밋

React의 렌더링 흐름은 트리거 → 렌더링 → 커밋 순서입니다.

React 라이프사이클 다이어그램

참고: React Lifecycle Methods diagram

  1. 렌더링 트리거: createRoot로 DOM 노드와 함께 렌더링 가능한 위치를 파악합니다. Vue의 v.$mount('#app')에 해당합니다.
  2. 초기 렌더링: React가 document.createElement와 동일하게 DOM 노드를 생성합니다.
  3. state 업데이트 시: 렌더링을 트리거한 컴포넌트를 호출합니다.
  4. 커밋: 렌더링 결과를 비교해 차이가 있는 경우에만 DOM 노드를 변경합니다.
  5. 브라우저 렌더링: React가 DOM을 업데이트한 뒤 브라우저가 화면을 다시 그립니다.

렌더링 결과가 동일하면 React는 DOM을 건드리지 않습니다.

스냅샷으로서의 state

  • React에서 props, 이벤트 핸들러, 로컬 변수는 렌더링 당시의 state를 사용합니다.
  • setState를 호출해도 state가 즉시 업데이트되지 않고 비동기로 처리됩니다.
  • React는 state를 업데이트하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다립니다. (batching)
  • Vue에서 스냅샷 개념이 없었기 때문에, Vue → React 전환 시 예상치 못한 케이스가 발생할 수 있는 부분입니다.

State 업데이트 큐

  1. React는 이벤트 핸들러의 모든 코드가 실행될 때까지 state 업데이트를 기다립니다. 이를 batching이라고 합니다.
  2. 하나의 이벤트 핸들러에서 setState를 여러 번 호출해도, 모든 호출이 완료된 뒤 리렌더링이 1회 발생합니다. 불필요한 리렌더링을 줄이는 효과가 있습니다.
  3. 다음 렌더링 전에 여러 번 업데이트해야 한다면 화살표 함수(함수형 업데이트)를 활용합니다.

객체 state 업데이트하기

객체 state를 직접 변경(mutation)하면 리렌더링이 발생하지 않습니다. 항상 새로운 객체를 생성해서 전달해야 합니다.

// 직접 할당 금지 (Vue도 마찬가지)
position.x = e.clientX;
position.y = e.clientY;
 
// 올바른 방법: spread 문법으로 새 객체 생성
setPosition({ ...position, x: e.clientX, y: e.clientY });
  • 반복적인 복사 코드를 줄이고 싶다면 Immer 라이브러리를 사용할 수 있습니다. 다만 일반 JavaScript로 작성한 것이 성능 면에서 더 좋으며, Proxy를 사용하기 때문에 구형 브라우저나 React Native 환경에서는 주의가 필요합니다.
  • 객체 업데이트 시 항상 깊은 복사/얕은 복사를 염두에 두어야 합니다.

배열 state 업데이트하기

배열도 객체와 동일하게 직접 변경하지 않고, 새로운 배열을 만들어서 업데이트해야 합니다.

Vue와 React의 배열 업데이트 선호 방식 비교

Vue에서 선호하던 배열 변경 메서드(push, splice, $set 등)가 React에서는 비선호 방식에 해당하므로, 전환 시 특히 주의가 필요한 부분입니다.