"좋은 개발자는 코드를 많이 작성하는 개발자가 아니라, 적은 코드로 많은 가치를 만드는 개발자다"

서론
개발을 하다 보면 종종 이런 상황에 마주칩니다. 배열을 그룹화해야 하는데 복잡한 로직을 직접 구현하거나, React에서 API 호출 상태를 관리하기 위해 수십 줄의 보일러플레이트 코드를 작성하는 경우 말입니다.
이런 상황에서 라이브러리에 위임할 것인가 직접 구현할 것인가 고민이 필요합니다. 이미 검증된 라이브러리들을 활용하면 우리는 정말 중요한 비즈니스 로직에 집중할 수 있습니다.
1.로우 레벨 의존성 위임의 핵심 가치
하위 의존성을 외부 라이브러리에 위임하는 것은 단순한 편의성을 넘어선 전략적 선택입니다. 그 핵심 가치들을 살펴보겠습니다.
1.1. 체계적인 문서화
외부 라이브러리는 체계적인 문서화를 통해 사용 가능한 모든 기능을 명확히 제시합니다. 이는 개발자가 "이런 기능이 있었나?" 하며 다시 구현하는 낭비를 방지합니다.
// lodash 문서를 통해 발견할 수 있는 유용한 기능들
import {
groupBy, // 배열을 특정 속성으로 그룹화
flow, // 함수 조합으로 파이프라인 구성
uniq, // 배열에서 중복 제거
pickBy, // 조건에 맞는 속성만 선택
isEmpty // 빈 값 체크 (null, undefined, [], {} 등)
} from 'lodash';
// 이런 기능들을 직접 구현했다면 몇 시간이 걸렸을까?
const validUsers = pickBy(users, user => !isEmpty(user.email));
const uniqueCategories = uniq(products.map(p => p.category));
const processData = flow([
data => groupBy(data, 'category'),
grouped => Object.values(grouped),
groups => groups.filter(group => group.length > 5)
]);
1.2. 도구 표준화를 통한 일관성
팀 전체가 동일한 도구를 사용하면 코드의 일관성이 높아지고, 팀원 간 협업이 원활해집니다.
// ❌ 각자 다른 방식으로 구현한 경우
// 개발자 A의 방식
const groupedA = data.reduce((acc, item) => {
(acc[item.category] = acc[item.category] || []).push(item);
return acc;
}, {});
// 개발자 B의 방식
const groupedB = {};
data.forEach(item => {
if (!groupedB[item.category]) groupedB[item.category] = [];
groupedB[item.category].push(item);
});
// ✅ 표준화된 방식
const grouped = groupBy(data, 'category');
// 누구나 이해하고 수정할 수 있는 코드
1.3. 안정성과 신뢰성 보장
하위 의존성은 자주 변경되지 않아야 하며, 높은 신뢰성을 보장해야 합니다. 이는 시스템 전체의 안정성과 직결됩니다.
// 신뢰성이 중요한 하위 의존성 예시
// 1. 깊은 객체 비교 - 순환 참조, 특수 객체 등 엣지케이스
import { isEqual } from 'lodash';
// 2. 빈 값 체크 - null, undefined, [], {}, "", 0 등 모든 경우
import { isEmpty } from 'lodash';
// 3. 함수 조합 - 복잡한 데이터 파이프라인
import { flow } from 'lodash';
1.4. 집단 지성과 지속적인 개선
오픈소스 라이브러리는 수많은 개발자들의 집단 지성과 실전 경험이 축적된 결과물입니다.
// lodash의 debounce 함수 발전 과정
// - 2012년: 기본적인 debounce 구현
// - 2014년: leading/trailing 옵션 추가
// - 2016년: maxWait 옵션으로 무한 지연 방지
// - 2018년: 메모리 누수 방지 개선
// - 2020년: TypeScript 지원 강화
2.우리가 이미 실행하고 있는 의존성 위임 사례
사실 우리는 이미 많은 영역에서 하위 의존성 위임을 성공적으로 활용하고 있습니다. 이미 검증된 사례들을 살펴보며, 왜 이 접근법이 효과적인지 확인해보겠습니다.
2.1. 서버 상태 관리: React Query 위임
서버 상태 관리의 복잡성을 React Query에 위임하여 여러 까다로운 문제들을 해결했습니다.
// ❌ 직접 구현했다면...
function useUserData(userId) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [lastFetch, setLastFetch] = useState(null);
const fetchUser = useCallback(async () => {
// Race condition 처리
const currentFetch = Date.now();
setLoading(true);
try {
// 중복 요청 방지 로직
if (lastFetch && Date.now() - lastFetch < 5000) {
return; // 5초 내 중복 요청 차단
}
const response = await fetch(`/api/users/${userId}`);
// 오래된 응답 무시 (race condition)
if (currentFetch < lastFetch) return;
if (!response.ok) throw new Error('Failed to fetch');
const userData = await response.json();
setData(userData);
setLastFetch(currentFetch);
} catch (err) {
if (currentFetch >= lastFetch) {
setError(err);
// 재시도 로직
setTimeout(() => fetchUser(), 1000);
}
} finally {
if (currentFetch >= lastFetch) {
setLoading(false);
}
}
}, [userId, lastFetch]);
// 캐싱, 백그라운드 갱신, 의존성 관리 등 수십 줄의 추가 로직...
return { data, loading, error, refetch: fetchUser };
}
// ✅ React Query로 위임
import { useQuery } from '@tanstack/react-query';
function useUserData(userId) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
staleTime: 5 * 60 * 1000, // 5분간 fresh 상태 유지
retry: 3, // 실패 시 3번 재시도
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
});
}
// React Query가 자동으로 처리하는 것들:
// ✅ 중복 요청 제거 (de-duplication)
// ✅ Race condition 방지
// ✅ 백그라운드 리페칭
// ✅ 캐싱 및 가비지 컬렉션
// ✅ 오프라인/온라인 상태 감지
// ✅ Window focus 시 리페칭
// ✅ 자동 재시도 및 지수 백오프
2.2. 폼 검증: React Hook Form 위임
복잡한 폼 상태 관리와 검증 로직을 React Hook Form에 위임했습니다.
// ❌ 직접 구현했다면...
function UserRegistrationForm() {
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = (name, value) => {
let error = '';
switch (name) {
case 'email':
if (!value) error = '이메일은 필수입니다';
else if (!/\S+@\S+\.\S+/.test(value)) error = '유효하지 않은 이메일입니다';
break;
case 'password':
if (!value) error = '비밀번호는 필수입니다';
else if (value.length < 8) error = '비밀번호는 8자 이상이어야 합니다';
else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
error = '비밀번호는 대소문자와 숫자를 포함해야 합니다';
}
break;
// ... 더 많은 필드들
}
setErrors(prev => ({ ...prev, [name]: error }));
return !error;
};
const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
if (touched[name]) {
validateField(name, value);
}
};
const handleBlur = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
validateField(name, values[name]);
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
// 모든 필드 검증
const isValid = Object.keys(values).every(key =>
validateField(key, values[key])
);
if (isValid) {
try {
await submitForm(values);
} catch (error) {
// 서버 에러 처리
}
}
setIsSubmitting(false);
};
// 수십 줄의 JSX와 이벤트 핸들러들...
}
// ✅ React Hook Form으로 위임
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email('유효하지 않은 이메일입니다'),
password: z.string()
.min(8, '비밀번호는 8자 이상이어야 합니다')
.regex(/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, '대소문자와 숫자를 포함해야 합니다'),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: '비밀번호가 일치하지 않습니다',
path: ['confirmPassword']
});
function UserRegistrationForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({
resolver: zodResolver(userSchema),
mode: 'onBlur' // 포커스 이탈 시 검증
});
const onSubmit = async (data) => {
await submitForm(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '등록 중...' : '등록'}
</button>
</form>
);
}
// React Hook Form이 자동으로 처리하는 것들:
// ✅ 폼 상태 관리 (values, errors, touched, dirty, valid)
// ✅ 성능 최적화 (불필요한 리렌더링 방지)
// ✅ 접근성 (aria 속성 자동 설정)
// ✅ 다양한 검증 라이브러리 통합 (Yup, Zod, Joi)
// ✅ 필드 배열 관리 (동적 필드 추가/제거)
2.3. 날짜 연산: Day.js 위임
복잡한 날짜 계산과 포맷팅을 Day.js에 위임했습니다.
// ❌ 직접 구현했다면...
function calculateBusinessDays(startDate, endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
let businessDays = 0;
// 주말 제외 계산
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dayOfWeek = d.getDay();
if (dayOfWeek !== 0 && dayOfWeek !== 6) { // 일요일(0), 토요일(6) 제외
businessDays++;
}
}
// 공휴일 처리는 어떻게...?
// 타임존 처리는...?
// 윤년 계산은...?
return businessDays;
}
function formatRelativeTime(date) {
const now = new Date();
const target = new Date(date);
const diffMs = now - target;
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return '방금 전';
if (diffMins < 60) return `${diffMins}분 전`;
if (diffHours < 24) return `${diffHours}시간 전`;
if (diffDays < 7) return `${diffDays}일 전`;
// 더 복잡한 경우들... (월, 년 단위, 국제화)
return target.toLocaleDateString();
}
// ✅ Day.js로 위임
import dayjs from 'dayjs';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/ko';
dayjs.extend(isSameOrBefore);
dayjs.extend(customParseFormat);
dayjs.extend(relativeTime);
dayjs.locale('ko');
// 비즈니스 로직에만 집중
function calculateProjectDeadline(startDate, workingDays) {
let current = dayjs(startDate);
let remainingDays = workingDays;
while (remainingDays > 0) {
current = current.add(1, 'day');
// Day.js가 처리하는 복잡한 날짜 연산
if (!current.day() === 0 && !current.day() === 6) { // 주말 제외
remainingDays--;
}
}
return current.format('YYYY-MM-DD');
}
function formatEventTime(eventDate) {
const event = dayjs(eventDate);
const now = dayjs();
// Day.js가 자동으로 처리하는 것들:
// ✅ 국제화 (한국어 지원)
// ✅ 타임존 처리
// ✅ 다양한 상대시간 표현
// ✅ 윤년, 월말 경계 처리
return event.from(now); // "3시간 전", "2일 후" 등
}
// 복잡한 날짜 범위 계산
function getQuarterDateRange(year, quarter) {
const startMonth = (quarter - 1) * 3;
const start = dayjs().year(year).month(startMonth).startOf('month');
const end = start.add(2, 'month').endOf('month');
return {
start: start.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD'),
label: `${year}년 ${quarter}분기`
};
}
이미 위임하고 있는 다른 사례들
// UI 컴포넌트 - Material-UI
// 상태 관리 - Redux, Zustand
// 라우팅 - React Router, Next.js Router
// HTTP 클라이언트 - Axios
// 번들링 - Webpack, Vite
// 테스트 - Jest, React Testing Library
// 타입 체킹 - TypeScript
// 린팅 - ESLint, Prettier
이처럼 우리는 이미 복잡하고 범용적인 기능들을 검증된 라이브러리에 위임하여 성공적으로 개발하고 있습니다. 데이터 조작이나 유틸리티 함수들도 같은 원칙을 적용할 수 있습니다.
3.의존성 안정성의 중요성( SDP )
더 안정적인 컴포넌트에 덜 안정적인 컴포넌트가 의존해야 한다는 **안정된 의존성 원칙(Stable Dependencies Principle,SDP)**에 의하면 시스템의 여러 부분에서 의존하는 로우 레벨의 모듈일수록 더 안정적이어야 하고, 자주 변경되어서는 안 됩니다.
"변화가 많은 비즈니스 로직은 변화가 적고 안정적인 범용 라이브러리(lodash, react-use)에 의존하게 함으로써, 변경 전파를 차단하고 유지보수 비용을 낮춘다."
즉, 변화 가능성이 높은 쪽(비즈니스 로직,feature layer)이 변화 가능성이 낮은 쪽(shared layer)을 의존하는 구조를 만들라는 것.
여기서 안정성(stability)은 변화에 얼마나 저항적인지를 뜻합니다
- 변화에 강하다 = 안정적
- 변화가 자주 발생한다 = 불안정
정량적으로는 두 지표로 판단합니다.
- Fan-in:
- 이 컴포넌트를 사용하는 외부 컴포넌트 개수 (의존을 받는 정도)
- 높을수록, 이 컴포넌트가 변경되면 많은 곳에 영향이 가므로 변경에 민감하고 안정성이 높음.
- Fan-out:
- 이 컴포넌트가 의존하는 외부 컴포넌트 개수 (의존하는 정도)
- 많을수록, 외부 변경의 영향을 많이 받으므로 불안정.
안정성 계산식

의존성 그래프로 본 안정성
// 🔴 위험한 구조: 직접 구현한 유틸리티에 많은 모듈이 의존
// lib/array - 직접 구현 (불안정)
export function groupBy(array, key) {
// 100줄의 복잡한 구현...
// 버그 수정, 성능 개선으로 인한 잦은 변경
}
// 20개 이상의 컴포넌트에서 사용
import { groupBy } from '@shared/lib';
// 문제: groupBy의 변경이 20개 모듈에 영향
// - 버그 수정할 때마다 회귀 테스트 필요
// - 성능 개선 시 예상치 못한 사이드 이펙트
// - 새로운 요구사항으로 인터페이스 변경 위험
// ✅ 안전한 구조: 안정된 외부 라이브러리에 의존
import { groupBy } from 'lodash';
// lodash는:
// - 수백만 다운로드, 수천 개의 이슈와 PR로 검증됨
// - 의미론적 버전 관리로 안정성 보장
// - 하위 호환성 유지
// - 광범위한 테스트 커버리지
신뢰성 측면에서의 비교
// ❌ 직접 구현한 깊은 복사 함수의 위험성
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof Array) return obj.map(item => deepClone(item));
const cloned = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(key); // 🐛 버그: obj[key]가 아닌 key를 복사
}
}
return cloned;
}
// 이런 미묘한 버그들:
// - Symbol 프로퍼티 무시
// - 순환 참조 처리 누락
// - Map, Set, RegExp 등 특수 객체 처리 누락
// - Prototype chain 처리 문제
// ✅ Lodash의 cloneDeep은 이미 모든 엣지케이스를 처리
import { cloneDeep } from 'lodash';
const cloned = cloneDeep(complexObject);
// - 10년간의 이슈 리포트와 수정사항 반영
// - 모든 JavaScript 타입에 대한 완벽한 처리
// - 순환 참조, Symbol, WeakMap 등 모든 엣지케이스 처리
// - 수천만 개의 프로젝트에서 검증됨
4 사례 보기
4.1. Lodash로 데이터 조작 간소화
❌ 바퀴의 재발명 (Bad)
/**
* 사용자 정의 판단 함수를 받아 객체에서 "빈" 필드를 제거하는 함수
* @param obj 처리할 객체
* @param isEmpty 값이 "비어있음"을 판단하는 함수
* @returns 빈 필드가 제거된 새 객체
*/
export function removeEmptyFields<T extends Record<string, any>>(
obj: T,
isEmpty: (value: any) => boolean = defaultIsEmpty,
): Partial<T> {
const result: Partial<T> = {};
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (!isEmpty(value)) {
result[key as keyof T] = value;
}
});
return result;
}
✅ Lodash 활용 (Good)
import _ from 'lodash';
export function removeEmptyFields<T extends Record<string, any>>(
obj: T,
isEmpty: (value: any) => boolean = defaultIsEmpty,
): Partial<T> {
return _.pickBy(obj, (value) => !isEmpty(value));
}
실제 비즈니스 사례: 대시보드 데이터 처리
// ❌ 수십 줄의 복잡한 로직
function processSalesData(rawData) {
// 날짜별 그룹화
const groupedByDate = {};
rawData.forEach(item => {
const date = item.date.split('T')[0];
if (!groupedByDate[date]) {
groupedByDate[date] = [];
}
groupedByDate[date].push(item);
});
// 각 날짜별 합계 계산
const dailySums = {};
Object.keys(groupedByDate).forEach(date => {
dailySums[date] = groupedByDate[date].reduce((sum, item) => {
return sum + (item.amount || 0);
}, 0);
});
// 상위 5개 날짜 추출
const sortedDates = Object.keys(dailySums).sort((a, b) => {
return dailySums[b] - dailySums[a];
});
return sortedDates.slice(0, 5).map(date => ({
date,
amount: dailySums[date]
}));
}
// ✅ Lodash로 간결하게
function processSalesData(rawData) {
return _(rawData)
.groupBy(item => item.date.split('T')[0])
.mapValues(items => _.sumBy(items, 'amount'))
.toPairs()
.orderBy(1, 'desc')
.take(5)
.map(([date, amount]) => ({ date, amount }))
.value();
}
4.2. React-Use로 React Hook 보일러플레이트 제거
❌ 바퀴의 재발명 (Bad)
// 로컬스토리지 동기화
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading localStorage:', error);
return initialValue;
}
});
const setStoredValue = useCallback((newValue) => {
try {
setValue(newValue);
window.localStorage.setItem(key, JSON.stringify(newValue));
} catch (error) {
console.error('Failed to save to localStorage:', error);
}
}, [key]);
return [value, setStoredValue];
}
// 디바운스 처리
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// 이전 값 추적
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
✅ React-Use 활용 (Good)
import { useLocalStorage, useDebounce, usePrevious, useAsync, useEffectOnce } from 'react-use';
// 같은 기능을 간단하게
function SearchComponent() {
const [searchTerm, setSearchTerm] = useLocalStorage('searchTerm', '');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const previousSearchTerm = usePrevious(debouncedSearchTerm);
const searchState = useAsync(async () => {
if (!debouncedSearchTerm) return [];
const response = await fetch(`/api/search?q=${debouncedSearchTerm}`);
return response.json();
}, [debouncedSearchTerm]);
useEffectOnce(() => {
console.log('검색 컴포넌트가 초기화되었습니다');
// 초기 설정이나 분석 이벤트 전송 등
});
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="검색어를 입력하세요"
/>
{searchState.loading && <div>검색 중...</div>}
{searchState.error && <div>오류: {searchState.error.message}</div>}
{previousSearchTerm && previousSearchTerm !== debouncedSearchTerm && (
<div>이전 검색어: {previousSearchTerm}</div>
)}
{searchState.value && <SearchResults results={searchState.value} />}
</div>
);
}
5. 다시 보는 의존성 위임의 전략적 가치
5.1 안정성
시스템에서 의존성의 안정성은 변경의 파급효과와 직결됩니다. 내가 직접 작성한 유틸리티 함수를 10개 컴포넌트에서 사용한다면, 해당 함수의 작은 변경도 10개 컴포넌트의 테스트와 검증을 필요로 합니다.
// 📊 변경 영향도 시뮬레이션
// Case 1: 직접 구현한 debounce 함수
// utils/debounce.js (자체 구현)
export function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// 사용하는 곳: 15개 컴포넌트
// - SearchInput.jsx
// - AutoComplete.jsx
// - FilterPanel.jsx
// ... 12개 더
// 문제 발생: leading edge 옵션 필요
// 해결책: 함수 시그니처 변경 → 15개 컴포넌트 영향
// Case 2: Lodash debounce 사용
import { debounce } from 'lodash';
// 새로운 요구사항? 이미 옵션 제공:
const debouncedSearch = debounce(search, 300, {
leading: true,
trailing: false
});
// 변경 영향도: 0개 컴포넌트 (기존 API 유지)
5.2 신뢰성
범용적 기능을 외부 라이브러리에 위임하는 것은 신뢰성을 높이는 전략입니다.
// 🔬 신뢰성 비교 분석
// 직접 구현한 빈 값 체크
function isEmpty(value) {
return value == null || value === '';
}
// 놓친 케이스들:
isEmpty([]); // false이어야 하는데 false 반환 (우연히 맞음)
isEmpty({}); // false이어야 하는데 false 반환 (우연히 맞음)
isEmpty(0); // false이어야 하는데 false 반환 (논란의 여지)
isEmpty(false); // false이어야 하는데 false 반환 (논란의 여지)
// vs
// lodash isEmpty
import { isEmpty } from 'lodash';
// - 수년간의 이슈 리포트와 논의
// - 모든 JavaScript 타입에 대한 완벽한 처리
// - Map, Set, Buffer 등 특수 객체 처리
// - 수천만 개의 프로젝트에서 검증됨
// 신뢰성 지표:
// 직접 구현: 1명의 개발자 × 제한된 테스트 = 낮은 신뢰성
// 라이브러리: 수백명의 개발자 × 수천개 테스트 × 실전 검증 = 높은 신뢰성
5.3 비즈니스 로직에 집중
라이브러리에 범용 기능을 위임하면, 우리의 제품 개발자로서 해결해야 하는 문제인 비즈니스 로직에만 집중할 수 있습니다.
// ❌ 유틸리티 로직과 비즈니스 로직이 섞인 코드
class OrderAnalytics {
calculateMonthlyRevenue(orders) {
// 범용 로직: 날짜 그룹핑 (버그 위험, 유지보수 부담)
const monthlyGroups = {};
orders.forEach(order => {
const date = new Date(order.createdAt);
const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
if (!monthlyGroups[monthKey]) {
monthlyGroups[monthKey] = [];
}
monthlyGroups[monthKey].push(order);
});
// 비즈니스 로직: 수익 계산 (핵심 도메인 로직)
const result = {};
Object.keys(monthlyGroups).forEach(month => {
const monthOrders = monthlyGroups[month];
result[month] = {
revenue: monthOrders.reduce((sum, order) => {
return sum + order.total - (order.discount || 0);
}, 0),
orderCount: monthOrders.length,
// 비즈니스 규칙: VIP 고객은 수수료 면제
vipRevenue: monthOrders
.filter(order => order.customer.tier === 'VIP')
.reduce((sum, order) => sum + order.total, 0)
};
});
return result;
}
}
// ✅ 범용 로직은 위임하고 비즈니스 로직에 집중
import { groupBy, sumBy } from 'lodash';
import { format } from 'date-fns';
class OrderAnalytics {
calculateMonthlyRevenue(orders) {
// 범용 로직은 신뢰할 수 있는 라이브러리에 위임
const monthlyGroups = groupBy(orders, order =>
format(new Date(order.createdAt), 'yyyy-MM')
);
// 순수한 비즈니스 로직만 집중
return Object.entries(monthlyGroups).reduce((result, [month, monthOrders]) => {
result[month] = {
// 핵심 비즈니스 규칙: 할인 적용 수익 계산
revenue: sumBy(monthOrders, order => order.total - (order.discount || 0)),
orderCount: monthOrders.length,
// 도메인 특화 로직: VIP 고객 수수료 면제 정책
vipRevenue: sumBy(
monthOrders.filter(order => order.customer.tier === 'VIP'),
'total'
),
// 비즈니스 인사이트: 평균 주문 가치
avgOrderValue: monthOrders.length > 0 ?
sumBy(monthOrders, 'total') / monthOrders.length : 0
};
}, {});
}
}
6. 마무리
"바퀴의 재발명"을 피하는 것은 단순히 시간을 절약하는 것 이상의 의미가 있습니다. 이는 소프트웨어 아키텍처의 안정성과 비즈니스 로직의 순수성을 보장하는 전략적 선택입니다.
의존성 위임의 핵심 가치:
- 안정성: 자주 변경되지 않는 검증된 모듈에 의존
- 신뢰성: 집단 지성과 실전 검증을 통한 높은 품질
- 집중: 범용 로직 대신 고유한 비즈니스 가치에 시간 투자
- 유지보수성: 변경의 파급효과 최소화
범용적 기능은 위임하고, 도메인 특화 로직에 집중하라
시스템에서 많은 모듈이 의존하는 범용적 기능일수록, 직접 구현보다는 검증된 라이브러리에 위임하는 것이 현명합니다. 우리의 코드는 오직 비즈니스 도메인의 고유한 가치를 구현하는 데만 집중해야 합니다.
언제 직접 구현해야 할까?
- 특수한 성능 요구사항이 있을 때
- 기존 라이브러리로는 구현할 수 없는 도메인 특화 로직일 때
- 번들 크기가 매우 중요하고 필요한 기능이 매우 단순할 때
결국 좋은 개발자는 무엇을 위임하고 무엇을 직접 구현할지 올바르게 판단할 수 있는 개발자입니다. 범용적이고 복잡한 기능은 검증된 라이브러리에 맡기고, 우리만의 비즈니스 가치를 만드는 일에 집중합시다.
"코드의 양이 아니라 해결한 문제의 질로 평가받는 개발자가 되자"
참고
'react' 카테고리의 다른 글
| 컴포넌트 설계: 시나리오별 인터페이스 분리 전략 (2) | 2025.08.17 |
|---|---|
| React에서 선언적 표현을 위한 모듈 격리: 클로저 디펜던시 문제 해결하기 (3) | 2025.08.09 |
| [Nextjs] 다국어(i18n, internationalization) 적용하기 (0) | 2024.07.19 |
| [설계] 격리된 컴포넌트간의 통신 설계 (feat. Eventbus) (0) | 2024.06.28 |
| [Nextjs] next13에서 react-query의 필요성 (0) | 2024.06.27 |