[리액트] 신입 리액트 개발자가 알아야 할 기본 개념

Woonil·2025년 8월 22일
0

리액트

목록 보기
1/4
post-thumbnail

React(React.js 또는 ReactJS)는 자바스크립트 라이브러리로서, 사용자 인터페이스(User Interface, UI)를 만들기 위해 사용된다. 페이스북(현 메타)에 의해 제작되었으며 유지보수되고 있다. React는 SPA나 모바일 애플리케이션 개발에 사용될 수 있다. 대규모 또는 복잡한 리액트 애플리케이션 개발에는 보통 라우팅, API 통신 등의 기능이 요구되는데 리액트에서는 기본적으로 제공되지 않기 때문에 추가 라이브러리를 사용해야 한다.

사용자 정의 태그를 만드는 기술을 사용하며, 가상 돔(Virtual DOM)을 사용하여 우리가 원하는 페이지를 효율적이고 빠르게 브라우저에 렌더링해준다. 이때, 렌더링이란 React에서 컴포넌트를 호출하는 것을 의미한다.

  • 특징 [출처: ChatGPT]
    • 데이터 바인딩: Vue.js는 양방향 데이터 바인딩을 제공합니다. 이는 모델과 뷰가 서로를 자동으로 업데이트할 수 있게 해주어, 데이터 관리를 좀 더 직관적으로 할 수 있습니다. 반면, React는 단방향 데이터 흐름을 사용합니다. 데이터가 컴포넌트 계층을 통해 위에서 아래로만 흐르게 함으로써, 상태 관리의 예측성과 유지보수성을 높입니다.
    • 가상 DOM: 두 프레임워크 모두 가상 DOM을 사용하지만, 구현 방식에 차이가 있습니다. React는 컴포넌트의 상태가 변경될 때마다 가상 DOM을 재구성하고, 이를 실제 DOM과 비교하여 업데이트합니다. Vue.js는 각 컴포넌트의 의존성을 추적하여, 상태 변경이 발생하면 관련된 컴포넌트만 리렌더링합니다.
    • 프레임워크 대 라이브러리: Vue.js는 더 완전한 프레임워크로서, 라우터와 상태 관리 솔루션 등을 내장하거나 공식적으로 지원하는 반면, React는 주로 UI 라이브러리로서, 라우팅이나 상태 관리 등은 외부 라이브러리를 통해 추가해야 합니다.

🤔개념

🔗Component

UI를 구성하는 독립적이고 재사용 가능한 코드 조각(마크업, CSS, JS를 포함)이며, 이는 특정 기능이나 UI 요소를 캡슐화한다. 과거에는 클래스형 컴포넌트가, 최근에는 Hooks의 도입으로 함수형 컴포넌트가 주로 사용되고 있다. 함수형 컴포넌트는 클래스형 컴포넌트에 비해 더욱 간결하고 이해하기 쉬운 코드를 작성할 수 있게 한다.

객체지향설계의 SOLID 중 S, 즉 SRP(Single Responsibility Principle, 단일 책임 원칙)에 맞게 컴포넌트를 잘 설계하면 재사용성과 유지보수성을 높일 수 있다.

React 컴포넌트명은 대문자로 시작
React 컴포넌트는 일반 JavaScript 함수이지만, 이름은 대문자로 시작해야 한다.

  • 구조: React는 JSX라는 JavaScript 확장 문법을 사용하여 마크업과 로직을 함께 작성할 수 있게 하며, 이는 JavaScript의 전체 기능을 활용할 수 있게 해준다(Vue.js는 HTML 기반의 템플릿을 사용하고, 템플릿 내에서 간단한 JavaScript 표현식을 사용할 수 있다). 이는 HTML과 CSS에 더 친숙한 개발자들에게 접근성을 높여준다.
  • 패턴: 합성 컴포넌트 패턴, IoC 패턴 등 개발작 경험을 향상할 수 있는 컴포넌트 작성법이 있다. 자세한 내용은 별도의 포스팅에서 다룰 예정이다.

🔗JSX

JavaScript를 확장한 문법으로, JavaScript 파일을 HTML과 비슷하게 마크업을 작성할 수 있도록 해준다. React에서는 렌더링이 JSX의 순수한 계산이어야 하며, DOM 수정과 같은 부수 효과를 포함해서는 안된다.

부수 효과를 렌더링 연산에서 분리하기 위해서는 useEffect로 감싸야 한다.

규칙

  • 하나의 루트 엘리먼트로 반환: JSX는 HTML처럼 보이지만 내부적으로는 일반 JavaScript 객체로 변환되므로, 하나의 배열로 감싸지 않은 하나의 함수에서는 두 개의 객체를 반환할 수 없다. 따라서 한 컴포넌트에서 여러 엘리먼트를 감싸서 반환할 때, <div> 또는 <>,</> (Fragment)를 사용할 수 있다. Fragment는 HTML 트리 구조에 포함되지 않고 그룹화 해준다.
    • React.Fragment: 루트 엘리먼트를 <div> 로 묶을 수 있지만 이를 남발할 경우 css스타일링의 불편하고, <table>처럼 정해진 구조를 따라할 경우 문제가 생긴다. 따라서 <Fragment> (<> 로도 사용 가능함)를 사용하는 것이 권장된다.
    • 모든 태그는 닫아준다.
    • 속성은 대부분 camelCase로 작성한다.

      aria-* , data-* 속성은 HTML에서와 동일하게 - (대시)를 사용하여 작성한다.

JSX는 React에 종속되는 개념?
JSX와 React는 서로 다른 별개의 개념으로, 종종 함께 사용되기도 하지만 독립적으로 사용할 수도 있다. JSX는 확장된 문법이고, React는 JavaScript 라이브러리이다.

Fragment를 사용해야 하는 경우
map() 사용 시, 각 요소를 <>로 묶으면 key를 사용할 수 없으므로, 이러한 경우 반드시 <Fragment>로 명시해야 한다.

JSX 내에서 자바스크립트 사용

  • 따옴표로 문자열 전달
      export default function Avatar() {
        return (
          <img
            className="avatar"
            src="https://i.imgur.com/7vQD0fPs.jpg"
            alt="Gregorio Y. Zara"
          />
        );
      }
  • 중괄호 사용
    • JSX 태그 내 문자: 마크업에서 바로 JavaScript 논리와 변수를 가져와 사용할 수 있다.
      export default function TodoList() {
        const name = 'Gregorio Y. Zara';
        return (
          <h1>{name}'s To Do List</h1>
        );
      }
    • = 바로 뒤에 오는 속성
      export default function Avatar() {
        const avatar = 'https://i.imgur.com/7vQD0fPs.jpg';
        const description = 'Gregorio Y. Zara';
        return (
          <img
            className="avatar"
            src={avatar}
            alt={description}
          />
        );
      }
  • 이중 중괄호 사용: 객체를 전달할 때 사용한다. 인라인 스타일을 적용할 때도 style 속성에 객체를 전달한다.
    export default function TodoList() {
      return (
        <ul style={{
          backgroundColor: 'black',
          color: 'pink'
        }}>
          <li>Improve the videophone</li>
        </ul>
      );
    }

🔗조건부 렌더링

조건부로 JSX 포함

  • 삼항 조건 연산자: ? : 문법을 사용하여 조건에 따른 렌더링을 수행할 수 있다.
    if (isPacked) {
      return <li className="item">{name}</li>;
    }
    return <li className="item">{name}</li>;
    return (
      <li className="item">
        {isPacked ? name + ' ✅' : name}
      </li>
    );
  • 논리 AND 연산자 &&: 조건이 참일 때 일부 JSX를 렌더링하거나, 그렇지 않으면 아무것도 렌더링하지 않을 때 사용될 수 있다.
    return (
      <li className="item">
        {name} {isPacked && '✅'}
      </li>
    );

🔗LifeCycle

리액트의 LifeCycle(생명주기)란 컴포넌트가 생성, 변경, 제거되는 사이클을 의미한다. 크게 마운트, 업데이트, 언마운트 단계로 나눌 수 있다.

  • Mount(최초 렌더링): 특정 컴포넌트가 화면에 처음으로 렌더링되는 작업으로, appendChild() DOM API를 사용하여 생성한 DOM 노드를 화면에 표시한다.
  • Update(리렌더링): 특정 컴포넌트가 화면에 다시 렌더링되는 작업
    • Trigger(렌더링 유발)
      • useState의 setter가 실행되는 경우
      • useReducer의 dispatch가 실행되는 경우
      • key props가 변경되는 경우
    • 과정
      1. Render (렌더 단계): 함수 컴포넌트의 내부 코드를 실행하는 단계로, 가상 DOM에 발생할 변경 사항을 기록한다.
      2. React updates DOM (커밋 단계): Render에서 기록된 변경사항들을 가상 DOM에 적용
      3. Cleanup LayoutEffects: useLayoutEffect에 전달된 정리함수가 호출되는 시점
      4. Run LayoutEffects: useLayoutEffect 훅에 전달된 콜백함수를 실행하는 단계이며, 커밋 단계 이후, 브라우저 렌더링 이전에 수행된다.
      5. Browser paints screen: 가상 DOM에 발생한 변경 사항들을 브라우저(실제) DOM에 적용하는 시점 (브라우저 렌더링)
      6. Cleanup Effects: useEffect에 전달된 정리함수가 호출되는 시점
      7. Run Effects: useEffect에 전달된 콜백함수가 호출되는 시점으로, 보통 이 단계에서 서버에 대한 API 요청을 수행한다.
  • Unmount (제거): 특정 컴포넌트가 화면에 렌더링 해제되는 작업으로, 언마운트될 때마다 정리함수(Clean LayoutEffects, Effects)가 실행된다.

예제

다음 리액트 코드의 실행 순서에 대해 설명해주세요. [자료출처: 매일메일]

  • 최초 마운트 시: 1 -> 4 -> 5 -> 2
  1. Parent 함수가 실행되면서 "1"이 출력됩니다.
  2. Parent 실행 도중 내부의 Child 함수가 호출되므로 "4"가 출력됩니다.
  3. 이후, 마운트 후 실행되는 useEffect 콜백들이 실행됩니다. 먼저 Child의 useEffect 콜백이 실행되어 "5"가 출력됩니다.
  4. 이어서 Parent의 useEffect 콜백이 실행되어 "2"가 출력됩니다
  • 버튼 클릭 시: 1 -> 4 -> 3 -> 2
  1. 사용자가 버튼을 클릭하면 setCount를 통해 count 상태가 변경되고, Parent 컴포넌트가 다시 렌더링됩니다. 이때 console.log의 흐름은 다음과 같습니다.
  2. Parent 함수가 다시 실행되어 "1"이 출력됩니다.
  3. Parent 내부 Child 함수도 다시 실행되어 "4"가 출력됩니다.
  4. 그 후, Parent의 useEffect 콜백이 실행되기 전에 cleanup 함수가 먼저 호출되어 "3"이 출력됩니다.
  5. 그런 다음 Parent의 useEffect 콜백이 다시 실행되며 "2"가 출력됩니다.
  6. Child의 useEffect는 빈 배열을 의존성으로 가지고 있으므로 처음 마운트될 때만 실행되고, 리렌더시에는 실행되지 않습니다. 따라서 "5"이나 "6"은 출력되지 않습니다.

🔗Virtual DOM

가상돔(Virtual DOM)이란 한마디로 ‘실제 DOM을 JS 객체 형태로 복제한 사본’이라고 할 수 있다. UI의 이상적인 또는 ‘가상’적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 ‘실제’ DOM과 동기화하는 프로그래밍 개념이다. 이는 웹 페이지의 구조를 효율적으로 업데이트하기 위해 사용한다.

  • 특징
    • 항상 두 개의 가상 DOM 객체 1) 렌더링 이전 화면 구조를 나타내는 가상 DOM, 2) 렌더링 이후에 보이게 될 화면 구조를 나타내는 가상 DOM을 지니고 있다.
    • DOM의 복사본이므로 실제 DOM의 모든 요소들과 속성을 동일하게 갖고 있다.
    • 브라우저에 있는 문서에 직접 접근할 수 없다.

과정

  1. 상태 변경: 컴포넌트의 상태나 props의 변경되면 가상 DOM이 다시 생성된다.
  2. Reconciliation(재조정): 변경된 내용이 화면에 새롭게 그려지기 이전에, 두 개의 가상 DOM을 비교하여 어떤 부분이 바뀌었는지 효율적으로 비교하여 파악한다(Diffing 과정).
    • 재조정이 일어나는 경우
      • 리렌더링 이후 엘리먼트가 달라졌을 때
      • 리렌더링 이후 동일한 엘리먼트들의 순서가 달라졌을 때 (만약, 의도하지 않았다면 key를 통해 해결 가능)
  3. re-render: 변경된 부분들을 파악한 이후에는, 계산된 차이에 따라 Batch Update를 수행하여 실제 DOM에 변경 내용을 한 번에 적용하여 변경이 필요한 부분만 업데이트된다.

비교(diffing) 알고리즘의 효율화

리액트는 O(n^3)의 복잡도를 가질 수 있는 트리 비교 문제를, 휴리스틱을 통해 O(n)으로 최적화하였다. 휴리스틱 알고리즘은 아래와 같이 크게 두 가지 가정을 두고 있다.

  • 서로 다른 타입의 두 요소는 서로 다른 트리를 만든다: DOM 요소의 타입이 다르면 비교를 수행하지 않고, 해당 요소와 자식들을 모두 새로 생성한다. 타입이 다른 경우, 보통 완전히 다른 컴포넌트로 교체되는 상황이 많기 때문에 이러한 가정은 효율적이라고 볼 수 있다. 만약 동일한 타입의 요소라면, 동일한 내역은 유지하고 변경된 속성만 갱신한다.
  • 개발자가 key prop을 통해 여러 렌더링 사이에 어떤 자식 요소가 변경되면 안되는지 표시할 수 있다: 같은 레벨의 자식들끼리 비교할 때 개발자가 부여한 key prop을 사용하여 요소를 식별한다. 이를 통해 리스트의 일부만 수정됐을 때 모든 아이템 요소들을 불필요하게 갱신하지 않고, 실제 변경된 요소만 감지하여 효율적으로 갱신된다.

🔗State

시간에 따라 변화하는 데이터를 의미하며, 어떠한 컴포넌트에든 추가할 수 있고 필요에 따라 업데이트할 수도 있다. 일반 변수와 달리, 특정 함수 호출이나 코드 내의 특정 위치와 관련이 없다. 동일한 이름의 컴포넌트가 두 개 이상 존재할지라도 각 컴포넌트(복사본)의 state는 별도로 저장된다.

  • Prop과의 차이
    • Prop : 컴포넌트를 사용하는 외부자를 위한 데이터
    • State : 컴포넌트를 만드는 내부자를 위한 데이터이다. 선언한 컴포넌트에 완전히 비공개이므로, 부모 컴포넌트는 자식 컴포넌트의 state를 변경할 수 없다.

state 보존

리액트는 같은 컴포넌트가 같은 자리에 렌더링되는 한 state를 유지한다. 리렌더링할 때 state를 유지하고 싶다면, 트리 구조가 같아야 한다.

// 리액트 공식문서 예시
import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() {
    const [text, setText] = useState('');

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Clicked {counter} times</button>
    </>
  );
}

MyComponent를 렌더링할 때마다 또 다른 MyTextField 함수가 만들어진다. 동일한 함수에서 다른 컴포넌트를 렌더링할 때마다 React는 그 아래의 모든 state를 초기화한다. 이런 문제를 피하려면 항상 컴포넌트를 중첩해서 정의하지 않고 최상위 범위에서 정의해야 한다.

🔗Props

JSX 태그에 전달하는 정보로, Props를 통해 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달할 수 있다. 즉, 컴포넌트 간 통신을 할 수 있게 해주는 것이다. props에는 객체, 배열, 함수를 포함한 모든 JavaScript 값을 전달할 수 있다. 따라서 props는 함수의 인수와 동일한 역할을 한다고 볼 수 있으며, React 컴포넌트 함수는 하나의 인자, 즉 props 객체를 받는다. 보통은 전체 props 자체를 필요로 하지는 않기에, 개별 props로 구조 분해 할당하여 사용한다.

  • defaultProps: 컴포넌트의 prop의 default 값을 지정할 수 있다. 컴포넌트에 필요한 prop이 전달되지 않거나 undefined이라면 디폴트 값으로 대체된다.
    • 프로젝트 활용 예시: 사진 게시판 수정에서 기존의 컴포넌트를 재활용하게 되었고, 수정중 여부를 알려주는 prop의 디폴트 값을 지정할 필요가 생겼다.
      GalleryBoardPostForm.defaultProps = {
        isModifying: false,
      };
  • spread 문법으로 props 전달: props가 반복되는 경우 spread 문법을 사용할 수 있다. 이는 가독성을 낮추는 대신, 간결함을 추구하는 경우 유용하다.
    function Profile(props) {
      return (
        <div className="card">
          <Avatar {...props} />
        </div>
      );
    }
  • children: 자체 컴포넌트를 중첩하고 싶을 때, 부모 컴포넌트는 children 이라는 prop으로 자식 컴포넌트를 받을 수 있다.
    import Avatar from './Avatar.js';
    
    function Card({ children }) {
      return (
        <div className="card">
          {children}
        </div>
      );
    }
    
    export default function Profile() {
      return (
        <Card>
          <Avatar />
        </Card>
      );
    }

props의 불변성

props는 ‘변경할 수 없다’라는 의미의 불변성을 지닌다. props는 읽기 전용 스냅샷으로, 렌더링할 때마다 새로운 버전의 props를 받는다. 상호작용과 같이 컴포넌트가 props를 변경해야 하는 경우, 부모 컴포넌트에 다른 props, 즉 새로운 객체를 전달하도록 요청해야 한다. 그러면 이전의 props는 버려지고, 결국 자바스크립트 엔진은 기존 props가 차지했던 메모리를 회수하게 된다. 따라서 이러한 경우에는 props를 변경하는 대신, state를 변경(set state)해야 한다.

Props Drilling

리액트에서는 부모에서 자식(단방향)으로의 컴포넌트 간 데이터 전달이 일어나고, 이때 리액트의 속성인 prop을 통해 데이터를 전달하게 된다. 따라서 부모의 n번째 자식 컴포넌트에게 부모에서 정의한 데이터를 전달하려면 모든 중간 자식 컴포넌트들의 prop에도 이 데이터를 전달해야 한다.

자식 컴포넌트 입장에서는 본인은 props를 사용하지도 않는데 그저 자신의 자식을 위해서 props를 받아서 넘겨주는 셈이다. 이 과정이 만약 여러 깊이를 거쳐 일어난다면, 해당 데이터가 어느 지점으로부터 왔는지, 데이터를 사용하는 곳이 어디인지 추적하기 힘들 것이다. 이와 더불어, 리액트는 props의 변경에도 리렌더링을 수행하므로 중간 자식 컴포넌트에서는 불필요한 렌더링을 수행할 수 밖에 없어진다.

  • 해결 방법: 그렇다면 이런 문제를 어떻게 해결할 수 있을까? 우선 다음과 같이 리액트의 children을 활용할 수 있을 것이다.
    // Slot/children 기반 컴포지션 예시
    function Card({ title, children, footer }) {
      return (
        <section className="card">
          <h2>{title}</h2>
          <div>{children}</div>
          <div>{footer}</div>
        </section>
      );
    }
    
    // 사용 예: 필요한 컴포넌트를 "직접 꽂아 넣기"
    <Card
      title="프로필"
      footer={<Button>수정</Button>}
    >
      <ProfileSummary />
      <ProfileDetails />
    </Card>
    하지만 이러한 방법은 데이터(상태) 접근 자체를 전역적으로 해결해 주지는 못하고, 깊이가 깊어질수록 여전히 “부모에서 만든 값을 자식이 쓰기 위해 구조를 맞춰 꽂아 넣어야 한다”는 제약이 있다. 깊이가 계속 깊어지는 시점이 전역 상태 관리가 필요한 때인 것이다.

사용자 정의 태그의 onClick{}과 같은 함수는 이벤트 리스너가 아닌 prop이다.

🔗Key

각 컴포넌트가 어떤 배열 항목에 해당하는지 React에 알려주어 추적하게끔 도우는 속성이다. 이는 배열 항목이 정렬 등으로 인해 이동하거나 삽입되거나 삭제될 수 있는 경우 중요하다. 재정렬로 인해 위치가 변경되더라도 key는 React가 생명주기 내내 해당 항목을 식별할 수 있게 해준다. 즉, key를 잘 선택하면 React가 정확히 무슨 일이 일어났는지 추론하고 DOM 트리에 올바르게 업데이트 하는데 도움이 된다.

function Nav(props) {
  const lis = []
  for(let i=0; i<props.topics.length; i++) {
    let t = props.topics[i];
    lis.push(<li key={t.id}><a href={'/read/'+t.id}>{t.title}</a></li>)
		// key에 각각의 id (여기서는 topics 배열의 순차적인 id값들)를 부여
  }
  return (
    <nav>
      <ol>
        {lis}
      </ol>
    </nav>
  )
}

...
const topics = [
    {id:1, title:'html', body: 'html is ...'},
    {id:2, title:'css', body: 'css is ...'},
    {id:3, title:'js', body: 'javascript is ...'},
 ]

key가 될 수 있는 소스 예시

  • 데이터베이스의 데이터: 고유 id 사용
  • 로컬 생성 데이터: uuid와 같은 패키지 사용
  • 중첩된 key
    import { recipes } from './data.js';
    
    export default function RecipeList() {
      return (
        <div>
          <h1>Recipes</h1>
          {recipes.map(recipe =>
            <div key={recipe.id}> {/* 각 배열에 key가 필요 */}
              <h2>{recipe.name}</h2>
              <ul>
                {recipe.ingredients.map(ingredient =>
                  <li key={ingredient}>
                    {ingredient}
                  </li>
                )}
              </ul>
            </div>
          )}
        </div>
      );
    }
    import { recipes } from './data.js';
    
    function Recipe({ id, name, ingredients }) {
      return (
        <div>
          <h2>{name}</h2>
          <ul>
            {ingredients.map(ingredient =>
              <li key={ingredient}>
                {ingredient}
              </li>
            )}
          </ul>
        </div>
      );
    }
    
    export default function RecipeList() {
      return (
        <div>
          <h1>Recipes</h1>
          {recipes.map(recipe =>
    			  // 컴포넌트를 추출할 때 추출된 JSX 외부에 key를 남겨두어야 함
    			  // 재사용성과 가독성 향상
            <Recipe {...recipe} key={recipe.id} />
          )}
        </div>
      );
    }
    

Index와 key

동적인 리스트를 만들 때는 index값을 key로 전달하면 안된다. 요소들을 불러올 때 처음부터 고유한 id를 갖는 객체 배열로 받아와서 item.id 과 같이 key를 지정한다.

key로 Index를 사용해도 되는 경우
배열에 추가, 수정, 삭제가 일어나지 않는 경우

🔗Controlled Component & Uncontrolled Component

  • <input> : value 속성을 통해 자체적인 데이터를 가지며, 사용자가 입력한 값이 value 속성에 저장된다. 이떄, value 속성은 DOM에 존재하므로, input을 통한 사용자의 입력 데이터는 DOM에 저장된다고 볼 수 있다.
제어 컴포넌트비제어 컴포넌트
특징리액트가 값을 관리DOM이 값을 저장
사용자의 입력이 항상 state로 push입력 값이 필요할 때, element에서 pull
리액트가 값이 항상 일치함을 보장값이 항상 일치함을 보장하지 않음
리렌더링 발생 O리렌더링 발생 X
단점리렌더링 이슈 존재 / 모든 form 요소에 react의 state를 연결해야 함 / non-React 코드로 작성된 form 요소 코드 통합의 어려움

제어 컴포넌트(Controlled Component)

리액트 상태(state)를 통해 입력 값을 제어하는 컴포넌트이며, 상태를 신뢰 가능한 단일 출처로써 사용한다. 항상 최신의 값을 보장하며, 매 입력마다 리렌더링이 발생한다. 따라서 매 입력마다 입력 값을 특정 동작을 수행해야 하는 경우 유용하다. 예를 들어, 입력값을 다른 곳에 렌더링하는 경우, 사용자 입력에 대한 즉각적인 유효성 검증을 해야 하는 경우가 있겠다.

비제어 컴포넌트(Uncontorlled Component)

원하는 시점에 값을 가져오며, 매 입력마다 리렌더링이 발생하지 않는다. 예를 들어, 매 입력마다 최신의 값이 꼭 필요하지 않은 경우, 매 렌더링마다 복잡한 연산이 발생하는 경우가 있겠다.

🔗커스텀 훅

커스텀 훅은 이름이 use로 시작하는 자바스크립트 함수이다. 다른 리액트 훅을 호출할 수 있으며, 특별한 기능이라기보다 기본적으로 Hook의 디자인을 따르는 관습이라 할 수 있다.

적절한 추상화를 지키기 위한 방법

  • 코드 분리와 캡슐화: 각 기능을 독립적으로 분리하고 캡슐화하여 하나의 함수가 하나의 기능을 할 수 있게 만들어야 한다.
  • 단일 책임 원칙 준수: 하나의 훅이 하나의 관심사만 가져 많은 기능을 포함하지 않도록 한다.
  • 재사용성과 결합도: 재사용이 가능하게 해야하며, 특정한 컴포넌트에 대한 결합력이 너무 높게 하지 않는다.
  • 추상화 수준 선택: 너무 낮은 추상화 레벨은 코드 세부 사항을 이해하기 어렵게 하고, 반대의 경우 코드를 이해하기 어렵게 한다.

🔗Strict Mode

리액트에서 Strict Mode(엄격 모드) 는 주로 개발 중에 발생할 수 있는 잠재적인 문제를 사전에 감지하고 예방하기 위해 사용된다.

Strict Mode에서 코드가 두 번씩 실행되는 현상은 개발 모드에서만 발생하고, 실제 프로덕션 빌드에서는 발생하지 않는다.

😎실습

삼항연산자 사용하여 조건부 렌더링하기

attendancerank는 데이터로 넘어온 이차원 배열을 한 번 까야지 나오는 데이터이다. 두 개의 주간/월간 출석자 배열 중 하나만 빈 배열일 수도 있다. 따라서, 무난하게 삼항연산자를 활용하였다.

const AttendanceBannerContent = ({ slideIdx }) => {
    const {
        data: {
            weeklyStatisticsDtoList: weeklyAttendanceRank = [],
            monthlyStatisticsDtoList: monthlyAttendanceRank = [],
        } = {},
    } = useGetAttendanceRanks();

    return (
        <>
            {[weeklyAttendanceRank, monthlyAttendanceRank]?.map((attendanceRank, idx) => (
                <div
                    key={idx}
                    className={`RankSlide absolute left-0 top-0 h-full w-full ${
                        slideIdx === idx + 1 ? 'visible' : 'invisible'
                    }`}
                >
                    {attendanceRank.length === 0 ? (
                        <p className='w-full pt-10 text-center text-xs text-stone-500'>출석자가 없습니다</p>
                    ) : (
                        <ul className='w-full px-4 text-black'>
                            {attendanceRank.map((ranker, idx) => (
                                <li key={idx} className='mb-1 flex h-4 w-full flex-row text-xs'>
                                    <span className='mr-16 w-2 pl-2'>{idx + 1}</span>
                                </li>
                            ))}
                        </ul>
                    )}
                </div>
            ))}
        </>
    );
};

0 말고 컴포넌트 보여달라니까?

조건을 테스트하기 위해 JavaScript는 자동으로 왼쪽을 부울로 변환한다. 하지만 왼쪽이 0이면 전체 식이 0이 되고, React는 아무것도 아닌 0을 렌더링할 것이다. 실제로, 이러한 코드 상의 실수를 인턴 수행 중 수행했던 프로젝트 코드를 수정하면서 직접 확인할 수 있었다. 이전 작성자가 이 부분을 해결하려고 삼항연산자로 거짓일 때는 빈 문자열(’’)로 처리를 해놨는데, 이 부분을 간과한게 아닐까 추측한다. 실수하기 좋은 부분이니 잘 숙지하고 넘어가자.

// 잘못된 예시
messageCount && <p>New messages</p>
// 올바른 예시
messageCount > 0 && <p>New messages</p>

Suspense의 key 속성

Suspense는 일반적으로 컴포넌트 스트리밍에 쓰인다. 아래 예시와 같이 쿼리 파라미터가 변하는 경우 로딩 상태를 표시하기 위해 Suspense를 사용한 경우를 생각해보자. 예상한 것과 달리 resolvedSearchParams의 q가 변하더라도 폴백 UI가 표시되지 않는다. 왜 이런 현상이 발생하는 것일까?

key 없이 사용하면 이는 q 값의 변경이 일어나도 Suspense 컴포넌트는 변경사항을 감지할 방법이 없기 때문이다. 따라서 Suspense 컴포넌트의 key에 쿼리 파라미터 값을 넘김으로써, 리액트에게 해당 값이 변하면 Suspense도 변경되어야 하는 것을 알리며 로딩 상태를 다시 보여줄 수 있다. 즉, 일종의 트릭을 사용하여 검색어가 변해서 결과를 기다리는 시간 동안에도 로딩 상태를 보여줄 수 있는 것이다.

// 이정환님의 한 입 크기로 잘라먹는 Next.js 중
// 검색창에 서로 다른 검색어를 입력할 때 실시간으로 목록을 갱신하는 상황을 구현
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const resolvedSearchParams = await searchParams;

  return (
    <Suspense key={resolvedSearchParams.q} fallback={<div>로딩중...</div>}>
      {/* SearchResult: 비동기 fetching 진행 */}
      <SearchResult q={resolvedSearchParams.q || ""} />
    </Suspense>
  );
}

key에는 항상 배열 인덱스를 사용하면 안되나요?

// 단순히 일정한 개수의 사진 데이터를 불러와 보여줌
        {photos.map((photo, idx) => (
        	<div key={idx} className='ImageCell inline-block h-fit w-[20rem] px-2'>
        		<img
        			className='brightness-98 rounded-sm shadow-md'
        			src={StringCombinator.getImageURL(photo.saveFilePath, photo.saveFileName)}
        			alt={'포토존 사진'}
        		/>
        	</div>
        ))}

도움되는 사이트
1. 컴포넌트 만들기 · GitBook
리액트 강의

도움되는 영상
React.Fragment는 무엇? 리액트 개발자라면 꼭 알아야됨 - 유튜브 별코딩
Virtual DOM과 Internals – React
재조정 (Reconciliation) – React
React의 가상돔 (Virtual DOM)이 뭔가요? (짱 쉬움)
[10분 테코톡] 텐텐의 리액트의 렌더링
매일메일 - Virtual DOM에 대해서 설명해주세요.
[10분 테코톡] 프룬의 리액트 Props Drilling
React - List와 Key의 중요성. 디버깅의 악몽을 피하자!
[10분 테코톡] 후이의 제어 컴포넌트 vs 비제어 컴포넌트
[10분 테코톡] 세인의 제어 컴포넌트와 비제어 컴포넌트
커스텀 Hook으로 로직 재사용하기 – React
[10분 테코톡] 헤일리의 Custom Hook

참고자료
React

profile
프론트 개발과 클라우드 환경에 관심이 많습니다:)

0개의 댓글