우선 이 글은 SPA 이후 프론트엔드 개발과 Component 기반 View단 개발과 관련있음을 밝히고, 논리적 사실에 근거하지 않은 지극히 주관적인 관점에서 작성되어 있으니 잘못된 점 혹은 수정되어야 할 부분, 불편한 부분 등을 지적해주시면 해당 부분을 수정하도록 하겠습니다.

이 글에서는 프론트엔드에서 보편적인 상식으로 이해되는 '컴포넌트 기반 개발 방법(이하 CBD)'은 무엇을 말하는지, 보편적인 컴포넌트의 종류는 어떤 것이 있는지를 중점으로 이야기해보려 합니다.

컴포넌트 기반 개발 방법이 무엇일까?

우선 CBD은 '컴포넌트 기반 소프트웨어 공학(CBSE)'에서 시작됩니다. 위키피디아의 정의는 다음과 같습니다.

컴포넌트 기반 소프트웨어 공학(Component-based software engineering, CBSE), 컴포넌트 기반 개발(component-based development, CBD)은 기존의 시스템이나 소프트웨어를 구성하는 컴포넌트를 조립해서 하나의 새로운 응용 프로그램을 만드는 소프트웨어 개발방법론이다. 기업들은 쇼핑바구니, 사용자 인증, 검색엔진, 카탈로그 등 상업적으로 이용 가능한 컴포넌트를 결합하여 그들의 전자상거래 응용 프로그램을 개발하는 컴포넌트 기반 개발을 사용한다.

이처럼 CBD는 개발 방법론의 핵심이 되는 '재사용성', '생산성' 등을 향상시키며 요구사항이 수시로 바뀌는 현대의 서비스 개발에 특화되어 빠르게 서비스를 피벗하거나 사용자가 급등할 수 있는 환경을 고려해 유연한 환경을 조성하는데 도움이 됩니다.

말로만 설명하면 어려울 수 있으니 간단한 코드를 살펴보도록 하겠습니다.

import React, { useState } from 'react'

const Header = ({onClickHeader, ...rest}) => (
  <header onClick={() => onClickHeader(true)>Header</header>
)
const Footer = () => <footer>Footer</footer>
const Layout = ({handleClickHeader, children,  ...rest}) => (
  <>
    <Header onClickHeader={handleClickHeader}/>
    {children}
    <Footer />
  </>
)
const MainContents = () => <main>Main</main>

const App = () => {
  const [hasContents, setHasContents] = useState(false)
  return (
    <Layout handleClickHeader={setHasContents}>
    {hasContents ? <MainContents /> : <main>you don't have MainContents</main>}
    </Layout>
  )
}

export default App

이처럼 하나의 App을 구성하기 위해 Header, Footer, MainContents를 컴포넌트로 나눴습니다(직접 확인해보지 않아서 동작하는지는 모르니 대충 이런 동작을 한다고 이해하시면 됩니다. 또한, 각 테그가 표준이 아닐 수 있기 때문에 실제로 실행하면 경고가 뜰 수 있습니당).

더불어, Header와 Footer 하나의 컴포넌트(Layout)로 감싸기도 했고, App에서 만든 'setHasContents'라는 메서드를 전달해주기도 했습니다. 이를 컴포넌트 기반으로 개발하는 CBD라고 부를 수 있습니다(React 자체가 컴포넌트 기반 개발을 추구합니다).

마지막으로 최근에는 hooks 등을 통해 위처럼 함수형 컴포넌트에 대한 지원이 늘어나 과거 class 기반 컴포넌트보다 깔끔한 코드를 작성할 수 있게 되었습니다(깔끔하다는 건 그만큼 유지보수가 쉽고, 재사용성이 높다는 의미입니다).

컴포넌트는 왜 이렇게 많은 거야..

사실 이 글을 쓰는 가장 큰 목적은 컴포넌트의 목록화입니다.

보편적으로 컴포넌트 기반으로 개발할 때 메서드가 실행되는 컴포넌트, View만을 담당하는 컴포넌트, 직접 제어가 가능한 컴포넌트 등 여러 컴포넌트가 복합적으로 사용됩니다.

그런데 사용은 하고 있지만, 각각에 대한 구분 혹은 뷰 설계에서의 각 컴포넌트를 역할(feature) 별로 구분하고자 할 때 머리 속에 각각을 목록화 시켜놓지 않으니 헷갈리기 일쑤였기 때문에 이 글을 통해 보편적인 컴포넌트는 어떤 것들이 있고, 각각을 어떻게 부르는지를 적어놓으려 합니다.

Uncontrolled component & Controlled Component

Uncontrolled 컴포넌트는 상태(state)를 직접(React에서는) 제어하지 않기 때문에 언컨트롤드 컴포넌트라고 부릅니다(실제 Dom에서는 상태(값)을 지니고 있습니다).

거의 모든 로직에서 상태를 프로그래머가(React를 통해) 제어하기 때문에 잘 사용하지는 않습니다. 하지만 render를 아예 하지 않기 때문에 굳이 상태를 제어하지 않는 컴포넌트가 있다면 효율적으로 사용할 수도 있습니다.

마찬가지로 Controlled Component는 리액트의 대부분 컴포넌트 형태이고, 상태를 직접 제어하는 컴포넌트입니다. 당연하게도 state의 변화에 따라 render를 거치기 때문에 상태를 지닌 컴포넌트를 작성할 때 사용하며 render timing을 유의해 사용해야 합니다.

위에서 작성한 코드에서 MainContents, Header, Footer는 Uncontrolled component이고, App은 Controlled Component라고 부를 수 있습니다.

참고자료 - Recommendation: fully uncontrolled component with a key

PureComponent

PureComponent는 리액트를 이용한 컴포넌트 기반 개발 방법에서 가장 기본적인 컴포넌트입니다. 물론 렌더링 성능을 사용자가 제어할 수 있기 때문에 복잡하다고 생각할 수도 있지만, 가장 보편적으로 사용되기 때문에 기본이라고 할 수 있습니다.

일반 컴포넌트와 퓨어 컴포넌트의 차이는 거의 없지만, 일반 컴포넌트는 상태나 Props에 변화가 없어도 이벤트에 따라 항상 다시 render를 실행하지만, 퓨어 컴포넌트는 이벤트에 따라 상태와 Props를 얕게(shallow) 비교해서 변경점이 없으면 다시 render를 실행하지 않습니다.

물론 퓨어 컴포넌트가 만능은 아닙니다. Dan Abramov의 한 트윗을 보면 어느 곳에서나 퓨어 컴포넌트를 사용하는 경우 성능을 떨어ㄷ트리며, 컴포넌트의 props 값이 항상 얇게 다르면(shallowly unequal) 항상 리렌더가 일어날 것이라고 경고하기도 합니다.

위에서 작성한 컴포넌트에서 만약 App이 일반 컴포넌트로 작성되어 있다면, Header를 누를 때마다 렌더가 일어나겠지만, 퓨어 컴포넌트(React.PureComponent, React.memo)로 작성되어 있다면, 처음 Header를 클릭했을 때만 렌더가 일어나게 되는 것입니다.

이처럼 퓨어 컴포넌트는 값의 변화가 있을 때만 렌더를 하는 컴포넌트이며 때에 따라 사용처가 다를 수 있음을 알았습니다.

Portal(Global) Component

이 컴포넌트는 주로 DOM 구조에서논리적으로 상위 컴포넌트를 하위 컴포넌트로 덮어야 하는 상황에서 사용합니다. 가령 fixed나 z-index를 통해 제어할 수 없는 상황1, 상황2 등에서 사용해 해결할 때 사용할 수 있습니다.

동작 원리는 Portal Component가 담길 컨테이너를 지정하고, 특정 상황에서 Portal Component를 컨테이너에 렌더시킨다고 할 수 있습니다. 만들어진 컴포넌트를 어느 컴포넌트 아래에 있건간에, Portal의 대상 컨테이너 아래에 렌더링할 수 있어 유용하게 사용할 수도 있습니다.

Functional Component

함수형 컴포넌트라고 부르며, 우리가 위에서 작성한 컴포넌트들을 함수형 컴포넌트라고 부릅니다. 물론 클래스 기반 컴포넌트가 존재하지만 개인적으로 함수형으로 작성하는 게 편하고 좋기 때문에 이 글에서는 따로 클래스 기반 컴포넌트를 언급하지 않도록 하겠습니다.

일반적으로 16.8 전까지는 함수형 컴포넌트와 클래스 기반 컴포넌트의 성능 차이는 거의 드러나지 않았습니다. 하지만 16.8에서 hooks가 도입되면서 함수형 컴포넌트에서 성능 최적화(useMemo, useCallback 등)를 진행할 수 있고, 라이프사이클(useEffect)을 제어할 수 있어 성능적으로는 함수형 컴포넌트가 우위에 있다고 할 수 있습니다.

다만, 이를 체감하기 위해서는 대규모 서비스를 운영해야 하기도 하고, 프론트엔드에서의 성능 최적화 방법이 다양하기 때문에 함수형 컴포넌트로 전환하며 성능 최적화를 고민해야 할 절대적인 이유는 없다고 생각하기 때문에 만능이라고는 할 수 없습니다. 다만, 이제 시작하는 프로젝트에서는 함수형으로 컴포넌트를 구성하는 것을 추천합니다.

Presentational Component & Container Component

Presentational Component는 데이터와 관련된 이벤트 혹은 스테이트 관리가 없이 사용자에게 보여지는 뷰만을 담당합니다. Presentational Component에는 DOM Elements, style 등이 들어가며 ReadOnly Component라고도 부를 수 있습니다. 또한, 대부분의 경우 state 를 갖고있지 않으며, 갖고있을 경우엔 데이터에 관련된것이 아니라 UI 에 관련된것이어야 합니다.

개인적으로 함수형 컴포넌트로 작성하는 것이 바람직하다고 생각합니다(16.8 이후에는 더욱더 함수형 컴포넌트로 작성되어 관리하는 것이 용이하다고 생각합니다). 물론 보편적으로도 함수형 컴포넌트로 작성됩니다.

Container Component는 여러 컴포넌트를 관리하기 위해 작성됩니다. 일반적으로 내부에 DOM Elements를 직접적으로 수정하거나 관리하지 않습니다. 만약 DOM Elements가 사용되는 경우는 감쌀 때만 사용합니다. 더불어 스타일을 가지지 않는 것을 권장합니다. 보편적으로 스타일은 모두 프리젠테이셔널 컴포넌트에서 정의되어야 합니다.

대부분의 액션을 컨테이너 컴포넌트에서 사용하는 것이 보편적이지만, 16.8 이후에는 함수형 컴포넌트로 액션 제어가 가능하다보니 각 컴포넌트를 유기적으로 사용하고 독립성을 보장하기 위해 각 컴포넌트 별로 액션을 사용하는 것을 권장하고자 합니다.

다만, 리액트에서 권장하는 스테이트의 관리는 상위 컴포넌트에서 이루어져야 하기 때문에 최상위에 위치하는 컨테이너 컴포넌트에서 관리되는 것이 보편적입니다.

마지막으로

지금까지 2019년의 컴포넌트 기반 개발 방법에 대한 고민이었습니다. 다만, 개인적으로 함수형 컴포넌트 개발 방법을 좋아하기에 다소 치우쳐진 견해가 있을 수 있으니 이 점 유의하여 읽어주시면 감사하겠습니다.

또한, 이 글을 쓰게 된 계기는 아직도 주변에서 모든 스테이트 관리를 컨테이너에서 하고 함수형 컴포넌트는 거의 프리젠테이셔널 컴포넌트로 사용하려는 경향이 있는 것 같아 개인적으로 정리해보고 싶은 마음이 있었기 때문입니다.

그럼 20000!