Learn React 1장 / UI 구성하기

user profile img

신현호

React

React Docs

Post Thumbnail

목차

    1장 / UI 구성하기

    첫 번째 컴포넌트

    React 애플리케이션은 컴포넌트라고 불리는 분리된 UI 조각으로 구축됩니다. React 컴포넌트는 마크업으로 뿌릴 수 있는 JS 함수입니다.
    컴포넌트는 버튼처럼 작을 수도 있고 전체 페이지처럼 클 수도 있습니다. 다음은 그 예시입니다.

    우리는 웹에서 HTML의 다양한 태그를 통해 풍부한 구조의 문서를 만들 수 있습니다.

    html

    <article>
      <h1>My First Component</h1>
      <ol>
        <li>Components: UI Building Blocks</li>
        <li>Defining a Component</li>
        <li>Using a Component</li>
      </ol>
    </article>
    

    이 마크업은 아티클을 article 태그로, 제목을 h1 태그로, 목차를 ol태그로 나타내고 있습니다.
    이와같은 마크업은 스타일을 위한 CSS, 상호작용을 위한 JS와 결합되어 웹에서 볼 수 있는 모든 사이드바, 아바타, 모달, 드롭다운 등 모든 UI의 기반이 됩니다.

    시멘틱 태그

    위와 같은 h1, ol, article 등의 태그를 시맨틱 태그 라고 합니다
    시맨틱 태그의 사용은 접근성 향상, SEO 향상, 가독성 향상의 장점을 가져옵니다.

    React를 사용하면 마크업, CSS, JS를 앱의 재사용 가능한 UI 요소인 컴포넌트로 결합할 수 있습니다.
    그 예시로 React를 사용하면 위의 HTML태그를 간단하게 <TableOfContents /> 로 사용할 수 있습니다.

    jsx

    export default function TableOfContents() {
      return (
        <article>
          <h1>My First Component</h1>
          <ol>
            <li>Components: UI Building Blocks</li>
            <li>Defining a Component</li>
            <li>Using a Component</li>
          </ol>
        </article>
      )
    }
    

    컴포넌트를 생성할 때는 export default 접두사를 사용하면 됩니다. 이 접두사를 사용하면 나중에 다른 파일에서 해당 컴포넌트를 로드할 수 있습니다.

    export default?

    export default 구문은 ES6 구문입니다
    이전에는 모듈 관리를 위해 CJS(CommonJS)문법require/exports 방식을 사용했습니다.

    React 컴포넌트는 반드시 대문자로 시작해야합니다. 대문자로 시작하지 않으면 작동하지 않습니다. (JSX 문법을 사용하는 React 컴포넌트 한정입니다.)

    JSX란?

    JSX(Javascript Syntax eXtension) 은 JS의 확장 문법이며, 주로 React로 프로젝트를 개발할 때 사용됩니다.
    JSX는 브라우저에서 실행되기 전에 Babel을 사용하여 일반 자바스크립트 형태의 코드로 변환됩니다.

    jsx

    // bad
    export default function profile() {
      ...
    }
    
    // good
    export default function Profile() {
      ...
    }
    

    마크업을 추가하기 위해 다음과 같이 로직을 작성할 수 있습니다.

    jsx

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

    한 줄의 경우 다음과 같이 작성할 수 있지만, 여러줄을 return해야하는 경우 괄호로 묶어야 합니다.

    jsx

    return (
      <div>
        <img src="https://i.imgur.com/MK3eW3As.jpg" alt="Katherine Johnson" />
      </div>
    )
    

    컴포넌트 import 및 export

    하나의 파일에 여러 컴포넌트를 선언할 수 있지만 파일이 커지면 탐색하기가 어려워집니다.
    따라서 이 문제를 해결하기 위해 컴포넌트들을 분리하여 export(내보내기) 한 다음 다른 파일에서 해당 컴포넌트를 import(가져오기) 해야합니다.

    앞서 말했던 것 처럼, 컴포넌트를 내보내기 / 가져오기 하기 위해서는 import, export를 사용하면 됩니다. 사용 방법도 간단합니다.

    1. 컴포넌트를 넣을 JS파일을 생성합니다.
    2. 새로 만든 파일에서 함수 컴포넌트를 export합니다. (default 또는 name export 방식을 사용합니다.)
    3. 컴포넌트를 사용할 파일에서 import합니다. (default 또는 named export에 대응하는 방식으로 import합니다.)

    default export / named export

    한 파일에서 default export는 단 하나만, named export는 여러개가 존재할 수 있습니다.
    한 파일에서 하나의 컴포넌트만 export하면 default export를, 그렇지 않으면 named export를 사용합니다.

    import문에서의 확장자

    가끔 import문에 .js와 같은 파일 확장자가 있을 때도 있고 없을 때도 있습니다.
    ES6 문법을 사용한다면 .js와 같은 확장자를 붙여주는게 좋습니다. (있는게 ES6 문법에 가깝습니다.)

    JSX로 마크업 작성하기

    웹은 HTML, CSS, JS를 기반으로 만들어져왔고, 수년 동안 웹 개발자들은 컨텐츠는 HTML, 디자인은 CSS, 로직은 JS로 작성해왔습니다.
    이전까지는 분리된 파일로 관리를 해왔으나 웹이 인터랙티브 해지면서 로직이 컨텐츠를 결정하는 경우가 많아졌습니다.

    그래서 JS가 HTML을 담당하게 되었고, 이것이 React에서 렌더링, 마크업 로직이 같은 위치에 존재하는 이유입니다.

    만약 버튼을 React 컴포넌트로 만든다고 생각해봅시다. 버튼의 렌더링 로직과 마크업 로직이 함께 존재한다면 모든 편집에서 동기화 상태를 유지할 수 있을 것입니다.

    반대로, 버튼의 마크업과 사이드바의 마크업처럼 서로 관련이 없는 항목들은 서로 분리되어있기 때문에 각각 개별적으로 변경하는 것이 안전합니다.

    JSX와 React

    JSXReact는 서로 다른 별개의 개념입니다.
    주로 같이 사용되지만, 독립적으로 사용할 수도 있습니다. (JSX는 React외에도 Next, Gatsby, ...etc)
    JSX는 JS문법의 확장이고, React는 JS 라이브러리입니다.

    React는 프레임워크? 라이브러리?

    프레임워크는 어떠한 기능을 만들기 위한 전반적인 틀을 제공합니다.
    라이브러리는 전체적인 틀이 아닌 하나의 기능만을 도구처럼 제공합니다.
    리액트는 UI를 만드는 기능만을 제공하기때문에 프레임워크가 아닌 라이브러리로 볼 수 있습니다.
    더욱 자세한건 IoC(제어의 역흐름) 을 알아보시면 됩니다.

    html

    <h1>Hedy Lamarr's Todos</h1>
    <img src="https://i.imgur.com/yXOvdOSs.jpg" alt="Hedy Lamarr" class="photo" />
    <ul>
      <li>Invent new traffic lights</li>
      <li>Rehearse a movie scene</li>
      <li>Improve the spectrum technology</li>
    </ul>
    

    위와 같은 html 로직이 있습니다. 이걸 React 컴포넌트로 그대로 변환한다고 해서 React가 렌더링 할 수 있을까요?
    정답은 안된다입니다. 왜냐하면 JSX는 HTML보다 더 염격하고 몇 가지 규칙이 더 존재하기 때문입니다.

    해당 오류를 해결하기 위해서는 단일 엘리먼트만을 반환해야합니다. 아래의 예 처럼요.

    jsx

    <div>
      <h1>Hedy Lamarr's Todos</h1>
      <img
        src="https://i.imgur.com/yXOvdOSs.jpg"
        alt="Hedy Lamarr"
        class="photo"
      >
      <ul>
        ...
      </ul>
    </div>
    

    <div>말고도 <>와 </>를 사용해서 나타낼 수도 있습니다. 이런 빈 태그를 Fragment라고 합니다.
    Fragment는 브라우저상의 HTML 트리 구조에서 흔적을 남기지 않고 그룹화해줍니다.

    jsx

    <>
      <h1>Hedy Lamarr's Todos</h1>
      <img
        src="https://i.imgur.com/yXOvdOSs.jpg"
        alt="Hedy Lamarr"
        class="photo"
      >
      <ul>
        ...
      </ul>
    </>
    

    JSX 태그를 하나로 감싸줘야하는 이유?

    JSX는 HTML처럼 보이지만 내부적으로는 JS 객체로 변환됩니다.
    하나의 배열로 감싸지 않은 하나의 함수에서는 두개의 객체를 반환할 수 없습니다.

    JSX에서는 태그를 명시적으로 닫아야합니다. <img>와 같이 자체적으로 닫는 태그도 마찬가지입니다.
    <li>와 같은 래핑 태그 역시도 반드시 닫아줘야합니다.

    jsx

    <>
      <img src="https://i.imgur.com/yXOvdOSs.jpg" alt="Hedy Lamarr" class="photo" />
      <ul>
        <li>Invent new traffic lights</li>
        <li>Rehearse a movie scene</li>
        <li>Improve the spectrum technology</li>
      </ul>
    </>
    

    JSX는 JS로 바뀌고 JSX로 작성된 어트리뷰트는 JS 객체의 키가 됩니다.
    컴포넌트 안에서 어트리뷰트를 변경하고 싶을 때가 있을 것입니다. 하지만 JS 변수명에는 제한이 있습니다. 예를들어 class같은 이름들 말입니다.

    이것이 React에서 많은 HTML과 SVG 어트리뷰트들이 카멜 케이스로 작성되는 이유입니다. 예를 들어 SVG의 stroke-width 대신 strokeWidth를 사용하는 것 처럼요

    JSX에서 JS 이용하기

    JSX에 문자열 속성을 전달하려면, 작은 따옴표 또는 큰 따옴표로 묶습니다. (백틱 ` 도 가능합니다)

    jsx

    export default function Avatar() {
      return (
        <img
          className="avatar"
          src="https://i.imgur.com/7vQD0fPs.jpg"
          alt="Gregorio Y. Zara"
        />
      )
    }
    

    근데 src나 alt에 동적으로 값을 지정하고싶으면 어떻게 해야할까요? 이럴 때는 큰 따옴표를 중괄호로 대체하여 JS의 값을 사용할 수 있습니다.

    jsx

    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} />
    }
    

    JSX는 JS를 작성하는 특별한 방법입니다. 그것은 즉, 중괄호 안에서 JS를 사용할 수 있다는 의미입니다.
    아래의 로직에서 name값을 변경하면 투두리스트의 제목 또한 변경됩니다.

    jsx

    export default function TodoList() {
      const name = 'Gregorio Y. Zara'
      return <h1>{name}'s To Do List</h1>
    }
    

    중괄호 사이에는 모든 JS 표현식이 작동합니다.

    jsx

    const today = new Date()
    
    function formatDate(date) {
      return new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(date)
    }
    
    export default function TodoList() {
      return <h1>To Do List for {formatDate(today)}</h1>
    }
    

    JSX 내부에서는 중괄호를 두 가지 방법으로만 사용할 수 있습니다.

    1. JSX 태그 안에 직접 텍스트로 사용하기 : ex) <h1>{name}</h1>
    2. = 기호 바로 뒤에 오는 속성 : ex) src={avatar}

    문자열, 숫자 및 기타 JS 표현식 외에도 JSX로 객체를 전달할 수도 있습니다. 객체는 중괄호로 표시할 수 있습니다.
    따라서 JSX에서 JS 객체를 전달하려면 다른 중괄호 쌍으로 객체를 감싸야합니다.

    JSX의 인라인 CSS스타일에서 이것을 볼 수 있습니다. (사실 React에서는 딱히 인라인 스타일을 사용할 필요가 없습니다.)
    하지만 필요한 경우 다음과 같이 객체를 전달할 수 있습니다.

    jsx

    export default function TodoList() {
      return (
        <ul
          style={{
            backgroundColor: 'black',
            color: 'pink',
          }}>
          <li>Improve the videophone</li>
          <li>Prepare aeronautics lectures</li>
          <li>Work on the alcohol-fuelled engine</li>
        </ul>
      )
    }
    

    JSX 인라인 CSS 스타일을 쓸 때 주의점

    JSX에서 인라인 CSS 스타일을 쓸 때는 속성 이름을 반드시 카멜 케이스로 작성해야합니다.

    컴포넌트에 props 전달하기

    React 컴포넌트는 서로 통신하기 위해 props를 사용합니다. 모든 부모 컴포넌트는 자식 컴포넌트에 props를 전달할 수 있습니다.
    props라고 하면 HTML 어트리뷰트를 떠올릴 수 있지만 객체, 배열, 함수, 심지어 JSX를 포함한 모든 JS값을 전달할 수 있습니다.

    다음 예제에서는 Card 컴포넌트가 props로 children을 받고있습니다.

    children?

    children은 태그와 태그 사이에 있는 값을 말합니다.
    ex) <h1>안녕</h1> -> children : 안녕

    jsx

    import { getImageUrl } from './utils.js'
    
    export default function Profile() {
      return (
        <Card>
          <Avatar
            size={100}
            person={{
              name: 'Katsuko Saruhashi',
              imageId: 'YfeOqp2',
            }}
          />
        </Card>
      )
    }
    
    function Avatar({ person, size }) {
      return (
        <img
          className="avatar"
          src={getImageUrl(person)}
          alt={person.name}
          width={size}
          height={size}
        />
      )
    }
    
    function Card({ children }) {
      return <div className="card">{children}</div>
    }
    

    children 말고 다른 일반적인 props를 전달하고 싶다면 다음과 같이 사용하면 됩니다.

    jsx

    export default function Profile() {
      return (
        <Avatar person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }} size={100} />
      )
    }
    

    이제 Avatar 컴포넌트 내부에서는 person을 조회하여 name과 imageId 값을, size를 통해 100이라는 값을 참조할 수 있습니다.
    자식 컴포넌트에서는 다음과 같이 사용하면 됩니다.

    jsx

    function Avatar({ person, size }) {
      // person and size are available here
    }
    

    props를 선언할 때 주의점

    반드시 내부에 중괄호 쌍을 넣어줘야 합니다.
    ex) function Avatar({ person, size }) { ... }

    props에 기본값을 지정하는 방법도 있습니다.

    jsx

    function Avatar({ person, size = 100 }) {
      // ...
    }
    

    때때로 전달되는 props들은 반복적입니다. 이럴때는 spread 구문을 통해 간결하게 나타낼 수 있습니다.

    jsx

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

    단, 전개구문은 제한적으로 사용되어야 합니다. 다른 모든 컴포넌트에 이 구문이 적용되어있다면 문제가 있는 것입니다.
    이는 종종 컴포넌트를 분할하여 자식을 JSX로 전달해야 함을 나타냅니다.

    props는 항상 고정되어 있지 않습니다. 부모 컴포넌트로부터 받게되는 데이터가 변경되면 props는 그때마다 변경된 데이터를 반영합니다.
    그러나 props는 불변으로, 컴퓨터 과학에서 변경할 수 없다는 뜻의 용어입니다.
    컴포넌트가 props를 변경해야 하는 경우 부모 컴포넌트에 다른 props, 즉 새로운 객체를 전달하도록 요청해야합니다.

    그렇게 되면 이전의 props는 버려지고, JS 엔진은 기존 props가 차지했던 메모리를 회수(가비지 콜렉팅) 하게 됩니다.
    props 변경을 시도하지 마세요 사용자 입력에 따라 props가 변경되어야 하는 경우에는 후에 나올 set state가 필요할 것입니다.

    조건부 렌더링

    상품이 포장되었는지 여부를 표시할 수 있는 여러개의 Item을 렌더링하는 PackingList 컴포넌트가 있다고 가정해봅시다.

    일부 Item 컴포넌트의 isPacked prop이 false가 아닌 true로 설정되어 있는 것을 확인할 수 있습니다.
    이 경우, true 항목에 대해서 체크 표시를 추가하고 싶습니다. 그렇다면 어떤식으로 이를 구현할 수 있을까요?

    if/else 문으로 해당 기능을 구현할 수 있습니다.

    어떤 상황에서는 아무것도 렌더링하고 싶지 않을수도 있습니다. 예를들어, 포장된 아이템을 전혀 표시하고 싶지 않다고 가정해보겠습니다.
    컴포넌트는 무언가를 반환해야만 합니다. 이런 경우에는 null을 반환하면 됩니다.

    실제로 컴포넌트에서 null을 반환하는 것은 렌더링하려는 개발자를 놀라게 할 수 있기 때문에 일반적이지 않습니다.
    부모 컴포넌트의 JSX에 컴포넌트를 조건부로 포함하거나 제외하는 경우가 더 많습니다. 이러한 방법은 다음에서 설명하도록 하겠습니다.

    이전 예제에서는 컴포넌트가 반환할 JSX 트리가 있는 경우를 제어했습니다. 그런데, 해당 렌더링 출력을 가만히 보다보면 중복을 발견할 수 있습니다.

    jsx

    <li className="item">{name}</li>
    

    jsx

    <li className="item">{name}</li>
    

    위와 아래는 매우 유사합니다. 이러한 중복은 해롭지는 않지만 코드를 유지 관리하기 어렵게 만들 수 있습니다.
    className을 변경하려면 어떻게 해야할까요? 코드의 두 곳 모두에서 변경해야 할 것입니다.

    이런 상황에서는 조건부로 약간의 JSX를 포함시켜 코드를 더 DRY하게 만들 수 있습니다.

    삼항 연산자를 사용하면 이를 간결하게 나타낼 수 있습니다.

    jsx

    // bad
    if (isPacked) {
      return <li className="item">{name}</li>
    }
    return <li className="item">{name}</li>
    
    // good
    return <li className="item">{isPacked ? name + ' ✔' : name}</li>
    

    또 다른 단축표현으로는 논리 AND 연산자 (&&) 가 있습니다.
    React 컴포넌트 내에서 조건이 참일 때 일부 JSX를 렌더링하거나 아무것도 렌더링하지 않으려 할 때 자주 사용됩니다.

    jsx

    return (
      <li className="item">
        {name} {isPacked && '✔'}
      </li>
    )
    

    JS && 표현식은 왼쪽(조건)이 참이면 오른쪽의 값을 반환합니다.
    하지만 조건이 거짓이라면 전체가 false가 되어 아무것도 렌더링하지 않습니다.

    아래는 위의 단축표현을 사용하여 구현한 예제입니다.

    &&를 사용할 때 주의점

    &&의 조건 사이드(왼쪽)에 숫자를 넣지 마세요. 조건 검사시 JS는 왼쪽을 자동으로 boolean형으로 변환합니다.
    예를 들어, count && <p>New Messages</p> 와 같은 코드를 작성한다면 count가 0일 때 0 자체를 렌더링하게됩니다.
    이럴 때는 왼쪽을 boolean 형태로 만들면 됩니다.
    ex) messageCount > 0 && <p>New Messages</p>

    단축키가 일반 코드를 작성하는데 방해가 된다면 if문과 변수를 사용해 보세요. let으로 정의된 변수는 재할당할 수 있습니다!

    jsx

    let itemContent = name
    

    if문을 사용하여 isPacked가 true면 JSX 표현식을 itemContent에 재할당합니다.

    jsx

    if (isPacked) {
      itemContent = name + ' ✔'
    }
    

    이 다음 반환된 변수를 JSX 트리에 삽입하여 이전에 계산된 표현식을 JSX 안에 중첩시킵니다.

    jsx

    <li className="item">{itemContent}</li>
    

    아래는 이를 사용한 예제입니다.


    목록 렌더링

    jsx

    <ul>
      <li>Creola Katherine Johnson: mathematician</li>
      <li>Mario José Molina-Pasquel Henríquez: chemist</li>
      <li>Mohammad Abdus Salam: physicist</li>
      <li>Percy Lavon Julian: chemist</li>
      <li>Subrahmanyan Chandrasekhar: astrophysicist</li>
    </ul>
    

    위와 같은 콘텐츠 목록이 있다고 가정해보겠습니다.
    이러한 목록을 React에서 어떻게 효과적으로 렌더링할 수 있을까요?

    다음은 배열에서 항목 목록을 생성하는 방법에 대한 간단한 예시입니다.

    1. 데이터를 배열로 이동시킵니다.

    js

    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 노드 배열에 매핑합니다.

    jsx

    const listItems = people.map((person) => <li>{person}</li>)
    
    1. 컴포넌트에서 ul로 감싼 listItems를 반환합니다.

    jsx

    return <ul>{listItems}</ul>
    

    만약 항목 배열을 필터링 하고싶다면 이 또한 간단합니다. JS의 배열 함수인 Array.filter() 를 사용하여 위의 과정을 수행하면 됩니다.

    react key error

    매핑된 항목들에 key가 존재하지 않을 때 해당 오류가 발생합니다. 이 오류에 주목해봅시다.
    React에서 각 배열 항목에는 해당 배열의 항목들 사이에서 고유하게 식별할 수 있는 문자열 또는 숫자인 key를 부여해야합니다.

    key는 다음 규칙을 따라야 합니다.

    • key는 형제간에 고유해야 합니다.
    • key는 변경되지 않아야 합니다.

    그럼 이러한 key들은 어디에서 얻을 수 있을까요?

    • 데이터베이스의 데이터의 경우 데이터베이서의 key/ID를 사용할 수 있습니다.
    • 로컬에서 생성된 데이터의 경우 uuid와 같은 패키지를 사용하면 됩니다.

    그래서 왜 Key가 필요한건데요?

    key가 없는 경우 가상 DOM과 비교하는 과정에서 순차적으로 비교하며 변화를 감지합니다.
    key가 있는 경우 가상 DOM과 비교하는 과정에서 key를 사용하여 변화를 더욱 빠르게 감지할 수 있습니다.

    배열의 인덱스를 Key로 쓰지 마세요

    배열의 인덱스는 고정적이지 않습니다. 배열의 원소가 추가되거나 삭제되며 변할 수 있습니다.
    이때문에 가상 DOM트리와 비교하며 DOM트리를 업데이트하는 과정이 정상적으로 작동하지 않을 수 있습니다.
    비슷한 이유로 Math.random도 쓰지 않는것을 권장합니다.

    컴포넌트 순수성 유지

    순수 함수란 다음과 같은 특징을 가진 함수입니다.

    • 자신의 일에만 신경씁니다. 호출되기 전에 존재했던 객체나 변수를 변경하지 않습니다.
    • 동일 입력, 동일 출력. 동일한 입력이 주어지면 항상 동일한 결과를 반환 해야합니다.

    아래와 같은 함수가 JS에서 순수함수라고 할 수 있습니다.

    js

    function double(number) {
      return 2 * number
    }
    

    React는 이 개념을 중심으로 설계되었습니다. React는 우리가 작성하는 모든 컴포넌트가 순수 함수라고 가정합니다.
    우리가 작성하는 React컴포넌트는 동일한 입력이 주어졌을 때 항상 동일한 JSX를 반환해야합니다.

    컴포넌트를 레시피라고 생각할 수 있습니다. 레시피를 따르고 요리 과정에서 새로운 재료를 넣지 않으면 매번 같은 요리를 얻을 수 있습니다.
    React에서 그 요리는 컴포넌트가 렌더링에 반응하기 위해 제공하는 JSX입니다.

    React의 렌더링 프로세스는 항상 순수해야 합니다. 컴포넌트는 오직 JSX만을 반환해야하며, 렌더링 전에 존재했던 객체나 변수를 변경해서는 안됩니다.

    jsx

    let guest = 0
    
    function Cup() {
      // Bad: changing a preexisting variable!
      // 나쁨: 기존 변수를 변경합니다!
      guest = guest + 1
      return <h2>Tea cup for guest #{guest}</h2>
    }
    
    export default function TeaSet() {
      return (
        <>
          <Cup />
          <Cup />
          <Cup />
        </>
      )
    }
    

    이 컴포넌트는 다음과 같은 문제를 야기합니다.

    • 외부에서 선언된 guest 변수 때문에 호출할 때마다 다른 JSX가 생성됩니다.
    • 다른 컴포넌트가 guest를 읽으면 렌더링된 시점에 따라 JSX도 다르게 생성됩니다.

    우리는 guest를 props로 전달함으로써 이러한 사이드이펙트를 방지할 수 있습니다.

    jsx

    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} />
        </>
      )
    }
    

    이제 컴포넌트가 반환하는 JSX는 guest props에만 의존하므로 순수하다고 할 수 있습니다.

    일반적으로 컴포넌트가 특정 순서로 렌더링될 것이라고 기대해서는 안됩니다.
    예를들어 y = 2x라는 수식을 y = 5x앞에 호출하든 뒤에 호출하든요. 두 수식은 서로 독립적으로 해결됩니다.
    마찬가지로 각 컴포넌트는 렌더링 중에 다른 컴포넌트와 조율하거나 의존하지 말고 스스로 생각하게 해야 합니다.

    위의 예시에서는 컴포넌트가 렌더링하는동안 기존 변수를 변경하는 것이 문제였습니다.
    하지만 React에서 렌더링하는동안 '방금' 생성한 변수와 객체를 변경하는 것은 완전히 괜찮습니다. 아래는 그 예시입니다.

    jsx

    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에서 사이드 이펙트는 보통 이벤트 핸들러에 속합니다. 이벤트 핸들러는 사용자가 어떤 동작을 수행할 때 React가 실행하는 함수입니다.
    이벤트 핸들러가 컴포넌트 내부에 정의되어 있긴 하지만 렌더링중에는 실행되지 않습니다. 따라서 이벤트 핸들러는 순수할 필요가 없습니다

    다른 모든 옵션을 다 사용했는데도 사이드 이펙트게 적합한 이벤트 핸들러를 찾을 수 없다면, 컴포넌트에서 useEffect호출을 통해 반환된 JSX에 이벤트 핸들러를 첨부할 수 있습니다.
    하지만 이 방법은 최후의 수단으로 사용되어야 합니다.

    트리로서의 UI

    트리는 아이템 간의 관계 모델이며 UI는 트리 구조를 사용하여 표현되는 경우가 많습니다.
    예를 들어 브라우저는 트리 구조를 통해 HTML(DOM) 및 CSS(CSSOM)를 모델링합니다. 모바일 플랫폼 또한 트리를 사용하여 뷰 계층 구조를 나타냅니다.

    browser render tree

    React역시 마찬가지입니다.

    • React는 모듈과 컴포넌트 사이의 관계를 트리 형태로 구성합니다.
    • React의 렌더 트리는 컴포넌트간의 부모와 자식관계를 나타낼 수 있습니다.

    react render tree

    렌더 트리는 렌더 패스마다 다를 수 있지만 일반적으로 이러한 트리는 React 앱의 최상위 컴포넌트와 리프 컴포넌트가 무엇인지 식별하는데 도움이 됩니다.
    최상위 컴포넌트의 경우 그 아래에 있는 모든 컴포넌트의 렌더링 성능에 영향을 미치며 종종 가장 복잡한 컴포넌트를 포함합니다.
    리프 컴포넌트는 트리 하단에 있으며 하위 컴포넌트가 없고 자주 리렌더링 됩니다.

    트리로 모델링 할 수 있는 React 앱의 또 다른 관계는 앱의 모듈 종속성입니다. 컴포넌트와 로직을 별도의 파일로 분리하면서 컴포넌트, 함수 또는 상수를 내보낼 수 있는 JS 모듈을 만듭니다.

    • 모듈 종속성 트리의 각 노드는 모듈이고 각 분기는 해당 모듈의 import 문을 나타냅니다.
    • 이전 Inspirations 앱을 사용하면 모듈 종속성 트리, 줄여서 종속성 트리를 구축할 수 있습니다.

    react module dependency tree

    트리의 루트 노드는 entrypoint 파일이라고도 알려진 루트 모듈입니다. 루트 컴포넌트를 포함하는 모듈인 경우가 많습니다.

    위의 렌더 트리와 비교해보면 구조는 비슷하지만 몇 가지 차이점이 있습니다.

    • 트리를 구성하는 노드는 컴포넌트가 아닌 모듈입니다.
    • Inspirations.js와 같은 컴포넌트가 아닌 모듈도 트리에 표시됩니다. 반면에 렌더 트리는 컴포넌트만 트리로 캡슐화합니다.
    • Copyright.jsApp.js의 하위 노드로 나타나지만 렌더트리에서 Copyright 컴포넌트는 InspirationGenerator의 하위 컴포넌트로 나타납니다.
      • 이는 JSX를 ChildProp으로 전달하여 자식 컴포넌트로 렌더링하지만 모듈을 가져오지는 않기 때문입니다.

    종속성 트리는 React 앱을 실행하는 데 필요한 모듈들을 결정하는 데 유용합니다. 프로덕션용 React 앱을 빌드할 때 일반적으로 클라이언트에 제공하는 데 필요한 모든 JS를 번들로 묶는 빌드 단계가 있습니다.

    이를 담당하는 도구를 번들러라고 하며, 번들러는 종속성 트리를 사용하여 어떤 모듈을 포함할지 결정합니다.

    앱이 성장함에 따라 번들 크기도 커지는 경우가 많습니다. 큰 번들 크기는 다운로드 / 실행하는데 비용이 많이 듭니다.
    번들 크기가 크면 UI가 그려지는 시간이 지연될 수 있습니다. 앱의 종속성 트리를 이해하면 이러한 문제를 디버깅하는데 도움이 될 수 있습니다.

    번들러?

    번들러란 HTML, CSS, JS 등의 파편화된 자원들을 모아서, 하나 혹은 최적의 소수파일로 결합하는 도구입니다.
    결합을 위해 프로젝트를 해석하는 과정에서 불필요한 주석이나 공백 제거, 단독화, 파일 압축 등의 기본적인 작업에 더해 최신 문법이나 개발을 용이하게 하는 특수 기능 등을 브라우저가 지원하는 형태로 변환하는 작업도 수행합니다.
    Webpack, Rollup, Parcel과 같은 도구들이 대표적입니다.

    참고

    Profile Image

    신현호

    Frontend Developer

    프론트엔드 개발자를 꿈꾸고 있는 대학생입니다. 끊임없이 배우고 성장하는 개발자가 되기 위해 노력하고 있습니다.

    React Docs

    총 5개의 포스트가 존재합니다.