시스템 설계가 중요한 이유
우리가 프로젝트를 진행할 때 초기에는 기능을 구현하는 것이 수월하게 진행된다.
하지만 6개월이 지나고, 초기 계획 했던 기능 외에도 추가적인 많은 기능을 구현해야 합니다.
이때, 기존 프로젝트에 새로운 기능 또는 변경할 기능을 적용하는 것이 점점 더 어려워집니다.
그리고 어느 시점에서는 구현에 시간이 너무 오래 걸려서 일부 새로운 기능과 변경 사항은 거부하기 시작했다.
이처럼 최초에 설계를 잘해서 구성 요소를 쉽게 변경할 수 있도록 하지 않으면 점차 유지보수에는
어려움이 따라오게 되어있다.
독립 구성 요소
A 와 B가 서로 독립 ( Orthogonal )이면 A를 변경해도 B는 변경되지 않는다.
이는 반대의 경우에도 마찬가지로 이것은 독립성의 개념이다.
무선 장치에서 볼륨과 방송국 선택 컨트롤은 서로 독립적이다.
볼륨 컨트롤은 사운드만 변경하고, 방송국 선택 컨트롤은 방송국만 변경한다.
만약에 무선장치의 볼륨 컨트롤이 볼륨 뿐만 아니라 방송국도 같이 변경한다고 생각해보세요.
그리고 이것을 우리는 튜닝해야한다면 쉬울까요?
이처럼 밀접하게 결합된 구성요소 ( 고장난 볼륨 컨트롤 ) 에 변경 사항을 추가하는 것은
매우 어려운 작업입니다.
손 쉽게 변경 사항을 적용하려면 구성 요소들이 독립적이어야 합니다.
이 원칙은 React 애플리케이션 디자인 원칙에도 적용된다.
▶ UI 요소
▶ 비동기 데이터 조회 ( REST API or GraphQL, 라이브러리 가져오기 등... )
▶ 글로벌 상태 관리
▶ 영속성 로직 ( local storage, cookies )
컴포넌트가 하나의 작업을 담당하고, 격리되고, 자체 포함적(자기 설명적)이며 캡슐화되도록 한다.
이렇게 작업한다면 컴포넌트는 독립적이고 변경 사항은 하나의 컴포넌트에만 집중된다.
비동기 로직 분리
직원 목록을 가져와야 한다고 가정했을 때, EmployeerPage 컴포넌트가 있다.
import axios from "axios";
import { useEffect } from "react";
import { useState } from "react";
const EmployeesPage = () => {
const [isFetching, setFetching] = useState(false);
const [employees, setEmployees] = useState([]);
useEffect(() => {
(async function () {
setFetching(true);
const response = await axios.get("/employees");
setEmployees(response);
setFetching(false);
})();
}, []);
if (isFetching) {
return <div>직원 목록을 조회중입니다.</div>;
}
return <EmployeesList employees={employees} />;
};
현재 컴포넌트의 문제점이 뭘까?
바로 데이터를 가져오는 방법에 따라 다르다는 것이다. 당장은 axios 라이브러리에 대해서 알고 GET 요청이
수행되는 것을 알고 있다.
하지만 나중에 axios의 REST API에서 GraphQL로 전환되면 어떤게 될까?
axios 로직과 결합된 수십 개의 컴포넌트가 있는 경우, 모든 컴포넌트를 수동으로 변경해야 한다.
이러한 방법을 대신해서 더 좋은 방법이 있다.
React의 Suspense 기능을 사용해서 비동기 로직을 분리하는 것이다.
import axios from "axios";
export const employeesResource = () => {
return {
employees: wrapPromise(axios.get("/employees").then(({ data }) => data)),
};
};
const wrapPromise = (promise) => {
let status = "padding";
let result;
let suspender = promise.then(
(response) => {
status = "success";
result = response;
},
(error) => {
status = "error";
result = error;
}
);
return {
read() {
if (status === "pending") {
throw suspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
},
};
};
먼저 비동기 로직을 분리했다.
employeersResource 에서 employeers에 대한 axios 요청을 하고,
wrapPromise를 통해서 비동기의 상태에 따라 read() 함수의 결과를 나눠주었다.
import { Suspense } from "react";
import { employeesResource } from "./employeesResource";
const EmployeesPage = () => {
return (
<Suspense fallback={<div>직원 목록을 조회중입니다.</div>}>
<EmployeesFetch resource={employeesResource} />
</Suspense>
);
};
const EmployeesFetch = ({ resource }) => {
const employees = resource.employees.read();
return <EmployeesList employees={employees} />;
};
EmployeesFetch 컴포넌트가 resource를 비동기로 읽을 때까지 Suspense의 fallback이 대신 나타나며,
EmployeesPage 컴포넌트는 일시 중지된다.
여기서 중요한 것은 EmployeesPage와 Axios 로직이 독립적인 것이다.
Axios를 Fetch 혹은 GrahpQL로 변경하더라도 EmployeesPage 컴포넌트는 신경을 쓰지 않는다.
스크롤 리스너와 뷰 로직 분리
사용자가 500px 이상 아래로 스크롤할 때 나타나는 맨 위로 이동 버튼이 있다고 하자
이 버튼을 클릭한다면 페이지가 자동으로 상단으로 스크롤된다.
간단한 구현으로 ScrollToTop 컴포넌트를 만들 수 있다.
import { useEffect } from "react";
import { useState } from "react";
const DISTANCE = 500;
const ScrollToTop = () => {
const [crossed, setCrossed] = useState(false);
useEffect(() => {
const handler = () => setCrossed(window.scrollY > DISTANCE);
handler();
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
});
const handleClickButton = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
if (!crossed) {
return null;
}
return <button onClick={handleClickButton}>Jump to Top</button>;
};
여기서도 스크롤이 나타나는 조건 등에 대한 변경이 UI에 포함되어있다.
더 나은 독립적인 디자인은 UI에서 스크롤 리스너를 분리하는 것이다.
스크롤 리스너 로직을 Custom Hook인 useScrollDistance로 추출해보겠습니다.
import { useEffect, useState } from "react";
const useScrollDistance = (distance) => {
const [crossed, setCrossed] = useState(false);
useEffect(() => {
const handler = () => setCrossed(window.scrollY > distance);
handler();
window.addEventListener("scroll", handler);
return window.removeEventListener("scroll", handler);
});
};
useScrollDistance는 기본적인 로직은 동일하고 distance를 매개변수로 받아와서 사용한다.
const IfScrollCrossed = ({ children, distance }) => {
const isBottom = useScrollDistance(distance);
return isBottom ? children : null;
};
IfScrollCrossed 컴포넌트는 사용자가 지정한 distance 만큼 스크롤한 경우에 children을 표시한다.
const DISTANCE = 500;
function App() {
return (
<IfScrollCrossed distance={DISTANCE}>
<ScrollToTop />
</IfScrollCrossed>
);
}
const ScrollToTop = () => {
const handleClickButton = () => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
};
return <button onClick={handleClickButton}>Jump to Top</button>;
};
마지막으로 ScrollToTop 컴포넌트를 수정하면 다음과 같다.
여기서 핵심은 IfScrollCrossed 컴포넌트가 스크롤 리스너 변경 사항을 격리하고 있다는 것이다.
또한 UI 요소 변경 사항은 ScrollToTop 컴포넌트에서 격리되고 있다.
즉, 스크롤 리스너 로직과 UI 로직은 서로 격리되고 있다.
추가적인 이점으로 IfScrollScorred 컴포넌트는 다른 UI와도 결합하여 사용자가 특정 거리 아래로 스크롤 했을 때
UI를 표시할 수 있다.
독립적인 설계의 이점
독립적인 디자인은 많은 이점을 제공한다.
쉽게 변경
구성 요소가 독립적이기 때문에 변경 사항이 생기면 해당 구성 요소 내에서 처리가 가능하다.
가독성
구성 요소는 하나의 책임을 가지고 있으므로 해당 구성 요소가 수행하는 작업을 훨씬 쉽게 이해할 수 있다.
테스트 편의성
구성 요소는 단일 작업 구현에 집중하기 때문에 테스트의 단위를 구성하기에도 편하다.
주의할 점
독립적인 디자인은 YAGNI 원칙에 의해 진행됩니다.
즉, 실제로 필요할 때 구현해야하며, 필요하다고 예상되는 경우에는 구현하지 말아야 한다.
모든 부분을 독립적으로 만들려면 필요하지 않는 추상화의 영역까지 생성하게 된다.
정리
독립적인 디자인 설계는 유지보수와 수정을 쉽게 할 수 있도록 도와준다.
독립적인 디자인 설계를 위해서 React의 많은 기능을 사용하였다.
▶ Suspense : 비동기 데이터를 컴포넌트와 독립적이게 만들어준다.
▶ Custom React Hook : UI 렌더링과 논리적인 상태를 독립적이게 만들어준다.
분명 독립적인 디자인은 개발에 도움을 주는 것은 확실하다.
하지만 모든 것을 전부 독립적으로 만들려면 필요하지 않는 추상화의 영역까지 생성하게 된다.
모든 디자인 패턴이 그러하듯 본인의 기준을 만드는 것이 중요하다고 생각한다.
그리고 나는 아직 나의 기준을 만들지 못했다.
출처
https://dmitripavlutin.com/orthogonal-react-components/
'React > 실험실' 카테고리의 다른 글
[React] Context Module Function 패턴 (0) | 2023.02.02 |
---|---|
[React] Context API 언제 사용해야할까? (0) | 2023.01.31 |
[React] Suspense (0) | 2023.01.28 |
[React] Derived State (0) | 2023.01.25 |
[React] 컴포넌트를 Dry 하게 작성하기 (0) | 2023.01.22 |