Framework/React,Next.js

React 맛보기 - React Component

  • -
반응형

본 포스팅은 스터디를 통해 작성한 글을 복사한 내용입니다.

여기서 더 많은 글들을 확인하실 수 있습니다.


예제에 사용된 버전은 다음과 같습니다.

  • npm 9.5.0
  • react 18.2.0
  • typescript 4.9.5

리액트 프로젝트를 생성해보겠습니다. 타입스크립트를 사용하기 위해 패키지를 추가합니다.

npx create-react-app@latest study-react --template typescript

 

프로젝트를 실행합니다.

npm run start

 

컴포넌트

리액트에서 말하는 컴포넌트는 쉽게 말해 브라우저에 표시되는 페이지의 각 요소들을 말합니다.

 

이런 사이트가 있다고 할 때 1.메뉴, 2.카드가 컴포넌트인 것입니다.

1. 메뉴는 레이아웃 컴포넌트로 사용되고 2. 카드는 리스트에서 하나의 아이템 컴포넌트로 사용되고 있습니다. 이러한 컴포넌트들은 HTML 태그에 대응되어 브라우저에 표시됩니다.

애플리케이션 단위가 커지면 어떨까요? 컴포넌트의 수가 많아지게 되고 설계에 따라 복잡해질 수 있습니다. 때문에 효율적인 컴포넌트 설계가 중요합니다.

 

컴포넌트 사용해보기

1) 컴포넌트 정의

컴포넌트를 정의하는 방법은 함수 컴포넌트와 클래스 컴포넌트가 있습니다. 

*컴포넌트의 명명 규칙은 대문자로 시작하는 파스칼 케이스를 따릅니다.

 

함수 컴포넌트

함수 컴포넌트는 객체 인자를 받아 리액트 엘리먼트로 반환하는 javascript 함수이기 때문에 함수 컴포넌트로 불립니다. 함수 컴포넌트는 선언형 표현식(화살표 함수)으로 사용이 가능합니다.

 

[선언형]

function Header() {
  return (
    <header>헤더 영역</header>
  );
};

export default Header;

 

[표현식]

const Header = () => {
  return (
    <header>헤더 영역</header>
  );
};

export default Header;

 

클래스 컴포넌트

import React from "react";

class Header extends React.Component {
  render() {
    return <header>헤더 영역</header>;
  }
}

export default Header;

 

두 방식 중에서는 함수 컴포넌트가 주로 사용됩니다.

공부중인 참고 서적에서는 그 이유를 다음과 같이 서술합니다.

...(중략)...
* 콜백 함수에서 props나 state에 참조하려면 사전에 this 콘텍스트를 바인드해야 한다. * 라이프사이클을 다루는 메서드가 많아서 복잡하다. * 상태가 함께 있어서 작동을 다른 컴포넌트와 공통화하기 어렵다. 훅을 사용한 함수 컴포넌트를 사용함으로써 위 문제점을 해소하고 간단하게 컴포넌트를 기술할 수 있습니다. 이러한 이유로 현재는 함수 컴포넌트가 주류가 됐습니다.
- [타입스크립트, 리액트, Next.js로 배우는 실전 웹 애플리케이션 개발]

 

 

2) 헤더, 푸터 컴포넌트

실전 예제를 통해 컴포넌트를 만들어보고 사용해보겠습니다.

웹 사이트의 가장 기본적인 구조가 되는 헤더, 섹션, 푸터를 만들고 바디 영역 안에 json 데이터를 통해 컴포넌트를 배치하는 예제를 만들어보겠습니다.

 

CRA(Create React App) + typescript로 생성된 디렉토리 구조입니다.

 

구조를 살펴볼까요?

  • node_modules : 프로젝트를 구성하는 패키지 소스 코드가 존재합니다.
  • public : 정적 파일들을 포함하고 있습니다. 가상 DOM을 위한 html 파일이 존재합니다.(index.html)
  • App.css : 애플리케이션의 전역 css를 담당합니다. App.tsx에서 이 파일이 임포트되어 적용된 것을 확인할 수 있습니다.
  • App.tsx : 애플리케이션의 레이아웃과 라우팅을 담당합니다.
  • index.css : App.css의 상위에서 적용되는 css입니다.
  • index.tsx : 애플리케이션을 렌더링해주는 역할을 담당합니다. 

이 코드들이 빌드되어 최종적으로 결합, 최소화된 HTML 코드가 되어 출력됩니다.

 

src 디렉토리에 components - Header / Footer 디렉토리를 만들어보겠습니다.

그리고 Header.tsx, Footter.tsx와 각각의 scss 파일을 추가했습니다.

 

module.scss 추가 후 아래와 같은 에러가 발생한다면 sass를 설치하면 됩니다.

npm install sass

 

그럼 이제 컴포넌트를 추가해보겠습니다.

 

Header.tsx

import styles from "./Header.module.scss";

const MENU_LIST = [
  {
    id: 1,
    title: "메뉴1",
  },
  {
    id: 2,
    title: "메뉴2",
  },
];

const Header = () => {
  return (
    <header>
      <div className={styles.container}>
        <a className={styles.logo}>
          <img src={`${process.env.PUBLIC_URL}/logo192.png`}></img>
        </a>
        <ul className={styles.menuList}>
          {/* MENU_LIST만큼 루프를 돌며 메뉴가 그려집니다. */}
          {MENU_LIST.map((menu, index) => (
            <li key={index} className={styles.listItem}>
              {menu.title}
            </li>
          ))}
        </ul>
      </div>
    </header>
  );
};

export default Header;

 

리액트를 처음 접하신 분들은 문법이 생소하실 수 있습니다.

HTML에서 사용되는 속성 중 자바스크립트 예약어에 해당하는 속성은 JSX(TSX) 에서 그대로 사용할 수 없습니다. class나 for 같은 경우가 있습니다.

JSX에서는 class => className, for => htmlFor로 표현됩니다.

 

Footer.tsx

import styles from "./Footer.module.scss";

const Footer = () => {
  return (
    <footer>
      <div className={styles.content}>ⓒ 2023 코드드림. All rights reserved.</div>
    </footer>
  );
};

export default Footer;

 

스타일을 입히기 위해 css도 추가합니다.

Header.module.scss

header {
  background-color: #384b60;
}

.container {
  max-width: 860px;
  min-height: 60px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;

  .logo {
    height: 40px;

    img {
      height: 100%;
    }
  }

  .menuList {
    display: flex;
    list-style: none;
    gap: 60px;

    .listItem {
      font-size: 16px;
      color: #fff;
    }
  }
}

 

Footer.module.scss

footer {
  height: 100px;
  margin-top: auto;
  background-color: #e6e6e6;
}

.content {
  width: 96%;
  display: flex;
  justify-content: center;
  align-items: center;
  max-width: 860px;
  margin: 0 auto;
  height: 100%;
}

 

이제 추가한 컴포넌트를 사용해보겠습니다.

레이아웃과 라우팅을 담당하는 App.tsx로 돌아가보겠습니다.

컴포넌트를 임포트 하는 방법은 다음과 같습니다. 

import Header from "./components/Header/Header";

우리가 컴포넌트를 만들 때 export default Header; 를 하게 되는데 이것은 컴포넌트를 내보내겠다는 의미로 이 과정을 통해 여러 곳에서 해당 컴포넌트를 임포트해 사용할 수 있는 것입니다.

 

App.tsx에 헤더와 푸터를 추가해봅니다.

App.tsx

import React from "react";
import "./App.css";
import Header from "./components/Header/Header";
import Footer from "./components/Footer/Footer";
import Home from "./pages/Home";

function App() {
  return (
    <>
      <Header />      
      <Footer />
    </>
  );
}

export default App;

 

여기까지 잘 따라오셨다면 아래와 같은 화면을 확인하실 수 있습니다.

 

3) 컨텐츠 페이지

App.tsx는 레이아웃과 라우팅을 담당한다고 했습니다.

라우팅은 요청 주소에 맞는 페이지를 보여주는 것이라고 생각하면 됩니다. 이 부분은 Next.js로 넘어가서 자세히 알아보겠습니다.

 

App.tsx에 컨텐츠 영역이 될 부분을 페이지로 분리시켜 봅니다.

src 디렉토리에 pages 디렉토리를 추가하고 Home.tsx를추가하겠습니다.

 

페이지를 작성하기 전에 Home에서 사용될 컴포넌트 Card를 만들어보겠습니다.

components 디렉토리에 Card 디렉토리를 추가하고 Card.tsx를 추가하겠습니다.

 

Card.tsx

import styles from "./Card.module.scss";

const Card = () => {
  return (
    <div className={styles.cardItem}>
      <div>Hello react</div>
    </div>
  );
};

export default Card;

 

Card.module.scss

.cardItem {
  max-width: 416px;
  height: 240px;
  border-radius: 16px;
  box-shadow: 4px 12px 30px 0 rgba(0, 0, 0, 0.09);
  background-color: #fff;
  overflow: hidden;

  div {
    display: flex;
    height: 100%;
    align-items: center;
    justify-content: center;
  }
}

 

다시 돌아와서 페이지를 작성해봅니다.

페이지에서는 위에서 생성한 Card 컴포넌트를 임포트해서 사용합니다.

 

Home.tsx

import styles from "./Home.module.scss";
import Card from "../components/Card/Card";

const PROJECT_LIST = [
  {
    id: 1,
    title: "광명찾자",
  },
  {
    id: 2,
    title: "하루단어",
  },
  {
    id: 3,
    title: "폭탄찾자",
  },
];

const Home = () => {
  return (
    <div className={styles.container}>
      <h1>프로젝트 </h1>
      <ul className={styles.cardList}>
        {/* PROJECT_LIST만큼 루프를 돌며 카드 컴포넌트를 그립니다. */}
        {PROJECT_LIST.map((item, index) => (
          <Card />
        ))}
      </ul>
    </div>
  );
};

export default Home;

 

Home.module.scss

.container {
  max-width: 860px;
  margin: 0 auto;
}

 

이제 실행해보면 컴포넌트로 생성한 카드들이 나열된 것을 확인할 수 있습니다.

 

 

4) Props

위 단계에서 각 카드들을 보면 카드 이름이 고정으로 되어있습니다. PROJECT_LIST의 title을 입력받아 카드마다 다른 이름으로 그려지면 좋을 것 같습니다.

Props를 이용하면 컴포넌트를 호출할 때 인자 객체를 전달할 수 있습니다.

 

Card.tsx를 수정해보겠습니다.

타입스크립트에 대해 배웠으니 타입을 한번 선언해볼까요?

CardProps 라는 타입을 만들고 title을 추가합니다. title은 string 타입을 입력받도록 합니다. 그리고 div 안에 { title }을 추가합니다.

 

Card.tsx

import styles from "./Card.module.scss";

type CardProps = {
  title: string;
};

const Card = (props: CardProps) => {
  const { title } = props;

  return (
    <div className={styles.cardItem}>
      <div>{title}</div>
    </div>
  );
};

export default Card;

 

여기까지 진행했을 때 Card 컴포넌트를 호출하는 Home 페이지에서는 에러가 발생합니다.

 

Card 컴포넌트 호출시에 CardProps로 선언된 값이 속성에 부여되지 않았기 때문입니다. Home에서 Card 속성에 title을 추가합니다.

const Home = () => {
  return (
    <div className={styles.container}>
      <h1>프로젝트 </h1>
      <div className={styles.cardList}>
        {/* PROJECT_LIST만큼 루프를 돌며 카드 컴포넌트를 그립니다. */}
        {PROJECT_LIST.map((item, index) => (
          <Card key={item.id} title={item.title} />
        ))}
      </div>
    </div>
  );
};

 

*key는 리액트가 여러 컴포넌트를 렌더링했을 때 어떤 항목을 변경할지 식별하는데 도움이 됩니다. 공식문서에서는 key로 사용되는 값에 배열의 인덱스보다는 항목을 고유하게 식별할 수 있는 유니크한 값을 사용하기를 권장합니다. key가 지정되지 않는 경우 아래와 같은 경고가 발생합니다.

 

다시 실행해보겠습니다.

카드의 이름이 배열 데이터에 맞게 입력된 것을 확인할 수 있습니다.

 

여기까지 리액트 컴포넌트에 대해서 공부를 해봤는데요.

글을 정리를 하며 디렉토리 분리에 대한 기준과 컴포넌트 설계에 대한 고민들이 생겼던 것 같습니다.

"컴포넌트 디렉토리와 페이지 디렉토리를 어떤 기준에 의해 분리시키면 좋을까"

"컴포넌트는 어떻게 설계 하면 좋을까"

 

공부를 하며 우리가 내린 결론은 이렇습니다.

  • 디렉토리 분리에 대한 명확한 기준은 프로젝트 크기, 팀별로 다를 수 있다
  • 로그인 페이지, 사용자 정보 페이지, 회원 가입 같은 (머리로 떠올렸을 때, 페이지라고 판단되는) 페이지를 페이지 디렉토리로 나누기
  • 페이지 안에 독립적으로 사용되는 요소들은 컴포넌트로 분리시키기(사무실 책상이 페이지라면 책상 위에 존재하는 키보드, 마우스, 모니터 같은 것들은 컴포넌트라고 생각하니 정리가 좀 됐습니다)
  • 컴포넌트 설계에 대해서는 기본에 충실하기
  • 컴포넌트는 하나의 기능을 담당하도록 만들기
  • 컴포넌트간 의존성을 낮추기

다음 포스팅에서는 리액트 훅에 대한 내용으로 돌아오겠습니다.

 

예제에 사용된 모든 코드는 여기서 확인하실 수 있습니다.

저희는 스터디를 통해 글을 기록하고 있습니다. 피드백은 언제나 환영입니다 :)

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.