Computer >> 컴퓨터 >  >> 프로그래밍 >> JavaScript

React 컴포넌트 완벽 정리: 함수형 컴포넌트 vs 클래스 컴포넌트

React는 개발자가 코드를 캡슐화하여 재사용성을 극대화할 수 있도록 도와주는 JavaScript 라이브러리입니다. 이렇게 캡슐화된 코드 조각을 컴포넌트(Component)라고 부릅니다. 컴포넌트는 자체적인 로직과 상태(state)를 가질 수 있으며, DOM(Document Object Model)의 동작에 간섭하지 않고 독립적으로 작동합니다.

웹사이트의 일부분을 이처럼 작은 단위의 컴포넌트로 추상화하고 데이터를 주고받게 하면, 코드가 재사용 가능해지고 더욱 DRY(Don't Repeat Yourself, 반복하지 마라)한 구조가 됩니다. React에서 만나게 될 컴포넌트에는 크게 두 가지 유형이 있습니다. 바로 함수형 컴포넌트(Functional Component)클래스 컴포넌트(Class Component)입니다.


React 컴포넌트의 기본 개념

높은 수준에서 보면, React 컴포넌트는 기본적으로 props를 매개변수로 받아 화면에 무엇이 표시되어야 하는지를 설명하는 React 엘리먼트를 반환하는 JavaScript 함수입니다.

import React from 'react';
import ReactDOM from 'react-dom';
 
const Greeting = (props) => {
   return (
       <div>
           Hello, {props.name}
       </div>
   );
};
 
const element = <Greeting name="Yolanda" />;
 
ReactDOM.render(
   element, document.getElementById('root')
)

위 예제에서 React 엘리먼트는 텍스트를 담고 있는 <div>입니다. 이 텍스트는 컴포넌트에 전달된 props 객체를 사용합니다. props 객체는 부모 컴포넌트로부터 자식 컴포넌트로 데이터를 전달하는 역할을 합니다.

이 예제에서 전달된 prop은 name이며, element가 부모 역할을 합니다. <Greeting /> 컴포넌트에 전달하는 모든 속성은 props 객체에 담겨 Greeting 내부에서 사용할 수 있게 됩니다. 덕분에 어떤 이름이든 원하는 대로 넘길 수 있어 Greeting 컴포넌트의 재사용성이 매우 높아집니다.

함수형 컴포넌트(Functional Components)

앞서 언급했듯이 함수형 컴포넌트는 기본적으로 JavaScript 함수입니다. React 컴포넌트를 만들 때 ES5(EcmaScript 5) 또는 ES6(EcmaScript 2015) 문법을 모두 사용할 수 있습니다. 한 가지 규칙은, React 컴포넌트는 반드시 대문자로 시작해야 한다는 점입니다. 이를 통해 해당 요소가 실제로 컴포넌트임을 나타냅니다.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Basic React Component</title>
</head>
<body>
   <!-- App is inserted at the root node -->
   <div id="root"></div>
  
   <!-- React CDN -->
   <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
   <!-- React-DOM CDN -->
   <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
  
   <!-- React Components can be rendered here -->
   <script async>
       'use strict';
 
       const element = React.createElement;
 
       function Greeting() {
           return element('h4', null , `Hello, World!`);
       };

       function App() {
           return element(Greeting);
       };
 
       const domContainer = document.querySelector('#root');
           ReactDOM.render(element(App), domContainer);
   </script>
</body>
</html>

위 코드는 JSX 없이 순수한 바닐라 JavaScript만으로 작성한 ES5 함수형 컴포넌트의 실제 작동 예제입니다. HTML 부분은 크게 신경 쓰지 않아도 되며, JavaScript 로직에 집중하세요. 엘리먼트를 생성하고, 무언가를 반환하는 함수를 정의한 뒤, 그 함수를 ReactDOM에 렌더링하는 구조입니다.

JSX(JavaScript XML Extension)는 JavaScript 안에서 HTML과 유사한 문법을 작성할 수 있게 해줍니다. 위 예제에서는 순수 JavaScript만으로 HTML 엘리먼트를 생성하고, React의 createElement 메서드를 통해 <h4> 요소를 <div id="root">에 삽입했습니다.

그런데 애플리케이션 규모가 커지면 이런 방식으로 작성하는 것이 금방 번거로워집니다. JSX는 사실상 React.createElement 메서드를 위한 문법적 설탕(syntactic sugar)으로, 앱을 훨씬 빠르게 확장할 수 있도록 만들어졌습니다.

함수형 컴포넌트는 ES5와 ES6 어느 쪽으로도 작성할 수 있습니다. JSX를 사용한 두 버전의 예제를 비교해 보겠습니다.

ES5 함수형 컴포넌트

import React from 'react';
import ReactDOM from 'react-dom';
 
function Animal(props) {
           const element = <h1>My cat's name is {props.name}</h1>;
           return element;
      }
       function App() {
               return <Animal name="Marshmallow"/>
       };
      const domContainer = document.querySelector('#root');
          ReactDOM.render(<App/>, domContainer);

ES6 함수형 컴포넌트

const Animal = (props) => {
           const element = <h1>My cat's name is {props.name}</h1>;
           return element;
      }
       const App = () => {
               return <Animal name="Marshmallow"/>
       };
      const domContainer = document.querySelector('#root');
          ReactDOM.render(<App/>, domContainer);

React v16 이전에는 함수형 컴포넌트를 stateless(상태 없는) 컴포넌트라고 불렀습니다. 즉, 함수형 컴포넌트의 주된 목적은 프레젠테이션, 즉 화면에 보기 좋게 표시하는 것이었습니다. 대개 데이터를 전달받아 UI에 무언가를 렌더링하는 역할을 담당했습니다.

React v16의 등장으로 이 모든 것이 달라졌습니다. 자세한 내용은 잠시 후 다루겠습니다. 우선은 함수형 컴포넌트가 부모나 전역 객체로부터 데이터를 받아 클라이언트에게 무언가를 보여주기 위해 존재한다는 점만 기억하면 됩니다.

클래스 컴포넌트(Class Components)

클래스 컴포넌트는 "단순한" JavaScript 함수보다 조금 더 복잡합니다. React의 클래스 컴포넌트는 ES6 JavaScript 클래스의 개념을 차용합니다. JavaScript에서 클래스의 구조는 속성(attribute)과 메서드(method)를 가진 객체와 거의 같으며, this 키워드를 사용해 그 객체의 인스턴스에 접근하고 상호작용합니다.

React에서도 구조는 대체로 동일합니다. React 클래스 컴포넌트는 객체의 인스턴스이며, 이 객체는 state(상태)를 가집니다. 상태란 컴포넌트가 보유한 데이터를 의미합니다. 클래스에 속성을 설정하고 바인딩하는 또 다른 방식이라고 생각하면 됩니다. 개발자는 이 데이터를 활용해 화면에 표시하거나, 다른 컴포넌트에 전달하거나, 추가 로직을 수행하는 등 원하는 무엇이든 할 수 있습니다.

ES6 클래스 컴포넌트 역시 함수형 컴포넌트처럼 항상 대문자로 시작합니다. 트랜스파일러가 이것이 컴포넌트임을 인식할 수 있도록 하는 React의 규칙입니다. React 자체로부터 컴포넌트 구조를 상속받기 때문에 React의 Component 클래스를 extend(확장)해야 합니다. 그 코드 블록 안에 상태를 정의합니다.

import React from 'react';
import ReactDOM from 'react-dom';
import Name from './Name';
 
   // 클래스의 인스턴스를 다룰 때는 'this' 키워드가 필요합니다.
   // render 메서드 안에서 메서드나 state를 참조할 때 앞에 붙입니다.
class Animal extends React.Component {
   constructor(props) {
       super(props);
       this.state = {
           species: ['fox', 'tiger', 'bear', 'kangaroo', 'fish', 'cat', 'dog', 'squirrel']
       }
   }
  
   render() {
       return (
           <div>
               {this.state.species.map(animal => {
                   return <Name animal={animal}/>
               })}
           </div>
       )
   }
}
ReactDOM.render(<Animal />, document.getElementById('root'));

state는 속성(property)과 값(value)으로 이루어진 객체입니다. 여기서 state는 species라는 속성을 가지며, 그 값은 동물 이름들로 가득한 배열입니다. 이 배열을 참조하거나 조작하려면 this.state.species를 사용합니다.

위 클래스 컴포넌트의 목적은 동물의 이름을 <Name /> 컴포넌트로 전달하는 것입니다. <Name /> 컴포넌트는 데이터를 받으면 그것으로 무언가를 처리하는 역할을 합니다. Animal의 state는 함수형 또는 다른 클래스 컴포넌트로 전달될 때 props 객체의 일부가 되며, 계속해서 하위로 전달되는 한 자식 컴포넌트에서 접근할 수 있습니다.

React에서 데이터는 부모 컴포넌트에서 자식 컴포넌트로 한 단계씩만 아래로 흐른다는 점을 꼭 기억하세요. 부모의 손자 컴포넌트에서 데이터가 필요하다면, 중간 단계를 거쳐 한 번 더 전달해야 합니다.

라이프사이클 메서드(Lifecycle Methods)

클래스 컴포넌트의 또 다른 특징은 라이프사이클 메서드를 통해 상태를 추적하고 관리할 수 있다는 점입니다. React 라이프사이클은 일반적으로 세 단계로 구성됩니다. 생성(Mounting), 생존(Update), 소멸(Unmounting)입니다. 각 단계마다 상태에 접근하거나 변경할 수 있는 메서드가 제공됩니다.

  1. componentDidMount() – 컴포넌트가 마운트된 직후 호출되는 메서드로, AJAX/네트워크 요청을 통해 초기 상태를 설정할 때 사용합니다. setState()로 가져온 데이터를 state에 저장합니다.
  2. componentDidUpdate() – 사용자가 애플리케이션과 상호작용한 후 발생하는 상태 업데이트를 처리하는 메서드입니다.
  3. componentWillUnmount() – 컴포넌트가 언마운트될 때 실행되는 정리(cleanup) 함수입니다. 타이머 해제, AJAX 요청 취소 등을 처리합니다.

이 외에도 더 많은 라이프사이클 메서드가 있으며, 위에 나열된 것들은 핵심 메서드들입니다. 자세한 내용은 React 공식 문서를 참고하세요.

마지막으로, 함수형 컴포넌트의 return 문과 달리 클래스 컴포넌트는 render() 메서드를 사용합니다. 이 메서드는 ReactDOM.render()가 Animal 컴포넌트를 전달하고 React가 생성자를 호출한 후에 실행됩니다. 이후 state가 초기화되고, render 메서드가 호출되어 실제로 화면에 콘텐츠가 표시됩니다.

정리하면, React v16 이전의 함수형 컴포넌트는 주로 프레젠테이션 용도였습니다. 상태를 직접 다루지 않고 단순히 표시만 했죠. 반면 클래스 컴포넌트는 애플리케이션의 모든 상태 로직을 담당하고, 정보를 props 형태로 다른 컴포넌트에 전달했습니다.

함수형 컴포넌트와 useState()

2018년, React는 React Hooks라는 새로운 개념을 도입했습니다. Hooks는 함수형 컴포넌트 안에서 라이프사이클 메서드와 상태를 깔끔하고 간결하게 활용할 수 있는 방법입니다.

지금까지 배운 내용과 대부분 유사합니다. 상태가 있고, 그것으로 무언가를 처리해야 하며, 다른 곳으로 전달해야 합니다. 목표는 같지만 문법이 훨씬 깔끔해졌습니다. 익숙해지는 데 약간의 시간만 필요합니다.

권장하는 학습 방법은 이 페이지를 북마크해두고, 먼저 클래스 컴포넌트를 충분히 연습하며 React에서 데이터 흐름이 어떻게 작동하는지 완전히 이해한 뒤, 다시 돌아와 상태를 가지는 함수형 컴포넌트(stateful functional components) 학습을 마무리하는 것입니다.

앞서 살펴본 코드부터 시작해 보겠습니다.

import React from 'react';
import ReactDOM from 'react-dom';
 
const Animal = (props) => {
           const element = <h1>My cat's name is {props.name}</h1>;
           return element;
      }
       const App = () => {
		const [ state, setState ] = useState('Marshmallow');
               return <Animal name={state}/>
 
       };
      const domContainer = document.querySelector('#root');
          ReactDOM.render(<App/>, domContainer);

여기에는 Animal과 App 두 개의 컴포넌트가 있습니다. App이 Animal을 반환하면서 값이 "Marshmallow"인 name이라는 prop을 전달하고 있습니다.

믿기 어렵겠지만, 이 코드를 상태를 가지는 로직으로 변환하는 데는 많은 작업이 필요하지 않습니다. <App> 컴포넌트를 살펴보고 다음 단계를 따라가 보겠습니다.

import React, {useState} from 'react';

— 이 줄이 useState 훅을 import하는 부분입니다.

const [ state, setState ] = useState('Marshmallow');

— 이 줄이 상태를 초기화합니다. 여기서 state와 setState는 임의의 이름일 뿐이며, 원하는 대로 지을 수 있습니다. 다만 값의 의미에 맞게 짓는 것이 관례입니다.

  • state: 실제 상태값입니다. 초기 상태는 useState()의 괄호 안에 들어갑니다.
  • setState: this.setState()와 유사한 역할을 합니다. 애플리케이션을 진행하면서 상태를 변경하는 메서드입니다.
               return <Animal name={state}/>

— "Marshmallow"를 {state}로 교체합니다. JSX 안에서 JavaScript를 작성할 때는 중괄호({})를 사용하며, 이를 통해 변수를 전달할 수 있습니다.

클래스 컴포넌트를 정의했던 핵심 특징인 React 라이프사이클은, 다양한 메서드들을 하나의 기본 훅으로 압축한 형태로 제공됩니다! useEffect() 훅 하나로 자신이 속한 React 컴포넌트의 마운트, 업데이트, 언마운트를 모두 처리할 수 있습니다.

결론

React v16 이전에는 함수형 컴포넌트가 순수하게 프레젠테이션 뷰 계층으로만 사용되었으며, 클래스 컴포넌트로부터 props로 전달받는 경우를 제외하면 상태를 활용하지 않았습니다. 클래스 컴포넌트는 애플리케이션의 모든 상태를 보유하고 데이터를 주고받았으며, 라이프사이클 메서드를 통해 React 컴포넌트의 마운트, 업데이트, 언마운트를 관리했습니다. 그리고 React v16에서 새롭게 공개된 패턴인 React Hooks를 사용하면 함수형 컴포넌트도 상태를 가질 수 있고, 자체적인 라이프사이클 메서드 역할을 하는 훅을 사용할 수 있게 되었습니다.

React Hooks가 점점 더 널리 사용되고 있더라도, 모든 유형의 컴포넌트에 익숙해지는 것이 중요합니다. 레거시 코드에서는 당분간 클래스 컴포넌트가 사용될 가능성이 높기 때문에, 이를 이해하고 다룰 수 있어야 하기 때문입니다!