본문 바로가기

React/일반

재사용 가능한 리액트 컴포넌트를 구현하는 방법

 

리액트는 컴포넌트로 이루어져 있다.

모든 컴포넌트의 부모가 되는 index.js와 App.js를 시작으로 수많은 컴포넌트가 가지를 이뤄 뻗어 나간다.

 

컴포넌트 덕분에 개발자는 자연스레 기능 단위로 모듈을 만든다.

이펙트가 화려한 버튼 하나를 잘 만들어 놓으면 필요한 곳에서 쉽게 쓸 수 있다.

자바스크립트보다 훨씬 손쉽게 재사용 가능한 컴포넌트를 만들 수 있다.

 

그러나 의존성 없이 제대로 구현하지 않으면 본래의 재사용이 아닌

복사 붙여 넣기로 조금 다른 모듈을 새로 만드는 해프닝이 발생할 수 있다.

 

작은 앱에서야 상관없지만 복잡한 앱이 될수록

컴포넌트나 함수 등으로 잘 정리해 놓지 않으면

스스도 파악이 안 돼 손 쓸 수 없는 상태가 된다.

 

본 글에서는 어떻게 하면 위와 같은 문제를 피해 리액트 컴포넌트를 제대로 구현할 수 있는지 이모저모를 다뤄보고자 한다.

 

1. Presentational 컴포넌트와 Container 컴포넌트

Presentational 컴포넌트라는 말은 그대로 써도 좋을 듯하나 Container 컴포넌트라는 말은 적합할지 잘 모르겠다.

리액트도 개발된 지 꽤나 많은 시간이 지났고 class 컴포넌트를 쓰지 않듯 더 이상 맞지 않는 레거시 개념이 있을 수 있다.

Presentational Component와 Container Component라는 말도 오래전에 나왔고

여기서 본래의 의미로 사용하는 것이 아닐 수 있음을 미리 밝혀둔다.

 

Presentational 컴포넌트는 재사용 가능한 컴포넌트다.

보이는 껍데기만 구현한다고 해서 Presentational이라 한다.

 

Presentational 컴포넌트는 props로 받는 값에 따라서만 달라진다.

컴포넌트가 홈 화면에 위치하든 로그인 화면에 위치하든 상관없이 

props를 제외하고, 다른 컴포넌트나 모듈 따위에 의존하지 않고 독립적이어야 한다.

 

예를 들어 Button 컴포넌트를 구현해보자.

 

const Button = (props) => {
  const { children, onClick } = props;

  return (
    <button onClick={onClick}>{children}</button>
  )
}

export default Button;

 

props로 children과 onClick 변수를 받고 있다.

 

이 컴포넌트를 다른 컴포넌트에서 import 해 아래처럼 사용할 수 있다.

 

import Button from './Button'

const App = () => {
  const handleClick = () => {
    // 서버에 POST 요청
  }

  return (
    <div>
      <p>버튼을 클릭해주세요.</p>
      <Button onClick={handleClick}>클릭</Button>
    </div>
  )
}

 

Button 컴포넌트 자체적으로 하는 일이라고는 버튼을 그려주는 일밖에 없다.

props인 onClick에 넣은 handleClick 함수 chlidren(클릭)에 따라서 

버튼을 클릭했을 때 서버에 데이터를 POST 하는 handleClick 함수가 동작하고

children에 '클릭'을 넣어줬기 때문에 버튼에 클릭이라는 글자가 써진다.

 

이 컴포넌트에 onClick만 다르게 넣으면 버튼 클릭 시 어떤 동작도 일어나게 할 수 있다.

만약 서버에 POST 요청을 하는 handleClick 함수가 Button 내부에 있다면

Button 컴포넌트는 어떻게든 handleClick 함수와 관련이 있을 수밖에 없다.

다른 말로 의존성이 생긴다.

 

const Button = (props) => {
  const { children } = props;
  
  const handleClick = () => {
    // 서버에 POST 요청
  }

  return (
    <button onClick={handleClick}>{children}</button>
  )
}

export default Button;

 

위처럼 구현한다면 Button 컴포넌트를 클릭시에 무조건 특정 POST 요청을 한다.

오직 하나의 POST 요청을 위해서 구현한 컴포넌트가 되고 다른 곳에서 사용하기 어렵다.

 

handleClick과 같은 서버와 관련 있는 로직이 컴포넌트에 포함된다면

대부분 다른 곳에서는 쓸 수 없다.

state도 마찬가지다.

예를 들어 password 값을 기록하는 state는 로그인 창에서 밖에 쓸 수 없다.

 

때문에 보통 이런 서버 로직이나 state를 다루는 컴포넌트는 따로 둔다.

이를 Container 컴포넌트라고 부른다.

로지컬 컴포넌트 또는 스테이트풀 컴포넌트 등으로 바꿔 부를 수 있겠다.

리덕스를 사용하는 곳이기도 하다.

 

위의 예에서 App이 Container 컴포넌트에 해당한다.

App에서 서버 요청이나 state와 같은 로직을 작성하고

단지 보이는 부분은 Presentational 컴포넌트가 담당한다.

 

특별한 로직 없이 보이는 부분만 담당하기 때문에 어디서든 재사용할 수 있다.

 

2. Material UI와 Antd

위에서 컴포넌트를 크게 둘로 나눴고

이제는 Container 컴포넌트가 아닌, 재사용이 가능한 Presentational 컴포넌트에 한정해서 진행하겠다.

 

Material UI와 Antd는 리액트에서 가장 인기 있는 컴포넌트 라이브러리다.

리액트 좀 한다는 사람들이 만든 라이브러리이기 때문에 배울 수 있는 점들이 많다.

 

모달이나 드롭다운과 같이 손이 많이 가는 컴포넌트를 일일이 구현하기보다

Material UI나 Antd에서 잘 만들어 놓은 컴포넌트를 가져다 수정해 쓰는 게 좋을 때가 있다.

귀찮은 애니메이션까지도 알아서 해주니 빠르게 개발해야 할 상황이라면 괜찮다.

 

두 라이브러리에서 제공하는 컴포넌트는 당연 재사용할 수 있는 Presentational 컴포넌트이고

많이 활용하다 보면 자연스레 재사용할 수 있는 컴포넌트는 어때야 하는지를 파악할 수 있다.

 

여기서 눈여겨볼 점은 props다.

사실 컴포넌트는 props가 전부이기도 하다.

컴포넌트를 이용하는 개발자로서 props를 이용해 컴포넌트를 어떻게 써야 하는지만 알면 되지

굳이 컴포넌트 내부의 복잡한 로직까지 신경 쓸 필요 없다.

 

Props Naming

그중에서도 props의 이름을 눈여겨보자.

명확한 이름을 지으면 어떤 값을 넣어야 하는지를 한 번에 파악할 수 있기 때문에

컴포넌트를 쉽게 재사용할 수 있다.

 

Material UI와 Antd는 많은 사람이 고심해서 만든 컴포넌트이고

props의 이름도 신중하게 결정됐을 것이니

그들이 마련해놓은 이름 짓는 방식에 익숙해지는 게 좋다.

 

3. 복합적인 컴포넌트는 피하자

하나의 함수에는 한 가지 기능만을 구현하라는 말이 있다.

리액트 컴포넌트도 마찬가지다.

한 컴포넌트에 많은 기능이 들어 있으면 많은 기능이 버무려진 채 사용할 수밖에 없다.

이보다는 컴포넌트를 분리할 때 더 다양한 조합으로 사용할 수 있다.

 

prop도 마찬가지다.

컴포넌트를 사용하는 쪽의 코드가 깔끔해진다는 점에서 가능하다면 prop이 적은 게 좋지만

하나의 prop에 따라 여러 가지가 변하면 세부적인 변화를 줄 수 없게 된다.

 

예를 들어 input 컴포넌트의 string을 받는 error prop이 있다.

string을 받으면 컴포넌트의 색이 빨갛게 변하면서

에러 메시지가 input 아래에 나타나도록 구현했다.

이 컴포넌트는 에러 메시지를 빼고 빨갛게 만들 수는 없다.

대신 error라는 prop을 받으면 빨갛게 만들고

errorMessage라는 prop을 받을 때 메시지만 별도로 나타나게 한다면

더 다채롭게 컴포넌트의 변화를 줄 수 있다.

 

4. Controlled 컴포넌트와 Uncontrolled 컴포넌트

리액트의 input 컴포넌트는 값을 변경할 때 onChange 함수가 호출되고

onChange의 파라미터로 받을 수 있는 value를 state에 저장한다.

그리고 이 state를 다시 input의 value prop으로 넘긴다.

 

이런 식으로 input의 값을 state가 있는 부모 컴포넌트에서 관리할 수 있어

Controlled 컴포넌트라고 부른다.

반면 경우에 따라서 input 컴포넌트의 state를 부모에서 관리하지 않게 구현할 수도 있다.

이를 Uncontrolled 컴포넌트라고 부른다.

리액트 공식 매뉴얼에서는 특별한 목적이 있는 게 아니고서는 Controlled 컴포넌트를 구현하라고 권한다.

https://ko.reactjs.org/docs/uncontrolled-components.html