본문 바로가기

FrontEnd

React 디자인 패턴 : 관심사의 분리 (Seperation Of Concern)

반응형

https://dmitripavlutin.com/orthogonal-react-components/

 

The Benefits of Orthogonal React Components

How to design React components that are easy to change, maintain, and test? Strive to orthogonal React components.

dmitripavlutin.com

Orthogonal components

A와 B가 직교하는 경우 A를 변경해도 B는 변경되지 않으며 그 반대의 경우도 마찬가지입니다.

그것이 직교성(통계적 무연관)의 개념입니다.

 

아래 그림에서 FM/AM 라디오 방송국 선택과 볼륨 조절은 서로 영향을 주면 안됩니다.

 

볼륨과 스테이션 선택은 서로 관련이 없다.

한 컴포넌트의 변경이 다른 컴포넌트에 영향을 미치지 않으면 두 컴포넌트는 직교합니다.

예를 들어 직원 목록을 표시하는 로직은 직원을 가져오는 로직과 직교해야 합니다.

좋은 React 애플리케이션 디자인은 다음과 같이 관심사가 직교합니다.

  • UI 요소(프레젠테이션 컴포넌트)
    • 레이아웃 (stateless)
    • 데이터 페치 컴포넌트 (stateful)
    • 데이터 렌더링 컴포넌트 (stateless)
    • 서스펜스 / 에러바운더리 (stateful)
  • 데이터 가져오기(fetch 라이브러리, REST 또는 GraphQL)
  • 글로벌 상태 관리 (Redux)
  • 영속성 로직 (로컬 저장소, 쿠키).

컴포넌트가 하나의 작업을 구현하고, 격리되고, 자체 포함적(자기설명적)이며 캡슐화되도록 합니다.

이렇게 하면 컴포넌트가 직교하게 되고 변경 사항은 격리되어 하나의 컴포넌트에만 집중됩니다.

이것이 예측 가능하고 개발하기 쉬운 시스템을 위한 방법입니다.


Making the component orthogonal to fetch details (fetch 로직 분리)

직원 목록을 가져와야 한다고 가정해 보겠습니다.

https://codesandbox.io/s/non-orthogonal-fetch-s6f9e

import React, { useState } from 'react';
import axios from 'axios';
import EmployeesList from './EmployeesList';
function EmployeesPage() {
  const [isFetching, setFetching] = useState(false);
  const [employees, setEmployees] = useState([]);
  useEffect(function fetch() {
    (async function() {
      setFetching(true);
      const response = await axios.get("/employees");
      setEmployees(response.data);
      setFetching(false);
    })();
  }, []);

  if (isFetching) {
    return <div>Fetching employees....</div>;
  }
  return <EmployeesList employees={employees} />;
}

현재 구현의 문제는 <EmployeesPage/>가 데이터를 가져오는 방법에 의존한다는 것입니다.

컴포넌트는 axios 라이브러리에 대해 알고 GET 요청이 수행되었음을 알고 있습니다.

나중에 axios와 REST에서 GraphQL로 전환하면 어떻게 될까요?

fetch 로직과 결합된 수십 개의 컴포넌트가 있는 경우, 모든 컴포넌트를 수동으로 변경해야 합니다.

 

더 나은 접근 방식이 있습니다. 컴포넌트에서 fetch 로직 세부 정보를 분리해 보겠습니다.

이를 수행하는 좋은 방법은 React의 새로운 Suspense 기능을 사용하는 것입니다.

https://codesandbox.io/s/orthogonal-fetch-crko3

import React, { Suspense } from "react";
import EmployeesList from "./EmployeesList";

function EmployeesPage({ resource }) {
  return (
    <Suspense fallback={<h1>Fetching employees....</h1>}>
      <EmployeesFetch resource={resource} />
    </Suspense>
  );
}

// fetch 로직을 포함한 컴포넌트
function EmployeesFetch({ resource }) {
  const employees = resource.employees.read();
  return <EmployeesList employees={employees} />;
}

<EmployeesFetch/>가 리소스를 비동기적으로 읽을 때까지 <EmployeesPage/>가 이제 일시 중단(suspense)됩니다.

중요한 것은 <EmployeesPage/>가 fetch 로직와 직교한다는 것입니다.

<EmployeesPage/>는 axios가 fetch를 구현하는 것을 신경 쓰지 않습니다.

axios를 기본 fetch 쉽게 변경하거나 GraphQL로 이동할 수 있습니다. <EmployeesPage/>는 영향을 받지 않습니다.


Making the view orthogonal to scroll listener (스크롤 리스너와 뷰 로직 분리)

사용자가 500px 이상 아래로 스크롤할 때 표시되는 맨 위로 이동 버튼을 원한다고 가정해 보겠습니다.

버튼을 클릭하면 페이지가 자동으로 맨 위로 스크롤됩니다.

<ScrollToTop/>의 첫 번째 나이브한 구현은 다음과 같습니다.

https://codesandbox.io/s/non-orthogonal-scroll-detect-si3hf

import React, { useState, useEffect } from 'react';
const DISTANCE = 500;
function ScrollToTop() {
  const [crossed, setCrossed] = useState(false);
  useEffect(
    function() {
      const handler = () => setCrossed(window.scrollY > DISTANCE);
      handler();
      window.addEventListener("scroll", handler);
      return () => window.removeEventListener("scroll", handler);
    },
    []
  );
  function onClick() {
    window.scrollTo({
      top: 0,
      behavior: "smooth"
    });
  }
  if (!crossed) {
    return null;
  }
  return <button onClick={onClick}>Jump to top</button>;
}

<ScrollToTop/>은 스크롤 리스너를 구현하고 페이지를 맨 위로 스크롤하는 버튼을 렌더링합니다.

문제는 이러한 개념이 다른 속도로 변경될 수 있다는 것입니다.

 

더 나은 직교 디자인은 UI에서 스크롤 리스너를 분리하는 것입니다.

스크롤 리스너 로직을 사용자 정의 hook useScrollDistance로 추출해 보겠습니다.

import { useState, useEffect } from 'react';
function useScrollDistance(distance) {
  const [crossed, setCrossed] = useState(false);
  useEffect(function() {
    const handler = () => setCrossed(window.scrollY > distance);
    handler();
    window.addEventListener("scroll", handler);
    return () => window.removeEventListener("scroll", handler);
  }, [distance]);
  return crossed;
}

그런 다음 컴포넌트 내부에서 useScrollAtBottom()을 사용합니다.

function IfScrollCrossed({ children, distance }) {
  const isBottom = useScrollDistance(distance);
  return isBottom ? children : null;
}

<IfScrollCrossed>는 사용자가 특정 거리를 스크롤한 경우에만 children을 표시합니다.

마지막으로 클릭하면 맨 위로 스크롤하는 버튼이 있습니다.

function onClick() {
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  });
}
function JumpToTop() {
  return <button onClick={onClick}>Jump to top</button>;
}

이제 <JumpToTop/><IfAtBottom/>children으로 전달하면 됩니다.

https://codesandbox.io/s/orthogonal-scroll-detect-wko57

중요한 것은 <IfScrollCrossed/>가 스크롤 리스너의 변경 사항을 격리한다는 것입니다.
UI 요소 변경 사항은 <JumpToTop/> 컴포넌트에서 격리됩니다.

스크롤 리스너 로직과 UI 요소는 직교합니다.

추가적인 이점은 <IfScrollCrossed/>를 다른 UI와 결합할 수 있다는 것입니다.
예를 들어 사용자가 300px 아래로 스크롤했을 때 뉴스레터 양식을 표시할 수 있습니다.

import React from 'react';
// ...
const DISTANCE_NEWSLETTER = 300;
function OtherComponent() {
  // ...
  return (
    <IfScrollCrossed distance={DISTANCE_NEWSLETTER}>
      <SubscribeToNewsletterForm />
    </IfScrollCrossed>
  );
}


The "Main" component

변경 사항을 별도의 컴포넌트로 분리하는 것이 직교성의 핵심이지만 다른 이유로 변경될 수 있는 컴포넌트가 있을 수 있습니다.
이들은 소위 "메인"(일명 "앱") 컴포넌트입니다.

index.jsx 파일 내에서 "Main" 컴포넌트를 찾을 수 있습니다: 응용 프로그램을 시작하는 컴포넌트 입니다.
응용 프로그램에 대한 모든 "더러운" 세부 정보를 알고 있습니다.
전역 상태 공급자(예: Redux)를 초기화하고, 가져오기 라이브러리(예: GraphQL Apollo)를 구성하고, 경로를 구성 요소와 연결하는 등의 작업을 수행합니다.

클라이언트 측(브라우저 내에서 실행) 및 서버 측(서버 측 렌더링 구현)에 대해 여러 "Main" 컴포넌트가 있을 수 있습니다.


The benefits of orthogonal design

직교 설계는 많은 이점을 제공합니다.

Easy to change

컴포넌트 직교하도록으로 설계되면 컴포넌트에 대한 모든 변경 사항이 컴포넌트 내에 격리됩니다.

Readability

직교 컴포넌트에는 하나의 책임이 있기 때문에 컴포넌트가 하는 일을 훨씬 더 쉽게 이해할 수 있습니다.
여기에 속하지 않는 세부 사항으로 어수선하지 않습니다.

Testability

직교 컴포넌트는 단일 책임 구현에만 집중합니다.
당신이 해야 할 일은 컴포넌트가 책임을 올바르게 수행하는지 테스트하는 것입니다.

Think in principles

hooks, 서스펜스 등과 같은 새로운 React 기능이 마음에 듭니다. 하지만 이러한 기능이 좋은 디자인을 따르는 데 도움이 되는지 탐구해 봅시다.

  • 왜 React hooks입니까? UI 렌더링 논리를 상태 및 부작용 논리와 직교하게 만듭니다.
  • 왜 서스펜스인가? fetch 관련 세부 정보와 컴포넌트를 직교하게 만듭니다.

영화 "스타워즈 시스의 복수"의 한 장면을 떠올려보봅시다.
Anakin Skywalker가 그의 전 멘토인 Obi-Wan Kenobi에게 패배한 후 Obi-Wan Kenob는 다음과 같이 말합니다.

Bring balance to the Force, not leave it in darkness!

 
직교 설계는 "You aren't gonna need it"(YAGNI) 원칙에 따라 균형을 이룹니다.
YAGNI는 익스트림 프로그래밍의 원칙입니다.
실제로 필요할 때 구현하고 미리 구현하지 마세요.
 
직교성과 YAGNI 모두의 극단을 피하십시오.
내 실수는 의도치 않게 변경을 위해 설계되지 않은 컴포넌트를 만든 것입니다.
그것은 YAGNI의 극단입니다.
 
반면에 논리의 모든 부분을 직교하도록 만들면 결국 필요하지 않은 추상화를 생성하게 됩니다.
그것은 직교 설계의 극딘입니다.
 
실질적인 접근은 변화를 예측하는 것입니다.
응용 프로그램이 해결하는 도메인 문제를 자세히 연구하고 클라이언트에게 잠재적 기능 목록을 요청하십시오.
어떤 곳이 바뀔 것이라 생각한다면 직교 원리를 적용하십시오.

요점정리

소프트웨어 작성은 응용 프로그램의 요구 사항을 구현하는 것만이 아닙니다.
컴포넌트를 잘 설계하기 위해 노력하는 것도 중요합니다.
 
좋은 디자인의 핵심 원칙은 가장 변경될 가능성이 높은 논리를 분리하는 것입니다.
즉, 로직이 직교하도록 만드는 것입니다.
그러면, 전체 시스템이 변경 사항이나 새로운 기능 요구 사항에 유연하고 적응할 수 있습니다.
직교 원칙을 간과하면 밀접하게 결합되고 종속적인 컴포넌트를 생성할 위험이 있습니다.
한 곳의 약간의 변경이 예기치 않게 다른 곳에서 반향을 일으켜 변경, 유지 관리 및 새로운 기능 생성 비용이 증가할 수 있습니다.
 
더 알고 싶으십니까? 그런 다음 다음 단계는 Pragmatic-Programmer-journey-mastery를 읽는 것입니다.

 

반응형