5가지 React Architecture Practice

Daun Jung·2022년 4월 25일
0
post-thumbnail

번역글입니다.
출처글 확인

리액트가 UI를 만드는 방법을 혁신해왔다는 것에 대해서는 누구도 부정하지 못한다. 리액트는 배우기 쉽고, 사이트에 일관된 모양과 느낌을 제공하는 재사용 가능한 컴포넌트를 쉽게 만들 수 있다.

그러나 리액트는 애플리케이션의 뷰 게층만을 관리하기 때문에, 특정 아키텍처(MVC나 MVVM같은)를 적용하지는 않는다. 이러한 점은 React 프로젝트가 커지더라도 코드베이스를 체계적으로 유지하지 못하게 하는 원인이 되기도 한다.

앱을 구성하는데 있어 우리는 좋은 퍼포먼스와 적은 용량의 빌드 그리고 굉장히 유연한 코드를 원하기 마련이지만, 큰 프로젝트가 되어 갈수록 좋은 아키텍처의 필요성은 늘 대두된다. 검색을 통해 발견한 이 아키텍처에 대해 살펴봄으로써 현재 내가 만들고 있는 프로젝트들의 아키텍처를 고민해보고 더 나은 방향에 대해 고민해보고자 한다.

1. Directory Layout

원래 스타일링과 컴포넌트에 대한 코드는 분리되어 있었다. 모든 스타일은 공통된 CSS파일(또는 SCSS파일)에 존재하고 있었고, 실제 우리가 사용하고 있는 컴포넌트의 구조는 아래와 같았다.

├── components
│   └── FilterSlider
│       ├──  __tests__
│       │   └── FilterSlider-test.js
│       └── FilterSlider.jsx
└── styles
    └── photo-editor-sdk.scss

여러 리팩토링을 거쳐서,이 접근방식이 잘 확장되지 않는다는 것을 발견했다. 앞으로 SDK나 개발 중인 여러 내부 프로젝트 간에 컴포넌트를 공유해야 하기 때문에 우리는 컴포넌트 중심의 파일 레이아웃으로 방향을 선회하였다.

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── FilterSlider.jsx
        └── FilterSlider.scss

이 방식은 모든 컴포넌트에 종속되는 요소들을 하나의 폴더에 위치시키는 것이었다. 이 방식은 코드를 Npm모듈로 추출하거나 다른 폴더로 간편하게 옮기거나 할 때 가장 최적의 방법이었다. 그럼 이게 정답일까?

컴포넌트 import 제대로 하기

위 디렉토리 구조의 단점 중 하나는 컴포넌트를 가져오려면 다음과 같이 정규화된 경로를 가져와야 한다는 것이다.

import FilterSlider from 'components/FilterSlider/FilterSlider'

하지만, 보통 이런 식으로 쓰는게 깔끔하다.

import FilterSlider from 'components/FilterSlider'

물론, 이걸 해결하기 위해서는 index.js 파일을 만들어서 아래와 같이 적어주면 된다.

export { default } from './FilterSlider';

또 다른 솔루션은 조금 더 광범위하지만 Node.js 표준 해결 메커니즘을 사용하여 견고하면서도 차후를 대비할 수 있다. 이렇게 package.json파일을 만들어서 해결할 수 있다.

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── FilterSlider.jsx
        ├── FilterSlider.scss
        └── package.json

그리고 package.json 안에, main 속성을 사용하여 진입점을 다음과 같이 component에 설정한다.

{
  "main": "FilterSlider.jsx"
}

이렇게 추가해줌으로써 import문은 이렇게 변경이 된다.

import FilterSlider from 'components/FilterSlider'

2. CSS in Javascript

스타일링, 특히 테마는 항상 약간의 문제가 된다. 위에서도 말했듯이, 처음 앱을 작성하다보면 커다란 CSS 파일에 온갖 클래스들이 넘쳐나는 것을 발견하곤 한다. 이름의 중복을 피하기 위해서는 우린 반드시 prefix를 붙이고 BEM conventions
를 따라서 클래스를 정하지만, 앱이 커지면 커질 수록 이런 접근방식이 옳지 못하다는 것을 깨닫게 되며 다른 대체재를 찾게 된다. 대체재의 첫번째로 CSS 모듈 방식은, 성능 문제가 있다. 또한 webpack의 Extract Text Plugin을 통해 CSS를 추출하는 것은 제대로 작동하지 않기도 한다.(작성할 때는 정상이어야 함). 또한 이 접근 방식은 webpack에 대한 의존도가 매우 높았고 테스트를 상당히 어렵게 만들었다.

다음으로, 우리는 몇가지 CSS-in-JS 방식으로 된 근래의 떠오르는솔루션들을 적용해보고자 한다.

  • Styled Components: 가낭 대중적인 선택지이며, 큰 커뮤니티가 있다.
  • EmotionJS: styled components의 라이벌이다.
  • Linaria: 런타임이 제로인 솔루션이다.

다음 라이브러리 중 하나를 선택하는 것은 사용 사례에 따라 크게 달라진다.

  • production을 위해 컴파일된 CSS 파일을 만들기 위해 라이브러리가 필요한다면 EmotionJS와 Linaria가 적합할 것이다. Linaria는 런타임이 필요하지도 않다. CSS 변수를 통해 CSS에 props를 매핑하므로 IE11 지원이 배제됩니다. 하지만 IE11은 이미 역사속으로...
  • 서버에서 실행해야 하는가? 최신 버전의 모든 라이브러리에서는 문제가 되지 않는다.

폴더 구조를 위해 모든 스타일을 styles.js에 모을 것이다.

export const Section = styled.section`
	padding: 4em;
	background: papayawhip;
`;

이러한 방식으로 프런트엔드(front-end)개발자들은 리액트를 다루지 않고도 일부 스타일을 편집할 수 있지만 최소한의 자바스크립트와 CSS 속성에 props을 매핑하는 방법을 배워야 한다.

components
    └── FilterSlider
        ├── __tests__
        │   └── FilterSlider-test.js
        ├── styles.js
        ├── FilterSlider.jsx
        └── index.js

HTML에서 주요 컴포넌트 파일을 이런식으로 정리해내는게 좋습니다.

리액트 컴포넌트의 단일 책임을 위한 노력

고도화된 UI 컴포넌트를 개발하다보면 관심사를 분리하는데 꽤나 애를 먹는 경우가 많다. 몇가지 관점에서 당신의 컴포넌트는 모델의 특정한 도메인 로직을 필요로 하고, 그것은 곧 문제가 된다. 다음 섹션에서는 구성 요소를 잘 분리하는 몇 가지 방법을 보이려고 한다. 소개할 방법들은 기술적으로는 중복되지만, 아키텍처에 적합한 기술을 선택하는 것은 확실한 사실에 근거하기보다는 개인의 스타일에 따라 다르다. 하지만 먼저 사용 사례를 소개해 보겠다.

  • 로그인한 사용자의 상황을 인식하는 구성 요소를 처리하기 위한 메커니즘을 도입하기.
  • 접기가 가능한(collapsible) <tbody> 태그 여러 개를 렌더하는 테이블 만들기.
  • state에 따라서 다르게 render 되는 컴포넌트를 만들기.

이번 섹션에서, 나는 위에서 말한 바와 같이 문제마다 다른 해답을 제시할 것이다.

3. Custom Hooks

당신은 사용자가 로그인한 경우에만 리액트 컴포넌트가 표시되는지 확인해야할 때가 있다. 처음에는 여러 번 렌더링하는 동안 몇 가지 로그인 상태에 대한 검사를 수행한다. 그 코드를 지우는 임무를 수행하면서 조만간 커스텀 훅을 작성해야 할 것이다. 겁은 먹지 말자. 그렇게 어렵지 않으니 예제를 살펴보자.

import { useEffect } from 'react';
import { useAuth } from './use-auth-from-context-or-state-management.js';
import { useHistory } from 'react-router-dom';

function useRequireAuth(redirectUrl = "/signup") {
  const auth = useAuth();
  const history = useHistory();

  // 만약에 auth.user 가 false 이면
  // 로그인 상태가 아니기 때문에 반드시 리다이렉트를 진행한다.
  useEffect(() => {
    if (auth.user === false) {
      history.push(redirectUrl);
    }
  }, [auth, history]);
  return auth;
}

useRequireAuth 훅은 사용자가 로그인했는지 확인하고 그렇지 않으면 다른 페이지로 리디렉션한다. useAuth hook의 로직은 Context, MobX, Redux와 같은 상태 관리 시스템을 통해 제공될 수 있다.

4. Function as Children

축소 가능한 테이블 행을 만드는 것은 쉬운 작업이 아니다. 접기 버튼은 어떻게 렌더링할까? 테이블이 펼쳐지지 않았을 때 children을 어떻게 표현할까? JSX 2.0에서는 단일 태그 대신 배열을 반환할 수 있기 때문에 훨씬 쉬워진 것을 알고 있지만, 다음 예제에서는 자식 패턴으로 함수를 사용할 수 있는 좋은 사용 사례를 설명해보려 한다.
아래의 Table컴포넌트를 살펴보자.

export default function Table({ children }) {
  return (
    <table>
      <thead>
        <tr>
          <th>Just a table</th>
        </tr>
      </thead>
      {children}
    </table>
  );
}

그리고 접을 수 있는(collapsible) TableBody를 살펴보자.

import { useState } from 'react';

export default function CollapsibleTableBody({ children }) {
  const [collapsed, setCollapsed] = useState(false);

  const toggleCollapse = () => {
    setCollapsed(!collapsed);
  };

  return (
    <tbody>
      {children(collapsed, toggleCollapse)}
    </tbody>
  );
}

아마 당신은 이것을 이용해 이렇게 표현할 것이다.

<Table>
  <CollapsibleTableBody>
    {(collapsed, toggleCollapse) => {
      if (collapsed) {
        return (
          <tr>
            <td>
              <button onClick={toggleCollapse}>Open</button>
            </td>
          </tr>
        );
      } else {
        return (
          <tr>
            <td>
              <button onClick={toggleCollapse}>Closed</button>
            </td>
            <td>CollapsedContent</td>
          </tr>
        );
      }
    }}
  </CollapsibleTableBody>
</Table>

함수를 하위 항목으로 전달하면 상위 구성 요소로 호출된다.이 기술을 "render callback" 또는 "render props"이라고도 한다.

5. Render Props

Render Props라는 용어는 마이클 잭슨(가수 아님)이 고차함수(HOC) 패턴을 "Render Props"와 함께 일반 컴포넌트로 100% 대체할 수 있다고 제안하면서 만들어졌다. 여기서 기본 아이디어는 모든 리액트 구성요소가 함수이며 컴포넌트로 전달될 수 있다는 것이다. 그렇다면 왜 컴포넌트를 통해 React 구성 요소를 전달하지 않는가?! 단순하다!

다음 코드는 API에서 데이터를 가져오는 방법을 일반화한 것이다. (단순 예제일 뿐이다. 실제 프로젝트에서는 이 가져오기 로직을 useFetch hook으로 추상화하여 UI에서 훨씬 더 멀리 분리할 수 있다) 예제를 살펴보자.

import { useEffect, useState } from "react";

export default function Fetch({ render, url }) {

  const [state, setState] = useState({
    data: {},
    isLoading: false
  });

  useEffect(() => {
    setState({ data: {}, isLoading: true });

    const _fetch = async () => {
      const res = await fetch(url);
      const json = await res.json();

      setState({
        data: json,
        isLoading: false,
      });
    }

    _fetch();
  }, https%3A%2F%2Feditor.sitepoint.com);

  return render(state);
}

보시다시피 렌더링 프로세스 중에 호출되는 함수인 렌더라는 속성이 있다. 내부에서 호출된 함수는 매개 변수로 완전한 상태를 얻고 JSX를 반환한다. 이제 다음 사용법을 살펴보자.

<Fetch
  url="https://api.github.com/users/imgly/repos"
  render={({ data, isLoading }) => (
    <div>
      <h2>img.ly repos</h2>
      {isLoading && <h2>Loading...</h2>}

      <ul>
        {data.length > 0 && data.map(repo => (
          <li key={repo.id}>
            {repo.full_name}
          </li>
        ))}
      </ul>
    </div>
  )} />

보다시비 dataisLoading 변수는 state 객체에서 구조화되지 않으며 JSX의 응답을 처리하는 데 사용할 수 있다. 이 경우 Prmoise가 resolve 되지 않는 한 "Loading" 헤드라인이 표시된다. render props에 전달되는 state 부분과 사용자 인터페이스에서 해당 요소를 사용하는 방법은 이제 당신에게 달렸다. 이런 식의 패턴은 전반적으로 동일한 UI 동작을 경험하게 하는 매우 강력한 메커니즘이다. 위에서 설명한 children 패턴과 같은 기능은 기본적으로 속성이 자식인 패턴과 동일하다.

요점: render props 패턴은 Children 패턴으로 함수를 일반화한 것이므로 한 구성 요소에 여러 개의 렌더 프롭이 있는 것을 가능케 한다. 예를 들어, Table 구성요소는 헤더에 대한 render props를 가져온 다음 본문에 대한 다른 render prop 을 가져올 수 있습니다.

더 얘기해보면 좋을 것들

리액트 아키텍처 패턴에 대한 이 게시물이 즐거우셨기를 바란다. 이 글에서 누락한 사항이 있거나(더 많은 모범 사례는 언제든 있기에) 다양한 의견을 덧대어 많은 패턴을 공유해주시면 좋겠습니다.

번역 후기

리액트로 프론트엔드 개발을 이제 약 3년 가까이 해오다보니, 특정한 패턴이나 작업방식으로 고착화 되어있는 내 자신을 발견하게 되어 아키텍처와 관련된 글을 번역해보며 나름 새로운 인사이트를 얻고자 번역을 시작하게 됐다.

특별한 인사이트를 주었다기보다는 부족한 영어실력을 느껴가며 번역기의 도움을 받을 수 밖에 없었다. 그저 눈으로 영문의 글들을 읽는것보다도 그것을 누군가가 이해할 수 있을만한 문장으로 변환하는 작업이 얼마나 어려운일인지를 깨닫는 귀한 시간이었던듯하다.

사실 번역글 자체를 작성하기 시작했던 시점이 꽤나 되었는데, 항상 임시글로만 남아있던 것이 마음에 걸려서 뒤늦게나마 마무리를 지어야하겠다는 생각이 들어서 끝맺음을 지어보았다.

다시 읽기 무서울정도로 부끄러운 글이 될 수도 있을테지만, 누가 알겠는가.. 이 글을 보며 내가 그래도 많이 성장했구나 하고 자위할 수 있는 글이 될런지.. :)

그래도 나름 이 글을 번역하며 받은 인사이트를 한가지 꼽자면
마지막 Render Props와 관련된 부분에서 해당 방식이 react-query의 시작이 아니었을까 하는 생각이 들었다. api 통신을 바탕으로 일관된 UI 처리 작업을 하기 위해서 그 착안점이 되는 패턴이 아니었을까..

프로그래밍을 하면 할수록 배워야하는게 끝도 없다는 생각이 늘 들지만, 이렇게 고민을 적고 나눠주는 사람들이 있기에 공부를 해나가는 여정이 외롭지 않다는 생각이 든다.

다음 번역은 좀 더 나은 번역을.. 그리고 좀 더 좋은 컨텐츠를 정리해보기를 고대하면 번역 글 여기서 끝!

profile
개발자 Daun입니다! React를 이용한 Front-end 개발을 주로하고 있습니다 :)

0개의 댓글