본문 바로가기

FrontEnd/React

[React] 컴포넌트 이해하기

 컴포넌트란?

  컴포넌트는 스몰토크(Smalltalk)에서 유래한 매우 오래된 용어이며 화면 UI를 처리하는 클래스를 의미한다.  리액트는 컴포넌트(component) 기반의 구조를 띄는 특징을 갖고있다.  모든 페이지가 컴포넌트로 구성되어 있고 하나의 컴포넌트는 또 다른 여러개의 컴포넌트의 조합으로 구성될 수 있다.  

  16.8버전 이후 리액트 훅(react hooks)이라는 새로운 매커니즘을 고안해 객체지향 언어에서 의미하는 클래스가 아닌 단순한 함수 형태로도 컴포넌트를 구현할 수 있게 되었다. (함수 컴포넌트, 리액트 훅의 사용을 권장한다)

 

 리액트 컴포넌트와 사용자 컴포넌트

  리액트 컴포넌트라는 용어는 리액트가 제공하는 리액트 컴포넌트와 사용자가 구현하는 사용자 컴포넌트라는 2가지 의미를 포함한다.  리액트 컴포넌트의 이름은 div, h1처럼 첫글자를 소문자로 시작하는 반면 사용자 컴포넌트의 이름은 MyComponent처럼 첫 글자를 대문자로 시작하는 카멜 표기법(camel-case notation)을 따른다.

  • 리액트 컴포넌트
    - 리액트는 HTML5의 각 태그에 대응하는 리액트 컴포넌트를 제공한다.
    // HTML 태그가 아닌 리액트 프레임워크가 제공하는 h1컴포넌드
    const h1 = <h1>Hello world!</h1>
    
    // React.createElement로 생성
    const h1 = React.createElement('h1', null, 'Hello world!')
  • 사용자 컴포넌트
    - React.createElement 함수 선언문의 첫번째 매개변수인 type이 string, FunctionComponent<P>, ComponentClass<P> 타입일 수 있다.  리액트에서 함수 방식 컴포넌트 방식은 FunctionComponent<P>이고 클래스 방식 컴폰넌트 타입은 ComponentClass<P>이다.  여기서 타입 변수 P는 Property의 머리글자이다.
        function createElement<P extends {}>(
            type: FunctionComponent<P> | ComponentClass<P> | string,
            props?: Attributes & P | null,
            ...children: ReactNode[]
        ): ReactElement<P>;

    - 사용자 컴포넌트를 만드는 이유는 React.createElement호출이나 JSX문으로 생성하는 가상 DOM 생성 모드를 사용자 컴포넌트를 생성 해 코드를 간결하게 하기 위함이다.
    export default function App() {   // app.tsx
      return (
        <ul>
          <li>
            <a href="http://www.google.com">
              <p>go to google</p>
            </a>
          </li>
        </ul>
      )
    }
    
    // index.tsx
    import ReactDOM from 'react-dom/client'
    import App from './App'
    
    const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
    root.render(<App />)

 클래스 컴포넌트 만들기

  리액트의 컴포넌트는 클래스 기반 컴포넌트와 함수형 컴포넌트가 있다.  리액트의 클래스 컴포넌트는 반드시 react패키지가 제공하는 Component 클래스를 상속해야 한다.

import React, {Component} from 'react'
export default class ClassComponent extends Component {}

 

  그리고 Component를 상속한 ClassComponent는 render 메서드를 포함해야 한다.  render 메서드는 null이나 React.createElement 호출로 얻은 반환값, JSX문 등으로 가상 DOM 객체를 반환해야 한다.

import React, {Component} from 'react'
export default class ClassComponent extends Component {
	render() { return null }
}

 

 클래스 컴포넌트 방식으로 구현하기

  컴포넌트를 작성한 부분을 살펴보면 const isLoading = true 같은 타입스크립트 코드를 확인할 수 있다.  이처럼 타입스크립트코드와 JSX를구문을 함께 쓸 수 있는 것이 사용자 컴포넌트를 제작하는 또 다른 이유이다.  다만 하단의 ClassComponent에 속성을 추가할 수 있다면 좀더 확장성 있게 사용할 수 있을 것 이다.

// ClassComponent.tsx
import {Component} from 'react'
export default class ClassComponent extends Component {
  render() {
  	const isLoading = true
    if(isLoading) return <p>loading...</p>
    return (
      <li>
        <a href="http://www.google.com">
          <p>go to Google</p>
        </a>
      </li>
    )
  }
}

// app.tsx
import {Component} from 'react'
import ClassComponent from './ClassComponent'

export default class App extends Component {
  render() {
    return (
      <ul>
        <ClassComponent />
        <ClassComponent />
      </ul>
    )
  }
}

 

 속성이란?

  객체지향 프로그래밍에서 속성(property)은 클래스의 멤버 변수를 의미한다.  속성은 가변/불변적인 설정으로 관리될 수 있다.  다만 리액트 프레임워크의 속성은 객체지향 언어의 속성과는 다른 부분이 있어 주의해야 한다.
  리액트 프레임워크에서의 속성은 부모 컴포넌트가 자식 컴포넌트 쪽에 정보를 전달하는 목적으로 사용된다.  또한 클래스의 속성은 값을 저장하고 변경하는 기능을 하지만 리액트의 속성은 값이 변하면 해당 컴포넌트를 재렌더링하여 수정된 속성 값을 화면에 반영하는 기능도한다. 

  즉, 리액트 컴포넌트 관점에서 속성은 객체지향 프로그래밍의 속성 + 재렌더링을 의미하는 객체 타입 변수이다.

function createElement<P extends {}>(
    type: FunctionComponent<P> | ComponentClass<P> | string,
    props ?: Attributes & P | null,
    ...children : ReactNode[]): Reactelement<P>;
)


React.createElement의 함수 선언문 중 2번째 매개변수 props는 'properties' 속성을 의미한다.  또한 선언문에서 속성 P는 {}를 확장한다(P extends{})는 타입 제약(type constraint)이 걸려 있으며 이는 속성이 객체여야 함을 의미한다.  또한 props 뒤 ? 기호가 붙어 있으므로 선택 속성이기에 속성이 꼭 필요할 필요는 없다.

 

 JSX 속성 설정 구문

<Person name='Jack' />
<Person name='Jack' age={22} />
<Person person={{name='Jack', age=22}}

  JSX는 XML이므로 속성은 모두 string 타입이다.  속서은 XML같은 마크업 언어에서는 'attribute'를, 타입스크립트와 같은 프로그래밍 언어에서는 'proeperty'를 의미한다.  JSX는 XML을 확장한 것이므로 string 타입 속성은 name='Jack' 형태의 값을 설정할 수 있다.  하지만 number 타입은 string이 아니므로 중괄호 {} 로 감싸야 한다.  또한 속성이 객체이면 {{name:'Jack', age: 32}} 같은 형태로 적어야 한다.  이때 안쪽 {}은 객체를 만드는 구문이고 바깥쪽 {}은 JSX구문이다.

 

 ClassComponent에 속성 구현하기  

  리액트 관점에서 name이나 age의 속성이 유효한지 알 수 있는 방법이 없다.  그러므로 Component 타입에 속성 이름과 타입을 기입한 Props와 같은 속성 타입을 새로 넘겨줄 것을 요구한다.  선언된 Props는 컴포넌트 내부에서 this.props 형태로 외부에서 넘어온 속성을 사용할 수 있다.   모든 클래스 컴포넌트 부모 타입인 Component는 props 속성을 제공한다.

import {Component} from 'react'

type ClasscomponentProps = {  // 컴포넌트의 속성은 객체 타입이어야 한다는 조건을 갖춤(type객체사용)
    name: string
    age: number
}

export default class Classcomponent extends Component<ClassComponentProps> {
    render() { 
        const name = this.props.href
        const age = this.props.age
    return null 
    }
}

 

 클래스 컴포넌트에 속성을 추가한 최종 결과

// ClassComponent.tsx
import {Component} from 'react'

export type ClassComponentProps = {
  href: string
  text: string
}
export default class ClassComponent extends Component<ClassComponentProps> {
  render() {
    const {href, text} = this.props
    return (
      <li>
        <a href={href}>
          <p>{text}</p>
        </a>
      </li>
    )
  }
}

// App.tsx
import {Component} from 'react'
import ClassComponent from './ClassComponent'

export default class App extends Component {
  render() {
    return (
      <ul>
        <ClassComponent href="http://www.google.com" text="go to Google!" />
        <ClassComponent href="http://www.naver.com" text="go to naver!" />
      </ul>
    )
  }
}

 

 함수 컴포넌트 만들기

import {Component} from 'react'
export default from App extends Component {
    render() {
        return <h1>class component</h1>
    }
}

 

  함수 컴포넌트는 위와 같이 단순화 한다면 render 메서드를 구현할 수 있게 하는 상용구(프로그래밍 언어의 문법을 갖추는 코드)가 대부분인 것을 알 수 있다.  리액트 개발팀은 이걸 변화하고자 render 메서드 부분을 가단히 함수로 만들 수 있게 했고 이를 함수컴포넌트(functional component)라고 이름을 붙였다.  함수 컴포넌트의 가장 큰 장점은 상용구가 없기 때문에 좀더 간결하게 구현할 수 있다.

//function 키워드 방식 함수 컴포넌트 생성
export default function App() {
    return <h1>function component</h1>
}

// 화살표 방식 함수 컴포넌트 생성
const App = () => {
    return <h1>function Component</h1>
}
export default App

 

  여기서 화살표함수를 주목해 볼 필요가 있다.  타입스크립트에서 화살표 함수이름을 가질 수 없는 익명 함수(anonymous function) 이다.  따라서 App라는 변수에 익명 함수를 설정하는 방식으로 구현해야 한다.  다만 타입스크립트는 export default를 붙이지 못하므로 별도로 하단에 명시를 해줘야 한다.

 

함수 컴포넌트 타입

function createElement<P extends {}>(
    type: FunctionComponent<P> | ComponentClass<P> | string,
    props ?: Attributes & P | null,
    ...children : ReactNode[]): Reactelement<P>;
)

 

  React.createElement 선언문의 1번째 매개변수 중 FunctionCompoennt<P>는 함수컴포넌트의 타입이다.  그런데 FunctionComponent<P>라는 이름이 너무 길어 리액트는 이를 짧게 줄여 FC라는 이름의 타입을 제공한다.  결국 함수 컴포넌트 타입은 FC<P> (=FunctionComponent<P>)이다.

 

 화살표 함수 방식으로 동작하는 ArrowComponent 구현하기

// ArrowComponent.tsx
import type {FC} from 'react'

export type ArrowComponentProps = {
  href: string
  text: string
}
const ArrowComponent: FC<ArrowComponentProps> = props => {
  const {href, text} = props
  return (
    <li>
      <a href={href}>
        <p>{text}</p>
      </a>
    </li>
  )
}
export default ArrowComponent

// App.tsx
import {Component} from 'react'
import ClassComponent from './ClassComponent'
import ArrowComponent from './ArrowComponent'

export default class App extends Component {
  render() {
    return (
      <ul>
        <ClassComponent href="http://www.google.com" text="go to Google!" />
        <ArrowComponent href="http://www.naver.com" text="go to naver!" />
      </ul>
    )
  }
}