🔍 의존성 주입(DI)이란 무엇인가요?
**의존성 주입(Dependency Injection, 이하 DI)**은 어떤 객체가 필요한 의존 객체를 스스로 생성하지 않고 외부에서 주입받는 설계 패턴입니다. 대표적으로 Java의 Spring Framework에서 많이 사용되며, 제어의 역전(Inversion of Control, IoC) 원칙을 따릅니다.
✅ DI를 사용하면 얻을 수 있는 이점
- 결합도 감소: 객체 간의 연결을 약하게 유지해 유연한 구조 설계 가능
- 테스트 용이성: 테스트 시 모킹(mocking) 처리하기 쉬움
- 확장성: 구현체 교체가 자유로워짐
하지만, React는 전통적인 클래스 기반 프레임워크가 아닙니다. 그래서 자연스럽게 “React에서 DI가 필요할까?”라는 질문이 나옵니다.
⚛️ React에서도 의존성은 존재한다
React는 함수형 컴포넌트를 중심으로 설계되어 있어, DI 컨테이너나 생성자 기반 주입이 자연스럽진 않습니다. 그럼에도 불구하고 다음과 같은 의존성은 분명히 존재합니다.
🎯 흔히 마주치는 React의 의존성 예시
- API 호출 모듈 (axios, fetch wrapper 등)
- 유틸 함수, 설정값 (예: API base URL)
- 로깅, 분석 툴
- localStorage, sessionStorage 접근 로직
문제는 이 의존성들을 각 컴포넌트에서 직접 import해 사용하는 순간, 테스트와 유지보수가 어려워진다는 점입니다.
❌ 직접 import 방식이 가지는 문제점
import { userService } from './userService.js';
export default function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
userService.getUser(userId).then(setUser);
}, [userId]);
...
}
😨 어떤 문제가 발생할까요?
- 강한 결합: 컴포넌트가 특정 구현체(userService)에 직접 의존
- 유지보수성 하락: 서비스 로직 변경 시 다수 컴포넌트 수정 필요
- 테스트 어려움: Mocking이 까다롭고 복잡
- 환경별 대응 미흡: dev, prod 구분이 어려움
- 재사용성 부족: 컴포넌트의 범용성이 떨어짐
이 문제는 “props로 서비스 객체를 내려주는 방식”으로 개선할 수 있지만, 또 다른 문제인 Props Drilling으로 이어집니다.
🧩 Props Drilling의 딜레마
Props를 통해 서비스 인스턴스를 전달하는 방식은 단기적으로는 괜찮지만, 하위 트리로 갈수록 문제가 됩니다.
🙅♂️ Props Drilling의 문제점
- 불필요한 전달: 중간 컴포넌트들이 필요 없는 props를 받음
- 확장성 부족: 새로운 의존성 추가 시 모든 경로 수정
- 가독성 저하: 어떤 props가 어디에서 필요한지 추적 어려움
🧠 그렇다면 React에서 의존성 주입을 어떻게 구현할 수 있을까?
React에서는 아래 두 가지 패턴을 통해 DI의 효과를 실현할 수 있습니다.
1. Context API
전역적으로 의존성을 주입하고, 필요한 곳에서 useContext로 꺼내 쓰는 방식.
2. Custom Hooks
서비스 접근 로직을 캡슐화하여 필요한 기능만 내보내는 방식.
💡 Context API를 통한 의존성 주입
🌱 구조
App
└── [Provider] UserServiceContext
└── UserProfile
└── UserSettings
🔧 구현 예시
// UserServiceContext.js
import { createContext } from 'react';
import { userService } from '../services/userService';
export const UserServiceContext = createContext(null);
export function UserServiceProvider({ children }) {
return (
<UserServiceContext.Provider value={userService}>
{children}
</UserServiceContext.Provider>
);
}
// App.jsx
import { UserServiceProvider } from './contexts/UserServiceContext';
function App() {
return (
<UserServiceProvider>
<UserProfile userId={1} />
</UserServiceProvider>
);
}
// UserProfile.jsx
import { useContext, useEffect, useState } from 'react';
import { UserServiceContext } from '../contexts/UserServiceContext';
function UserProfile({ userId }) {
const userService = useContext(UserServiceContext);
const [user, setUser] = useState(null);
useEffect(() => {
userService.getUser(userId).then(setUser);
}, [userId]);
...
}
✅ 장점
- 하나의 지점에서 의존성 관리 가능
- Props Drilling 제거
- 전역 설정이나 공통 서비스에 유리
⚠️ 한계
- 구체 구현체에 여전히 의존
- Provider 남용 시 리렌더링 부담
🧪 테스트는 어떻게 될까?
😵 기존 방식의 문제점
jest.mock('./userService', () => ({
userService: {
getUser: jest.fn(),
updateUser: jest.fn()
}
}));
- 모듈 단위로 Mock 처리 → 구조 복잡
- 여러 호출 케이스 관리 어려움
- 서비스 로직과 테스트 코드의 결합도 증가
Context API 방식은 value에 Mock 객체를 주입하면 해결됨:
render(
<UserServiceContext.Provider value={mockService}>
<UserProfile userId={1} />
</UserServiceContext.Provider>
);
🧰 Custom Hook으로 DI 캡슐화하기
서비스 로직을 훅 내부로 감싸 UI와 서비스 레이어를 분리
🛠 구현 예시
// useUser.js
import { useEffect, useState } from 'react';
import { userService } from '../services/userService';
export function useUser(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
userService.getUser(userId).then(setUser);
}, [userId]);
const updateUser = (newData) =>
userService.updateUser(userId, newData);
return { user, updateUser };
}
// UserProfile.jsx
import { useUser } from '../hooks/useUser';
function UserProfile({ userId }) {
const { user, updateUser } = useUser(userId);
return (
<div>
<h2>{user.name}</h2>
<UserSettings user={user} updateUser={updateUser} />
</div>
);
}
✅ 장점
- 로직 캡슐화: API 변경 시에도 훅 내부만 수정
- 재사용성 강화: 다양한 컴포넌트에서 동일한 훅 사용 가능
- 의도 명확화: useUser, useAuth, useTodos 등 네이밍만으로 역할 이해 가능
⚠️ 한계
- 훅의 인터페이스에 컴포넌트가 강하게 결합됨
- 여전히 userService는 내부에서 사용되므로 완전한 추상화는 아님
🎯 어떤 방식이 더 나을까?
구분 Context API Custom Hook
목적 | 전역 의존성 관리 | 캡슐화 및 로직 재사용 |
유연성 | 적당함 | 좋음 |
테스트 용이성 | 높음 (mocking 용이) | 높음 (hook 자체를 mock 가능) |
Props Drilling | 없음 | 없음 |
재사용성 | 제한적 | 높음 |
리렌더링 부담 | 있을 수 있음 | 상대적으로 적음 |
적용 난이도 | 낮음 | 중간 |
🧵 결론 및 정리
React는 전통적인 DI 컨테이너는 없지만, 의존성 문제는 여전히 존재합니다. 그리고 이를 해결하기 위해 Context API와 Custom Hook을 실용적인 방식으로 사용할 수 있습니다.
🧭 핵심 요약
- 전역 공유 & 관리가 목적이라면 Context API
- 재사용 가능한 캡슐화된 API가 필요하다면 Custom Hook
- 두 방식을 조합하여 사용하는 하이브리드 방식도 권장
- TypeScript를 함께 사용하면 구조화 및 타입 안정성 확보
📌 실무 팁
- Service Interface 정의 → 여러 구현체 테스트 가능
- MockService 만들기 → 테스트 환경에서 context value 주입
- 훅의 리턴 값은 명확한 구조로 만들기 ({ data, loading, error, mutate } 패턴)
✨ 마무리하며
React에서의 의존성 주입은 ‘완전한 DI’가 아니라 ‘실용적인 DI’입니다. 완벽한 추상화보다도 변경에 강하고 테스트하기 좋은 구조를 만드는 것이 중요합니다. 오늘 소개한 두 가지 방식(Context API, Custom Hook)을 잘 활용하면 유지보수성과 생산성을 동시에 잡을 수 있습니다.
'IT' 카테고리의 다른 글
HTTP와 HTTPS의 차이, 그리고 브라우저 자물쇠 아이콘의 진짜 의미 (2) | 2025.07.25 |
---|---|
해시(Hash)란? 해시 값의 개념, 특징, 활용 예시까지 한 번에 정리! (1) | 2025.07.25 |
리액트(React)란? 뜻과 개념부터 오해까지, 프레임워크가 아닌 이유 (1) | 2025.07.24 |
웹사이트 완성도를 좌우하는 세 가지 디테일: 웹폰트 최적화, 파비콘, OG 태그의 개념과 중요성 (1) | 2025.07.24 |
검색되는 웹사이트를 만드는 힘: Next.js SEO 완벽 실무 가이드 (1) | 2025.07.24 |