[생각을 곁들인 리액트] 1. 네임스페이스 컴포넌트

seungrodotlog·2024년 2월 2일
post-thumbnail

시리즈를 여는 글(이 조금 깁니다,,,)
2024년 새 해가 밝고, 첫 달도 벌써 끝나가고 있습니다. 요즘 회사에서 저는 개발 환경 개선을 가장 중요한 목표로 2024년의 첫 분기를 꾸려나가고 있습니다. 적은 인원을 가지고 '어떻게 하면 효율적으로 개발할 수 있을까?'에 대한 고민을 많이 하고 있습니다

브랜치 전략이나 개발 프로세스, 명확한 문서화 외에도, 가장 관심을 가지고 있는 부분은 바로 코드 컨벤션리팩토링입니다. 소스코드의 스타일이 통일되어 있고, 유지보수 하기 좋은 구조로 잘 작성되어 있다면, 그 만큼 업무 효율도 올라가기 때문입니다.

비교적 프레임워크에서 강제하는 구조에 의해, '관심사의 분리'나 '모듈화'가 자연스럽게 진행되는 백엔드 개발에 비해, 그 동안 유독 프론트엔드 소스코드를 깔끔하게 유지하는 것에는 어려움을 많이 느꼈던 것 같습니다.

'왜 그럴까?' 생각을 해 보았습니다. 코드 분리에 명확한 근거가 다소 부족했었지 않았나라는 생각이 들었습니다. 사실 지금까지는 관리하는 소스코드의 양이 아주 많지는 않기에 큰 문제는 없지만, 여러 명이 함께 작업하는 소스코드의 스타일을 오직 개인의 '감'에 의존하여 관리한다면 시간이 지날수록 업무 효율이 떨어지게 될 것은 불 보듯 뻔해 보였습니다.

'생각을 곁들인 리액트' 시리즈에서는 이러한 고민들과 공부, 실험을 바탕으로 좀 더 안전하고 효율적인 개발 작업을 위해 현재 적용중인 프론트엔드 코드 리팩토링 아이디어들을 공유하고자 합니다.

네임스페이스?

먼저 네임스페이스라는 단어의 뜻부터 살펴 볼까요? 네임스페이스란 직역하면 '이름 공간'입니다. 쉽게 이야기 하자면 하나의 이름에는 하나의 의미가 부여되도록 유효 범위를 설정하는 공간이라고 생각하면 됩니다.

namespace Mila {
  string dog = "Molly";
}

namespace Daniel {
  string dog = "Bella";
}

using namespace std;

int main() {
  cout << Mila::dog << endl; // Molly
  cout << Daniel::dog << endl; // Bella
    
  return 0;
}

위 예제에서 Mila::dogMila라는 네임스페이스 내에 정의된 dog을 의미하고, Daniel::dogDaniel이라는 네임스페이스 내에 정의된 dog을 의미합니다.

둘 다 같은 이름(dog)이지만, 네임스페이스가 어디냐에 따라 그 값이 달라지게 됩니다. 즉, 네임스페이스가 어디냐에 따라 같은 이름에 다른 의미가 부여되고 있습니다.

이러한 개념을, 프론트엔드 컴포넌트를 개발할 때에도 적용할 수 있습니다.

네임스페이스 컴포넌트의 필요성

const Page = () => {
  return (
    <div>
      <Alert>
        <p>some alert contents...</p>
        <button id="alert-close-btn">Close</button>
      </Alert>
      <Form>
        <SomeFormContents />
        <button id="form-submit-btn">Submit</button>
      </Form>
    </div>
  )
}

위 예시에서, #alert-close-btn#form-submit-btn은 같은 button입니다. 그러나 두 버튼은 하는 역할이 서로 분명하게 달라야 할 것입니다. 만약 버튼의 기능을 미리 Button이라는 이름의 컴포넌트로 구현하고자 한다면 어떻게 해야 할까요?

const Button = (props) => {
  // Submit을 해야 할까 Close를 해야 할까,,,
}

음,,, 아무래도 이대로는 안 될 것 같습니다. Button이라는 하나의 컴포넌트로는 (별도의 prop이나 event 전달 없이 Submit / Close 버튼의 요구사항을 모두 충족시키기 어려울 것 같습니다. 여기서 우리는 이 문제를 해결하기 위한 많은 선택지 중에, 네임스페이스 컴포넌트 패턴을 사용할 수 있습니다.

네임스페이스 컴포넌트의 생성

그렇다면 네임스페이스 컴포넌트는 어떻게 생성할 수 있을까요? 생각보다 별다른 것은 없습니다. 그저 자바스크립트의 한 특성을 활용하면 됩니다.

자바스크립트에서 함수는 객체이다.
Function_Object_Screenshot

자바스크립트에서 함수는 객체의 일종이고, 리액트 컴포넌트(FC)는 함수이기 때문에, 우리는 컴포넌트에 아래와 같이 멤버를 할당할 수 있습니다.

const SomeComponent = () => { /* Some Magics */ }

SomeComponent.someMember = "I'm some member!";

이를 활용하여 한 컴포넌트의 멤버에 또 다른 컴포넌트를 선언해 줄 수도 있을 것입니다.

const Alert = (props) => { /* Some Magics */ }

Alert.Button = (props) => { /* Some Close Logics */ };

const Form = (props) => { /* Some Magics */ }

Form.Button = (props) => { /* Some Submit Logics */ };

그리고 이렇게 생성한 컴포넌트들을 아래와 같이 사용할 수 있을 것입니다.

const Page = () => {
  return (
    <div>
      <Alert>
        <p>some alert contents...</p>
        <Alert.Button id="alert-close-btn">Close</Alert.Button>
      </Alert>
      <Form>
        <SomeFormContents />
        <Form.Button id="form-submit-btn">Submit</Form.Button>
      </Form>
    </div>
  )
}

이렇게 되면, Alert.ButtonForm.Button은 둘 다 이름이 Button이지만, 서로 다른 역할을 하는 완전히 다른 컴포넌트가 됩니다.

왜 네임스페이스 컴포넌트인가

사실, 꼭 네임스페이스 컴포넌트를 사용하지 않더라도, 이런식으로 서로 다른 버튼은 충분히 만들 수 있습니다.

const Alert = (props) => { /* Some Magics */ }

const Form = (props) => { /* Some Magics */ }

const CloseButton = (props) => { /* Some Close Logics */ };

const SubmitButton = (props) => { /* Some Submit Logics */ };

위와 같이 그냥, <어떤>Button 형식으로 컴포넌트 명을 지정하면 됩니다. 다만, 네임스페이스 컴포넌트를 사용하는 이유는 단순히 이것 외의 장점이 또 있기 때문입니다.

컴포넌트 간 포함관계가 명확해진다.

예를 들어 아래 두 소스코드 중, 어떤 것이 AlertButton의 관계를 좀 더 명확하게 표현하고 있는 것 같으신가요?

<Alert>
  <p>some alert contents...</p>
  <SubmitButton id="alert-close-btn">Submit</SubmitButton>
</Alert>
<Alert>
  <p>some alert contents...</p>
  <Alert.Button id="alert-close-btn">Submit</Alert.Button>
</Alert>

Alert라는 컴포넌트에 SubmitButton은 필수적인 존재이고, 그런 의미에서 Alert 컴포넌트는 SubmitButton 컴포넌트를 포함하고 있다고 볼 수 있습니다. 그러나 전자는 AlertSubmitButton 사이에 관계가 잘 나타나지 않아 이러한 사실이 잘 드러나지 않습니다.

반면, 후자의 경우, SubmitButtonAlert.Button이라는 이름으로 사용되면서, AlertAlert.Button 사이의 포함관계가 명확해졌습니다. 물론, 이 버튼이 SubmitButton이라는 것을 강조하고 싶다면, Alert.SubmitButton과 같은 네이밍도 괜찮을 것 같습니다.

응집도가 높아지고 결합도가 낮아진다.

앞서 설명한 내용과도 어느정도 연관이 있는 내용입니다만, 네임스페이스 컴포넌트를 활용하면 응집도가 높아지고 결합도가 낮아지는 효과를 볼 수 있습니다.

앞서 살펴본 예시에서 AlertSubmitButton은 서로 다른 모듈이라고 볼 수 있습니다. 그러나 서로 분명한 상호작용을 하고 있기 때문에, 결합도가 상당히 높은 상태입니다.

그러나, Alert.Button을 생성하여 AlertSubmitButton을 하나의 모듈로 묶는다면 두 컴포넌트가 같은 모듈 내부에서 상호작용을 하기 때문에 응집도가 높아진다고 할 수 있습니다.

물론, 단순히 네임스페이스 컴포넌트 패턴을 적용하는 것 뿐만 아니라 추가적인 작업을 통해 응집도를 더욱 높일수도 있습니다. 이에 대해서는 이어지는 시리즈에서 다루도록 하겠습니다.

실습

이번 시리즈를 진행해나가면서, 각 챕터에서 다룬 내용들을 바탕으로 Modal 컴포넌트를 만들고, 개선해나가는 과정을 공유할까 합니다.

먼저 이번 글에서 다룬 네임스페이스 컴포넌트 패턴을 활용하여 Modal 컴포넌트를 구성해보겠습니다.

저는 개인적으로 컴포넌트를 구성할 때, 컴포넌트를 사용할 때의 형태를 먼저 잡아두고 개발을 진행하는 편입니다.

<Modal>
  <Modal.Header title="I'm Modal!" />
  <Modal.Body>
    I'm Modal Body!
  </Modal.Body>
  <Modal.Footer>
    <Modal.Close css={negativeButtonStyle} onClick={closeModal}>
      Close
    </Modal.Close>
    <Modal.Submit css={positiveButtonStyle} onClick={openModal}>
      OK
    </Modal.Submit>
   </Modal.Footer>
</Modal>

위와 같은 형태라면 모듈의 각 구성요소와 역할이 한 눈에 잘 보일 것 같습니다.

다음으로, 모듈의 각 구성요소들을 작성합니다.

모듈의 기능 구현이나, 네임스페이스 패턴 적용 외에 리팩토링은 본 글의 주제를 벗어나므로, 별도의 설명이나 적용은 생략하겠습니다.

/** Modal */

type ModalProps = ComponentPropsWithCSS<'div'> & {
  visible: boolean;
};

const Modal = forwardRef<HTMLDivElement, ModalProps>(
  ({ visible, css, ...props }, ref) => {
    return (
      <>
        {visible && (
          <div css={backdropStyle}>
            <div css={[css, modalStyle]} ref={ref} {...props} />
          </div>
        )}
      </>
    );
  }
);

/** Modal.Header */

type ModalHeaderProps = ComponentPropsWithCSS<'div'> & {
  title: string;
};

const Modal_Header = forwardRef<HTMLDivElement, ModalHeaderProps>(
  ({ title, children, css, ...props }, ref) => {
    return (
      <div ref={ref} css={[css, modalHeaderStyle]} {...props}>
        <h1>{title}</h1>
        <div>{children}</div>
      </div>
    );
  }
);

/** Modal.Body */

const Modal_Body = forwardRef<HTMLDivElement, ComponentPropsWithCSS<'div'>>(
  ({ css, ...props }, ref) => {
    return <div ref={ref} css={[css, modalBodyStyle]} {...props} />;
  }
);

/** Modal.Footer */

const Modal_Footer = forwardRef<HTMLDivElement, ComponentPropsWithCSS<'div'>>(
  ({ css, ...props }, ref) => {
    return <div ref={ref} css={[css, modalFooterStyle]} {...props} />;
  }
);

/** Modal.Close */

const Modal_Close = forwardRef<
  HTMLButtonElement,
  ComponentPropsWithCSS<'button'>
>((props, ref) => {
  return <button ref={ref} {...props} />;
});

/** Modal.Submit */

const Modal_Submit = forwardRef<
  HTMLButtonElement,
  ComponentPropsWithCSS<'button'>
>((props, ref) => {
  return <button ref={ref} {...props} />;
});

이제 만들어 준 구성요소들을 Modal이라는 네임스페이스로 묶어줄 차례입니다. 위에서 언급한 것과 같이 Modal.* 형식으로 작성해볼까요?

ForwardRef_Error

Modal의 선언부를 보면 아까와 달리 forwardRef라는 함수를 사용하고 있는 것을 알 수 있습니다. forwardRef는 컴포넌트에 ref를 넘겨줄 수 있도록 하기 위해 자주 사용되는 함수인데, 특정한 타입을 가지는 컴포넌트를 리턴합니다. 그리고 그 특정한 타입에는 Header와 같은 속성이 없기 때문에 타입스크립트 오류가 발생합니다.

이를 해결하기 위해서는 Modal이라는 객체와 구성요소들을 속성으로 가지는 객체를 병합해주면 되는데요, Object.assign 메소드를 사용하여 이를 구현할 수 있습니다.

Object.assign(Modal, {
  Header: Modal_Header,
  Body: Modal_Body,
  Footer: Modal_Footer,
  Close: Modal_Close,
  Submit: Modal_Submit,
});

이렇게 네임스페이스로 묶어주는 작업까지 완료되면 아래와 같은 전체 코드가 작성됩니다.

import { css } from '@emotion/react';
import { ComponentPropsWithCSS /** Manually defined on 'emotion.d.ts' */, forwardRef } from 'react';

type ModalProps = ComponentPropsWithCSS<'div'> & {
  visible: boolean;
};

const modalStyle = css`
  display: flex;
  flex-direction: column;

  padding: 0.75rem 1rem;
  border-radius: 0.75rem;

  background-color: white;
`;

const backdropStyle = css`
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;

  display: flex;
  justify-content: center;
  align-items: center;

  background-color: rgba(0, 0, 0, 0.3);
`;

const _Modal = forwardRef<HTMLDivElement, ModalProps>(
  ({ visible, css, ...props }, ref) => {
    return (
      <>
        {visible && (
          <div css={backdropStyle}>
            <div css={[css, modalStyle]} ref={ref} {...props} />
          </div>
        )}
      </>
    );
  }
);

/** Modal.Header */

type ModalHeaderProps = ComponentPropsWithCSS<'div'> & {
  title: string;
};

const modalHeaderStyle = css`
  display: flex;
  justify-content: space-between;
`;

const Modal_Header = forwardRef<HTMLDivElement, ModalHeaderProps>(
  ({ title, children, css, ...props }, ref) => {
    return (
      <div ref={ref} css={[css, modalHeaderStyle]} {...props}>
        <h1>{title}</h1>
        <div>{children}</div>
      </div>
    );
  }
);

/** Modal.Body */

const modalBodyStyle = css`
  flex-grow: 1;

  padding: 0.5rem 0;
  border-width: 1px 0;
  border-style: solid;
  border-color: black;
  margin: 0.5rem 0;
`;

const Modal_Body = forwardRef<HTMLDivElement, ComponentPropsWithCSS<'div'>>(
  ({ css, ...props }, ref) => {
    return <div ref={ref} css={[css, modalBodyStyle]} {...props} />;
  }
);

/** Modal.Footer */

const modalFooterStyle = css`
  display: flex;

  & > * {
    margin-right: 0.5rem;
  }
`;

const Modal_Footer = forwardRef<HTMLDivElement, ComponentPropsWithCSS<'div'>>(
  ({ css, ...props }, ref) => {
    return <div ref={ref} css={[css, modalFooterStyle]} {...props} />;
  }
);

/** Modal.Close */

const Modal_Close = forwardRef<
  HTMLButtonElement,
  ComponentPropsWithCSS<'button'>
>((props, ref) => {
  return <button ref={ref} {...props} />;
});

/** Modal.Submit */

const Modal_Submit = forwardRef<
  HTMLButtonElement,
  ComponentPropsWithCSS<'button'>
>((props, ref) => {
  return <button ref={ref} {...props} />;
});

const Modal = Object.assign(_Modal, {
  Header: Modal_Header,
  Body: Modal_Body,
  Footer: Modal_Footer,
  Close: Modal_Close,
  Submit: Modal_Submit,
});

export default Modal;

여기에서 예시를 직접 내려받아 확인해보실 수 있습니다.

아직 만족스러운 코드는 아니지만, 우선 첫번째 목표였던 네임스페이스 컴포넌트 적용을 완료하였습니다.

마무리

컴포넌트를 효율적으로 개발하고 사용하기 위한 다양한 디자인 패턴들이 존재합니다. 뻔한 말이지만 정답은 정말 없는 것 같습니다. 각자의 팀이 처한 상황, 해결하고자 하는 문제, 선호하는 스타일, 중요하게 생각하는 포인트가 다 다르기 때문입니다.

제가 네임스페이스 컴포넌트를 바탕으로 컴포넌트 디자인 패턴을 설정하고 있는 이유는, 가독성과 사용 편의성을 가장 중점으로 보았고, 이를 바탕으로 판단했을 때 네임스페이스 컴포넌트의 직관성이 제가 원하던 바를 어느정도 충족시켜주었다고 생각했기 때문입니다.

물론, 제가 원하는 결과물을 얻기 위해서는 단순히 네임스페이스 컴포넌트만을 적용시키는 것으로는 부족합니다. 이에 대해서는 앞으로 이어질 시리즈에서 더욱 자세하게 풀어나갈 생각입니다. 혹시 저와 같은 고민을 하시고 계신 분들이 계시다면 함께 이야기 나누어 보면 좋을 것 같습니다!

profile
주니어 승로의 개발승록.

0개의 댓글