React Basics 게시물에서 이어지는 내용이다.

 

실제로 미니 프로젝트를 하나 만들어보자!

App.js, index.js, index.css가 들어있는 src 폴더 내에 components라는 이름으로 모든 컴포넌트를 저장할 폴더를 만들었다. App.js는 가장 최상단에 위치한 요소(root)이며, 해당 App.js 안에 여러 컴포넌트들이 중첩될 것이니까 폴더 depth를 달리 주었다.

 

리액트에서는 각 컴포넌트의 파일명을 대문자로 시작하며, camelcase 방식을 따르는 것이 보편적이라고 한다. 따라하자! 컴포넌트가 무엇인지 App.js를 통해 살펴보니, 별 거 없다. 하나의 JS 함수다! 그리고 각 컴포넌트는 일반 html 태그와 구분하기 위해 무조건 대문자로 시작해야 한다. 

function ExpenseItem() {
  return <h2>Expense item!</h2>;
}

export default ExpenseItem;

이 요소를 루트 컴포넌트, App.js에 추가한다.

import ExpenseItem from "./components/ExpenseItem";

function App() {
  return (
    <div>
      <h2>Let's get started!</h2>
      <ExpenseItem />
    </div>
  );
}

export default App;

 

리액트에서 주의해야할 점은 리턴하는 요소가 하나의 root 요소여야 한다는 것이다. 나란히 나란히 div 태그를 붙여서 리턴하면 vscode가 엄청나게 눈치를 줄 것이다. ExpenseItem 컴포넌트에 기본적으로 들어가야 될 요소들을 넣어보자.

function ExpenseItem() {
  return (
    <div>
      <div>March 7th 2022</div>
      <div>
        <h2>Birthday Cake</h2>
        <div>$10.7</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

위 코드를 저장하면 아주,,, 못생긴 화면이 반겨준다. 자, Styling 하러 가보자!

 

React Styling

리액트에서 CSS 방식은 크게 다르지 않다. 그냥 CSS 파일 만들어서 필요한 내용들 작성해주면 된다. 그리고 css 파일을 import해주면 끝이다! 일반적인 JS에서 CSS를 적용해줄 때 class 속성을 부여했지만, JSX 문법에서는 className이라는 속성을 사용하여 클래스 명을 부여한다. 

 

import "./ExpenseItem.css";

function ExpenseItem() {
  return (
    <div className="expense-item">
      <div>March 7th 2022</div>
      <div className="expense-item__description">
        <h2>Birthday Cake</h2>
        <div className="expense-item__price">$10.7</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

* css 작업은 일단 넘어가겠다..! 일단은 리액트 문법에 익숙해지는 걸 일순위로

 

Dynamic Data 표현하기

현재 방식은 ExpenseItem이라는 컴포넌트가 재사용성이 좋다고는 결코 말할 수 없다. 매번 새로운 아이템이 생길 때마다 하드코딩을 해야되는 상황이다. 그러면, dynamic하게 data가 들어오는 경우 어떻게 컴포넌트로 넘길 수 있을까? 어떻게하면 컴포넌트를 원 목적대로 재사용이 가능하도록 작성할 수 있을까? 

 

하드코딩된 데이터를 모두 변수에 넣어, 컴포넌트 함수는 단순히 전달받은 데이터를 띄우는 blueprint 역할을 하게 하면 될 것이다. 다음과 같이 말이다.

import "./ExpenseItem.css";

function ExpenseItem() {
  const expenseData = new Date(2022, 2, 7);
  const expenseTitle = "Birthday Cake";
  const expenseAmount = 10.7;

  return (
    <div className="expense-item">
      <div>{expenseData.toISOString()}</div>
      <div className="expense-item__description">
        <h2>{expenseTitle}</h2>
        <div className="expense-item__price">${expenseAmount}</div>
      </div>
    </div>
  );
}

export default ExpenseItem;

 

하지만 여전히, 화면에 띄우고자 하는 원본 데이터 자체가 컴포넌트 안에 들어있는 상태이다. 즉, 재사용성은 보장되지 않은 상태이다. 그러면 컴포넌트에 데이터를 전달하려면 어떻게 해야할까?

 

Props로 컴포넌트 간 데이터 전달하기

리액트에서 다른 컴포넌트에 저장된 데이터를 단순히 갖다 쓰는 것은 불가능하다. 따라서 우리는 컴포넌트에 속성값으로 데이터를 넘겨준 뒤, 해당 컴포넌트에서 전달받은 속성을 통해 그 데이터를 꺼내쓸 수 있도록 할 것이다. html 요소는 얼마든지 속성을 가질 수 있다. 리액트 컴포넌트 역시, props를 통해 값을 전달받을 수 있다!

 

컴포넌트는 하나의 객체(props)를 전달받는다. props 안에 속성으로 넘겨준 값들이 들어가 있다. 속성을 키로 받아오기 때문에, 컴포넌트로 넘긴 속성과 props를 통해 접근하는 키 값은 반드시 일치해야 한다.

 

이제 App.js에서 ExpenseItem 컴포넌트로 데이터를 전달하는 상황을 가정하자. 원래는 서버에서 값을 받아오는 식으로 진행되겠지만, 여기에서는 더미 데이터를 사용했다.

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
        title={expenses[1].title}
        amount={expenses[1].amount}
        date={expenses[1].date}
      />
      <ExpenseItem
        title={expenses[2].title}
        amount={expenses[2].amount}
        date={expenses[2].date}
      />
      <ExpenseItem
        title={expenses[3].title}
        amount={expenses[3].amount}
        date={expenses[3].date}
      />
    </div>
  );
}

export default App;

* 물론 여기서 배열로 접근하는 부분은 최적화 가능한 부분이다. 일단은 넘어가자. 

 

이렇게 App 컴포넌트에서 ExpenseItem 컴포넌트로 값을 전달해줬음에도 화면에는 차이가 없다. 아직 ExpenseItem 컴포넌트는 전달받은 값으로 띄우고 있지 않기 때문에, props를 사용해서 전달받은 데이터를 렌더링하도록 고쳐보자.

 

import "./ExpenseItem.css";

function ExpenseItem(props) {
  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;

* 여기에서 destructoring 사용하여 props를 { date, title, amount} 꺼내줘도 된다. 그치만 일단은 props 개념을 익히기 위해 굳이굳이 키워드를 사용하여 객체에서 키값으로 접근해보자.

 

JS 로직을 적용해보자

현재 날짜가 아주 읽기가 어렵게 되어있다. Month - Year - Day의 포맷으로 바꿔보자. 이 부분은 JS에서 제공하는 기본 기능으로 충분히 해결이 가능하다. 다만 JSX 내에서 JS 로직을 직접 적용하지는 말고, JSX 밖에서 변수에 값을 넣어준 뒤 JSX 코드 자체는 깔끔하게 유지하는 방향으로 코딩하자.

 

import "./ExpenseItem.css";

function ExpenseItem(props) {
    const month = props.date.toLocaleString('en-US', {month: 'long'});
    const day = props.date.toLocaleString('en-US', {day : "2-digit"});
    const year = props.date.getFullYear();

  return (
    <div className="expense-item">
      <div>
        <div>{month}</div>
        <div>{year}</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;

 

슬슬 component를 쪼개볼까?

컴포넌트에 이것저것 기능을 추가하다 보면, 어쩔 수 없이 컴포넌트가 점점 무거워지는 모습을 볼 수 있다. 당연한 과정이다! 자, 이제 각각 하나의 기능만 수행하도록 컴포넌트 안의 요소를 다른 컴포넌트로 쪼개보자.

 

방금 다룬 date 정보만 해도 하나의 컴포넌트로 쪼개기 참 좋아보인다. 자 쪼개보자. 

쪼개는 것을 어려워할 필요는 없다. 안에 들어있던 로직, JSX 코드를 그대로 새 컴포넌트로 옮긴 뒤 원 코드 영역에 해당 컴포넌트만 적어주면 된다!

import "./ExpenseDate.css";

function ExpenseDate(props) {
  const month = props.date.toLocaleString("en-US", { month: "long" });
  const day = props.date.toLocaleString("en-US", { day: "2-digit" });
  const year = props.date.getFullYear();

  return (
    <div className="expense-date">
      <div className="expense-date__month">{month}</div>
      <div className="expense-date__year">{year}</div>
      <div className="expense-date__day">{day}</div>
    </div>
  );
}

export default ExpenseDate;

* CSS 파일은 생략

 

ExpenseItem 컴포넌트에서 ExpenseDate 컴포넌트를 사용할 때는 잊지말고 props로 date 정보를 넘겨줘야 된다. 이제 App -> ExpenseItem -> ExpenseDate 로 데이터가 총 3번 전달된 상황이다. 

 

import ExpenseDate from "./ExpenseDate";
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;

 

App.js 를 더 간결하게 만들어보자

App 컴포넌트 내에 ExpenseItem이 계속해서 생겨나는 것에 약간 부담이 있으니 코드를 쪼개보자! [0] [1] [2] [3] 으로 접근하기가 너무 싫어서 강의에 나와있는 코드랑은 약간 방향을 틀었다.

 

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 data={expenses} />
    </div>
  );
}

export default App;
import ExpenseItem from "./ExpenseItem";
import "./Expenses.css";

function Expenses(props) {
  return (
    <div className="expenses">
      {props.data.map((data) => (
        <ExpenseItem title={data.title} amount={data.amount} date={data.date} />
      ))}
    </div>
  );
}

export default Expenses;

 

Composition에 대하여 (children props)

현재 레이아웃 상으로 css가 겹치는 부분이 존재한다. 동글동글 테두리 부분이 겹친다. 이런 식으로 겹치는 부분을 단순히 감싸주기만 하는 wrapper component로 쪼개려면 사용해보지 않았던 개념이 등장한다. 

 

이 부분은 공식문서를 참고하면 더 좋을 듯 하다! 

https://ko.reactjs.org/docs/composition-vs-inheritance.html

 

합성 (Composition) vs 상속 (Inheritance) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

Card라는 이름으로 각각의 요소를 감싸주는 Wrapper 컴포넌트를 만들어보자. 

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

export default Card;
import Card from "./Card";
import ExpenseDate from "./ExpenseDate";
import "./ExpenseItem.css";

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

export default ExpenseItem;

위와 같이 겹치는 css를 포함하는 card 컴포넌트를 wrapper로 전달했더니 화면에 아무것도 표시가 되지 않는다. <> </> 사이의 요소를 전달받지 못해서 이런 문제가 발생했다. 따라서 <> 태그와 </> 태그 사이의 요소를 받아올 수 있도록 기본적으로 전달받은 props의 children 속성을 사용한다! (children은 예약어)

 

코드를 열심히 수정해서 card 컴포넌트 내부에 값을 제대로 넣어줬음에도 깨지는 에러가 발생한다.

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

export default Card;

레이아웃이 박살났어요

 

자 이 문제는 expense-item 클래스명이 card로 제대로 들어가지 않아 생긴 문제다. 즉, children 뿐만 아니라 원래 들어있던 클래스명도 같이 가져와야 한다는 것이다. className 이라는 속성을 통해 접근할 수 있다. (className 역시 예약어)

import "./Card.css";

function Card(props) {
  const classes = "card " + props.className;
  return <div className={classes}>{props.children}</div>;
}

export default Card;

 

이제 ExpenseItem과 Expenses로 가서 Card 컴포넌트로 감싸주면 원래 UI와 동일하게 구현된다. composition 개념 잘 잡고 가자!

import Card from "./Card";
import ExpenseDate from "./ExpenseDate";
import "./ExpenseItem.css";

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

export default ExpenseItem;
import Card from "./Card";
import ExpenseItem from "./ExpenseItem";
import "./Expenses.css";

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

export default Expenses;

 

완성~

 

다만 여전히 데이터는 static하고 유저와 상호작용하는 부분이 없다. 더 나아가보자!

 


JSX 좀 더 뜯어보기

리액트 구 버전에서는 모든 컴포넌트 상단에 react 라이브러리를 import 해줬어야 했다. 그 이유를 살펴보자면 다음과 같다. JSX 코드를 사용하지 않는 방식으로 코드를 바꿔보자.

return React.createElement({'div', {}, 
	React.createElement('h2', {}, "Let's get started!"),
	React.createElement(Expenses, {data:expenses} )
);
  return (
    <div>
      <h2>Let's get started!</h2>
      <Expenses data={expenses} />
    </div>
  );

위와 아래는 같은 코드다. JSX 코드를 위와 같은 코드로 변환해주기 위해 과거에는 React를 직접 import 해줬어야 했다.

createElement에 전달되는 3개의 인자는 태그, 전달값, 태그안의 값들(여러개가 될 수 있음)이다. 따라서 항상 기억하자! JSX 코드가 제대로 작동되도록 하는 데에는 import React from 'react'가 생략되었으나 기본적으로 들어가 있기 때문에 가능한 것이다.

 

또한 위 코드를 보면 자연스럽게 왜 JSX가 최종적으로 리턴할 수 있는 태그가 한개인 이유를 알 수 있다. 

 

 

Component 파일들 정리하기

점점 컴포넌트가 늘어날수록 정리의 필요성은 커질 것이다. 각각의 기능과 관련된 컴포넌트를 그 기능별로 묶어주고, 전체적으로 통용되는 UI는 UI로 묶어주면 더욱 보기 수월할 것이다! 한번 실제로 정리해보자. 원래는 components 폴더 안에 함께 들어있던 코드들을 다음과 같이 정리해주었다.

Expenses 폴더 / UI 폴더로 나눠 정리

물론 정리 방식은 개인의 자유다 :) 그렇지만 하나의 큰 폴더 안에 모든 컴포넌트를 몽땅 넣어놓는 극악무도한 짓은 하지 말자.

 

Alternative Function Syntax

function 문법 대신 arrow fn을 사용해도 괜찮다. 정말로 개인의 선택이기 때문에 편한 방식을 택하면 된다~! 하지만 나는 arrow function으로 코드 전부 바꿔줄거다 ㅎㅎ

 

여기까지 작업한 파일은 깃헙에 업로드했다!

https://github.com/cheonyeji/react_recap_ch3

 

GitHub - cheonyeji/react_recap_ch3

Contribute to cheonyeji/react_recap_ch3 development by creating an account on GitHub.

github.com

 

+ Recent posts