[React 공식문서 정독] Describing the UI

김진서·2025년 4월 24일

우아한테크코스 7기

목록 보기
33/56
post-thumbnail

공식문서에 잘 정리되어 있는 내용을 그동안 내가 무의식적으로 사용해 온 기능들, 알고 있던 지식들과 연결시키며 공부할 수 있었다. 조금 더 기본기가 탄탄해졌다는 느낌이 들었다. :]

React는 사용자 인터페이스(UI)를 렌더링하기 위한 JavaScript 라이브러리입니다. UI는 버튼, 텍스트, 이미지와 같은 작은 요소로 구성됩니다. React를 통해 작은 요소들을 재사용 가능하고 중첩할 수 있는 컴포넌트로 조합할 수 있습니다. 웹 사이트에서 휴대전화 앱에 이르기까지 화면에 있는 모든 것을 컴포넌트로 나눌 수 있습니다. 이 장에서는 React 컴포넌트를 만들고, 사용자화하며, 조건부로 표시하는 방법에 대해서 알아봅시다.

컴포넌트

  • React 컴포넌트: 마크업으로 뿌릴 수 있는 JavaScript 함수.

  • 일반 JavaScript 함수이지만, 이름은 대문자로 시작해야 하며 그렇지 않으면 작동하지 않는다.

  • return문은 한 줄에 모두 작성할 수 있다. 그러나 여러 줄이 되면, ()로 묶어야 한다. 그렇지 않으면, return 뒷 라인에 있는 모든 코드가 무시된다.

// 한 줄
return <img src="https://i.imgur.com/MK3eW3As.jpg" alt="Katherine Johnson" />;

// 여러 줄
return (
  <div>
    <img src="https://i.imgur.com/MK3eW3As.jpg" alt="Katherine Johnson" />
  </div>
);
  • 컴포넌트는 다른 컴포넌트를 렌더링할 수 있지만, 그 정의를 중첩해서는 안된다.
export default function Gallery() {
  // 🔴 절대 컴포넌트 안에 다른 컴포넌트를 정의하면 안 됩니다!
  function Profile() {
    // ...
  }
  // ...
}

이는 매우 느리고 버그를 촉발한다. 대신 최상위 레벨에서 컴포넌트를 정의해야 한다.
자식 컴포넌트에 부모 컴포넌트의 일부 데이터가 필요한 경우, 정의를 중첩하는 대신 props로 전달할 것.

export, import

  1. 컴포넌트를 추가할 JS or TS 파일을 생성.
  2. 새로 만든 파일에서 함수 컴포넌트를 export(default 또는 named export 방식을 사용).
  3. 컴포넌트를 사용할 파일에서 import(적절한 방식을 선택해서 default 또는 named로 import).
  • 한 파일에서는 단 하나의 default export만 사용할 수 있지만 named export는 여러 번 사용 가능.

JSX

  • HTML처럼 작성되었지만 실제로는 JavaScript이며, 컴포넌트가 return하여 화면에 렌더링하는 구문.

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

규칙

  1. 하나의 루트 엘리먼트로 반환하기
  2. 모든 태그는 닫아주기
  3. 대부분 Camel Case로

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

  • 문자열 어트리뷰트를 JSX에 전달하려면 작은따옴표나 큰따옴표로 wrapping.

  • 동적으로 지정하려면 어떻게 해야 할까? ""{}로 바꿔 JavaScript의 값을 사용할 수 있다.

  • 이중 중괄호 사용하기: JSX의 CSS와 다른 객체, 문자열, 숫자 및 기타 JavaScript 표현식뿐만 아니라 객체를 전달. JSX 중괄호 안의 객체에 불과.

props

  • JSX 태그에 전달하는 정보.

  • 일반적으로 전체 props 자체가 필요한 경우는 없기 때문에, 받는 쪽에서 구조 분해 할당으로 받는다.

  • props를 간결하게 spread 문법으로 넘길 수 있다.

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

function Profile(props) {
  return (
    <div className="card">
      <Avatar {...props} />
    </div>
  );
}

컴포넌트를 JSX로 넘기기

  • JSX 태그 내에 콘텐츠를 중첩하면, 부모 컴포넌트는 해당 콘텐츠를 children이라는 prop으로 받는다.

  • 그 내부(Card)에서 무엇(children)이 렌더링 되는지 “알” 필요는 없다.

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person={{
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2'
        }}
      />
    </Card>
  );
}

  • props 자체는 읽기 전용 값이다. 그러나 state를 통해서 변경할 수 있다. 정확히는 부모 컴포넌트에 다른 props, 즉 새로운 객체를 전달하도록 요청하여 새로운 props로 덮어 씌우는 것이다.

조건부 렌더링

  • if문, &&? : 연산자와 같은 자바스크립트 문법을 사용하여 조건부로 JSX를 렌더링할 수 있다.

  • 조건부로 null을 사용하여 아무것도 반환하지 않을 수 있다.

  • 삼항 연산자 스타일은 간단한 조건에 잘 어울리지만, 적당히 사용하는 게 좋다.

  • 논리 AND 연산자 (&&)

// isPacked이면 (&&) 체크 표시를 렌더링하고, 그렇지 않으면 아무것도 렌더링하지 않는다.
return (
  <li className="item">
    {name} {isPacked && '✅'}
  </li>
);

리스트 렌더링

배열 렌더링

  1. 데이터를 배열화.
const people = [
  'Creola Katherine Johnson: mathematician',
  'Mario José Molina-Pasquel Henríquez: chemist'<,
  'Mohammad Abdus Salam: physicist',
  'Percy Lavon Julian: chemist',
  'Subrahmanyan Chandrasekhar: astrophysicist'
];
  1. people의 요소를 새로운 JSX 노드 배열인 listItems에 매핑.
const listItems = people.map(person => <li>{person}</li>);
  1. <ul>로 래핑된 컴포넌트의 listItems를 반환.
return <ul>{listItems}</ul>;

배열 필터링

  1. people에서 filter()를 호출해 person.profession === 'chemist'로 필터링해서 "chemist"로만 구성된 새로운 배열 chemists를 생성.
const chemists = people.filter(person =>
  person.profession === 'chemist'
);
  1. 이제 chemists를 매핑.
const listItems = chemists.map(person =>
  <li>
     <img
       src={getImageUrl(person)}
       alt={person.name}
     />
     <p>
       <b>{person.name}:</b>
       {' ' + person.profession + ' '}
       known for {person.accomplishment}
     </p>
  </li>
);
  1. 마지막으로 컴포넌트에서 listItems를 반환.
return <ul>{listItems}</ul>;

화살표 함수는 암시적으로 => 바로 뒤에 식을 반환하기 때문에 return 문이 필요하지 않다.

const listItems = chemists.map(person =>
  <li>...</li> // 암시적 반환!
);

하지만 => 뒤에 { 중괄호가 오는 경우 return을 명시적으로 작성해야 한다!

const listItems = chemists.map(person => { // 중괄호
  return <li>...</li>;
});

=> { 를 표현하는 화살표 함수를 “block body”를 가지고 있다고 말한다. 이 함수를 사용하면 한 줄 이상의 코드를 작성할 수 있지만 return 문을 반드시 작성해야 한다. 그렇지 않으면 아무것도 반환되지 않는다!

key를 사용해서 리스트 항목을 순서대로 유지

  • 각 배열 항목에 다른 항목 중에서 고유하게 식별할 수 있는 문자열 또는 숫자를 key로 지정해야 한다.
<li key={person.id}>...</li>

map() 호출 내부의 JSX 엘리먼트에는 항상 key가 필요하다!
즉석에서 key를 생성하는 대신 데이터 안에 key를 포함해야 한다.

key를 가져오는 곳

  1. 데이터베이스의 데이터: 데이터베이스에서 데이터를 가져오는 경우 본질적으로 고유한 데이터베이스 key/ID를 사용할 수 있다.
  2. 로컬에서 생성된 데이터: 데이터가 로컬에서 생성되고 유지되는 경우(예: 메모 작성 앱의 노트), 항목을 만들 때 증분 일련번호나 crypto.randomUUID() 또는 uuid 같은 패키지를 사용하자.

key 규칙

key는 형제 간에 고유해야 한다. 하지만 같은 key를 다른 배열의 JSX 노드에 동일한 key로 사용해도 괜찮다.
key는 변경되어서는 안 되며 그렇게 되면 key는 목적에 어긋난다! 렌더링 중에는 key를 생성하면 안된다. map()의 index 인자를 key로 사용해서는 안된다는 뜻이다.

배열에서 항목의 인덱스를 key로 사용하고 싶을 수도 있다. 실제로 key를 전혀 지정하지 않으면 React는 인덱스를 사용한다. 하지만 항목이 삽입되거나 삭제하거나 배열의 순서가 바뀌면 시간이 지남에 따라 항목을 렌더링하는 순서가 변경된다. 인덱스를 key로 사용하면 종종 미묘하고 혼란스러운 버그가 발생한다.

마찬가지로 key={Math.random()}처럼 즉석에서 key를 생성하지 말자. 이렇게 하면 렌더링 간에 key가 일치하지 않아 모든 컴포넌트와 DOM이 매번 다시 생성될 수 있다. 속도가 느려질 뿐만 아니라 리스트 항목 내부의 모든 사용자의 입력도 손실된다. 대신 데이터 기반의 안정적인 ID를 사용하자.

컴포넌트가 key를 prop으로 받지 않는다는 점에 유의하자. key는 React 자체에서 힌트로만 사용된다. 컴포넌트에 ID가 필요하다면 <Profile key={id} userId={id} />와 같이 별도의 prop으로 전달해야 한다.

순수한 컴포넌트

  • 순수 함수: 자신의 일에 집중한다. 함수가 호출되기 전에 존재했던 어떤 객체나 변수는 변경하지 않는다. 같은 입력, 같은 출력. 같은 입력이 주어졌다면 순수함수는 같은 결과를 반환한다.

  • React는 작성되는 모든 컴포넌트가 순수 함수일 거라 가정한다. 이러한 가정은 작성되는 React 컴포넌트에 같은 입력이 주어진다면 반드시 같은 JSX를 반환한다는 것을 의미한다.

function Recipe({ drinkers }) {
  return (
    <ol>
      <li>Boil {drinkers} cups of water.</li>
      <li>Add {drinkers} spoons of tea and {0.5 * drinkers} spoons of spice.</li>
      <li>Add {0.5 * drinkers} cups of milk to boil and sugar to taste.</li>
    </ol>
  );
}

export default function App() {
  return (
    <section>
      <h1>Spiced Chai Recipe</h1>
      <h2>For two</h2>
      <Recipe drinkers={2} />
      <h2>For a gathering</h2>
      <Recipe drinkers={4} />
    </section>
  );
}

Recipedrinkers={2}를 넘기면 항상 2 cups of water를 포함한 JSX 반환한다.
drinkers={4}를 넘기면 항상 4 cups of water를 포함한 JSX를 반환한다.

사이드 이펙트: 의도하지(않은) 결과

  • React의 렌더링 과정은 항상 순수해야 한다. 컴포넌트는 JSX만 반환해야 하며, 렌더링 이전에 존재했던 객체나 변수를 변경해서는 안 된다. 그렇게 하면 컴포넌트가 순수하지 않다!
let guest = 0;

function Cup() {
  // 나쁜 지점: 이미 존재했던 변수를 변경하고 있다!
  guest = guest + 1;
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

이 컴포넌트는 컴포넌트 바깥에 선언된 guest라는 변수를 읽고 수정하고 있다. 이건 컴포넌트를 여러 번 호출하면 다른 JSX를 생성한다는 것을 의미한다! 그리고 더욱이 다른 컴포넌트가 guest를 읽었다면 언제 렌더링 되었는지에 따라 그 컴포넌트 또한 다른 JSX를 생성할 것이다! 이건 예측할 수 없다.

props로 전달하는 것으로 수정.

function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}
  • 아래처럼 렌더링하는 동안 그냥 만든 변수와 객체를 변경하는 것은 전혀 문제가 없다.
function Cup({ guest }) {
  return <h2>Tea cup for guest #{guest}</h2>;
}

export default function TeaGathering() {
  let cups = [];
  for (let i = 1; i <= 12; i++) {
    cups.push(<Cup key={i} guest={i} />);
  }
  return cups;
}

사이드 이펙트를 일으킬 수 있는 지점

사이드 이펙트는 보통 이벤트 핸들러에 포함된다. 이벤트 핸들러는 React가 일부 작업을 수행할 때 반응하는 기능이다. 예를 들면 버튼을 클릭할 때처럼. 이벤트 핸들러가 컴포넌트 내부에 정의되었다 하더라도 렌더링 중에는 실행되지 않는다! 그래서 이벤트 핸들러는 순수할 필요가 없다.

트리로서의 UI

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

렌더 트리

모듈 의존성 트리

  • 트리로 모델링 할 수 있는 React 앱의 다른 관계는 앱의 모듈 의존성. 컴포넌트를 분리하고 로직을 별도의 파일로 분리하면 컴포넌트, 함수 또는 상수를 내보내는 JS 모듈을 만들 수 있다. 모듈 의존성 트리의 각 노드는 모듈이며, 각 가지는 해당 모듈의 import 문을 나타낸다.

  • 트리의 루트 노드는 루트 모듈이며, 엔트리 포인트 파일이라고도 한다. 일반적으로 루트 컴포넌트를 포함하는 모듈이다. 동일한 앱의 렌더 트리와 비교하면 유사한 구조가 있지만 몇 가지 차이점이 있다.

  • 트리를 구성하는 노드는 컴포넌트가 아닌 모듈을 나타낸다.

    • inspirations.js와 같은 컴포넌트가 아닌 모듈도 이 트리에 나타난다. 렌더 트리는 컴포넌트만 캡슐화한다.
    • Copyright.jsApp.js 아래에 나타나지만, 렌더 트리에서 Copyright 컴포넌트는 InspirationGenerator의 자식으로 나타난다. 이는 InspirationGenerator자식 props로 JSX를 허용하기 때문에, Copyright를 자식 컴포넌트로 렌더링하지만 모듈을 가져오지는 않기 때문이다.
  • 의존성 트리는 React 앱을 실행하는 데 필요한 모듈을 결정하는 데 유용하다. React 앱을 프로덕션용으로 빌드할 때, 일반적으로 클라이언트에 제공할 모든 필요 JavaScript를 번들로 묶는 빌드 단계가 있다. 이 작업을 담당하는 도구를 번들러라고 하며, 번들러는 의존성 트리를 사용하여 포함해야 할 모듈을 결정한다.

  • 앱이 커짐에 따라 번들 크기도 커진다. 번들 크기가 커지면 클라이언트가 다운로드하고 실행하는 데 드는 비용도 커진다. 또한 UI가 그려지는 데 시간이 지체될 수 있다. 앱의 의존성 트리를 파악하면 이러한 문제를 디버깅하는 데 도움이 될 수 있다.

profile
PAy IT forwaRD를 실천하는 프론트엔드 개발자.

0개의 댓글