비즈니스 로직 누수 현상

비즈니스 로직과 관련 없는 컴포넌트나 애플리케이션 영역으로 흘러 들어가는 현상

뷰 또는 UI 컴포넌트 내에서 데이터 변환이 함께 수행 되는것

function UserProfile ({ id }: { id: string }) {
	const [user, setUser] = useState<User | null>(null);
	
	useEffect(() => {
		async function fetchUser() {
			const response = await fetch(`/api/users/${id}`);
			const data = await response.json();
			
			setUser({
				id: data.user_identification,
				name: data.user_full_name,
				isPremium: data.is_premium_user,
				subscription: data.subscription_details.level,
				expire: data.subscription_details.expiry,
			});
		}
		
		fetchUser();
	}, [id]);
	
	if (!user) {
		return <div>Loading...</div>;
	}
	
	return (
		<div data-testid="user-profile">
			<h1>{user.name}</h1>
		</div>
	);
}

ACL (오류 방지 계층)

Anti-Curruption-Layer 에서 데이터 변환이 이뤄져야함 캐시처리, 오류 변환 전담

// 원격 서버에서 수신하는 데이터 형식
type RemoteUser = {
	user_identification: string;
	user_full_name: string;
	is_premium_user: boolean;
	subscription_details: {
		level: string;
		expiry: string;
	};
};
 
// 지역 변수로 사용하는 타입
type UserSubscription = "Basic" | "Standard" | "premium" | "Enterprise";
 
type User = {
	id: string;
	name: string;
	isPremium: boolean;
	subscription: UserSubscription;
	expire: string;
};
// transformer.ts (RemoteUser -> UserSubscription)
import { RemoteUser, User, UserSubscription } from "./types";
 
export const transformUser = (remoteUser: RemoteUser): User => {
	return {
		id: remoteUser.user_identification,
		name: remoteUser.user_full_name,
		isPremimium: remoteUser.is_premium_user,
		subscription: remoteUser.subscription_details.level as UserSubscription,
		expire: remoteUser.subscription_details.expiry,
	};
};

단순해진 컴포넌트

async function fetchUserData<T>(id: string) {
	const response = await fetch(`/api/users/${id}`);
	const rawData = await response.json();
	return transformUser(rawData) as T;
}
 
function UserProfile({ id }): { id: string }) {
	const [user, setUser] = useState<User | null>(null);
	
	useEffect(() -> {
		async function fetchUser() {
			const response = await fetchUserData<User>(id);
			setUser(response);
		}
		
		fetchUser();
	}, [id])
	
	if (!user) {
		return <div>Loading...</div>;
	}
	
	return (
		<div data-tested="user-profile">
			<h1>{user.name}</h1>
		</div>
	);
}

예외 상황 대응 또는 기본값

원격 서비스 응답 중에 특정 값이 비어있는 상태가 있을 수 있다 이때 미리 정의해둔 기본값을 보여줘야 한다

ACL이 없다면 다음과 같이 컴포넌트에 직접 로직을 추가할것이다.

function UserProfile({ user }: { user: User }){
	const fullName = user && user.name ? user.name : "Loading...";
	const subscriptionLevel = user && user.subscription ? user.subscription: "Basic";
	
	const subscriptionExpiry = user && user.expire ? user.expire: "Never";
	
	return (
		<div>
			<h1>{fullName}</h1>
			<p>Subscription Level: {subscriptionLevel}</p>
			<p>Subscription Expiary: {subscriptionExpiry}</p>
		</div>
	);
}

이런 로직은 transformUser 함수로 옮기면 좋음

export const transformUser = (remoteUser: RemoteUser): User => {
	return {
		id: remoteUYser.user_identification ?? 'N/A',
		name: remoteUser.use_full_name ?? 'Unknown User',
		isPremium: remoteUser.is_premium_user ?? false,
		subscription: (remoteUser.subscription_details?.level ?? 'Basic') as UserSubscription,
		expire: remoteUser.subscription_details?.expiry ?? 'Never',
	};
};

Prop Drilling

이 컴포넌트는 통합 검색 컴포넌트이다.

import React, { ChangeEvent, useState } from "react";
 
export type Item = {
	id: string;
	name: string;
	descriptoin: string;
};
 
const SearchableList = ({ items }: { items: Item[] }) => {
	const [filtereditems, setFilteredItems] = useState<Item[]>(items);
	
	const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
		setFilteredItems(
			items.filter((item) => item.name.includes(e.target.value))
		);
	};
	
	return (
		<div>
			<input type="text" onChange={handleChange} />
			<ul>
				{filteredItems.map((item, index) => (
					<li key={index}>{item.name}</li>
				))}
			</ul>
		</div>
	);
};
 
export default SearchableList;

이 컴포넌트는 SearchInput, List, ListItem 3개의 하위 컴포넌트로 세분화 할 수 있다. 별도로 분리한 개별 컴포넌트는 다음과 같다.

const ListItem = ({ item }: { item: Item }) = {
	return (
		<li>
			<h2>{item.name}</h2>
			<p>{item.description}</p>
		</li>
	);
};
const List = ({ items }: { items: Item[] }) = {
	return (
		<section data-testid="searchable-list">
			<ul>
				{items.map((item) => (
					<ListItem item={item} />
				))}
			</ul>
			<footer>Total items: {items.length}</footer>
		</section>
	);
};
const SearchInput = ({ onSearch }: { onSearch: (keyward: string) => void }) => {
	const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
		onSearch(e.target.value);
	};
	
	return <inpyt type="text" onChange={handleChange} />;
};
 
const SearchableList = ({ items }: { items: Item[] }) => {
	const [filteredItems, setFilteredItems] = useState<Item[]>(items);
	
	const onSearch = (keyward: string) => {
		serFilteredItems(items.filter((item) => item.name.includes(keyward)));
	};
	
	return (
		<div>
			<SearchInput onSearch={onSearch} />
			<List items={filteredItems} />
		</div>
	);
};

항목의 인기도 검색, 검색 기능의 활용도를 분석하기 위한 사용자 상호작용 추적 기능 추가 요청이 들어온다면, 아이템을 클릭했을 때 호출할 onItemClicked 콜백 함수와 검색이 수행될 때 호출할 onSearch 콜백 함수를 전달받을 2개의 prop을 추가해야 한다. 이를 위해 SearchableList에서 사용하는 새로운 타입을 만들어준다.

type SreachableListProps = {
	items: Item[];
	onSearch: (keyward: string) => void;
	onItemClicked: (item: Item) => void;
};
 
const SearchableList = ({
	items,
	onSearch,
	onItemClicked
}: SearchableListProps) => {
	// ...
}

onSearch와 onItemClicked 를 전달하기 위해 ListItem에 변경이 필요하다. prop에 onItemClicked를 추가하고 항목을 클릭하면 호출한다.

const ListItem = ({
	item,
	onItemClicked,
}: {
	item: Item;
	onItemClicked: (item: Item) => void;
}) => {
	return (
		<li onClick={() => onItemClicked(item)}>
			<h2>{item.name}</h2>
			<p>{item.description}</p>
		</li>
	);
};

ListItem은 외부에 직접 노출되지 않기 때문에, prop은 부모 컴포넌트인 List로부터 전달받는다.

const list = ({
	items, 
	onItemClicked,
}: {
	items: Item[];
	onItemClicked: (item: Item) => void;
}) => {
	return (
		<section data-testid="searchable-list">
			<ul>
				{items.map((item) => (
					<ListItem item={item} onItemClicked={onItemClicked} />
				))}
			</ul>
			<footer>Total items: {items.length}</footer>
		</section>
	);
};

List 컴포너트는 onItemClicked prop 을 전달받아 ListItem에게 가공 없이 그대로 전달해주는데, 여기서 코드 스멜의 징후가 보인다. 컴포넌트의 기능과 직접 연관이 없는 부분을 다르고 있기 때문이다. 그리고 onItemClicked prop은 List 컴포넌트의 부모 컴포넌트인 SearchableList로부터 전달받는다.

const SearchableList = ({
	items,
	onSearch,
	onItemClicked,
}: SearchableListProps) => {
	const [filterdItems, setFilteredItems] = useState<Item[]>(items);
	
	const handleSearch = (keyward: string) => {
		setFiltereditems(items.filter((item) => item.name.includes(keyward)));
	};
	
	return (
		<div>
			<SearchInput onSearch={handleSearch} />
			<List items={filteredItems} onItemClicked={onItemClicked} />
		</div>
	);
};

onItemClicked prop은 우선 List 컴포넌트로 전달되고, 아무런 사용 없이 ListItem 컴포넌트로 바로 전달된다. 이것이 전형적인 PropDrilling이다.