React Component란?

  • 리액트로 만들어진 앱을 이루는 최소한의 단위
  • 밑 사진 빨간 박스 하나하나가 모두 컴포넌트


리액트 컴포넌트 특징

  1. 데이터가 주어졌을 때 이에 맞추어 UI를 생성.

  2. 라이프 사이클 API를 이용해 컴포넌트가 화면에 Mount, Update, Unmount 될 때 주어진 작업 처리 가능

  3. 기존의 웹 프레임워크는 MVC방식으로 분리하여 관리하여 각 요소의 의존성이 높아 재활용이 어렵다는 단점이 있지만, 리액트 컴포넌트는 MVC의 V(View)를 독립적으로 구성하여 재사용을 할 수 있고 이를 통해 새로운 컴포넌트 생성 가능 → 타 프레임워크와 달리 뷰(View) 하나만 바뀌어도 나머지 Control, Model 영역에 영향을 주지 않습니다.

  4. props 라는 속성을 이용해 같은 컴포넌트여도 다르게 렌더링 가능합니다.

  5. 위에 언급했듯이 컴포넌트를 통해 UI를 재사용 가능한 개별적인 여러 조각으로 나누고, 각 조각을 재사용할 수 있습니다. → 컴포넌트 사용의 가장 큰 목적

예시

밑 사진의 컴포넌트를 재사용해

다음과 같은 컴포넌트를 화면을 구상할 수 있습니다.



컴포넌트 선언 방법

리액트에서 컴포넌트를 선언하는 방법은 클래스형 컴포넌트와 함수형 컴포넌트가 있습니다.

클래스형 컴포넌트 예시

import react, { Component } from "react";
//Component로 부터 상속
 
class App extends Component() {
  render() {
    //render 함수가 반드시 있어야합니다.
    //render 함수는 JSX 값을 반환해줍니다.
    const name = "react";
 
    return <div className="react">{name}</div>;
  }
}

함수형 컴포넌트 예시

import React from "react";
import "./App.css";
 
export default function App() {
  const name = "react";
  return <div className="react">{name}</div>;
}

함수형 컴포넌트는 이름 그대로 함수 형태, 클래스형 컴포넌트는 이름 그대로 클래스 형태를 하고 있다는 점이 직관적인 차이점이라 할 수 있습니다.

과거에는 클래스형 컴포넌트를 사용했다고 합니다.

클래스형 컴포넌트 특징

  1. 리액트의 기능을 최대로 끌어 올릴 수 있습니다.
  2. 함수형 컴포넌트는 라이프사이클 API 사용, 컴포넌트 내부의 state를 만들 수 없습니다.

하지만 현재 리액트 시장은 함수형 컴포넌트 사용을 선호하고 있습니다. 공식문서에서도 함수형 컴포넌트와 hook을 함께 사용하기를 권장하고 있습니다.

함수형 컴포넌트 장점

  1. 함수의 문법만 알면 사용할 수 있기 때문에 편리.
  2. 클래스형 컴포넌트는 로직과 상태를 컴포넌트 내에서 구현하기 때문에 함수형 컴포넌트보다 가독성이 떨어집니다.
  3. 2019년에 출시된 최신 리액트(v16.8)에서 hook이 도입되면서 내부적으로 state 관리, 라이프 사이클 api 사용 등 함수형 컴포넌트의 한계를 극복할 수 있는 기능들이 등장했기 때문. → 클래스형 컴포넌트 위주에서 함수형 컴포넌트 위주로 넘어가게된 가장 큰 원인. 4.메모리 자원을 덜 사용하기 때문에 프로젝트를 완성하여 빌드한 후 배포할 때도 함수형 컴포넌트를 사용하는 것이 결과물의 파일 크기가 더 작습니다. (사실 성능과 파일 크기 면에서 큰 차이는 없다고 합니다.)
  4. 제공되는 Hook 함수뿐만 아니라 커스텀 훅을 생성하여 사용할 수 있습니다.
커스텀 훅

Custom Hook?

  • useState와 useEffect들과 같은 특정 상태관리나 라이프사이클 로직들을 추상화하여 묶어서 재사용이 가능하도록 제작이 가능한 함수

커스텀 훅 사용 이유

  • 반복되는 로직을 하나로 묶어 재사용하기 위함

간략한 사용법

//custom hook 정의
const useToggle = (initialState = false) => {
  const [state, setState] = useState(initialState);
  const toggle = useCallback(() => setState((state) => !state), []);
 
  return [state, toggle];
};
 
// 사용예시
import { useCallback, useState } from "react";
 
function App() {
  const [isTextChanged, setIsTextChanged] = useToggle();
 
  return (
    <button onClick={setIsTextChanged}>
      {isTextChanged ? "Toggled" : "Click to Toggle"}
    </button>
  );
}

위처럼 useToggle을 호출하면 현재 토글상태과, 이 토글상태를 변경할 수 있는 함수가 리턴이 됩니다. 훅을 호출하면 useToggle함수의 return값이 [state, toggle] 이므로 분해구조 문법을 통해 리턴값을 특정 변수(state)와 바인딩하여 사용할 수 있습니다.

커스텀 훅의 강점은 불필요하게 반복되는 로직을 하나의 함수로 묶어서 추상화함으로써 재활용이 가능하도록 만든다는 점에서 효율적 측면이 있습니다.



하지만 클래스형 컴포넌트도 개념은 익혀놔야합니다. 어떠한 기업은 클래스형 컴포넌트를 사용할 수 있고(Legacy) 그 기업에 근무를 하게 될 수도 있기 때문입니다. 그 기업 프로젝트의 유지보수를 위해서는 클래스형 컴포넌트에 대한 개념은 알고 있어야 당황하지 않겠죠.

children props 고찰

3장 내용 중 children props가 나오길래 잘 모르고 사용한 것 같아서 딥다이브 하기로 했습니다.

First.js, Second.js 를 모두 포함하는 컴포넌트 (House.js)가 필요한 상황이라고 가정하겠습니다. 아래처럼 First,second 컴포넌트를 감싸는 <House/> 컴포넌트가 있습니다.

// House.js
import React from "react";
 
function House() {
  const style = {
    border: "4px solid green",
    padding: "16px",
  };
  return <div style={style}></div>;
}
 
export default House;
//Second.js
import React from "react";
 
function Second(props) {
  return <div> 둘 째 {props.name} 입니다</div>;
}
 
export default Second;
// First.js
import React from "react";
 
function First() {
  return <div>첫 째입니다</div>;
}
 
export default First;
// App.js
import React from "react";
import First from "./First";
import Second from "./Second";
import House from "./House";
 
function App() {
  return (
    <House>
      <First />
      <Second name="동2" color="blue" />
    </House>
  );
}
 
export default App;

렌더링 결과

House 컴포넌트는 정상적으로 초록색 박스가 그려지는데, 내부의 컴포넌트인 First, Second는 작동하지 않는 것을 볼 수 있습니다.

컴포넌트 태그(House) 사이의 컴포넌트(First, Second)의 값을 조회하고 싶을 때 props.children을 사용해야 합니다.

// House.js
import React from "react";
 
function House({ children }) {
  const style = {
    border: "4px solid green",
    padding: "16px",
  };
  return <div style={style}>{children}</div>;
}
 
export default House;

정상적으로 렌더링 되는 것을 확인할 수 있습니다.

즉 컴포넌트 사이에 컴포넌트가 있을 경우 children을 사용합니다.

children props를 활용한 렌더링 최적화

import React, { useState } from "react";
 
const ChildComponent = () => {
  console.log("ChildComponent is rendering!");
  return <div>Hello World!</div>;
};
 
const ParentComponent = () => {
  console.log("ParentComponent is rendering!");
  const [toggle, setToggle] = useState(false);
  return (
    <>
      <ChildComponent />
      <button
        onClick={() => {
          setToggle(!toggle);
        }}
      >
        re-render
      </button>
    </>
  );
};
 
const Container = () => {
  return (
    <div>
      <ParentComponent />
    </div>
  );
};

Container > ParentComponent > ChildComponent 구조로 프로젝트가 구성되있습니다. 이 상황에서 만약 re-render 버튼을 눌러 ParentComponent의 리렌더링을 유발한다면 당연히 ParentComponent가 리렌더 되는 순간 ChildComponent도 함께 리렌더 될 것입니다. 실제로 콘솔을 살펴보면 두 컴포넌트가 모두 렌더링 되고 있음을 확인할 수 있습니다.

아시다시피 이 현상은 아주 비효율적입니다. ParentComponent의 toggle state 변경은 ChildComponent와는 무관함에도 무의미하게 ChildComponent가 리렌더링 되는 것이기 때문입니다. 즉 ChildComponent는 리렌더링될 필요가 없음에도 불구하고 리렌더링되는 것입니다.

이러한 문제를 해결하기 위해 React.memo를 사용하기도 하지만 children props를 사용해도 해결할 수 있습니다.

import React, { useState } from "react";
 
const ChildComponent = () => {
  console.log("ChildComponent is rendering!");
  return <div>Hello World!</div>;
};
 
const ParentComponent = ({ children }) => {
  console.log("ParentComponent is rendering!");
  const [toggle, setToggle] = useState(false);
  return (
    <div>
      {children}
      <button
        onClick={() => {
          setToggle(!toggle);
        }}
      >
        re-render
      </button>
    </div>
  );
};
 
const Container = () => {
  return (
    <div>
      <ParentComponent>
        <ChildComponent />
      </ParentComponent>
    </div>
  );
};

ParentComponent 내부에 직접 ChildComponent를 배치하는 대신 children prop을 통해 간접적으로 ChildComponent를 return하고 있음을 확인할 수 있습니다. 이 상태에서 아까와 동일하게 re-render 버튼을 눌러 ParentComponent의 리렌더를 유발하게 하게 되면 콘솔에는 "ParentComponent is rendering!"이라는 문구만 찍히게 됩니다. 즉 ChildComponent가 렌더링 되지 않음으로써 최적화한 것입니다.

즉 children prop을 사용하면 React.memo 등의 도구를 사용하지 않고도 구조적으로 렌더링 최적화를 달성할 수 있습니다.

그렇다며 이유가 뭘까요??

이유는 아직 이해를 못했습니다... 담주까지 알아와서 마저 설명드리도록 하겠습니다ㅠ



웹팩 코드 검색 확장자
import React from "react";
import Header from "./header";
//웹팩 코드 검색 확장자가 있기 때문에
//파일을 import 할 때 파일 확장자를 적지않아도 됩니다.
 
export default function App() {
  return (
    <div>
      <Header name="변재정" />
      <p>안녕하세요 에픽입니다.</p>
    </div>
  );
}

"웹팩 코드 검색 확장자(webpack module resolution)" 기능 덕분에 import시 js, jsx 등 파일 확장자를 생략해도 자동으로 찾을 수 있습니다. 확장자가 파일 이름에 없는 경우 웹팩의 확장자 옵션(extentions)에 정의된 확장자 목록을 통해 확장자 이름을 포함한 파일이 있는지 확인하여 자동으로 import 한다.



3줄 요약

  • 리액트의 컴포넌트 선언방법은 함수형 컴포넌트와 클래스형 컴포넌트가 있으며, 현재 함수형 컴포넌트와 react hook을 조합해 사용하는 것을 권장하고있다.
  • children props는 부모컴포넌트가 자식 컴포넌트로 접근하기 위한 리액트 props이다.
  • 접근뿐만 아니라 불필요한 리렌더링을 방지하는 최적화에도 도움이 된다.
출처