얽힘 인식 훈련: 당신의 코드는 정말 '분리'되었습니까?
여러분은 컴포넌트를 나누고, 커스텀 훅으로 로직을 '추출'하는 데 익숙합니다. 코드가 길어지면 파일을 나누고, 함수를 분리하면 '깔끔해졌다', '추상화했다'고 생각합니다.
하지만 정말 그럴까요? 혹시 이런 경험 없으신가요?
•
분명히 함수를 분리했는데, 수정할 때는 여전히 여러 파일을 오가며 함께 고쳐야 합니다.
•
'재사용'하려고 훅을 만들었는데, 막상 다른 곳에 쓰려니 미묘하게 달라서 결국 복사/붙여넣기 후 수정하거나 수많은 boolean flag, options 인자를 옵셔널하게 받아 인터페이스가 지저분해집니다.
•
간단한 기능 추가인데, 예상치 못한 곳에서 버그가 터집니다.
이런 고통의 근본 원인은 당신이 '분리'는 했지만, 코드 요소들이 서로 '얽혀(Complected)' 있는 것을 보지 못했기 때문입니다. 복잡도를 낮추지는 못한 것입니다.
'얽힘'이란 무엇인가: 단순함(Simple)의 적
Rich Hickey는 복잡성(Complexity)의 어원이 'Complect(함께 엮다)'라고 말했습니다. 얽힌 코드는 마치 여러 가닥의 실이 복잡하게 엉킨 실타래와 같습니다. 
•
겉보기: 실(함수/컴포넌트)은 분리되어 있는 것처럼 보입니다.
•
실체: 한 가닥(수정)을 당기면 다른 가닥들이 함께 딸려오거나 더 엉켜버립니다.
•
결과: 풀기(이해/수정/테스트)가 극도로 어렵습니다.
우리가 추구해야 할 '단순함(Simple)'은 '엮이지 않음'을 의미합니다. 반면, 우리가 흔히 착각하는 '쉬움(Easy)'은 '친숙함'일 뿐, 얽힘을 해결해주지 않습니다. 오히려 '쉬운' 방법(e.g., 무지성 useContext 사용)이 코드를 더 심하게 얽히게 만들 때가 많습니다.
미니 연습: 코드 속 '얽힘' 징후 찾기
본격적으로 사례를 살펴보기에 앞서, 간단한 코드 조각들에서 '얽힘'의 징후를 찾아보는 연습 문제를 풀어봅시다.
•
예시 1: 책임의 얽힘
// 버튼 클릭 시 사용자 데이터 저장 및 알림 표시
function handleSaveProfile(userData) {
// 책임 1: 데이터 저장 (API 호출 등)
api.saveUserProfile(userData)
.then(() => {
// 책임 2: 성공 알림 표시 (UI 로직)
showSuccessToast('Profile saved!');
})
.catch(error => {
// 책임 3: 에러 로깅 (부가 기능)
logError(error);
// 책임 4: 실패 알림 표시 (UI 로직)
showErrorToast('Save failed!');
});
}
TypeScript
복사
◦
이것은 어떤 얽힘에 해당할까요? (힌트: 책임 분리 실패)
▪
이 handleSaveProfile 함수는 이름("프로필 저장")과 달리 몇 가지 서로 다른 책임(데이터 처리, UI 피드백, 로깅)을 동시에 수행하고 있나요?
▪
만약 나중에 알림 방식(showSuccessToast)만 바꾸고 싶다면, 데이터 저장 로직(api.saveUserProfile)까지 함께 건드려야 하는 이유는 무엇일까요?
•
예시 2: 구현과 의존성의 얽힘
import { globalUserStore } from './userStore'; // 전역 스토어 직접 임포트
function UserAvatar({ size = 'medium' }) {
// 컴포넌트가 전역 스토어의 '구현'에 직접 의존
const user = globalUserStore.useCurrentUser(); // 스토어 내부 구조(useCurrentUser)를 알아야 함
if (!user) return null;
const style = { /* ... size에 따른 스타일 ... */ };
return <img src={user.avatarUrl} alt={user.name} style={style} />;
}
TypeScript
복사
◦
이 컴포넌트의 **'구현'**은 무엇과 '얽혀' 있나요? (힌트: 특정 라이브러리 종속성):
▪
이 UserAvatar 컴포넌트는 user 데이터를 props로 받지 않고 전역 스토어에서 직접 가져옵니다. 만약 다른 프로젝트에서 이 아바타 컴포넌트만 재사용하고 싶거나, 스토어 라이브러리(globalUserStore)를 다른 것으로 교체한다면 어떤 문제가 발생할까요?
미니 연습 문제에 대한 정답과 해설입니다.
미니 연습 정답 및 해설
예시 1: 책임의 얽힘 (handleSaveProfile)
// 버튼 클릭 시 사용자 데이터 저장 및 알림 표시
function handleSaveProfile(userData) {
// 책임 1: 데이터 저장 (API 호출 등)
api.saveUserProfile(userData)
.then(() => {
// 책임 2: 성공 알림 표시 (UI 로직)
showSuccessToast('Profile saved!');
})
.catch(error => {
// 책임 3: 에러 로깅 (부가 기능)
logError(error);
// 책임 4: 실패 알림 표시 (UI 로직)
showErrorToast('Save failed!');
});
}
TypeScript
복사
질문: 이 handleSaveProfile 함수는 이름("프로필 저장")과 달리 몇 가지 서로 다른 책임(데이터 처리, UI 피드백, 로깅)을 동시에 수행하고 있나요? 만약 나중에 알림 방식(showSuccessToast)만 바꾸고 싶다면, 데이터 저장 로직(api.saveUserProfile)까지 함께 건드려야 하는 이유는 무엇일까요? 이것이 어떤 **'얽힘'**에 해당할까요?
정답:
•
이 함수는 이름과 달리 최소 4가지 책임(데이터 저장, 성공 알림, 에러 로깅, 실패 알림)을 동시에 수행하고 있습니다.
•
알림 방식(showSuccessToast, showErrorToast)을 바꾸려면 이 함수 내부를 수정해야 합니다. 왜냐하면 데이터 저장 로직과 UI 피드백 로직이 하나의 함수 안에 강하게 결합되어 있기 때문입니다.
•
이는 **책임의 얽힘 (Responsibility Entanglement)**에 해당합니다. 서로 다른 이유로 변경될 수 있는 로직들이 하나의 단위 안에 뒤섞여 있어, 한 부분을 수정할 때 다른 부분까지 영향을 받게 됩니다. 이상적으로는 데이터 저장 책임과 사용자 피드백 책임은 분리되어야 합니다.
예시 2: 구현과 의존성의 얽힘 (UserAvatar)
import { globalUserStore } from './userStore'; // 전역 스토어 직접 임포트
function UserAvatar({ size = 'medium' }) {
// 컴포넌트가 전역 스토어의 '구현'에 직접 의존
const user = globalUserStore.useCurrentUser(); // 스토어 내부 구조(useCurrentUser)를 알아야 함
if (!user) return null;
const style = { /* ... size에 따른 스타일 ... */ };
return <img src={user.avatarUrl} alt={user.name} style={style} />;
}
TypeScript
복사
질문: 이 UserAvatar 컴포넌트는 user 데이터를 props로 받지 않고 전역 스토어에서 직접 가져옵니다. 만약 다른 프로젝트에서 이 아바타 컴포넌트만 재사용하고 싶거나, 스토어 라이브러리(globalUserStore)를 다른 것으로 교체한다면 어떤 문제가 발생할까요? 이 컴포넌트의 **'구현'**은 무엇과 '얽혀' 있나요?
정답:
•
다른 프로젝트에서 재사용하려면, 그 프로젝트에도 똑같은 globalUserStore 구현체가 있어야 합니다. 그렇지 않으면 컴포넌트가 동작하지 않습니다. 스토어 라이브러리를 교체한다면, globalUserStore.useCurrentUser() 호출 방식이 달라질 수 있으므로 이 컴포넌트 내부 코드를 수정해야 합니다.
•
이 컴포넌트의 '구현'(데이터를 가져오는 방식, user 객체의 구조 사용 등)은 globalUserStore라는 **특정 외부 라이브러리(또는 모듈)의 '구현 세부사항'**과 강하게 얽혀 있습니다.
•
이는 **구현과 의존성의 얽힘 (Implementation & Dependency Entanglement)**에 해당합니다. 컴포넌트가 외부 의존성을 사용하는 방식이 너무 구체적이어서, 해당 의존성이 변경되거나 없는 환경에서는 컴포넌트의 재사용성 및 유지보수성이 크게 저하됩니다. 이상적으로는 UserAvatar는 user 데이터를 props로 받아야 하며, 데이터를 어디서 어떻게 가져오는지는 몰라야 합니다.
변경을 가정해보면 얽힘이 드러난다
내 코드의 '얽힘'을 가장 확실하게 진단하는 방법은 변경을 가정해보는 것입니다.
만약 작은 요구사항 변경이 코드베이스 전반에 걸쳐 큰 파급 효과를 일으킨다면, 당신의 코드는 심각하게 얽혀있다는 증거입니다.
이제, 여러분이 "잘 분리했다"고 생각할 만한 익숙한 코드를 통해 이 '변경' 시험을 직접 경험해봅시다.
사례 연구: '분리된' 데이터 페칭 로직의 함정
다음 코드를 봅시다. useEffect 내부의 데이터 페칭 로직을 fetchData 함수로 '추출'했습니다. 깔끔해 보이나요?
// ExampleComponent.jsx
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
// ✨ "분리된" 함수 ✨
async function fetchData(setData, navigate) { // (A)
try {
const response = await fetch('<https://some-api.com/data>');
if (!response.ok) throw new Error('Network response was not ok');
const result = await response.json();
setData(result.items); // (B) 상태 업데이트 직접 수행
if (result.needsRedirect) { // (C) 조건부 페이지 이동 로직 포함
navigate('/redirect-page');
}
} catch (error) {
console.error("Fetch error:", error);
// TODO: 에러 처리 UI 로직 필요
}
}
function ExampleComponent() {
const [items, setItems] = useState([]); // 데이터 상태
const navigate = useNavigate(); // 라우터 의존성
useEffect(() => {
fetchData(setItems, navigate); // 분리된 함수 호출
}, [navigate]); // navigate가 바뀌면 다시 호출? 🤔
// ... items를 사용하여 UI 렌더링 ...
return <div>{/* ... */}</div>;
}
export default ExampleComponent;
JavaScript
복사
여러분의 현재 멘탈 모델: "좋아, 로직을 함수로 뺐으니 재사용 가능하고 테스트하기 쉽겠군!"
[학습 과학 Point] 기존 지식(함수 분리=좋음)을 활성화했습니다. 이제 여기에 '인지 부조화'를 일으킬 차례입니다.
자, 이제 이 "깔끔하게 분리된" fetchData 함수에 다음 변경 요구사항들을 적용해 보세요. 머릿속으로 코드를 어떻게 수정해야 할지, 어떤 문제들이 발생할지 상상해 보세요.
•
변경 1: 재사용성
다른 컴포넌트(AnotherComponent)에서 이 fetchData 함수를 재사용하고 싶습니다. 그런데 AnotherComponent에서는 페이지 이동(navigate) 기능이 전혀 필요 없습니다. fetchData(setAnotherData, ???)를 어떻게 호출해야 할까요? Maps 자리에 무엇을 넣어야 코드가 어색하지 않고 안전하게 동작할까요? 
•
변경 2: 독립적인 테스트
fetchData 함수 자체의 네트워크 로직만 테스트하고 싶습니다. React나 React Router 라이브러리 없이, 순수 JavaScript 테스트 환경에서 이 함수를 어떻게 테스트할 수 있을까요? setData와 navigate는 어떻게 처리해야 할까요? 
•
변경 3: 기능 확장
ExampleComponent에 **데이터 로딩 중 상태(isLoading)**와 **에러 발생 상태(error)**를 추가하여 사용자에게 피드백을 주고 싶습니다. fetchData 함수를 어떻게 수정해야 할까요? setLoading과 setError도 인자로 추가해야 할까요? 아니면 ExampleComponent의 useEffect 내부에서 처리해야 할까요? 어느 쪽이 더 자연스럽고 유지보수하기 좋을까요? 
이제부터 전문가의 렌즈를 끼고 fetchData 함수를 해부해 볼게요.
머릿속 사고 과정을 따라가 봅시다.
전문가의 fetchData 함수 분석 (사고 과정)
•
1단계: 첫인상 및 책임 식별
"음, fetchData라는 이름이군. 데이터를 가져오는 함수겠지? 파라미터로 setData와 navigate를 받네... 잠깐, 데이터를 가져오는 함수가 왜 상태 설정 함수랑 네비게이션 함수를 직접 받지? 이름과 실제 역할이 불일치하는데?
"
함수를 훑어보며 책임을 나눠봅니다.
◦
(Layer 1) 네트워크 통신: fetch 호출, response.ok 확인, response.json() 호출. (데이터 가져오기)
◦
(Layer 2) 상태 업데이트: setData(result.items) 호출. (UI 상태 반영)
◦
(Layer 3) 라우팅 (조건부): if (result.needsRedirect) ... navigate(...) 호출. (애플리케이션 흐름 제어)
◦
(Layer 4) 에러 처리/로깅: try...catch, console.error. (예외 처리 및 알림)
불편함 감지: "이 함수 하나에 최소 4가지 다른 종류의 책임(추상화 레이어)이 뒤섞여 있네. 각 레이어는 변경되는 이유와 주기가 다를 텐데?"
•
2단계: '얽힘' 식별 및 불편한 지점 구체화
각 책임들이 어떻게 서로 얽혀있는지 구체적으로 짚어봅니다.
◦
[불편함 1] Layer 1 (네트워크)과 Layer 2 (상태)의 얽힘:
▪
setData(result.items): 네트워크 응답(result)의 특정 필드(items)를 직접 알아야 하고, React의 상태 설정 함수(setData) 구현에 직접 의존합니다.
▪
사고: "만약 API 응답 구조가 result.data.list로 바뀌면? 이 함수 고쳐야 하네. 만약 setData가 아니라 Redux dispatch를 써야 하면? 이 함수 또 고쳐야 하네. 네트워크 로직과 상태 업데이트 로직이 강하게 결합되어 있군." 
◦
[불편함 2] Layer 1 (네트워크)과 Layer 3 (라우팅)의 얽힘:
▪
if (result.needsRedirect): 네트워크 응답(result)의 특정 필드(needsRedirect)를 직접 알아야 하고, React Router의 네비게이션 함수(navigate) 구현에 직접 의존합니다.
▪
사고: "데이터 구조가 바뀌면 라우팅 로직도 깨지네. 다른 라우터 라이브러리(e.g., TanStack Router)를 쓰면 이 함수는 못 쓰겠군. 데이터 페칭 책임과 페이지 이동 책임이 엉뚱하게 얽혀있어." 엮는 중]
◦
[불편함 3] Layer 1 (네트워크)과 Layer 4 (에러 처리)의 얽힘:
▪
console.error: 에러 처리 방식이 '콘솔 출력'으로 하드코딩되어 있습니다.
▪
사고: "나중에 에러를 Sentry로 보내거나 사용자에게 토스트 메시지로 보여줘야 하면? 이 함수 또 수정해야 하네. 핵심 로직과 부가적인 에러 리포팅 방식이 분리되지 않았어." 
◦
[불편함 4] 테스트의 어려움 (모든 얽힘의 결과):
▪
사고: "이 함수 하나 테스트하려면 fetch 모킹하고, 가짜 setData 만들고, 가짜 navigate 만들고... 배보다 배꼽이 더 크겠는데? 각 레이어를 독립적으로 테스트할 수가 없잖아." 
•
3단계: 변경 가능성 감지 및 미래 예측
이런 '얽힘'이 미래에 어떤 문제를 일으킬지 예상해 봅니다.
◦
[예측 1] 재사용 시나리오: "다른 페이지에서는 데이터를 가져오기만 하고, 라우팅은 안 하고 싶을 텐데? setData 대신 다른 방식으로 상태를 관리하고 싶을 텐데? 이 함수는 재사용이 거의 불가능하겠군."
◦
[예측 2] 기능 확장 시나리오: "로딩 상태 보여주려면? 취소 기능 넣으려면? 캐싱하려면? 이 함수 구조로는 확장하기가 너무 복잡해. 여기저기 다 건드려야 할 거야."
◦
[예측 3] 구현 변경 시나리오: "fetch 대신 axios로 바꾸려면? React Router 대신 다른 거 쓰려면? 상태 관리를 Zustand로 바꾸려면? 이 함수는 변경에 너무 취약해."
결론: 전문가의 진단
"이 fetchData 함수는 '분리'는 되었을지 몰라도, 최소 4개의 다른 추상화 레이어(네트워크, 상태, 라우팅, 에러 처리)가 심각하게 얽혀있다. 각 레이어는 서로의 내부 구현에 너무 깊이 의존하고 있어, 변경에 취약하고, 재사용이 불가능하며, 테스트하기 어렵다. 이름(fetchData)과 달리 너무 많은 책임을 떠안고 있으며, 이는 명백한 SRP(단일 책임 원칙) 위반이다. 이 코드는 **단순함(Simple)**과는 거리가 멀고, 단지 **익숙한 방식('Easy')**으로 작성되었을 뿐이다."
처방: 각 레이어(책임)를 독립적인 모듈로 분리하고, 서로의 구현이 아닌 추상화된 인터페이스에 의존하도록 리팩토링해야 한다. (e.g., fetchData는 오직 데이터만 반환하고, 상태 업데이트와 라우팅은 호출하는 쪽에서 처리하도록 책임을 위임한다.)
어떤가요? 이 사고 과정이 전문가가 코드를 보며 느끼는 불편함과 '얽힘'을 감지하는 과정을 조금 더 생생하게 보여주나요?
리팩토링: TanStack Query v5 활용
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import {
QueryClient,
QueryClientProvider,
useSuspenseQuery,
queryOptions
} from '@tanstack/react-query';
// --- 레이어 1: 순수 데이터 페칭 (변경 없음) ---
const getSomeData = async () => {
console.log('Fetcher: Fetching data from API...');
const response = await fetch('<https://some-api.com/data>');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
};
// --- 레이어 2: 쿼리 정의 (변경 없음) ---
const someDataQueryOptions = queryOptions({
queryKey: ['someData'],
queryFn: getSomeData,
});
// --- QueryClient 인스턴스 (loader에서 접근 가능해야 함) ---
// 실제 앱에서는 모듈 스코프나 다른 방식으로 queryClient에 접근
const queryClient = new QueryClient();
// --- 라우트 로더 (Loader) ---
const exampleLoader = async () => {
console.log('Loader: Ensuring query data...');
// ensureQueryData가 실패하면 에러를 던지고, react-router가 처리할 것임
const data = await queryClient.ensureQueryData(someDataQueryOptions);
// 데이터 기반 리다이렉션 체크
if (data.needsRedirect) {
console.log('Loader: Redirecting...');
return redirect('/redirect-page');
}
console.log('Loader: Data ensured, proceeding to render.');
return data; // or null
};
// --- 레이어 3: 컴포넌트 (UI 렌더링) ---
// 이제 다시 useSuspenseQuery를 사용해도 loader가 캐싱을 보장
function ExampleComponent() {
// loader가 데이터를 캐시에 넣어줬으므로, useSuspenseQuery는 캐시된 데이터를 즉시 반환 (Suspense 불필요)
// 또는 loaderData를 사용해도 됨: const initialData = useLoaderData();
const { data } = useSuspenseQuery(someDataQueryOptions);
// 리다이렉션 로직은 loader로 이동했으므로 useEffect 불필요
return (
<div>
<h2>데이터 로드 완료:</h2>
<ul>
{data?.items?.map((item, index) => (
<li key={index}>{/* 아이템 렌더링 */}</li>
))}
</ul>
</div>
);
}
// --- 애플리케이션 레이아웃 및 에러/로딩 처리 ---
function RootLayout() {
return (
<ErrorBoundary fallbackRender={({ error }) => <div>데이터 로딩 에러: {error.message}</div>}>
{/* Suspense는 여전히 필요 (초기 로드 또는 다른 비동기 컴포넌트 대비) */}
<Suspense fallback={<div>페이지 로딩 중...</div>}>
<Outlet />
</Suspense>
</ErrorBoundary>
);
}
// --- 라우터 설정 ---
const router = createBrowserRouter([
{
path: '/',
element: <RootLayout />,
children: [
{
index: true,
loader: exampleLoader,
element: <ExampleComponent />,
},
{
path: '/redirect-page',
element: <div>리다이렉션 되었습니다!</div>,
},
],
},
]);
// --- 애플리케이션 진입점 ---
function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
);
}
export default App;
TypeScript
복사
식별된 레이어
1.
순수 데이터 페칭 레이어 (getSomeData):
•
책임: 특정 엔드포인트에 대해 순수하게 네트워크와 상호작용합니다. React, 라우팅, 상태 관리, 애플리케이션 흐름에 대해 아무것도 모릅니다. 재사용성이 높고 테스트 가능하며 독립적인 함수입니다. 유일한 임무는 데이터를 가져오거나 네트워크 에러를 던지는 것입니다.
•
비유: 특정 창고(URL)에 가서, 물건(fetch)을 픽업하고, 파손 여부(ok)를 확인한 뒤, 안에 든 것(json)을 그대로 가져오는 배달 기사. 왜 물건이 필요한지, 그 물건으로 무엇을 할지는 모릅니다. 
1.
쿼리 정의 레이어 (someDataQueryOptions):
•
책임: react-query에게 데이터를 어떻게 식별하고(queryKey) 어떻게 가져올지(queryFn) 알려주는 명세서 역할입니다. 캐싱이나 상태 관리는 직접 하지 않고 react-query에 위임합니다.
•
비유: 창고 관리자에게 전달하는 주문서. 어떤 물건(queryKey)을 어떤 배달 기사(queryFn)를 통해 가져와야 하는지 명시합니다. 실제 재고 관리나 배송 추적은 관리자가 알아서 합니다. 
1.
라우트 로딩 및 흐름 제어 레이어 (exampleLoader):
•
책임: 해당 라우트가 렌더링되기 전에 필요한 데이터를 **미리 확보(ensureQueryData)**하여 react-query 캐시에 넣고, 데이터 조건에 따라 **페이지 이동(redirect)**과 같은 애플리케이션 흐름을 제어합니다. 컴포넌트 렌더링 로직과 데이터 로딩/흐름 제어 로직을 분리합니다.
•
비유: 상점 개점 전 입고 담당자. 창고 관리자(queryClient)에게 주문서(someDataQueryOptions)를 보여주며 물건 확보를 요청(ensureQueryData)합니다. 물건에 특별 지시(needsRedirect)가 있으면, 고객을 바로 다른 곳(redirect)으로 안내하고 상점 진열은 진행하지 않습니다. 문제가 없으면 물건이 준비되었음을 알리고 개점을 허락합니다. 
1.
UI 렌더링 레이어 (ExampleComponent의 JSX):
•
책임: loader가 데이터를 준비해 준 후, react-query 훅(useSuspenseQuery)을 통해 캐시된 데이터를 받아 UI를 선언적으로 렌더링하는 데만 집중합니다. 데이터 로딩 상태나 조건부 리다이렉션 로직은 더 이상 알 필요가 없습니다. 로딩/에러 표시는 상위의 <Suspense> / <ErrorBoundary>에 위임합니다.
•
비유: 상점 진열 담당자. 입고 담당자가 물건 준비를 끝내면, 창고(cache)에서 최신 물건(useSuspenseQuery 결과)을 가져와 예쁘게 진열(JSX)합니다. 물건이 아직 오는 중이거나(Suspense) 문제가 생겼을 때(ErrorBoundary)의 안내는 다른 담당자가 처리합니다. 
이 업데이트된 계층 구조는 loader의 도입으로 데이터 로딩 시점과 흐름 제어 책임이 컴포넌트 외부로 명확하게 분리되었음을 보여줍니다. 이를 통해 컴포넌트는 더욱 렌더링에만 집중하는 단순한 구조가 되었습니다.
충격! 그리고 새로운 렌즈 
어떤가요? 미니 예제들과 fetchData 사례 연구를 통해, "깔끔하게 분리했다"고 생각했던 코드들이 사실은 온갖 책임과 의존성, 시간 축 문제로 **'얽혀있었다'**는 사실이 보이시나요? 이것이 바로 여러분이 매일 마주하지만, 인식하지 못했던 **'보이지 않는 복잡성'**입니다.
이 '얽힘'을 인식하는 것이 추상화의 첫걸음입니다. '분리'와 '추출'이라는 기존 렌즈를 버리고, **'얽힘 식별'**이라는 새로운 렌즈를 껴야 합니다.
이제 여러분은 코드를 볼 때 이렇게 질문해야 합니다:
•
책임의 얽힘: 이 함수/컴포넌트는 단 하나의 명확한 책임만 가지고 있는가? 아니면 여러 책임이 뒤섞여 있는가?
•
구현과 의존성의 얽힘: 이 함수/컴포넌트는 특정 라이브러리나 외부 환경의 구체적인 구현에 직접 의존하고 있는가?
•
시간과 상태의 얽힘: 비동기 작업이나 사용자의 인터랙션 과정에서 여러 상태(loading, data, error)가 시간 축에 따라 어떻게 변하고 관리되는가? 그 관리가 한 곳에서 응집력 있게 이루어지는가, 아니면 여러 곳에 흩어져 얽혀있는가?
이 질문들을 통해 코드의 '얽힘'을 발견하고, 그것을 풀어내는 과정이 바로 우리가 앞으로 탐구할 **'진짜 추상화'**입니다. 여러분의 낡은 멘탈 모델은 이제 폐기될 준비가 되었습니다. 새로운 시각을 탑재할 시간입니다. (다음 챕터 예고: FE 복잡도 유형 상세 분석)
