Jasontreks Blog

DM 보내기


Send

React 컴포넌트

React와 TypeScript의 결합은 현재 가장 트렌디하고 잘나가는 웹 개발 방법이다. React는 웹의 구성 요소를 동적으로 제어하기 좋고 더욱 직관적인 인터페이스로 설계할 수 있으며 매우 다양한 UI 라이브러리를 지원해 CSS에 의한 스트레스를 줄여준다. TypeScript는 기존의 JS의 단점을 보완하고 프로그램을 더욱 안전하고 타입 오류에 의해 많은 시간을 할애하게 되는 상황을 막아준다.

I. 개발 환경

먼저 Node.js를 설치해야 한다. 아마 개발자라면 대부분 이미 설치가 되어 있을것이다. https://nodejs.org/ko/download 이제 node.js를 설치했으니 npx, npm 명령어를 설치할 수 있다.

  • npm: 패키지 설치하는데 사용
  • npx: 패키지를 실행하는데 사용

react app을 시작하기 위해 터미널에서 실행하는 명령어는 다음과 같다.

npx create-react-app react-typescript-demo --template typescript

설치가 완료되면 해당 프로젝트가 생긴 경로에서 다음 명령어로 react app을 실행해볼 수 있다.

npm start

II. 앱 구성 요소

일단 기본적으로 만들어진 파일들이 많이 있다. 당장 이것들을 다 볼 필요는 없고, src/App.tsx, src/index.tsx, public/index.html 만 보면 된다.

  • App.tsx: 페이지에 그릴 컴포넌드들을 여기에 작성한다.
  • index.tsx: 필요한게 다 작성된 App.tsx을 HTML로 바꾼다.
  • index.html 변환된 HTML이 삽입된다.

즉 일단 우리가 주로 건드릴 파일은 App.tsx인것이다. 다만 App.tsx 안에서만 HTML로 하드코딩해 페이지를 구성하는것은 바람직하지 않다. React의 꽃은 '컴포넌트'이다. 컴포넌트란 페이지를 구성하는 단위로, 원하는대로 설계하고 조립하는 블록 조각이다. 즉 여러 컴포넌트를 만들어 웹페이지 구성 단위를 만들고 이를 App.tsx 안에 갖다 붙이는 방식으로 react app을 만들어 나가면 되는것이다. 이를 위해 src 폴더 안에 components라는 폴더를 하나 만들어둔다.


III. 컴포넌트 만들기

만들기에 앞서 App.tsx에 자동으로 작성된 코드들 중 일부를 제거할 필요가 있다. 다음과 같이 간소화한다.

import './App.css';

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

export default App;

우리가 만든 컴포넌트는 div 태그의 자식으로 넣게 된다. 인삿말을 표시하는 컴포넌트를 만들어보자. components 폴더에 Greet.tsx 파일을 만들고, 아래 내용을 작성한다.

type GreetProps = {
    name: string;
    messageCount?: number;
    isLoggedIn: boolean;
}

export const Greet = (props: GreetProps) => {
    const { messageCount = 0 } = props

    return (
        <div>
            <h2>
                {
                    props.isLoggedIn ? Hello! ${props.name}. you have ${props.messageCount} messages to read. : Welcome, Guest.
                }
            </h2>
        </div>
    )
}
  • props: 컴포넌트를 사용할 때 외부에서 전달하는 속성들이다. 타입스크리트이기에, 타입을 지정해주어야 오류가 발생하지 않는다.
  • return: 반환할 HTML 조각을 작성한다.

이제 이 컴포넌트를 App.tsx에서 임포트해서 넣어주면 된다.

import './App.css';
import { Greet } from './components/Greet';

function App() {
  return (
    <div className="App">
      <Greet name='Jason' isLoggedIn={true}/>
    </div>
  );
}

export default App;

IV. 고급 Props

1. 선택적 prop 인자

타입스크립트는 컴포넌트가 전달받을 props의 타입을 일일이 정하도록 요구한다. 그리고 전달받아야 할 모든 prop 인자가 없으면 오류를 일으킨다.

type SampleProps = {
    a: string
    b: number
}

이런 props 타입이 있다고 했을 떄

function App() {
  return (
    <div className="App">
      <Sample a='hello'/> // 오류 발생
    </div>
  );
}

이런식으로 인자가 없거나 하나만 전달하면 오류를 일으킨다. 그래서 있어도 되고 없어도 되는 prop 인자는 다음과 같이 선언한다.

type SampleProps = {
    a: string
    b?: number // b는 있어도 되고 없어도 됨
}

근데 전달받지 않은 경우 이 변수를 쓰려고 할 때 예기치 못한 문제가 생길 수 있으니 초기화를 해 주어야 한다.

type SampleProps = {
    a: string
    b?: number
}

export const Sample = (props: SampleProps) => {
    const { b = 0 } = props // 초기화

    return (
        <div>
            ...
        </div>
    )
}

2. 자식 prop 인자

props 타입을 정의 할 때 children이라는 예약된 이름을 사용하면, 다음이 가능해진다.

type HeadingProps = {
    children: string
}
function App() {
  return (
    <div className="App">
        <Heading>Hello World!</Heading> // 컴포넌트의 자식으로 인자를 전달할 수 있다.
    </div>
  );
}

안에 또다른 리액트 컴포넌트를 자식으로 전달받기 위해서는, React.ReactNode 타입을 사용한다.

type ContainerProps = {
    children: React.ReactNode
}

export const Container = (props: ContainerProps) => {
    return (
        <div>{props.children}</div>
    )
 }

3. 배열 prop 인자

prop 인자로 배열을 선언하는 방법이다.

type ListProps = {
    names : {
        first: string
        last: string
    }[] // 배열로 선언
}

export const PersonList = (props: ListProps) => {
    return (
        <div>
            {  // props로 전달받은 배열 순회
                props.names.map(name => {
                    return (
                        <h2 key={name.first}>{name.first} {name.last}</h2>
                    );
                })
            }

        </div>
    )
}

4. 함수 객체 prop 인자

다음과 같이 함수 객체를 props로 전달받을 수 있다. 이렇게 하면 버튼과 같은 요소를 외부에서 정의한 어떤 기능과 연결지을 수 있다.

type ButtonProps = {
    handleClick: () => void // 반환값 X
}

export const Button = (props: ButtonProps) => {
    return <button onClick={props.handleClick}>Click</button>
}

일반적으로 prop 인자로 함수 객체를 선언할 땐 event라는 매개변수로 이벤트 객체도 전달받는것이 바람직하다.

type ButtonProps = {
    handleClick: (event: React.MouseEvent<HTMLButtonElement>, id: number) => void
}

export const Button = (props: ButtonProps) => {
    return <button onClick={(event) => props.handleClick(event, 1)}>Click</button>
    // onClick 속성이 첫번째 인자로 React.MouseEvent<HTMLButtonElement> 객체를 전달해준다.
}

App.tsx에서 컴포넌트를 넣을땐 다음과 같이 기능이 정의된 함수 객체를 전달한다.

function App() {
    return (
    <div className="App">
        <Button handleClick={(event, id) => {
            console.log('Button clicked', event, id);
        }}/>
    </div>
    );
}
5. 스타일 prop 인자

HTML태그의 styles 속성에 전달할 CSS를 props로 전달할수도 있다. CSS 문법은 문자열이기도 하고 수치이기도 해서, React는 이를 위한 특별한 prop 타입을 지원한다.

type ContainerProps = {
    styles: React.CSSProperties
}

export const Container  = (props: ContainerProps) => {
    return (
        <div style={props.styles}>
            Text content goes here
        </div>
    )
}

React.CSSProperties으로 prop 인자의 타입을 지정해주면 된다. 그리고 이를 App.tsx에서 전달할때는 다음과 같이, 중괄호로 한번 더 감싸준다.

function App() {
  return (
    <div className="App">
      <Container styles={.{color: 'red'}}/> // CSS 스타일 속성 전달
    </div>
  );
}

props 관련 팁들

1. props 분해 파라미터

전달받은 props를 사용할 땐 다음과 같이 props의 타입에 따라 멤버 변수를 참조하듯 사용하는 것이 기본이지만,

type PersonProps = {
    name: string
    age: number
}

export const Person = (props: PersonProps) => {
    return (
        <div>
            Name: {props.name}, Age: {props.age}
        </div>
    )
}

다음과 같이 props를 미리 분해한 개별 파라미터로 사용할 수 있다.

export const Person = ({name, age}: PersonProps) => {
    return (
        <div>
            Name: {name}, Age: {age}
        </div>
    )
}

2. props 타입 정의 분리

여러 props를 사용하는 거대한 컴포넌트이거나 미리 정의해둔 타입을 재사용하기 위해선 별도의 파일에 정의한 후 불러오는 것이 바람직하다. 따라서 ~.types.ts 같이 타입만 정의해두는 별도의 파일을 생성한 후 export 키워드와 함께 정의한다.

// Person.types.ts

export type PersonProps = {
    name: string
    age: number
}

그리고 이 타입을 사용할 다른 .tsx 파일에서 import한다.

import { PersonProps } from "./Person.types" // type만 정의된 파일 import

export const Person = ({name, age}: PersonProps) => {
    return (
        <div>
            Name: {name}, Age: {age}
        </div>
    )
}

3. type in type

정의한 타입을 다른 타입 안에서 사용하여, 속성의 타입을 정의하는데 쓸수도 있다.

다음과 같이 복잡한 구조를 갖는 하나의 prop 인자를

export type PersonProps = {
    name: {
        first: string
        last: string
    }
    age: number
}

이렇게 별도의 타입으로 분리, 정의하여 재활용성을 높이는 것이다.

type Name = { // 분리 정의
    first: string
    last: string
}

export type PersonProps = {
    name: Name
    age: number
}