All the Things about IT & US stocks
FrontEnd

프론트엔드: 리액트(React) 프로그래밍 #2

리액트 프로그래밍 #2The Big Nerd Ranch Guide

#2 컴포넌트
현재 각 엘리먼트(element)는 App.js에 하드코딩 되어 있다. 일단 이렇게 해도 동작은 하지만 동일한 코드가 여러 번 반복된다. 만약 Ottergram이 실제 회사에서 운영하는 애플리케이션이라면, 다양한 사용자들이 자신들의 사진을 공유하게 될 것이다. 이렇게 되면, 지금과 같이 Post를 하드코딩 하는 방법을 사용할 수 없다.

다행히 이런 문제를 해결할 좋은 방법이 있다.

이번에는 컴포넌트를 생성하는 방법에 대해 알아볼 것이다.

App Organization
컴포넌트는 전체 애플리케이션의 구성요소이다.
대규모 웹 애플리케이션은 비행기의 부품처럼 애플리케이션의 각 부분은 고유한 기능을 가지고 있고 자체 로직을 관리하며, 다른 부분 기능들과 결합하여 전체 애플리케이션을 구성한다.

리액트에서는 이것을 컴포넌트(components)라고 한다. 컴포넌트는 리액트 엘리먼트를 반환하는 자바스크립트(JavaScript) 함수이다.

Header Component
현재 Ottergram의 구조는 아래와 같다.

페이지의 상단은 타이틀(Ottergram)을 표시하는 헤더(header)이다. 우리는 가장 먼저 이 헤더를 캡슐화(encapsulate)할 것이다.

비주얼 스튜디오 코드(VS Code)에서, src 디렉토리에 components 라는 새로운 폴더를 생성한다.
components 폴더 내에 Header.js 라는 파일을 생성한다.

Header함수는 자바스크립트 함수와 유사하다. 실제로 Header와 같은 컴포넌트는 리액트 엘리먼트를 반환하는 자바스크립트 함수이다.

자바스크립트 함수의 명명 규칙은 camelCase를 사용하지만 Header는 대문자로 시작한다. 리액트 컴포넌트의 경우, 기존 HTML 엘리먼트와 리액트 컴포넌트를 구분하기 위해 PascalCase를 사용한다.

Header 함수를 정의한 후 export default를 사용하여 export할 수 있다. 함수를 export하면 다른 파일에서 해당 함수를 사용할 수 있다.

새로 생성한 Header.js 파일에 App.js의 모든 헤더 코드를 반환하는 Header라는 함수를 생성한다.

function Header() {
    return (
        <header className='header-component'>
            <h1>Ottergram</h1>
        </header>
    )
}

export default Header;

함수를 정의한 후 export default를 사용하여 함수를 export 할 수 있다. 함수를 export 하면 다른 파일에서 해당 함수를 import 해서 사용할 수 있다.

Header.js를 저장한 다음 해당 Header 컴포넌트를 App.js에 import 한다.
그리고 기존 header 마크업(markup)을 제거한다.

import './App.css';
import Header from './components/Header';
import Barry from './otters/otter1.jpg';
...
function App() {
  return (
    <div>
      <header className='header-component'>
        <h1>Ottergram</h1>
      </header>
      <Header />
      <ul className='post-list'>
...

App.js를 저장하고 브라우저를 확인해보자. 브라우저의에서 보여지는 것은 그대로이지만, 헤더 컴포넌트가 포함되어 있다.

Header.js 파일에 CSS 스타일이 직접적으로 적용되어 있지 않지만, 스타일은 기존과 같이 유지된다. 그 이유는 모든 CSS는 전역화(global)되어 있기 때문에 전체 애플리케이션에서 사용할 수 있기 때문이다. 대부분의 컴포넌트는 CSS가 직접 포함되어 있지 않고 CSS는 별도로 분리되어 있다.

CSS 셀렉터(selectors)는 애플리케이션의 전체 컴포넌트에서 고유해야 한다. 동일한 이름을 가지는 컴포넌트가 없는 한, 일반적으로 컴포넌트 이름을 클래스 접두사로 사용하면 된다.
우리의 경우에는 src/App.css에 스타일 관련 코드를 관리한다.

JSX
JSX는 HTML과 유사하지만, Header 컴포넌트는 JSX를 리턴한다. 이미 앞에서 JSX를 언급했었다. JSX는 HTML과 유사하게, 리액트에서 선언적 코드(declarative code)를 작성할 수 있게 해주는 syntax extension 이다.
Babel과 같은 컴파일러는 JSX를 자바스크립트로 컴파일한 다음 브라우저에서 실행한다.

JSX를 사용하지 않으면 코드는 어떻게 달라질까? Header 컴포넌트는 아래와 같이 변경될 것이다.

   import React from 'react';
    
   const Header = () => React.createElement(
    'header',
     null,
     React.createElement(
        'h1',
        {className: "title"},
        'Ottergram'
      )
    );
    
    export default Header;

JSX를 통해 읽기 쉬운 태그를 사용하는 대신, JSX를 사용하지 않는 경우 Header function은 리액트 API인 createElement를 사용한다.
두 버전 모두 리액트 엘리먼트를 반환하는 유효한 코드이다.

하지만 위의 예시 보다 더 복잡한 컴포넌트를 생성하는 경우, 코드가 길어지고 이해하기 어렵게 될 수 있다.

JSX는 코드를 간결하고 이해하기 쉽게 작성할 수 있도록 도와준다.

Post 컴포넌트
Ottergram를 다시 살펴보면, 아래와 같이 otter의 이미지와 이름을 반복하는 엘리먼트가 있다.

새로운 컴포넌트를 추가하기 위해서는 고유한 버튼, 이미지, 텍스트로 신규 리스트 아이템을 추가해야 한다. 즉 각 아이템의 이미지와 이름을 개별적으로 지정해야 하므로 이전 아이템의 코드를 다시 사용할 수 없음을 의미한다. 하지만 각 아이템의 기본 구조가 동일하므로 구조를 재사용 가능한 단일 컴포넌트로 추출할 수 있다. 해당 컴포넌트는 웹 사이트의 단일 post를 포함하기 때문에 “post”로 명명한다.

components 디렉토리에 신규 파일을 생성하고 해당 파일의 이름을 Post.js로 입력한다.

App.js에서, <li class name=”post-component’> 부터 </li> 까지의 코드를 복사한다.

Post.js에서, Post 함수(function)를 정의하고 list 아이템 코드를 return 문에 붙여넣는다.
Barry 이미지 파일을 Import 한다. 마지막으로, export 구문을 추가한다.

import Barry from '../otters/otter1.jpg';

function Post() {
    return (
        <li className='post-component'>
          <button>
            <img src={Barry} alt='Barry'/>
            <p>Barry</p>
          </button>
        </li>
    )
}

export default Post;

Post.js를 저장 한 다음 App.js로 돌아간다. Post 컴포넌트를 import하고 Barry 리스트 아이템을 Post로 대체한다.

import './App.css';
import Header from './components/Header';
import Barry from './otters/otter1.jpg';
import Post from './components/Post';
import Robin from './otters/otter2.jpg';
...
function App() {
  return (
    <div>
      <Header />
      <ul className='post-list'>
        <li className='post-component'>
          <button>
            <img src={Barry} alt='Barry'/>
            <p>Barry</p>
          </button>
        </li>
        <Post />
        <li className='post-component'>
...

App.js를 저장하고 브라우저를 확인한다. 브라우저에 보이는 내용들은 이전과 동일하지만 이제 해당 웹 페이지는 2개의 리액트 컴포넌트를 포함하고 있다. React Developer Tools를 사용해서 해당 컴포넌트를 확인해 보자.

React Developer Tools
React Developer Tools는 크롬 브라우저의 확장 프로그램(extension)이다.
해당 확장 프로그램은 크롬 개발자 도구(Chrome Developer Tools)와 통합되어 브라우저에 포함된 컴포넌트에 대한 추가정보 및 유용한 정보를 제공한다.

해당 확장 프로그램을 설치하려면 크롬 웹 스토어(https://chrome.google.com/webstore/category/extensions)로 이동한 다음 “React Developer Tools”를 검색하고 크롬에 추가하면 된다.

브라우저에서 Chrome Dev Tools(개발자 도구)를 열면 Components와 Profiler, 2개의 새로 추가된 탭을 확인할 수 있다.

Components 탭을 클릭한다. React DevTools에서 Header와 Post 컴포넌트를 확인할 수 있다.

Post 컴포넌트를 클릭해보자.
다음 절에서 props 컴포넌트에 대해 자세히 살펴볼 것이다. Post 컴포넌트는 현재 props를 가지고 있지 않지만, Post 컴포넌트가 props를 가지는 경우 props 섹션에 표시된다.

React DevTools는 또한 우리가 생성한 컴포넌트가 어떻게 사용되는지에 대해서도 알려준다. Post 컴포넌트의 경우, App 컴포넌트를 통해 렌더링 된다(Post component is redered by the App component). App 컴포넌트는 Post 컴포넌트의 부모(parent) 컴포넌트라고 할 수 있다.

컴포넌트에 이름을 부여하는 이유는 무엇인가? 간단히 말하자면, 디버깅을 위해서이다.
이미 자바스크립트에 익숙한 경우, 왜 컴포넌트를 별도로 생성하고 export 했는지 궁금할 것이다.
예를 들어 Header의 경우 아래와 같은 화살표 함수(arrow function)를 사용하면 코드를 간략하게 사용할 수 있다.

    export default () => (
     <header>
       <h1>Header</h1>
     </header>
    );

Header를 위와 같이 정의해도 Header가 렌더링 된다. 하지만 React DevTools에서 Header라는 이름을 인식할 수 없다. 왜냐하면 React DevTools는 함수의 이름을 통해 컴포넌트의 이름을 인식하기 때문이다. DevTools에서 컴포넌트의 이름을 확인할 수 있으면 디버깅을 수행하는 경우 매우 유용하다. 따라서 컴포넌트를 생성하고 export 하기를 권장한다.

Props
App.js의 5개 리스트 아이템은 모두 동일한 구조를 가지며 동일한 컴포넌트를 사용한다.
하지만 지금까지 각각의 otter post에 Post 컴포넌트를 사용해 왔다. 왜냐하면 컴포넌트의 이미지와 텍스트가 해당 otter에만 적용되기 때문이다.

그렇다면 5개의 리스트 아이템에서 사용되는 Post 컴포넌트를 어떻게 하면 재사용할 수 있을까? 이에 대한 답변은 리액트의 또 다른 기능인 Props 이다.

리액트 컴포넌트는 자바스크립트 함수이기 때문에, 다른 함수와 마찬가지로 인수(arguments)를 취할 수 있다. 리액트에서는 props를 사용하여 컴포넌트에 데이터를 전달한다.
Props는 자바스크립트 문자열, 숫자, 객체, 배열, 함수와 같은 모든 유형의 데이터가 될 수 있다.

JSX에서는 HTML 속성(attributes)과 유사한 속성을 사용하여 컴포넌트에 props을 전달한다.
이러한 속성들을 props 객체(objects)로 그룹화하고, 컴포넌트 함수를 호출하는 경우 첫 번째 매개변수(parameter)로 전달할 수 있다.

Post는 표시해야 하는 otter의 image와 name을 알아야 한다. image와 name이 바로 App에서 전달해야 하는 props가 될 수 있다.

App.js에서, 속성을 사용하여 image와 name props의 값을 Post 컴포넌트에 전달하면 된다. 또한 Barry의 이미지를 다시 import 해야 한다.

속성을 통해 props 전달(App.js)

이제 Post.js는 props를 전달받을 준비가 되었다. 리액트는 image와 name 속성을 해당 값에 접근할 수 있는 props 객체의 key로 사용한다.

Post.js에서, props를 Post()의 첫 번째 매개변수로 추가한 다음, 기존에 하드코딩된 값(Barry)을 props로 전달된 동적 값(props.image, props.name)으로 대체한다.

prop 값 활용(Post.js)

중괄호{}를 사용하여 JSX 마크업에 자바스크립트를 삽입했다. 해당 구문(syntax)을 JSX 표현식(expression)이라고 한다. JSX 표현식은 확인을 거쳐 다른 JSX와 함께 렌더링된다.

React DevTools로 가서 Post 컴포넌트를 확인해 보자. 이제 우리가 Post.js에 전달한 props 값을 확인할 수 있다.

DevTools에서 post props 값 확인

image prop은 <img> 태그의 src로 사용되는 문자열 경로를 보여준다. name prop은 post에 표시되는 otter의 이름을 보여준다.
이제 Post 컴포넌트는 동적 정보를 처리할 수 있기 때문에 다른 otter들에게도 props를 활용할 수 있다.

컴포넌트 재사용
App.js의 나머지 otter들에 대한 코드도 각 otter의 이미지와 이름을 prop로 전달하는 Post 컴포넌트로 대체해보자.

Post 컴포넌트 재사용(App.js)

우리는 위와 같은 접근 방식의 한 가지 이점을 바로 확인할 수 있다. 즉, 코드가 훨씬 짧고 가독성이 좋아진다. 또한 Post가 구조화되는 방식이나 Ottergram에 표시되는 방식을 변경하고자 하는 경우, 각각의 이미지와 이름을 개별적으로 편집할 필요 없이 Post 컴포넌트를 한 번만 편집하면 된다.

React DevTools로 가서 컴포넌트를 확인해보자.

멀티플 컴포넌트

Post를 재사용할 수 있게 되었으니, 이번에는 otter의 이름이 눈에 더 잘 띄게 변경해보자.
App.css에 post-name을 추가해서 otter name의 font를 bold로 설정하고 size를 2em으로 변경해보자.

post-name 스타일 추가(App.css)

이번에는 Post 컴포넌트에 className=’post-name’ 을 아래와 같이 추가해야 한다.

className 추가(Post.js)

해당 파일을 저장하고 브라우저를 확인해보자. otter의 이름이 Bold체로 변경되어 있을 것이다.

객체 분해(Object destructuring)
props 객체는 컴포넌트가 기대하는 데이터에 대한 인사이트(insight)를 충분히 제공하지 않는다. props 인수(argument)는 자바스크립트 객체이기 때문에 이를 분해하여 개별 props를 얻을 수 있다.
컴포넌트 시그니처(signature)의 props를 분해하는 것은 추후 해당 컴포넌트를 사용하는 개발자에게 유용하다. props 분해는 컴포넌트가 필요로 하는 데이터에 대한 문서를 제공하는 데 도움이 된다. 또한 props 접두사(prefifx)를 제거하여 코드의 가독성을 높인다.

Post.js의 props 분해

Post.js를 저장하고 브라우저를 확인해 보자(변화는 없다).

리스트 렌더링
Post 컴포넌트는 App.js에 있는 리스트(li 관련 코드) 아이템 코드를 간략하게 만들었다. 하지만 post를 한 번에 하나씩 추가하는 것은 여전히 뭔가 수동적인 작업의 느낌이 난다. Post의 양이 많아지면 이러한 접근 방법은 실용적이지 않다.

유용한 방법이 있다.

실제 애플리케이션의 경우, 리스트에 추가되는 아이템은 어떤 식으로든 서로 관련되어 있을 확률이 높다. 데이터는 배열이나 다른 구조로 그룹화될 가능성이 높으며 외부 소스 또는 앱 내의 데이터 세트에서 가져올 수 있다.

또한 개별 컴포넌트 리스트로부터 데이터를 렌더링 하고자 하는 경우, 리액트는 이와 관련된 도구를 가지고 있다.

이것을 구현하기 위해서는, 현재 가지고 있는 데이터로 구성된 배열을 생성함으로써 보다 현실적인 셋업 모델링을 시작할 수 있다.(Although it would be most true to life for your data to come from an external source or a separate data file, go ahead and hardcode your array in App.js.)

otters 배열 생성(App.js)


위의 “otterArray” 배열을 사용하면, 기존의 Post 컴포넌트를 생성하는 반복 코드를 대체할 수 있다. 즉, 중괄호({}) 를 사용하여 자바스크립트 코드를 JSX 마크업에 삽입한다. 중괄호 내에서 자바스크립트의 Array.map 함수를 사용하면 배열 내에 포함된 image와 name 값을 순차적으로 반복(iterate)하고 해당 값에 대한 Post 컴포넌트를 반환하게 만들 수 있다.

자바스크립트에서 배열을 순차적으로 반복하는 방법에는 여러 가지가 있지만 컴포넌트를 렌더링하는 경우 일반적으로 map()을 사용한다. 왜냐하면 map 함수는 새로운 배열을 반환하고 리액트는 반환된 배열의 엘리먼트를 부모 <div> 의 자식으로 사용하는 것을 알고 있기 때문이다.

각 post를 image와 name으로 분리하고 해당 데이터를 props로 각각 따로 전달하는 이유는 무엇일까?

Post 컴포넌트는 주로 프레젠테이션, 즉 무엇인가를 보여주는 것과 관련되어 있다. 현재 post 항목에는 2 가지 속성(properties)만 포함되어 있지만, 애플리케이션을 개발함에 따라 해당 속성을 추가될 수 있다.
컴포넌트를 다른 데이터 형식에 의존하지 않도록 해야한다. 그렇게 하지 않으면 재사용성이 제한되기 때문이다. 대신 렌더링하는 데 필요한 정보만 Post 컴포넌트에 전달하도록 해야 한다.

키(Keys)
App.js를 저장하고 브라우저에서 확인해 보자.
문제가 발생한다. DevTools(개발자 도구)의 메뉴에서 콘솔(Console) 탭을 클릭해 보자.

Key warning in the browser

에러 메시지를 확인해 보자. 리스트의 각 자식(child)은 키(key)를 prop으로 가져야 한다.
키는 컴포넌트를 고유하게 식별하는 값이다. 리액트는 리스트의 항목(item)이 추가, 제거 또는 변경되는 경우 전체 리스트가 아닌 해당 항목만 렌더링하기 위해 키를 사용한다.
수천 개의 항목 리스트를 렌더링하는 애플리케이션을 가정해 보자. 사용자가 하나의 아이템을 변경한 경우, 리액트는 전체 리스트를 다시 렌더링하는 대신 키 속성을 사용하여 사용자가 변경한 하나의 아이템만 다시 렌더링 하면 된다.

각각의 키는 리스트 내의 아이템들 사이에서 고유해야 하며, 동일한 데이터에 일관성 있게 연결되어야 한다. 위에서 발생한 경고와 마찬가지로 배열에서 동일한 키를 여러 번 사용하면 키 경고가 발생하고, 의도하지 않은 현상이 발생할 수 있다.

랜덤 값을 사용할 수도 없다. 랜덤 값은 고유할 수 있지만, 리액트가 컴포넌트를 다시 렌더링할 때마다 변경되어 키를 활용한 최적화가 수행되지 않을 수 있다. 또한 배열 인덱스도 변경될 가능성이 있기 때문에 키 값으로 적절하지 않다.

때때로 데이터베이스는 각 엘리먼트가 리액트에서 컴포넌트 키로 사용할 수 있는 키 또는 ID를 갖도록 구축된다. 이러한 데이터베이스의 상황을 배열 키 값에 적용하면, otter 배열에 있는 각 항목에 id를 추가해 볼 수 있다.

Adding a key to each post (App.js)

App.js를 저장하고 브라우저를 확인해 보자. 콘솔 탭에 있던 경고 메시지가 사라졌을 것이다.
Component 탭을 클릭해 보면 아래와 Post에 key 값이 추가된 것을 볼 수 있다.

Keys in the Components tab

Elements 탭으로 이동해서 <li> 태그의 post-component를 확인해 보자. 렌더링된 엘리먼트는 키를 포함하지 않는다

키는 리액트의 성능 최적화 도구이다. 따라서 HTML에 추가되지 않으며 크롬 개발자 도구(Chrome DevTools) 탭에 표시되지 않는다.

정리
Ottergram 애플리케이션을 개별 컴포넌트로 구분하고 React DevTools에서 해당 컴포넌트와 해당 컴포넌트의 props를 확인할 수 있다. 또한 몇 줄의 코드만으로 대규모 데이터 배열을 렌더링할 수 있도록 개선했다.

레퍼런스:


답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

back to top