키워드 :
자바스크립트(javascript), 타입스크립트(typescript), 리팩토링(refactoring), 추상화(abstraction), 관심사의분리(seperation of concern)
원문 : https://refactor-like-a-superhero.vercel.app/en/chapters/08-abstraction
추상화
특정 데이터에 대해 로직을 적용하는 코드의 중복을 줄이는 방법은, 액션을 따로 파라미터화 하는 것이다.
이처럼 같은것을 묶고, 변하는 부분과 변하지 않는 부분을 잘 발라내어 코드의 중복을 줄이는 방법을 추상화라 한다.
// The generalized `applyDiscount` function will take
// the absolute discount value:
function applyDiscount(order, discount) {
return {...order, discount}
}
// Differences in the calculation (percentages, “discount of the day,” etc.)
// are collected as a separate set of functions:
const discountOptions = {
percent: (order, percent) => order.total * percent / 100
daily: daysDiscount()
}
// As a result, we get a generalized action for applying a discount
// and a dictionary with discounts of different kinds.
// Then, the application of any discount will now become uniform:
const a = applyDiscount(order, discountOptions.daily)
const b = applyDiscount(order, discountOptions.percent(order, 40))
추상화가 왜 필요한가?
코드는 짧을수록 이해하기 쉽다.
코드가 비즈니스 프로세스에 가까울 수록 이해하기 쉽다.
추상화는 무관한 것을 제거하고 본질적인 것을 증폭시키는 행위입니다.
의도와 구현
양식을 제출하는 함수를 handleForm이라 명명하고,
해당 함수 내에 로직을 캡슐화 할 수 있다.
이와 같은 방법은 디테일을 숨기고 함수의 핵심적인 목적을 나타낸다.
이는 1depth, 2depth로 로직을 추상화하는 것이라 볼 수 있다.
더 높은 depth는 의도를 나타내며, 더 낮은 depth는 해당 의도의 구현이라 볼 수 있다.
- 의도는 우리가 할 일을 설명한다.
- 구현은 일을 하는 방법을 설명한다.
async function handleOrderSubmit(event) {
const formData = serializeForm(event.target);
const validData = validateFormData(formData);
const order = createOrder(validData);
await sendOrder(order);
}
함수 명은 의도를 나타내며, 본문은 구현을 나타낸 다 볼 수 있다.
또한 해당 함수와 다른 함수를 사용할 때, 우리는 함수의 의도에 관심을 두지 구현에 관심을 두지 않는다.
// Name and signature reflect the intention...
function isChild(user) {
// ...And function body reflects the implementation.
return user.age < 18;
}
// When we use the function with others,
// we care about its purpose and goals,
// not about its implementation details:
if (isChild(user)) toggleParentControl();
우리의 뇌는 제한적인 정보만 처리할 수 있다.
추싱화는 중요한 정보에 집중할 수 있도록 한다.
다른 디테일에서 핵심을 분리해준다.
이메일의 유효성을 검사하는 함수가 있다.
function subscribeToFeed(email) {
if (!email.includes("@") || !email.includes(".")) return false;
const recipients = addRecipient(email);
confirmFeedSubscription(recipients);
}
다른 내부 호출 함수들에 비해 validation 로직이 너무 원시적임을 알 수 있다.
다른 함수들은 feed, subsciption 등의 자연어로 이야기 할 수 있다.
유효성 검사는 @, .를 포함해야 이메일임을 알려준다.
subscribeToFeed 함수가 하는 역할이 무엇일까?
- 이메일이 유효하지 않으면 구독하지 않는다
- 구독자의 메일을 리스트에 추가한다.
- 구독자의 구독을 승인한다.
이 세 가지가 구체적으로 어뗗게 수행되는지는 관심없으며, 단지 순서에 맞게 함수를 호출할 뿐이다.
따라서 1depth가 2depth가 하는 일을 모르게 하자.
이제 isValidEmail은 검증 작업의 집합이 된다.
검증 작업은 해당 함수 내에만 위치하면 된다.
function isValidEmail(email) {
return email.includes("@") && email.includes(".");
}
function subscribeToFeed(email) {
if (!isValidEmail(email)) return false;
const recipients = addRecipient(email);
confirmFeedSubscription(recipients);
}
또한 isValidEmail를 호출할 경우 주변 함수는, 이제 이메일이 유효하지 않으면 어떻게 동작해야 하는지에 집중하게 된다.
이처럼 추상화는 복잡한 개념과 프로세스를 점진적으로 "파악"하는 데 도움이 된다.
- 시스템에 대한 정보를 청크 단위로 제공한다.
- 각 "세부 수준"에 해당 수준에서 시스템을 이해하는 데 필요한 정보만 포함한다.
Mark Seemann은 이것을 프랙탈 아키텍처라고 부른다.
Fractal Architecture
리팩토링을 할 때 우리는 모든 것을 기억하기 얼마나 어려운지에 집중해야 한다,
모든 세부 사항을 기억하기 어렵다면 코드에 문제가 있는 것이다.
위 논리를 적용하여 리팩토링을 시도해보자.
로그인 한 사용자의 정보를 보여주는 대시보드가 있다고 가정한다.
이 코드는 사람이 기억하기 어렵다. 세부 사항이 7개 이상이기 때문이다.
const App = () => {
const user = currentUser();
const isManager = hasManagerRole(user);
const isPromoAccount = checkPromoAccount(location);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = () => {
/*...*/
};
return isManager || isPromoAccount ? (
<Dashboard />
) : (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={({ target }) => setEmail(target.value)}
/>
<input
type="password"
password={password}
onChange={({ target }) => setPassword(target.value)}
/>
<button>Login</button>
</form>
);
};
촤상위 수준에서 독자를 위한 추상화를 제공하고, 컴포넌트가 주생하는 세부 사항을 그 다음 수준으로 제공하면 코드를 이해하기 더 쉽다.
변수와 서브 컴포넌트는 의도 및 스토리를 나타낸다.
const App = () => {
const hasAccess = useHasAccess();
return hasAccess ? <Dashboard /> : <Login />;
};
/**
* If a user has access (`hasAccess`) to the control panel,
* the application will show them the panel component (`Dashboard`).
* If not, they will be prompted to log in (`Login`).
*/
function useHasAccess() {
const user = currentUser();
const isManager = hasManagerRole(user);
const isPromoAccount = checkPromoAccount(location);
return isManager || isPromoAccount;
}
/**
* We'll check if the current user (`currentUser`)
* is a manager (`hasManagerRole`).
* We'll also check if the application is running under a promo account,
* in which the control panel is available to everyone (`checkPromoAccount`).
*/
이제 최상위 세부 수준에서는 hasAccess, Dashboard 및 Login의 세 부분만 볼 수 있다.
각 이야기의 세부 사항이 궁금하면 한 단계 내려가면 된다.
그리고 언제늗지 레벨 간에 전환할 수 있다.
각 레벨은 우리가 처리할 수 있는 정보만 처리하며, 언제든지 zoom in, zoom out 할 수 있다.
관심사의 분리
"책임"과 "작업(task)"은 다소 모호한 용어지만,
다른 데이터 및 작업보다 서로 더 강력하게 관련된 제한된 데이터 및 작업 집합으로 이해할 수 있다.
작업 나누기
해당 코드에 얼마나 많은 데이터와 작업이 있는지를 파악한다.
async function submitLoginForm(event) {
const form = event.target;
const data = {};
if (!form.email.value || !form.password.value) return;
data.email = form.email.value;
data.password = form.password.value;
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(data),
});
return response.json();
}
- 양식 데이터 추출
- 데이터 유효성 검사
- 네트워크 요청.
작업의 조건(데이터)을 변경하여 어떤 코드가 변경되는지를 파악해 작업의 갯수를 판단할 수도 있다.
예를 들어 양식에 체크박스를 추가하면 데이터 추출 코드가 변경된다.
async function submitLoginForm(event) {
// ...
data.email = form.email.value;
data.password = form.password.value;
// New field will appear in the object:
data.rememberMe = form.rememberMe.checked;
// ...
}
async function submitLoginForm(event) {
// ...
// Argument for `fetch` will change:
const response = await fetch("/api/v2/login", {
method: "POST",
body: JSON.stringify(data),
});
// ...
}
이러한 것들은 코드 조각들을 연관짓는데 도움이 된다.
물론 큰 코드에 항상 많은 작업들이 있는것은 아니다.
알고리즘 문제 풀이와 같은 경우 많은 코드들이 하나의 작업만 한다.
단일 책임 원칙
다른 이유로 변경되는 코드는 별도 보관한다.
같은 이유로 변경되는 코드는 함께 보관한다.
아래 코드에서 데이터를 추출해보자
// Move data extraction to a separate function.
// Now it all is gathered here, and we know exactly
// where to look if we need to know its details.
function extractLoginData(form) {
const data = {};
data.email = form.email.value;
data.password = form.password.value;
return data;
}
async function submitLoginForm(event) {
const form = event.target;
// Inside `submitLoginForm` we now focus
// only on using the extracted data.
const data = extractLoginData(form);
if (!form.email.value || !form.password.value) return;
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(data),
});
return response.json();
}
// All validation is now gathered in the `isValidLogin` function.
// Inside it, we check the data, not the properties of the DOM object:
function isValidLogin({ email, password }) {
return !!email && !!password;
}
async function submitLoginForm(event) {
const form = event.target;
const data = extractLoginData(form);
if (!isValidLogin(data)) return;
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(data),
});
return response.json();
}
API 로직도 함수로 분리할 수 있다.
// All the networking now is in the `loginUser` function.
async function loginUser(data) {
const method = "POST";
const body = JSON.stringify(data);
const response = await fetch("/api/login", { method, body });
return await response.json();
}
async function submitLoginForm(event) {
const form = event.target;
const data = extractLoginData(form);
if (!isValidLogin(data)) return;
return await loginUser(data);
}
function isValidLogin({ email, password }) {
// Now checking that the email contains `@` character:
return email.includes("@") && !!password;
}
// Functions `extractLoginData`, `loginUser`, and `submitLoginForm`
// are not changed.
이러한 분리를 통해 서로 격리된 상태에서 기능을 테스트하고 개발하기가 더 쉬워진다.
격리도가 높을수록 코드를 업데이트할 때 우발적인 실수를 할 가능성이 낮아진다.
캡슐화
[캡슐화의] 가장 중요한 개념은 객체가 유효하지 않은 상태가 불가능하도록 보장해야 한다.
[캡슐화된] 객체는 "유효한"것이 무엇을 의미하는지, 그리고 이를 보장하는 방법을 가장 잘 알고 있다.
// purchase.js
import { createOrder } from "./order";
async function makePurchase(user, cart, coupon) {
if (!cart.products.length) throw new Error("Cart is empty!");
const order = createOrder(user, cart);
order.discount = coupon === "HAPPY_FRIDAY" ? order.total * 0.2 : 0;
await sendOrder(order);
}
문제점은 sendOrder 함수가 휴효한 order를 받을 것이라는 보장이 없다는 것이다.
makePurchase 함수는 다른 함수에서 가져온 order의 데이터를 변경한다.
이는 order의 내부 상태를 너무 잘 알고 있음을 의미한다.
데이터가 유효한지 확인하는 것은 특정 모듈의 내부 작업입니다.
따라서, 데이터 유효성 검사는 order에 할인을 적용하는 방법을 아는 모듈이 해야 한다.
우리의 경우 이것은 order.js다. 즉, 주문이 생성될 때 할인도 적용되어야 한다.
// order.js
export function createOrder() {
/*...*/
}
export function applyDiscount(order, coupon) {
const discount = coupon === "HAPPY_FRIDAY" ? order.total * 0.2 : 0;
return { ...order, discount };
}
// purchase.js
import { createOrder, applyDiscount } from "./order";
async function makePurchase(user, cart, coupon) {
if (!cart.products.length) throw new Error("Cart is empty!");
const order = createOrder(user, cart);
const discounted = applyDiscount(order, coupon);
await sendOrder(discounted);
}
// Now making sure the order data is valid
// is an internal task of the `order` module,
// not the responsibility of the code that calls it.
혹은 생성한 데이터를 절대 변경하지 못하도록 하는 방법도 있다.(불변)
또한 카드 모듈도 마찬가지다.
어떤 카트 상태가 유효한지 계속 아는 것처롬 동작한다.
// cart.js
export function isEmpty(cart) {
return !cart.products.length;
}
// purchase.js
import { isEmpty } from "./cart";
import { createOrder, applyDiscount } from "./order";
async function makePurchase(user, cart, coupon) {
if (isEmpty(cart)) throw new Error("Cart is empty!");
const order = createOrder(user, cart);
const discounted = applyDiscount(order, coupon);
await sendOrder(discounted);
}
- 업데이트된 코드에서 makePurchase 함수는 주문 상태를 직접 변경하지 않으며 장바구니가 유효한지 여부를 결정하지 않는다.
- 대신 다른 모듈의 공개 API를 호출한다.
- 또한 코드의 변경 범위를 제한했다.
- 모듈의 공개 API는 변경되지 않는다.
- 해당 모듈의 업데이트 및 수정은 해당 경계 내부로 제한되며 외부로 이동하지 않는다.
- 이 변경으로 오류가 사라지는 것은 아니다.
- 하지만 모듈 간 책임 분리로 어떤 오류가 발생하면 어떤 모듈을 수정해야 하는지 쉽게 알 수 있다.
'FrontEnd' 카테고리의 다른 글
웹 컴포넌트(Web Component) 딥 다이브 (0) | 2023.04.08 |
---|---|
[번역] 자바스크립트 함수형 파이프라인으로 리팩토링 (0) | 2023.04.07 |
[번역] 리액트 Compound Component 잘 사용하기 (0) | 2023.04.04 |
Leetcode 241: Different Ways to Add Parentheses(수식 모든 경우의 수 계산하기)[BFS,DFS] (0) | 2023.04.03 |
프론트엔드 클린 아키텍처 with React (0) | 2023.04.02 |