7 분 소요

리액트 (React) : 사용자 인터페이스를 만들기 위한 JavaScript 라이브러리

  • 가상 DOM : 실제 DOM가 상호 작용하면서 웹 어플리케이션의 빠른 렌더링이 가능
  • 선언형 뷰 (Declarative View) : 사용자 인터페이스를 어떻게 보일지 명시적으로 선언하고, 개발자가 그 명세를 작성
  • 컴포넌트 기반 구조 : 부모-자식 관계 및 상태 (State)를 갖는 재사용 가능한 모듈들로 어플리케이션 구성
  • 단방향 데이터 바인딩 : 데이터의 흐름이 단일 방향으로 진행하여 디버깅 용이 → 명시적인 상태 변경이 가능
  • JSX : 자바스크립트 코드 내에 마크업을 작성해 가독성 향상 및 명확한 표현 가능

리액트에서 자주 쓰이는 JS 문법?

  • 구조 분해 할당 (Destructuring Assignment) : 배열 또는 객체의 값을 분해해서 할당하는 것
// 객체 구조 분해 할당
const person = { name: 'John', age: 30 };
const { name, age } = person;

console.log(name); // 'John'
console.log(age);  // 30
// 배열 구조 분해 할당
const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;

console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
  • 전개 구문 (Spread Syntax) : 배열이나 객체를 확장하여 개별 요소로 분리하거나 복사
// 배열의 전개 구문을 사용하여 두 배열을 병합
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

const mergedArray = [...arr1, ...arr2];
console.log(mergedArray); // [1, 2, 3, 4, 5, 6]
// 객체의 전개 구문을 사용하여 두 객체를 병합
const obj1 = { x: 1, y: 2 };
const obj2 = { z: 3, w: 4 };

const mergedObject = { ...obj1, ...obj2 };
console.log(mergedObject); // { x: 1, y: 2, z: 3, w: 4 }
  • 객체 초기자 (Object Shorthand Assignment) : 객체를 간결히 생성하고 초기값을 설정
// 일반적인 객체 생성 방법
const person1 = new Object();
person1.name = 'Alice';
person1.age = 25;

// 객체 초기자를 사용한 방법
const person2 = {
  name: 'Alice',
  age: 25
};

console.log(person1); // { name: 'Alice', age: 25 }
console.log(person2); // { name: 'Alice', age: 25 }

  • Array 프로토타입 메소드 : map, filter, reduce, forEach
const numbers = [1, 2, 3, 4];

// map 메소드: 각 숫자를 두 배로 만듭니다.
const doubledNumbers = numbers.map((num) => num * 2);

// filter 메소드: 짝수만을 걸러냅니다.
const evenNumbers = numbers.filter((num) => num % 2 === 0);

// reduce 메소드: 모든 숫자의 합을 계산합니다.
const sum = numbers.reduce((acc, num) => acc + num, 0);

// forEach 메소드: 각 숫자를 콘솔에 출력합니다.
numbers.forEach((num) => console.log(num));

console.log('Doubled Numbers:', doubledNumbers);
console.log('Even Numbers:', evenNumbers);
console.log('Sum:', sum);

싱글 페이지 어플리케이션 (SPA) : 렌더링 및 라우팅에 필요한 기능들을 브라우저의 JS에 의존

  • 첫 페이지에서 데이터를 모두 불러온 이후, 페이지 전환을 위한 모든 작업이 JS 및 브라우저로 이루어짐
    • 사이트 렌더링에 필요한 <body> 내부 내용을 모두 JS 코드로 삽입한 이후에 렌더링
    • 페이지 전환 시에도 새로운 HTML 페이지 요청 대신, JS에서 다음 렌더링에 필요한 정보만 가져와 반영
  • JAM 스택 : JS, API, Markup : F/E에서 JS와 마크업 (HTML, CSS)을 미리 빌드해 정적으로 제공

JS 리소스 크기가 커지면서, 웹 페이지가 로딩되어 사용자 인터렉션까지 걸리는 시간이 길어지는 문제 발생!

서버 사이드 렌더링 (SSR) : 최초에 사용자에 보여줄 페이지를 서버에서 렌더링해 사용자에게 제공

  • 최초 페이지 진입이 비교적 빠름 : First Contentful Paint이 빠름
  • 검색 엔진, SNS 공유와 같은 메타데이터 제공이 쉬움
  • 누적 레이아웃 이동 (Cumulative Layout Shift)이 적음
  • 사용자의 디바이스 성능에 비교적 자유로움
  • 민감한 작업을 서버 내에서 진행하여 보안 위험에 더 안전함
  • 소스 코드 전반에 걸쳐 서버 환경에 대한 고려 필요
  • 사용자의 요청을 받아 렌더링을 수행할 수 있는 적절한 서버가 구축되어 있어야 함
  • 병목 현상과 같은 서비스 지연이 발생하면, 사용자에게 어떠한 정보도 제공할 수 없음

리액트는 기본적으로 SPA이지만, SSR을 위한 API 또한 제공한다!

JSX (JavaScript XML) : 기존 JSHTML, CSS를 더해 리액트 컴포넌트 작성

  • JSXElement : JSX 엘리먼트
    (<div>Hello, World!</div>에서 <div>)
  • JSXAttribute : JSX 엘리먼트의 속성
    (<div className="container">Hello, World!</div>에서 className="container")
  • JSXChildren : JSX 엘리먼트의 자식 요소
    (<div>Hello, <span>React</span>!</div>에서 Hello, <span>React</span>!)
  • JSXString : JSX 문자열
    (<div>Hello, World!</div>에서 "Hello, World!")

가상 DOM (Virtual DOM) : 실제 DOM (Document Object Model) 에 대한 가벼운 복사본

  • 리액트 파이버 (React Fiber) : 가상 DOM과 실제 DOM을 비교해 변경 사항을 수집
    • 동기성 : 렌더링 작업을 여러 프레임에 나눠 렌더링 작업이 한 번에 실행되지 않더라도 앱이 반응적으로 유지 가능
    • 우선순위 관리 : 각 작업에 우선순위를 할당하고, 중요한 작업을 먼저 처리하여 사용자 인터랙션에 빠르게 응답 가능
    • 종료 및 재시작 가능 : 사용자 인터랙션에 우선순위를 두면서도 작업을 중단하고 다시 시작 가능

DOMCSSOM으로 렌더링 트리가 만들어지는 과정?

  1. 브라우저가 사용자가 요청한 주소를 방문해 HTML 파일을 다운로드
  2. 브라우저의 렌더링 엔진은 HTML을 파싱해 DOM 노드로 구성된 트리를 생성
  3. CSS 파일을 만나면, 해당 CSS 파일도 다운로드
  4. CSS 파일을 파싱하여 CSS 노드로 구성된 트리 (CSSOM)를 생성
  5. 사용자의 눈에 보일 DOM 노드만을 순회
  6. 눈에 보이는 노드를 대상으로 해당 노드에 대한 CSSOM 정보를 찾고 CSS 스타일 정보를 적용
    • 레이아웃 (Layout) : 각 노드가 브라우저 화면에 어느 좌표에 나타날지 계산하는 과정
    • 페인팅 (Painting) : 레이아웃 단계를 거친 노드에 색과 같은 실제 유효한 모습을 그리는 과정

… 모든 DOM의 변경보다 결과적으로 만들어질 DOM의 최종 결과물만을 제공하자!

  1. 초기 렌더링 : 초기 상태에서 가상 DOM은 실제 DOM과 동일한 구조를 가짐
  2. 상태 변화 감지 : 사용자 상호 작용이나 데이터 변경 등의 이벤트가 발생하면, 렌더링 엔진은 새로운 가상 DOM 생성
  3. 가상 DOM 비교 : 새로운 가상 DOM과 이전의 가상 DOM을 비교하여 변경된 부분 탐색
  4. 실제 DOM 업데이트 : 변경된 부분만을 실제 DOM에 적용

컴포넌트 (Component) : 리액트 애플리케이션에서 UI를 구성하고 재사용 가능한 모듈을 표현

  • 속성 (Props) 을 받고, 상태 (state)와 메소드 (Method)를 가짐
  • 반복되는 UI 단위 (JS Code 또는 HTML-JSX) → 재사용성과 가독성을 위한 도구
  • 가능한 독립적으로 실행되도록 작성하고, 데이터 영역과 UI를 분리

상태 (State) : 사용자 상호작용에 응답하거나 컴포넌트의 생명주기에 변경되는 정보를 저장하는 데에 사용

  • 부모의 상태가 변경되면, 그 상태를 참조하는 모든 자식 컴포넌트가 Re-Rendering
    • 상태를 통합하면 Re-Rendering이 많이 일어나므로, 상태를 연관성에 맞게 분리해 좁게 사용해야 함
    • Setter로 값을 수신하면 Dispatcher 값이 세팅 → re-renderingtrigger (DOM 갱신)
import { useState } from 'react';

function ExampleComponent() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('안녕하세요!');

  return (
    <div>
      <p>{message}</p>
      <p>카운트: {count}</p>
    </div>
  );
}

초기화는 동기 방식이면 오래 걸리더라도 가능, Promiseasync/await로 처리해도 Promise가 세팅

속성 (Prop) : 부모 컴포넌트로부터 자식 컴포넌트로 데이터를 전달하는 데에 사용

  • 변수나 함수와 같은 속성들을 부모에서 자식으로 전달 (MyComponent.defaultProps = { ... })
    • Read-Only : 속성이 변경되도 Re-Rendering하지 않음! (값은 바꿔도 화면엔 아무런 영향 없음!)
// 부모 컴포넌트
import ChildComponent from './ChildComponent';

function ParentComponent() {
  const dataToPass = "안녕하세요!";
  
  return (
    <ChildComponent message={dataToPass} />
  );
}

// 자식 컴포넌트
function ChildComponent(props) {
  return (
    <div>
      <p>{props.message}</p>
    </div>
  );
}

컴포넌트 개발 원칙 : 코드의 가독성, 유지보수성, 재사용성을 향상하여 구성 요소를 생성

  • 가급적 순수 함수 컴포넌트 (Pure Functional Component)를 활용할 것!
    • 상태나 생명주기 메서드 없이 단순히 props를 받아 렌더링하는 컴포넌트 → 간결하고 재사용성이 높힐 수 있음
  • Container ComponentPresentational Component를 분리할 것
    • 데이터를 가져오는 로직과 UI를 그리는 로직을 분리하여 관리
  • 상태를 공유하는 단위로 분리할 것
    • 관련된 상태를 함께 관리하고, 필요한 경우 useContext를 사용하여 상태 공유를 구현
  • 아주 깊은 구조로 중첩되는 Container 컴포넌트는 피할 것
    • Context를 남발하는 대신, 상태 관리 등의 용도로 필요한 경우에만 적절히 사용할 것
  • 각 컴포넌트는 독립적으로 작성할 것
    • 컴포넌트 간의 결합도를 최소화해 재사용성을 높이고 유지보수를 용이하게 만들어야 함

렌더링 (Rendering) : HTML, CSS 리소스로 웹 페이지의 UI를 그리는 과정

  • 리액트의 렌더링 : 리액트 어플리케이션 트리 내의 모든 컴포넌트들이 갖고 있는 현재 상태와 속성의 값을 기반하여 어떻게UI를 구성하고 어떤 DOM 결과를 브라우저에 제공할지 계산하는 일련의 과정
    • 초기 렌더링 : 사용자가 처음 어플리케이션이 진입할 때 최초로 수행
    • 리렌더링 (Re-rendering) : 처음 어플리케이션 이후에 발생하는 모든 렌더링들을 총칭
      • 클래스 컴포넌트의 setState()forceUpdate()가 실행되는 경우
      • 함수 컴포넌트의 useState()의 두번째 배열 요소인 setter가 실행되는 경우
      • 함수 컴포넌트의 useReducer()의 두번째 배열 요소인 dispatch가 실행되는 경우
      • 컴포넌트의 key props가 변경되는 경우
      • 부모 컴포넌트로부터 전달받는 값인 props가 변경되거나 부모 컴포넌트가 리렌더링될 경우

렌더링 프로세스 (Rendering Process) : 리액트의 재조정을 위해 변경 사항을 탐색, 비교, 수집하는 과정

  1. 리액트가 컴포넌트의 루트에서부터 업데이트가 필요한 모든 컴포넌트를 탐색
  2. 클래스 컴포넌트는 클래스 내 render() 함수를, 함수 컴포넌트는 FunctionComponent() 자체를 호출
  3. JSX로 구성된 렌더링 결과물을 자바스크립트로 컴파일하면 React.createElement()를 반환
  4. React.createElement()가 브라우저 UI 구조를 설명하는 자바스크립트 객체를 반환

(1) 렌더 단계 (Render Phase) : 컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 작업

  1. 렌더링 프로세스에서 컴포넌트를 실행 (render() 또는 return)
  2. 이전 가상 DOM과 결과 가상 DOM를 비교
  3. type, props, key를 비교하여, 하나라도 변경된 것이 있으면 변경이 필요한 컴포넌트로 체크

(2) 커밋 단계 (Commit Phase) : 렌더 단계의 변경 사항을 실제 DOM에 적용해 사용자에게 표현

  1. 리액트가 먼저 DOM을 커밋 단계에서 업데이트
  2. 생성된 모든 DOM 노드 및 인스턴스를 가리키도록 리액트 내부의 참조를 업데이트
  3. 클래스 컴포넌트는 componentDidMount, componentDidUpdate 메소드를,
    함수 컴포넌트는 useLayoutEffect 훅을 호출
  • 리액트의 렌더링이 일어난다고 무조건 DOM 업데이트가 일어나지 않음! (커밋 단계 생략 가능)
    • 리액트의 렌더링은 가시적 변경 없이 발생 가능!! (커밋 단계가 생략되어 DOM 업데이트가 발생하지 않음)

렌더링 시나리오 - 어플리케이션의 숫자 증가 버튼

import { useState } from 'react'

export default function A() {
  return (
  <div className="App">
    <h1>Hello React!</h1>
    <B />
  </div>
  )
}

function B() {
  const [counter, setCounter] = useState(0)

  function handleButtonClick() {
    setCounter((previous) => previous + 1)
  }

  return (
    <>
      <label>
        <C number={counter}/>
      </label>
      <button onClick={handleButtonClick}></button>
    </>
  )
}

function C({number}: {number: number}) {
  return (
    <div>
      {number} <D/>
    </div>
  )
}

function D() {
  return (
    <>
      안녕 리액트!
    </>
    );
}
  1. B 컴포넌트의 setState 호출
  2. B 컴포넌트의 리렌더링 작업이 큐에 들어감
  3. 리액트는 트리 최상단에서부터 렌더링 경로를 갱신
  4. A 컴포넌트가 리렌더링이 필요한 컴포넌트로 표시되어 있지 않으므로, 커밋 단계를 생략
  5. B 컴포넌트가 리렌더링이 필요한 컴포넌트로 표시되어 있으므로, B를 리렌더링
  6. B 컴포넌트가 C 컴포넌트를 반환
  7. C 컴포넌트의 propsnumber가 업데이트
  8. C 컴포넌트가 리렌더링이 필요한 컴포넌트로 표시되어 있으므로, C를 리렌더링
  9. C 컴포넌트가 D 컴포넌트를 반환
  10. D 컴포넌트의 부모인 C 컴포넌트가 리렌더링이 필요한 컴포넌트로 표시되어 있으므로, D를 리렌더링

태그:

업데이트: