리액트 컴포넌트 만들기

ZeroJun·2022년 6월 1일
1

React

목록 보기
2/13
post-thumbnail

리액트 컴포넌트

리액트의 컴포넌트는 html, css, js의 조합이다.

리액트는 위와 같은 컴포넌트 트리 구조로 이루어져 있다. 따라서 컴포넌트 폴더도 그에 맞게 관리하면 된다. 이 구조에 따르면 최종적으로 맨 위에 있는 App컴포넌트만이 리액트 돔 렌더의 지시로 html페이지에 직접 렌더링 된다.

컴포넌트 파일의 작명법은 첫글자는 대문자, 여러 단어가 결합되어 있는 경우 중간에 시작하는 서브 단어는 대문자로 시작해야 한다.

컴포넌트를 만들고 적용하는 과정

폴더 및 컴포넌트 파일 생성

새로운 컴포넌트를 만들기 위해 src폴더에 components폴더를 만들고 ExpenseItem이라는 js파일을 생성했다. (물론 App.js에 작성하거나 components폴더안에 넣지 않아도 되지만 프로젝트 규모가 커질 것을 대비하여 여러 컴포넌트 파일을 잘 관리할 수 있는 형태로 구성하는 것이 좋다.)

컴포넌트 코드 작성

ExpenseItem.js

function ExpenseItem() { // ExpenseItem컴포넌트
    return <h2>Expense item!</h2>
}

export default ExpenseItem; // export를 통해 ExpenseItem컴포넌트를 default로 다른 곳에서 쓸 수 있게된다.
// export default를 사용하면 '해당 모듈엔 개체가 하나만 있다’는 사실을 명확히 나타낼 수 있다.
// 이렇게 하면 import할 때 {} 없이 import할 수 있다.

만든 컴포넌트 import및 적용

App.js

import ExpenseItem from './components/ExpenseItem' // import를 통해 외부 컴포넌트를 불러온다.
// ./는 현재 파일의 옆에 있는 경로를 의미한다. 
// from ./components/ExpenseItem는 결국 App.js옆에 있는 components폴더의 ExpenseItem파일을 의미한다.

function App() {
  return (
    <div>
      <h2>Let's get started!</h2>
      <ExpenseItem></ExpenseItem> // 이렇게 하면 ExpenseItem컴포넌트가 렌더링 된다. 
      // 내장 html요소와의 차이점은 대문자로 시작해야 한다는 것이다.
      // 대문자로 해야 리액트에서 사용자 지정 컴포넌트임을 인지한다.
    </div>
  );
}

export default App;

모든 컴포넌트는 이런식으로 적용하면 된다.

컴포넌트 만들기 연습

HTML구성

function ExpenseItem() {
  return (
    <div>
      <div>May 28th 2022</div>
      <div>
        <h2>Car Insurance</h2>
        <div>$294.67</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

CSS입히기

컴포넌트에 css를 적용할 때는 보통 컴포넌트의 이름과 똑같은 css파일을 컴포넌트 옆에 만든 후 원래 css를 작성하는 것처럼 똑같이 하면 된다. jsx에도 html처럼 똑같이 class등을 부여하면 되는데, 이 때 jsx는 결국 자바스크립트이고, 자바스크립트의 class는 예약어기 때문에 html의 class를 className으로 붙여주면 된다.

import "./ExpenseItem.css"; // css를 import시킨다.

function ExpenseItem() {
  return (
    <div className="expense-item"> // class가 아닌 className
      <div>May 28th 2022</div>
      <div className="expense-item__description">
        <h2>Car Insurance</h2>
        <div className="expense-item__price">$294.67</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

동적 데이터 출력 및 표현식 작업하기

import "./ExpenseItem.css";

function ExpenseItem() {
  const expenseDate = new Date(2022, 5, 1); // 날짜 객체 생성(js코드)
  const expenseTitle = "Car Insurance";
  const expenseAmount = 294.67;
  return (
    <div className="expense-item">
      <div>{expenseDate.toISOString()}</div>
      <div className="expense-item__description">
        <h2>{expenseTitle}</h2>
        <div className="expense-item__price">{expenseAmount}</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

props를 통한 데이터 전달 (재사용 가능한 컴포넌트로 만들기)

props? props란 properties를 나타내는 것으로 사용자 지정 컴포넌트의 속성을 지정할 수 있다.

//App.js

import ExpenseItem from "./components/ExpenseItem";

function App() {
  const expenses = [
    {
      id: "e1",
      title: "Toilet Paper",
      amount: 94.12,
      date: new Date(2020, 7, 14),
    },
    { id: "e2", title: "New TV", amount: 799.49, date: new Date(2021, 2, 12) },
    {
      id: "e3",
      title: "Car Insurance",
      amount: 294.67,
      date: new Date(2021, 2, 28),
    },
    {
      id: "e4",
      title: "New Desk (Wooden)",
      amount: 450,
      date: new Date(2021, 5, 12),
    },
  ];
  return (
    <div>
      <h2>Let's get started!</h2>
      <ExpenseItem
        title={expenses[0].title}
        amount={expenses[0].amount}
        date={expenses[0].date}
      ></ExpenseItem>
      <ExpenseItem
        title={expenses[1].title}
        amount={expenses[1].amount}
        date={expenses[1].date}
      ></ExpenseItem>
      <ExpenseItem
        title={expenses[2].title}
        amount={expenses[2].amount}
        date={expenses[2].date}
      ></ExpenseItem>
      <ExpenseItem
        title={expenses[3].title}
        amount={expenses[3].amount}
        date={expenses[3].date}
      ></ExpenseItem>
    </div>
  );
}

export default App;
// ExpenseItem.js
import "./ExpenseItem.css";

// props객체에서 key와 value로 이루어진 파일 포맷을 얻는데,
// 그것은 리액트에 의해 자동으로 전달된다.
// 여기서는 App에서 정의한 속성인 title,amount,date가 전달된다.
// 매개변수 이름은 props가 아닌 다른 어떤 것이든 상관 없다.
function ExpenseItem(props) {
  //   const expenseDate = new Date(2022, 5, 1); // 날짜 객체 생성(js코드)
  //   const expenseTitle = "Car Insurance";
  //   const expenseAmount = 294.67;
  // 이 컴포넌트에서 사용되는 데이터는 이제 모두 App.js에서 얻기 때문에
  // 위의 상수들은 이제 제거할 수 있다.
  return (
    <div className="expense-item">
      <div>{props.date.toISOString()}</div>
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">{props.amount}</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

결과

리액트에선 컴포넌트간 데이터를 이렇게 props를 통해 공유할 수 있다.

컴포넌트에 js논리 추가

import "./ExpenseItem.css";

function ExpenseItem(props) {
  const year = props.date.getFullYear(); // 이렇게 자바스크립트 내장 매서드를 이용할 수 있다.
  const month = props.date.toLocaleString("ko-KR", { month: "long" });
  const day = props.date.toLocaleString("ko-KR", { day: "2-digit" });
  return (
    <div className="expense-item">
      <div> // 날짜 div, 이 부분을 추후 다른 컴포넌트로 분리하고자 한다.
        <div>{year}</div>
        <div>{month}</div>
        <div>{day}</div>
      </div>
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">{props.amount}</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

컴포넌트 분리하기

어떤 프로젝트든 하나의 컴포넌트가 점점 커지는 경우가 다반사다. 커진 컴포넌트는 다루기 복잡하므로 나눠주는 것이 좋다.

위에서 작성한 코드에서 Date표출 부분을 분리하기 위해 파일을 하나 생성하고, 분리할 부분을 그대로 옮겨온다.

import ExpenseDate from "./ExpenseDate"; // 분리한 컴포넌트 import
import "./ExpenseItem.css";

function ExpenseItem(props) {
  return (
    <div className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">{props.amount}</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

분리한 컴포넌트의 위치에 그대로 컴포넌트를 넣고, props전달을 위해 date프로퍼티를 정의해준다. 이때 ExpenseDate에서 'props.날짜' 이런식으로 쓰고 싶다면 ExpenseItem에서는 날짜={props.date} 이런식으로 지정해주면 된다.

현재 데이터이 흐름을 보면 다음과 같다

// 1. App.js의 expenses Data
 const expenses = [
   {},
   {},
   {}
 ]
// 2. App.js의 jsx에 선언된 ExpenseItem컴포넌트의 프로퍼티 value로 expenses데이터를 할당
 return (
 <ExpenseItem
  title={expenses[0].title}
  amount={expenses[0].amount}
  date={expenses[0].date}
 ></ExpenseItem>
)
// 3. ExpenseItem.js의 컴포넌트의 매개변수인 props에 자동으로 프로퍼티로 지정한 객체가 넘어옴
// ExpenseDate의 컴포넌트에 date프로퍼티를 지정
// 동시에 App.js에서 받은 props를 통해 expenses.date의 데이터를 value로 할당
<ExpenseDate date={props.date} /> 
  
 
// 4. ExpenseDate.js 컴포넌트에서 props를 통해 ExpenseItem에서 지정한 date프로퍼티를 받아
// 그대로 사용
const year = props.date.getFullYear();

여기서 date data는 App -> ExpenseItem -> ExpenseDate 이렇게 전달되었다.

컴포넌트를 좀 더 분리하여 App을 깔끔하게 만들기

// Expenses 컴포넌트

import ExpenseItem from "./ExpenseItem";
import "./Expenses.css";
function Expenses(props) {
  return (
    <div className="expenses">
      <ExpenseItem
        title={props.items[0].title}
        amount={props.items[0].amount}
        date={props.items[0].date}
      />
      <ExpenseItem
        title={props.items[1].title}
        amount={props.items[1].amount}
        date={props.items[1].date}
      />
      <ExpenseItem
        title={props.items[2].title}
        amount={props.items[2].amount}
        date={props.items[2].date}
      />
      <ExpenseItem
        title={props.items[3].title}
        amount={props.items[3].amount}
        date={props.items[3].date}
      />
    </div>
  );
}

export default Expenses;
// 변경된 App.js

import Expenses from "./components/Expenses";

function App() {
  const expenses = [
    {
      id: "e1",
      title: "Toilet Paper",
      amount: 94.12,
      date: new Date(2020, 7, 14),
    },
    { id: "e2", title: "New TV", amount: 799.49, date: new Date(2021, 2, 12) },
    {
      id: "e3",
      title: "Car Insurance",
      amount: 294.67,
      date: new Date(2021, 2, 28),
    },
    {
      id: "e4",
      title: "New Desk (Wooden)",
      amount: 450,
      date: new Date(2021, 5, 12),
    },
  ];
  return (
    <div>
      <h2>Let's get started!</h2>
      <Expenses items={expenses} />
    </div>
  );
}

export default App;

핵심은 props 연결을 위해 Expenses의 속성으로 items를 주었고, Expenses 컴포넌트에 props.items으로 접근한 것이다.

컴포지션 ("children prop")

어떤 컴포넌트들은 어떤 자식 엘리먼트가 들어올 지 미리 예상할 수 없는 경우가 있다. 범용적인 ‘박스’ 역할을 하는 Sidebar 혹은 Dialog와 같은 컴포넌트에서 특히 자주 볼 수 있다.

이러한 컴포넌트에서는 특수한 children prop을 사용하여 자식 엘리먼트를 출력에 그대로 전달하는 것이 좋다.

아래 코드는 컴포넌트들의 중복되는 CSS(모서리를 둥글게 하는 CSS)를 담았으며 wrap 하는 Card컴포넌트(wrapper역할)다.



import "./Card.css";

function Card(props) {
    // 다른 컴포넌트에서 받은 className을 함께 지정하는 방법이다.
  const classes = "card " + props.className;
  return <div className={classes}>{props.children}</div>;
}

export default Card;

Card 컴포넌트는 아래처럼 적용할 수 있다. 이 때 {props.children}가 div사이에 없었다면 아래의 컴포넌트에서 Card사이에 있는 element는 출력되지 않는다. 그리고, Card컴포넌트에는 어떤 프로퍼티도 지정하지 않은 상태다. 즉, props.children은 예약어이며 사용자 지정 컴포넌트가 감싸고 있는 모든 element를 포함하는 것이다.

function ExpenseItem(props) {
  return (
    <Card className="expense-item">
      <ExpenseDate date={props.date} />
      <div className="expense-item__description">
        <h2>{props.title}</h2>
        <div className="expense-item__price">{props.amount}</div>
      </div>
    </Card>
  );
}

function Expenses(props) {
  return (
    <Card className="expenses">
      <ExpenseItem
        title={props.items[0].title}
        amount={props.items[0].amount}
        date={props.items[0].date}
      />
      <ExpenseItem
        title={props.items[1].title}
        amount={props.items[1].amount}
        date={props.items[1].date}
      />
      <ExpenseItem
        title={props.items[2].title}
        amount={props.items[2].amount}
        date={props.items[2].date}
      />
      <ExpenseItem
        title={props.items[3].title}
        amount={props.items[3].amount}
        date={props.items[3].date}
      />
    </Card>
  );
}

map으로 컴포넌트 list정리하기

map을 사용하면 아래처럼 깔끔하게 컴포넌트 리스트를 관리할 수 있다.

import ExpenseItem from "./ExpenseItem";
import Card from "../UI/Card";
import "./Expenses.css";

function Expenses(props) {
  const ExpenseItemList = props.items.map((el) => (
    <ExpenseItem title={el.title} amount={el.amount} date={el.date} />
  ));
  
  return <Card className="expenses">{ExpenseItemList}</Card>;
}

export default Expenses;

JSX 자세히 보기

import React from "react"; // 최신 리액트 프로젝트에는 존재하지 않는 코드
  // return (
  //   <div>
  //     <h2>Let's get started!</h2>
  //     <Expenses items={expenses} />
  //   </div>
  // );

// 위의 jsx코드의 원래 형태다.
// 원래의 리액트 프로젝트는 모든 jsx가 이런 형태로 변환되었기 때문에
// import React from "react"; 가 필요했다.
// 하지만 최신의 리액트 프로젝트는 import없이도 변환이 가능하게 해준다.
return React.createElement(
  "div",
  {},
  React.createElement("h2", {}, "Let's get started!"),
  React.createElement(Expenses, { items: expenses })
);

// 이를 토대로 왜 jsx에선 한 개의 래퍼 루트 <div>(wrapper)로 감싸야하는지 알 수 있다. 가령
return React.createElement(
  React.createElement("h2", {}, "Let's get started!"),
  React.createElement(Expenses, { items: expenses })
);
// 이런식으로 하면 에러가 뜨게 되고, 이는 하나 이상을 반환할 수 없다는 것이다.
// 항상 하나의 래퍼와 그 자식 요소로 이루어져야 하는 이유다.

컴포넌트 파일 구성하기

기존 파일 구조

폴더 정리 후 파일 구조

이렇게 컴포넌트를 기능적으로 분류하면 체계적으로 관리할 수 있다.

0개의 댓글