sohyeon kim

[React] 리액트 공식문서 정리(1) : UI 표현하기 - 불변성, JSX, key, 컴포넌트, 순수 함수, 트리 본문

React

[React] 리액트 공식문서 정리(1) : UI 표현하기 - 불변성, JSX, key, 컴포넌트, 순수 함수, 트리

aotoyae 2024. 10. 16. 21:43
728x90

 

 

💡 리액트 공식문서 정리 : UI 표현하기

 

1. export default : decault 키워드는 코드를 사용하는 다른 파일에서 이 함수가 주요 함수임을 알려준다.

export default function Square() {
  return <button className="square">X</button>;
}

 

Syntax Export 구문 Import 구문 다른 이름으로 가져오기
Default export default function Button () {} import Button from './button.js'; O
Named export function Button () {} import { Button } from './button.js'; X (그래서 Named import)

 

보편적으로, 한 파일에서 하나의 컴포넌트만 export 할 때 default export 방식을 사용하고

여러 컴포넌트를 export 할 경우엔 named export 방식을 사용

 

2. 불변성 유지  : nextSquares를 따로 만드는 이유는 상태를 불변성(immutability)을 유지하며 업데이트하기 위해서

왜 불변성을 유지해야 하는가?

  • 불변성 유지 : squares 배열을 직접 수정하지 않고, nextSquares 라는 새로운 배열을 만들어 setSquares 로 상태를 업데이트하면 React가 변경된 상태를 감지하고 UI를 올바르게 업데이트한다. 만약 squares 를 직접 수정하면, React 는 상태가 변한 것을 인식하지 못해 UI 가 재렌더링되지 않을 수 있다.
  • 성능 최적화 : 불변성을 유지하는 방식은 React 에서 상태가 변할 때 변경된 부분만 효율적으로 재렌더링할 수 있도록 도와준다.
  • '돌아가기' 기능(틱택토 게임 중) : 특정 작업을 실행 취소하고 다시 실행하는 기능은 앱의 일반적인 요구사항이다. 직접적인 데이터 변경을 피하면 이전 버전의 데이터를 그대로 유지하여 나중에 재사용(또는 초기화)할 수 있다.
  • 저렴한 비용 : 불변성을 사용하면 컴포넌트가 데이터의 변경 여부를 저렴한 비용으로 판단할 수 있다.
 const [squares, setSquares] = useState(Array(9).fill(null));

  function handleClick(i) {
    const nextSquares = squares.slice();
    nextSquares[i] = 'X';
    setSquares(nextSquares);
  }

 

3. JSX : JavaScript 를 확장한 문법, JS 파일을 HTML 과 비슷하게 마크업 작성을 할 수 있도록 해준다.

  • HTML 과 비슷해 보이지만, 조금 더 엄격하며 동적으로 정보 표시 가능
  • <></> : Fragment, 빈 태그로 브라우저상의 HTML 트리 구조에 흔적을 남기지 않고 엘리먼트들을 그룹화 해준다.
    • 왜 하나로 감싸줘야 할까? : JSX 는 HTML 처럼 보이지만 내부적으로는 일반 JS 객체로 변환된다.
      함수에서 두 개의 객체를 하나의 배열로 래핑하지 않고선 반환할 수 없기 때문에 다른 태그나 Fragment 로 감싸줘야 한다.

spread 문법으로 props 전달하기

때때로 컴포넌트의 재사용으로, 전달되는 props 가 반복적일 수 있다. 이를 간결하게 만들기 위해 spread 문법을 활용하면 좋다.

(하지만 과도하게 사용하지 말 것.)

// 기존
function Profile({ person, size, isSepia, thickBorder }) {
  return (
    <div className="card">
      <Avatar
        person={person}
        size={size}
        isSepia={isSepia}
        thickBorder={thickBorder}
      />
    </div>
  );
}
// spread props
function Profile(props) {
  return (
    <div className="card">
      <Avatar {...props} />
    </div>
  );
}

 

window.devicePixelRatio 를 고려해 높은 DPI 화면에서 더 선명한 이미지를 표시할 수 있다.

function Avatar({ person, size }) {
  let thumbnailSize = 's';
  if (size * ratio > 90) {
    thumbnailSize = 'b';
  }
  return (
    <img
      ...
    />
  );
}

3. key : map() 호출 내부의 JSX 엘리먼트에 항상 필요. 각 컴포넌트가 어떤 배열 항목에 해당하는지 React 에 알려준다.

왜 key 가 필요한가?

  • 배열 항목이 이동, 삽입, 삭제되는 경우 정확한 항목을 식별(선택)할 수 있도록 해준다.
  • 파일에 이름을 부여해준다고 생각.

참고, 주의사항

  • 렌더링 중 key 를 생성해선 안되며, 데이터에 key 를 포함해야 한다.
    • 데이터베이스의 데이터 : DB 에서 데이터를 가져오는 경우 DB의 key, id 를 사용
    • 로컬에서 생성된 데이터 : 데이터가 로컬에서 생성되는 경우, 항목을 만들 때 crypto.randomUUID() 또는 uuid 패키지 사용
  • 빈 <></> fragment 구문을 사용할 땐 <div> 나 더 명시적인 <Fragment> 문법으로 그룹화할 수 있다.
import { Fragment } from 'react';

// Fragment는 DOM에서 사라지므로 <h1>, <p>, <h1>, <p> 등의 평평한 리스트가 생성된다.
const listItems = people.map(person =>
  <Fragment key={person.id}>
    <h1>{person.name}</h1>
    <p>{person.bio}</p>
  </Fragment>
);

 

  • key = {math.random()} 처럼 즉석에서 key 를 생성하면 안된다.
    렌더링 간 key 가 일치하지 않아 모든 컴포넌트와 DOM 이 매번 다시 생성되고 속도가 느려질 수 있다.
  • 컴포넌트는 key 를 prop 으로 받지 않는다.
    컴포넌트에 id 를 넘겨주어야 한다면 <Profile key={id} userId={id} /> 와 같이 별도의 prop 으로 전달한다.
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>
  );
}

// ...recipe => // recipe 객체의 모든 속성을 props 로 전달
// ** 위 div 가 아니라 Recipe 자체에 key 지정
export default function RecipeList() {
  return (
    <div>
      <h1>Recipes</h1>
      {recipes.map(recipe =>
        <Recipe {...recipe} key={recipe.id} />
      )}
    </div>
  );
}

 

4 - 1. 컴포넌트 순수하게 유지하기 : 컴포넌트를 순수함수로 작성하면 예상 외 동작이나 버그를 피할 수 있다.

순수성, 순수 함수란?

  • 자신의 일에 집중. 함수가 호출되기 전에 존재했던 어떤 객체나 변수는 변경하지 않는다.
  • 같은 입력이 주어졌다면 같은 결과를 반환해야 한다.
    y = 2x 일 때, x = 2 라면 y 는 항상 4다.
function double(number) { // 순수함수 double
  return 2 * number;
}

 

React 는 이러한 컨셉을 기반으로 설계되어, 작성되는 모든 컴포넌트가 순수 함수일 것이라 가정한다.

➡️ 작성되는 컴포넌트에 같은 입력이 주어진다면 반드시 같은 JSX 를 반환한다는 것을 의미.

➡️ 마치 수학 공식이나 레시피처럼! 예측이 가능해야 한다!

 

4 - 2. 사이드 이펙트

의도하지 않은 결과 주의. 아래의 컴포넌트는 순수성 규칙을 위반하고 있다.

  • 컴포넌트 바깥에 선언된 guest 변수를 수정하고 있다. 만약 컴포넌트가 여러 번 불리면 다른 JSX 를 생성한다.
    • 현재 <React.StrictMode> 로 인해 함수가 두 번 호출되어 아래와 같은 결과가 나왔다.
      순수 함수는 연산만 하므로 두 번 호출해도 아무것도 변경되지 않는다.
      y = 2x 를 두 번 푸는게 y 의 답을 바꾸지 않는 것처럼, 항상 같은 입력이면 같은 출력이다.
  • 다른 컴포넌트가 guest 를 읽었다면 언제 렌더링 되었는지에 따라 다른 JSX 를 생성할 것. 결과를 예측할 수 없어진다.
    ❗️ 렌더링은 언제든지 발생할 수 있으므로 컴포넌트는 서로의 렌더링 순서에 의존하지 않아야 한다.

 

💡 guset 를 변수가 아닌 prop 으로 전달해 순수 컴포넌트로 수정할 수 있다.

 

사이드 이펙트란?

함수형 프로그래밍은 순수성에 크게 의존하지만 언젠간, 어딘가에서, 무언가가 바뀌어야 한다.

이러한 변화들 - 화면을 업데이트하고, 애니메이션을 시작하고, 데이터를 변경하는 것이 사이드 이펙트다.

렌더링 중 발생하는 것이 아니라 '사이드에서' 발생하는 현상.

  • 보통 이벤트 핸들러에 포함된다.
    이벤트 핸들러가 컴포넌트 내부에 정의되었더라도 렌더링 중엔 실행되지 않는다. (e.g. 버튼 클릭 시 실행)
    ➡️ 따라서 이벤트 핸들러는 순수할 필요가 없다.
  • 다른 옵션을 모두 사용했지만 사이드 이펙트에 적합한 이벤트 핸들러를 찾을 수 없는 경우, useEffect 를 사용할 수 있다.
    해당 이벤트는 렌더링 후 나중에 실행되며, 이 접근 방식은 마지막 수단이 되어야 한다.

React 는 왜 순수함을 신경쓰는가?

  • 컴포넌트는 서로 다른 환경에서 실행될 수 있는데(e.g. 서버),
    동일한 입력엔 동일한 결과를 반환하기 때문에 많은 사용자 요청을 처리할 수 있고, 캐시하기에 안전하다.
  • 입력이 변경되지 않은 컴포넌트는 렌더링을 건너뛰어 성능을 향상시킬 수 있다.
  • 깊은 컴포넌트 트리를 렌더링하는 도중 일부 데이터가 변경되는 경우,
    오래된 렌더링을 완료하는 데 시간을 낭비하지 않고 렌더링을 다시 시작할 수 있다.

 

5. 트리로서의 UI 

트리란? React 앱의 데이터 흐름과 렌더링 성능 및 앱 크기를 최적화하는 방법을 이해하는 데 유용한 도구

  • 요소 간의 관계를 나타내는 일반적인 방법으로 UI를 모델링하는 데 자주 사용된다.
    예를 들어, 브라우저는 HTML(DOM) 과 CSS(CSSOM) 를 모델링하기 위해 트리 구조를 사용하며, 모바일 플랫폼도 뷰 계층 구조를 나타내는 데 트리를 사용한다.
  • 애플리케이션을 트리로 생각하면 컴포넌트 간의 관계를 이해하는 데 도움이 된다.

요소 / 트리 / UI

React 의 렌더 트리 : 앱을 렌더링할 때 모델링하는 트리

  • 렌더트리에서 트리는 노드로 구성되어 있으며, 각 노드는 컴포넌트를 나타낸다.
  • 최상위 컴포넌트는 루트 컴포넌트에 가장 가까운 컴포넌트이며,
    그 아래의 모든 컴포넌트의 렌더링 성능에 영향을 미치며, 가장 복잡성이 높다.
  • 리프 컴포넌트는 트리의 맨 아래에 있으며 자식 컴포넌트가 없으며 자주 다시 렌더링 된다.

React 의 모듈 의존성 트리 : 앱의 모듈 의존성을 모델링하는 트리

  • 각 노드는 모듈이며, 각 가지는 해당 모듈의 import 문을 나타낸다.
  • 트리의 루트 노드는 루트 모듈이며, 엔트리 포인트 파일이라고도 한다. 일반적으로 루트 컴포넌트를 포함하는 모듈.
  • 의존성 트리는 React 앱을 실행하는 데 필요한 모듈을 결정하는 데 유용. 어떤 코드를 번들로 묶을지 최적화할 기회를 제공한다.
    • React 앱을 프로덕션용으로 빌드할 때, 일반적으로 클라이언트에 제공할 모든 필요 JavaScript 를 번들로 묶는 빌드 단계가 있다. 이 작업을 담당하는 도구를 번들러라고 하며, 번들러는 의존성 트리를 사용하여 포함해야 할 모듈을 결정한다.
    • 앱이 커짐에 따라 번들 크기도 커지고, 이에 따라 클라이언트가 다운로드하고 실행하는 데 드는 비용도 커진다.
      또한 UI가 그려지는 데 시간이 지체될 수 있는데, 앱의 의존성 트리를 파악하면 이러한 문제를 디버깅하는 데 도움이 된다.
  • ❗️ 동일한 앱의 렌더 트리와 비교했을 때 차이점
    • inspirations.js 와 같은 컴포넌트가 아닌 모듈도 이 트리에 나타난다. 렌더 트리는 컴포넌트만 캡슐화.
    • Copyright.js 가 App.js 아래에 나타나지만, 렌더 트리에서 Copyright 컴포넌트는 InspirationGenerator 의 자식으로 나타난다. 이는 InspirationGenerator 가 자식 props 로 JSX 를 허용하기 때문에, Copyright 를 자식 컴포넌트로 렌더링하지만 모듈을 가져오지는 않기 때문.

렌더 트리 / 모듈 의존성 트리

 

 

 

🔗 https://ko.react.dev/learn/describing-the-ui

 

UI 표현하기 – React

The library for web and native user interfaces

ko.react.dev

 

 

 

728x90
반응형