https://dmitripavlutin.com/orthogonal-react-components/
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!
실제로 필요할 때 구현하고 미리 구현하지 마세요.
요점정리
'FrontEnd' 카테고리의 다른 글
타입스크립트의 타입 추론과 힌들리 밀러 타입 시스템 (2) | 2022.05.26 |
---|---|
Recoil로 Excel(SpreadSheet) 만들기 (1) | 2022.05.22 |
Recoil, Redux. 상태관리 라이브러리의 Selector 개념 (0) | 2022.05.20 |
resizable table(row, col) with React (0) | 2022.05.19 |
Recoil 실전 패턴 : 상태의 계층화와 분리 (0) | 2022.05.18 |