Web Front-end/React

[React/Composition] 리액트 컴포넌트의 확장(React Composition)

React를 잘 사용하기 위해서는 , 컴포넌트 설계능력이 필수적이게 된다.  이건 경험적인 요소라 많이 쓸수록 자연스럽게 늘어나겠지만, 한번 의식적으로 확장을 해보자! 

 

웹 환경에서의 재사용

웹에서는 생각보다 반복적인 작업이 많다. 예를들어, 아래와 같은 Modal 창을 구현한다고하자. Modal창은 구체화되서, "로그인창"이 될 수도 있고, "상세정보를 보여주는 Modal창"이 될 수도있다. 물론 , 각각 폴더로 분리해서 각각의 Component로 구현해도 무방하다. 하지만, 이번에는 "의도적"으로 Modal창을 추상화시키고, Login창과 Detail창을 구현해보도록 하겠습니다. 마치 "상속"과 비슷하지 않나요!  

React에서는 상속대신 , Composition

https://reactjs.org/docs/composition-vs-inheritance.html

 

Composition vs Inheritance – React

A JavaScript library for building user interfaces

reactjs.org

 

React에서는 상속대신 , 컴포넌트끼리의 합성을 권장하고 있습니다. 저는 위의 예시처럼, Modal창을 이용해서 여러가지 Modal을 구체화시켜보겠습니다. Title과 Description을 갖고있는 Modal이라는 틀에 , 내가 구현한 JSX를 주입하는 식으로 구현해보겠습니다. 

 

초기버전

Modal.jsx

export default function Modal(props) {
  return (
    <div className="modal">
      <div className="modal_wrapper">
        <div className="title">TEST</div>
        <div className="description">TEST</div>
        {props.children}
      </div>
    </div>
  )
}

Modal이라는 창안에 , props.children의 JSX를 그대로 던져주면서, 더 큰 "확장성"을 노려볼 수 있게 되었습니다. LoginModal창을 간단하게 만들어보겠습니다.

 

LoginModal.jsx

export default function Login(){
  return(
    <Modal>
      <form>
        <label htmlFor='id'>ID:</label>
        <input id="id"></input>
        <br></br>
        <label htmlFor='pw'>PW:</label>
        <input id="pw"></input>
      </form>
    </Modal>
  )
}

흠.. 뭔가 아쉽습니다. 제가 원한건 , Title과 Description도 , 서비스의 종류에 따라 바꾸고 싶습니다. modal을 더 확장시켜보겠습니다.

1차 확장시킨 Modal

Modal.jsx

export default function Modal(props) {
  return (
    <div className="modal">
      <div className="modal_wrapper">
        <div className="title">{props.title}</div>
        <div className="description">{props.description}</div>
        {props.children}
      </div>
    </div>
  )
}

Title과 description props.title,props.description으로 확장을 했습니다. 이제, Login Modal에서 Login을 줘볼까요!

 

Login.jsx

export default function Login(){
  return(
    <Modal title="Login" description="Login">
      <form>
        <label htmlFor='id'>ID:</label>
        <input id="id"></input>
        <br></br>
        <label htmlFor='pw'>PW:</label>
        <input id="pw"></input>
      </form>
    </Modal>
  )
}

이제, Detail 모달창을 만드려면 어떻게하면 될까요?

 

DetailModal.jsx

import React from 'react'
import Modal from './Modal'
export default function DetailModal() {
  return (
    <Modal title="Detail" description="디테일창">
      <div>디테일 창입니다~</div>
      <div>사진사진~!</div>
    </Modal>
  )
}

App.js에서 DetailModal을 부르기만하면 됩니다!

 

2차로 확장시켜보자

Modal 창은 보통, 버튼으로 구현이 됩니다. 각각의 버튼을 눌러서, Login -> Login Modal을 뜨게하고, Detail -> Detail을 뜨게만들면되겠죠! 간단하게 , App.jsx에서 State를 정해준 뒤에,  Modal.jsx에 열려있는지 , 아닌지에 따라서, 화면을 바꿔주는 연습을 해보겠습니다. 

 

Flow : 메인화면 -> 버튼클릭 -> Modal창들 보여주기 -> 취소시 원래화면으로 복귀

 

App.jsx

function Dimmed(){
  return (
    <div className="dimmed">
    </div>
  )
}
function App() {
  const [LoginOpen,setLoginOpen] = React.useState(false)
  const [DetailOpen,setDetailOpen] = React.useState(false)

  const handleDetailModal = () =>{
    setDetailOpen((prev)=>!prev)
  }
  const handleLoginModal = () =>{
    setLoginOpen((prev)=>!prev)
  }
  return (
    <div className="App">
      <button onClick={handleLoginModal}>로그인창</button>
      <button onClick={handleDetailModal}>상세창</button>
      {/*로그인창 클릭시*/}
      {LoginOpen ? <Login handler={handleLoginModal}/>:null}
      {/*상세창 클릭시*/}
      {DetailOpen? <DetailModal handler={handleDetailModal}/>:null}
      {/*Dimmed 처리*/}
      {LoginOpen || DetailOpen ? <Dimmed></Dimmed>: null}
    </div>
  );
}

Login.jsx

  • props가 점점 늘어나고 있네요! 흠.. 좋지않은 징조입니다.  태그가 Heavy하면 나중에 좋지 않거든요! 
import React from 'react';
import Modal from './Modal';

export default function Login(props){
  return(
    <Modal title="Login" description="Login" handler={props.handler}>
      <form>
        <label htmlFor='id'>ID:</label>
        <input id="id"></input>
        <br></br>
        <label htmlFor='pw'>PW:</label>
        <input id="pw"></input>
      </form>
    </Modal>
  )
}

Modal.jsx

  • button에 드디어 handler를 전달했습니다. 컴포넌트를 타고타고 들어가는게 너무 번거롭네요 .
import React from 'react';
import '../../css/modal.css';

export default function Modal(props) {
  //console.log(props)
  return (
    <div className="modal">
      <div className="modal_wrapper">
        <div className="title">{props.title}</div>
        <div className="description">{props.description}</div>
          {props.children}
          <button className="button" onClick={props.handler}>취소</button>
      </div>
    </div>
  )
}

잠깐잠깐! 뭔가, 더 번거롭고, 복잡해진 느낌이 들지 않나요 ?

저희는 재사용성을 위해서,Modal을 컴포넌트화 시키고, Login,Detail에 따라서 다른 view를 구현해줬습니다. 그리고, Modal은 어떤 버튼을 클릭한 후, 해당 Modal창을 띄우는게 일반적이라서, 버튼을 기반으로 onClick으로 이벤트를 구성했습니다.

 

하지만, "취소"버튼은 최하위인 Modal Component에서 해줘야했습니다. 그런데, 부모의 상태를 자식이 알려면, props를 drilling 해줘야합니다. 지금은 1~2depth정도 밖에 되지 않습니다만, 웹 페이지는 수많은 기능을 갖고있고, 수많은 wrapper와 component를 갖고있을 수 있습니다. 즉 , 6~7depth까지도 가능할 수 있다는 이야기죠.

왜 Drilling이 생길 수 밖에 없을까 ? 

데이터 흐름을 따라가보면 됩니다.  App.js에서 해당 창을 띄울지 안띄울지 판단 -> 자식 컴포넌트에서 , 부모 컴포넌트 State를 조작해야함 -> State가 false가 되면, 원래 창으로 복귀

재사용성을 위해 컴포넌트를 분리한다는 이야기는 , Component -> Component 와 같이, 부모-자식관계가 끊임없이 생기는 것입니다. 즉 , 추상화를 시키니까 State 관리가 힘들어졌습니다.  그리고, React가 추구하는 단방향성 데이터 흐름이 깨졌습니다. 이래서, State를 관리하기 위한 Redux, Recoil 등의 상태관리가 생기게 된 것입니다! (Redux -> Flux 모델)

 

다음 글에서는 React Component의 상태관리 Redux, useReducer 등을 공부해보겠습니다.