[React] 개념 정리

이희주·2025년 1월 16일
0

React.js

목록 보기
2/3

리액트의 기본 요소

JSX (A syntax extension to JavaScript)

  • JavaScript와 XML/HTML을 합친 것.
  • JSX는 내부적으로 XML/HTML 코드를 자바스크립트로 변환하는 과정을 거치게됨.
    • 실제로 JSX로 코드를 작성해도 최종적으로는 자바스크립트 코드가 나오는 것.
  • 리액트에서 JSX 문법을 사용하면 내부적으로 모두 createElement라는 함수를 사용하도록 변환됨.
    • 최종적으로는 이 createElement() 함수를 호출한 결과로 자바스크립트 객체가 나오게됨.
       const element = (
       	<h1 className="greeting">
       		Hello, world!
       	</h1>
       )
       
       const element = React.createElement(
       	'h1',
       	{ className: 'greeting' },
       	'Hello, world!'
       )
       
       const element = React.createElement(
       	type,
       	[props],
       	[...children]
       )
  • 장점:
    • 코드가 간결해지고 가독성이 향상됨.
    • Injection Attack을 방어함으로써 보안성이 올라감.

엘리먼트

  • 리액트 앱의 가장 작은 빌딩 블록들
  • 실제 브라우저의 DOM에 존재하는 엘리먼트는 DOM 엘리먼트가 되는 것이고, 리액트의 Virtual DOM에 존재하는 엘리먼트가 리액트 엘리먼트가 되는 것.
    • 리액트 엘리먼트 === DOM 엘리먼트의 가상 표현
  • 렌더링되는 과정은 Virtual DOM에서 실제 DOM으로 이동하는 과정
  • 특징: 불변성
    • 엘리먼트는 생성 후에는 children이나 attributes를 바꿀 수 없다.
    • 즉, 한 번 생성된 다음에는 변경이 불가능하다.
    • 화면에 변경된 엘리먼트들을 보여 주기 위해서는 기존 엘리먼트를 변경하는 것이 아니라 새로운 엘리먼트를 만들면 된다. → 기존 엘리먼트와 바꿔치기하는 것
    • 상태 관리 & 화면이 갱신되는 횟수 → 엘리먼트가 새롭게 생성된다는 것을 이해하고 있으면 효율적으로 개발할 수 있음.

컴포넌트와 Props

  • 리액트 컴포넌트는 개념적으로 자바스크립트의 함수와 비슷하다.
    • 어떠한 속성(property, 줄여서 prop)들을 입력으로 받아서 그에 맞는 리액트 엘리먼트를 생성하여 리턴해 주는 것.
  • 컴포넌트의 모습과 속성을 결정하는 것이 바로 props.
  • props는 컴포넌트에 전달할 다양한 정보를 담고 있는 자바스크립트 객체이다.
  • props는 읽기 전용(Read-Only)
    • 모든 리액트 컴포넌트는 그들의 props에 관해서는 Pure 함수 같은 역할을 해야 한다.
    • 즉 props를 직접 바꿀 수 없고, 같은 props에 대해서는 항상 같은 결과를 보여줄 것.
  • 컴포넌트의 이름은 항상 대문자로 시작해야 된다.
    • 왜? 리액트는 소문자로 시작하는 컴포넌트를 DOM 태그로 인식하기 때문.

State

  • 리액트 컴포넌트의 상태 === 리액트 컴포넌트의 변경 가능한 데이터
  • state가 변경될 경우 컴포넌트가 재렌더링됨.
  • 렌더링이나 데이터 흐름에 사용되는 값만 state에 포함시켜야 함.
  • state는 하나의 자바스크립트 객체이다.
  • state는 직접적인 변경이 불가능하다고 생각하는 것이 좋다.
    • state를 직접 수정할 수는 있지만 리액트가 수정된 것을 제대로 인지하지 못할 수 있기 때문
    • state를 변경하고자 할 때에는 set함수를 사용하여 state 값을 변경
    • 클래스 컴포넌트 → state를 생성자에서 정의
    • 함수 컴포넌트 → state를 useState()라는 훅을 사용해서 정의

생명주기

지금은 클래스 컴포넌트를 거의 사용하지 않기 때문에 간단하게만 기억할 것.

  • 컴포넌트의 생명주기는 마운트(Mount), 업데이트(Update), 언마운트(Unmount) 과정으로 이루어짐.
  • 컴포넌트는 계속 존재하는 것이 아니라 시간의 흐름에 따라 생성되고 업데이트되다가 사라지는 과정을 겪음.

  • 함수 컴포넌트는 클래스 컴포넌트와 다르게 별도로 state를 정의해서 사용하거나 컴포넌트의 생명주기에 맞춰 어떤 코드가 실행되도록 할 수 없었다. 따라서 함수 컴포넌트에 이런 기능을 지원하기 위해서 나온 것이 바로 훅이다. 훅을 사용하면 함수 컴포넌트도 클래스 컴포넌트의 기능을 모두 동일하게 구현할 수 있다.
  • 리액트의 state와 생명주기 기능에 갈고리를 걸어 원하는 시점에 정해진 함수를 실행되도록 만든 것
  • 훅의 규칙:
    • 무조건 최상위 레벨에서만 호출해야 함
      • 반복문이나 조건문 또는 중첩된 함수들 안에서 훅을 호출하면 안 됨
      • 훅은 컴포넌트가 렌더링될 때마다 매번 같은 순서로 호출되어야 함
      • 그렇게 해야 리액트가 다수의 훅의 호출에서 컴포넌트의 state를 올바르게 관리할 수 있음
    • 리액트 함수 컴포넌트에서만 훅을 호출해야 함
      • 훅은 리액트 함수 컴포넌트에서 호출하거나 직접 만든 커스텀 훅에서만 호출할 수 있음
  • 커스텀 훅
    • 이름이 use로 시작하고 내부에서 다른 훅을 호출하는 단순한 자바스크립트 함수
    • 파라미터로 무엇을 받을지, 어떤 것을 리턴해야 할지를 개발자가 직접 정할 수 있음
    • 이름이 use로 시작하지 않으면 특정 함수의 내부에서 훅을 호출하는지를 알 수 없기 때문에 훅의 규칙 위반 여부를 자동으로 확인할 수 없음

이벤트 핸들링

  • 이벤트 처리
    DOM의 이벤트리액트의 이벤트
    표기이벤트의 이름을 모두 소문자로 표기이벤트의 이름을 카멜 표기법으로 표기
    함수 전달이벤트 처리할 함수를 문자열로 전달이벤트 처리할 함수를 그대로 전달
  • 이벤트 핸들러
    • 클래스 컴포넌트
      • 클래스의 함수로 정의하고 생성자에서 바인딩해서 사용
      • 클래스 필드 문법도 사용가능
    • 함수 컴포넌트
      • 함수 안에 함수로 정의하거나 화살표 함수(arrow function)를 사용해서 정의

조건부 렌더링

  • 조건에 따라 렌더링의 결과가 달라지도록 하는 것
    • 인라인 If → 논리 연산자 && 사용
    • 인라인 If-Else → 삼항 연산자 ? 사용
  • 리액트에서는 null을 리턴하면 렌더링되지 않음

리스트와 키

  • 리스트의 키
    • 리스트에서 아이템을 구분하기 위한 고유한 문자열
    • 리스트에서 어떤 아이템이 변경, 추가 또는 제거되었는지 구분하기 위해 사용
    • 리액트에서는 키의 값은 같은 리스트에 있는 엘리먼트 사이에서만 고유한 값이면 됨
  • 배열에서 아이템의 순서가 바뀔 수 있는 경우에는 키값으로 인덱스를 사용하는 것을 권장하지 않음
  • 리액트에서는 키를 명시적으로 넣어 주지 않으면 기본적으로 이 인덱스 값을 키값으로 사용

HTML 폼

  • 각 엘리먼트가 자체적으로 state를 관리
  • 자바스크립트 코드를 통해 사용자가 입력한 값에 접근하기에는 불편한 구조
  • 왜?
    • 직접 DOM에 접근 필요 → 폼 요소가 많아질수록 코드가 복잡해지고 유지보수가 어려워짐
    • 상태 관리의 부재 → HTML 자체에는 내장된 상태 관리 기능이 없어, 이벤트 리스너를 추가하고 해당 값을 수동으로 추적해야함
    • 이벤트 기반 값 접근 → 사용자가 입력한 값을 실시간으로 추적하려면 input이나 change 이벤트 리스너를 추가해야 하며, 매번 값을 갱신하는 로직을 작성해함
    • 동기화 문제 → JavaScript에서 값을 업데이트한 경우, DOM 요소와 JavaScript 변수 간의 동기화를 수동으로 처리해야함

리액트 폼

  • 리액트에서는 폼 요소를 제어 컴포넌트(controlled component)로 다룬다.

    • 제어 컴포넌트는 리액트에서 모든 값을 통제할 수 있는 구조를 갖고 있다.
    • 제어 컴포넌트를 사용하면 입력값이 리액트 컴포넌트의 state를 통해 관리 된다.
    • 즉, 여러개의 입력 양식 값을 원하는 대로 조종할 수 있다. 초깃값을 내가 원하는 대로 넣어줄 수 있으며, 다른 양식의 값이 변경되었을 때 또 다른 양식의 값도 자동으로 변경시킬 수 있다. 이처럼 제어 컴포넌트를 통해 사용자의 입력을 직접 제어할 수 있다.
  • 차이점 요약

    HTML 폼리액트 폼
    값 접근DOM 접근 필요 (document.getElementById)상태를 통해 값 접근 가능 (useState)
    상태 관리없음내장 상태 관리 (useState)
    입력값 추적이벤트 리스너 필요onChange로 실시간 추적
    복잡도폼이 복잡할수록 코드 관리 어려움폼 복잡도와 상관없이 상태 기반으로 관리 가능

State 끌어올리기

  • Shared State
    • 자식 컴포넌트들이 부모 컴포넌트와 공통된 state에 있는 데이터를 사용해야 하는 경우, 부모 컴포넌트의 state를 공유하여 사용하는 것
  • state 끌어올리기 (lifting state up)
    • 자식 컴포넌트에서 개별적으로 상태를 관리하지 않고, 부모 컴포넌트에서 상태를 관리하여 자식 컴포넌트 간의 값을 공유하고 연산 결과를 표시
    • 이 구조를 사용하면 부모 컴포넌트가 상태를 일괄적으로 관리할 수 있어, 여러 자식 컴포넌트가 상태를 공유하거나 협력해야 할 때 유용

합성 vs 상속

  • Containment
    • 하위 컴포넌트를 포함하는 형태의 합성 방법
    • 리액트 컴포넌트의 props에 기본적으로 들어 있는 children 속성을 사용
    • 여러 개의 children 집합이 필요한 경우 별도로 props를 각각 정의해서 사용
  • Specialization
    • 범용적인 개념을 구별되게 구체화하는 것
    • 범용적으로 쓸 수 있는 컴포넌트를 만들어 놓고 이를 구체화시켜서 컴포넌트를 사용하는 합성 방법
  • Containment와 Specialization을 함께 사용하기
    • props.children을 통해 하위 컴포넌트를 포함시키기 (Containment)
    • 별도의 props를 선언하여 구체화 시키기 (Specialization)
  • 상속
    • 리액트에서는 상속이라는 방법을 사용하는 것보다는 합성을 사용해서 개발하는 것이 더 좋음
    • 복잡한 컴포넌트를 쪼개 여러 개의 컴포넌트로 만들고, 만든 컴포넌트들을 조합하여 새로운 컴포넌트를 만들자

컨텍스트

  • 데이터를 기존의 props를 통해 전달하는 방식이 아닌 컴포넌트 트리(component tree)를 통해 곧바로 컴포넌트에 전달하는 방식
  • 여러 컴포넌트에서 계속 접근이 일어날 수 있는 데이터들이 있는 경우에 컨텍스트를 사용하는 것이 좋다.
  • 컨텍스트 API의 단점
    • Context API에서 상태가 변경되면, 해당 컨텍스트를 구독하고 있는 모든 하위 컴포넌트(consumer component)가 리렌더링됨.
    • 이는 트리 구조가 깊고 상태 변경이 잦은 애플리케이션에서 성능 문제를 야기할 수 있음.
    • 미들웨어를 통한 로깅, 디버깅, 비동기 작업 관리 등의 기능이 부족하여 복잡한 상태(API 호출, 캐싱, 유효성 검사 등)를 다루기에 적합하지 않음.
    • Context API는 단순한 데이터 전달에는 적합하지만, 성능 문제와 복잡성 때문에 Redux, Zustand 같은 전문 상태 관리 도구를 함께 사용하는 것이 더 효율적인 경우가 많음 → 상황에 따라 적절히 선택

리액트 18

자동 배칭 (Automatic Batching)

  • 배칭(batching) → 여러 state의 업데이트가 동시에 발생할때 여러 작업을 묶어서 한 번에 처리하는 작업
  • 기존 리액트에서는 이벤트 핸들러 내부의 state update 작업에 대해서만 Batching 이 가능
    • 하지만 Promise나 setTimeout, Native Event Handler 내부의 작업은 불가능
    • 왜냐하면 이전에는 브라우저의 이벤트가 실행되는 중에만 Batching 작업을 수행했기 때문에 이벤트가 종료된 후에 실행되는 경우는 Batching 작업이 불가능
  • 리액트 18에서 자동 배칭(Automatic Batching)이라는 기능이 새롭게 등장
    • 자동으로 여러 state의 업데이트 작업을 묶어서 한번에 처리
    • 단순히 이벤트 핸들러 내부 뿐만이 아니라 Promise나 setTimeout, Native Event Handler 같은 작업에 대해서도 Batching 작업을 자동으로 수행

트랜지션 (Transitions)

  • 리액트에서 긴급한 업데이트와 긴급하지 않은 업데이트를 구분해서 처리하기 위해 등장한 새로운 개념
    • React 애플리케이션에서는 여러 상태 업데이트가 동시에 발생할 수 있음 → Transition은 이러한 문제를 해결하기 위해 도입 → 어떤 상태 변화를 Transition(startTransition() 혹은 useTransition())으로 표시함으로써, 사용자 입력과 같은 바로 처리되어야 하는 상태 변화와 상대적으로 느려도 문제가 없는 상태 변화를 구분
    • 사용자에게 더 빠르고 더 나은 사용자 경험을 제공하기 위해서
    • 각종 업데이트가 서로 다른 우선순위를 가지고 효율적으로 관리될 수 있음
  • 긴급한 업데이트
    • 사용자와 직접적인 인터렉션이 일어나는 경우
    • 예) 글자 입력, 버튼 클릭 등
  • 긴급하지 않은 업데이트
    • 사용자와 직접적인 인터렉션이 일어나지 않는 경우
    • 예) 서버에서 결과를 받아와 보여 주는 경우
    • 긴급하지 않은 업데이트로 처리되는 경우 더 긴급한 업데이트가 발생하면 중단될 수 있음

서스펜스 (Suspense)

  • 하위 컴포넌트(children)가 준비되기 전까지 렌더링을 중단하고, 하위 컴포넌트가 준비된 이후에 렌더링을 진행 → 사용자 경험을 향상시켜줌.
  • 기존에는 클라이언트에서 Suspense가 Code Splitting과 함께 제한적으로 사용되었지만, 리액트 18부터 서버 렌더링과 Data Fetching(제한적으로)에서도 사용할 수 있게 되었다.
    • 서버에서 페이지의 일부가 느리게 준비되더라도 사용자는 점진적으로 로딩되는 페이지를 볼 수 있음.
    • 또한 lazy 컴포넌트가 아직 준비되지 않았지만 Suspense로 래핑 된 경우, 리액트는 청크 파일이 로드될 때까지 기다리지 않고 이미 렌더링 된 부분들을 hydrate 할 수 있음.

리액트 DOM 서버

  • 서버에서 동작하고 렌더링 된다는 특성상 다양한 종류의 백엔드 리소스에 접근할 수 있고 HTML이 아닌 ‘특별한’ 형태로 렌더링 되어 클라이언트에 전달되기 때문에 클라이언트로 전달되는 번들 사이즈 또한 감소시킬 수 있다.
  • 서버 컴포넌트는 서버에서 동작하기 때문에 데이터베이스, 파일 시스템 그리고 인터널 서비스 같은 서버 사이드 데이터 소스에 직접 접근할 수 있다.
  • 서버 컴포넌트 코드는 브라우저에 다운로드되지 않고 서버에서 미리 렌더링 된 static content를 전달하기 때문에 패키지를 추가해도 번들 사이즈에 영향을 끼치지 않는다.
  • 유저 인터랙션이 없는 컴포넌트들을 서버 컴포넌트로 마이그레이션 한다면 동일한 뷰를 제공함과 동시에 번들 사이즈와 초기 로딩 시간을 감소시킬 수 있다.
  • Code splitting을 서버 컴포넌트에서
    • 서버 컴포넌트에서 import 되는 모든 클라이언트 컴포넌트를 code splitting 포인트로 간주하기 때문에 더 이상 React.lazy로 메뉴얼 하게 명시하지 않아도 된다.
    • 서버에서 미리 필요한 컴포넌트를 선택하기 때문에 클라이언트는 렌더링 프로세스 초기에 번들을 다운로드할 수 있다.
  • 리액트 서버 컴포넌트의 도입으로 기존 컴포넌트는 목적에 따라 서버(data fetching), 클라이언트(유저 인터랙션) 그리고 공유 컴포넌트로 세분화할 수 있다.
    • 클라이언트 컴포넌트는 파일 시스템 접근과 같은 server-only feature를 사용할 수 없다.
    • 클라이언트 컴포넌트는 다른 클라이언트 컴포넌트만 import할 수 있다.
    • 서버 컴포넌트는 state와 같은 client-only feature를 사용할 수 없다.
  • 리액트 서버 컴포넌트(RSC)와 서버 사이드 렌더링(SSR)
    • 리액트 서버 컴포넌트는 서버 사이드 렌더링의 대체재가 아니다. 하지만 사용자 경험 향상을 위해 함께 사용할 수는 있다.
    • 서버 컴포넌트와 서버 사이드 렌더링은 서버에서 렌더링 된다는 유사점이 있지만 해결하고자 하는 문제점이 다르다.
    • 서버 컴포넌트의 코드는 클라이언트로 전달되지 않는다. 하지만 서버 사이드 렌더링의 모든 컴포넌트의 코드는 자바스크립트 번들에 포함되어 클라이언트로 전송된다.
    • 서버 컴포넌트는 클라이언트 상태를 유지하며 refetch 될 수 있다. 서버 컴포넌트는 HTML이 아닌 특별한 형태로 컴포넌트를 전달하기 때문에 필요한 경우 포커스, 인풋 입력값 같은 클라이언트 상태를 유지하며 여러 번 데이터를 가져오고 리렌더링하여 전달할 수 있다. 하지만 SSR의 경우 HTML로 전달되기 때문에 새로운 refetch가 필요한 경우 HTML 전체를 리렌더링 해야 하며 이로 인해 클라이언트 상태를 유지할 수 없다.
    • 서버 컴포넌트는 서버 사이드 렌더링 대체가 아닌 보완의 수단으로 사용할 수 있다. 서버 사이드 렌더링으로 초기 HTML 페이지를 빠르게 보여주고, 서버 컴포넌트로는 클라이언트로 전송되는 자바스크립트 번들 사이즈를 감소시킨다면 사용자에게 기존보다 훨씬 빠르게 인터랙팅한 페이지를 제공할 수 있을 것이다.
  • 서버 컴포넌트를 사용한다면 클라이언트 data fetching의 큰 문제점인 네트워크 waterfall을 해결할 수 있다.
  • 더불어 번들 사이즈 감소, 다양한 백엔드 data source 사용, 자동 코드 분할, 컴포넌트 별 관심사 분리 등 다양한 이점으로 사용자 경험뿐만이 아니라 개발자 경험 또한 향상될 수 있다.

새로운 Strict 모드 작동 방식

  • React 18에서는 useEffect가 여러번 실행될 때 멱등성을 보장하는것을 표면적으로 보여지도록 하기위해 즉 렌더링 횟수에따라 결과가 바뀌는 것을 방지하기 위해 Strict Mode에 해당 기능을 추가하였다.
  • Strict Mode는 Effect의 setup > cleanup > setup 로직을 실행하여, 특히 컴포넌트가 변경될 때마다 발생할 수 있는 버그를 찾아내는 데 유용하며 이를 통해 컴포넌트의 멱등성을 어느정도 보장해준다.
  • 컴포넌트 생명주기 함수가 예상과 다르게 여러 번 호출될 수 있는데, 이러한 점을 잘 인지하고 컴포넌트가 여러 번 마운트되어도 문제가 생기지 않도록 개발하는 것이 중요하다.



📚 참고문서: 소플의 처음 만난 리액트
🔎 React 18: 리액트 서버 컴포넌트 준비하기

0개의 댓글