1. 서론
개발을 하면서 제일 많이 하는 일은 기능 추가일 것이다. 물론 백지상태에서 새로운 프로젝트만을 개발하는 개발자들도 있겠지만, 기존의 프로젝트에 기능을 추가하거나 기존의 서비스를 더 발전시키는, 기능을 추가하는, 개발자가 더 많을 것이라고 생각한다.
대부분의 개발자들은 새로운 프로젝트, 백지상태에서 개발하는 것을 더욱 선호할 것 같다. 왜냐하면 기존의 코드들, 레거시 코드와 라이브러리 의존성 등이 기능 추가에 방해가 되는 경우가 많기 때문이다. 특히 나는 두 달전 Node.js contribute를 할 적에 window 환경의 reserved namespace 관련 버그 수정을 하는데 기존의 테스트 코드, 다른 코드들이 그 잘못된 동작이 정상적인 동작이라고 가정하고 짜여진 코드가 많아 breaking change를 만들지 않고 버그 수정을 하는데 꽤나 애를 먹었던 경험이 있다.
그래서 코드를 짤때 기능 구현, 테스트 커버리지, 퍼포먼스, 오류율도 중요하지만 결합도를 줄이는 것이 특히 중요하다고 생각한다. 왜냐하면 서로 종속된 코드들에서 새로운 기능을 추가하는 것은 너무 어렵기 때문이다. 그런데 결합도를 줄이는 것도 중요하지만 관심사 분리도 중요하다. 왜냐하면 하나의 함수가 너무 길어지면, 응집도를 높혀서가 아닌, 해당 함수의 기능이 너무 커져 단일 책임의 원칙을 어기게 되고 해당 함수가 무슨 일을 하는지 파악하기 어려워지기 때문이다.
그런데 여기서 한가지 모순이 생겼다. 과거 네이버 부스트캠프 챌린지 과정에서 조건 판별을 통해 분기해야 되는 함수를 만들 일이 있었다. 해당 함수를 구현해야 한다는 것은 명료하여 팀원들 모두가 해당 과제에서 조건을 판별하여 분기시키는 함수를 만들었었다. 그러나 기능 요구 사항에 하나의 함수가 10줄이 넘어서는 안된다는 조건이 있어, 팀원들은 모두 해당 함수에서 조건을 판별하는 코드를 별도의 함수로 분리하였다. 그러나 나는 그렇게 하지 않았었는데, 왜냐하면 그렇게 하면 관심사 분리는 되지만 두 함수간에 제어 결합이 생기기 때문이였다. 또한 조건처리를 통한 분기처리를 하나의 함수안에서 하는 것이 응집도를 높히는 행동이라고 생각하기도 했다. 왜냐하면 node.js contribute를 할 적에 window, linux의 경우 모두 조건 분기와 그에 따른 처리를 하나의 함수에서 하는 경우를 많이 보았기 때문이였다.
그러나 만약 조건을 판별하는 행동이 다른 함수들에서도 사용되어야 한다면 그 조건 판별을 별도의 함수로 분리하여 하는 것이 맞을 것이다. 그러나 그로 인해 필연적으로 생기는 제어 결합, 그로 인한 결합도를 어떻게 낮출 수 있을까라는 고민이 항상 있었다. 이 글에서는 그에 대한 내 나름의 답을 적어보고자 한다. 물론 나는 front end engineer이고, Ui library로 React를 사용하기에 react의 경우에 대해 써본다.
2. Custom hook 사용을 통한 결합도 줄이기
객체지향 프로그래밍에서는 주로 인터페이스와 의존성 주입 등을 통해 결합도를 낮추는 것 같았다. 그러나 react에서는 custom hook을 통해 관심사를 분리하고, 하나의 기능에 집중하며, 결합도를 낮추는 것이 좋은 방법같았다. 왜냐하면 나는 react를 fp 패러다임을 사용하고, 함수형 컴포넌트를 사용하여 개발하기때문이다. react의 Hook은 함수형 컴포넌트에서 상태(state)와 생명주기(lifecycle) 기능을 사용할 수 있게 하는 기능이다. 이를 통해 재사용 가능한 상태 관리 로직을 만들 수 있어 자주 사용되는 기능이다. 또한 Hook은 state 그 자체가 아닌 state 저장 로직을 공유하도록 하여, state를 로컬하게 분리하고 그 상태 관리 로직만 공유하여 높은 추상화와 낮은 결합도를 달성할 수 있게 해준다.
위에서 내가 고민한 조건문을 별도의 함수로 분리하여, 여기서는 Hook으로 분리하여, 결합도를 낮춰보고자 한다.
2.1 custom hook 작성
// useValidateUser.js
function useValidateUser(user) {
const isValidUser = user.isMember && user.age > 18 && user.purchaseHistory.length > 0;
return isValidUser;
}
export default useValidateUser;
useValidateUser 훅을 이용하면 조건 판별 로직이 컴포넌트에서 분리되어 결합도가 줄어들게 된다. 조건이 변경되더라도 훅만 수정하면 되므로 유지보수가 용이하다. state나 Effect를 안쓴 이유는 해당 조건은 prop으로 받은 User에 의해 결정되는 상태이기 때문이다. 따라서 별도의 state로 저장하지 않아도 되고, effect도 아니다(순수하게 렌더링 중에 계산될 수 있다.)
Component에서는 hook을 호출하여 쓰면 된다.
// ValidateButton.js
import React from 'react';
import useValidateUser from './useValidateUser';
function ValidateButton({ user }) {
const isValid = useValidateUser(user);
return (
<button disabled={!isValid}>
{isValid ? "Valid for Discount" : "Not Valid"}
</button>
);
}
export default ValidateButton;
이렇게 Hook을 사용하는 것이 제일 무난한, 기본적인 코드일 것이다. 이제 이 Hook을 업그레이드해보고자 한다.
2.2 조건 판별 확장
조건 판별 로직을 유연하게 사용하도록 useValidateUser 훅을 수정하였다. 조건을 component에서 관리하는 것이 관심사 분리에 위배된다고 생각할 수 있지만, 각 component별로 판별하는 조건이 다르다고 가정하였다. 이를 통해 hook을 좀 더 추상화하여 다양한 component에서 사용할 수 있게 만들었다. javascript에서 함수도 객체이기에 화살표 함수들을 배열의 원소로 사용하여 prop으로 전달하는 식으로 만들었다.
// useValidateUser.js
function useValidateUser(user, conditions) {
const isValidUser = conditions.every(condition => condition(user));
return isValidUser;
}
export default useValidateUser;
컴포넌트에서는 조건을 컴포넌트별로 관리하여 해당 hook에 Prop으로 전달하여 사용할 수 있다.
// ValidateButton.js
import React from 'react';
import useValidateUser from './useValidateUser';
function ValidateButton() {
const { user } = useUser();
// 조건을 함수로 정의하여 배열로 전달
const conditions = [
user => user.isMember,
user => user.age > 18,
user => user.purchaseHistory.length > 0
];
const isValid = useValidateUser(user, conditions);
return (
<button disabled={!isValid}>
{isValid ? "Valid for Discount" : "Not Valid"}
</button>
);
}
export default ValidateButton;
2.3 Suspense와 Error Boundary 사용
import { useState, useEffect } from 'react';
function useValidateUser(user) {
const [isValidUser, setIsValidUser] = useState(null);
useEffect(() => {
// 비동기 함수 정의
async function validateUser() {
try {
const response = await fetch(`/api/validateUser?userId=${user.id}`);
if (!response.ok) {
throw new Error('Failed to fetch user validation data');
}
const data = await response.json();
const isValid = data.isMember && data.age > 18 && data.purchaseHistory.length > 0;
setIsValidUser(isValid);
} catch (error) {
setIsValidUser(null);
throw error; // 에러 발생 시, ErrorBoundary에서 catch 가능
}
}
validateUser();
}, [user]);
if (isValidUser === null) {
// Suspense를 사용하여 로딩 상태를 처리하기 위해 Promise를 throw
throw new Promise((resolve, reject) => {
validateUser()
.then(resolve)
.catch(reject);
});
}
return isValidUser;
}
export default useValidateUser;
마지막으로 처리 실패와 로딩상태를 처리하기 위해 hook에서 promise를 return하도록 변환해보았다. client compoentn이기에 비동기를 effectf를 이용해 처리하였다. 물론 조건을 판별할 때 비동기 호출등을 사용 안할 수 있지만 조건판별문이 길어지져서 하나의 컴포넌트가 전체 컴포넌트의 렌더링을 block할 수도 있고, 데이터 패치를 통해 조건을 판별할 수도 있기에 그런 가정하에 코드를 작성해보았다. react 18부터 suspense는 react.lazy뿐만 아니라 promise도 catch하기에 React 18부터 사용 가능한 코드이다.
import React, { Suspense } from 'react';
import useValidateUser from './useValidateUser';
import ErrorBoundary from './ErrorBoundary';
function ValidateButton({ user }) {
const isValid = useValidateUser(user);
return (
<button disabled={!isValid}>
{isValid ? "Valid for Discount" : "Not Valid"}
</button>
);
}
function App() {
const user = { id: 1, isMember: true, age: 25, purchaseHistory: ['item1', 'item2'] };
return (
<ErrorBoundary>
<Suspense fallback={<p>Loading...</p>}>
<ValidateButton user={user} />
</Suspense>
</ErrorBoundary>
);
}
export default App;
3. References
'React' 카테고리의 다른 글
React.memo에 대하여 (1) | 2024.11.16 |
---|---|
React 18에서 변경된 Suspense (1) | 2024.11.04 |
React Custom Hook에 대하여 (1) | 2024.10.28 |
React에서 virtual dom을 쓰는 이유 (2) | 2024.10.07 |
React 19의 새로운 기능들 (1) | 2024.09.27 |