본문으로 건너뛰기

내 코드가 복잡해지는 이유

· 약 12분
최지훈

들어가며

글을 읽는 이유

  • “렌더링 퍼포먼스를 개선하는 방법”, “좋은 리액트 프로젝트 폴더 구조”, “훅을 잘 사용하는 방법”과 같은 기술에 대한 글은 찾기 쉽습니다. 하지만 “어떻게 하면 프로젝트를 잘 유지할 수 있는가”에 대한 글은 찾기가 어렵습니다.

요약

  1. 개발중 무의식으로 따르는 습관
  • 큰 고민없이 사용하는 상태는 ‘유지보수 하기 어려운 코드’를 만들어 낸다.

  • 리액트에서 다루는 상태란 View의 상태이다.

  • 페이지를 구성하는 값들 중 상태는 무엇인지 잘 구분하고 관리하자.

  1. View 로직과 비지니스 로직
  • 어떻게 보여줄지 논의하는 것은 View 로직이며 이를 제외하면 모두 비지니스 로직이다.
  1. 로직 예시 (input과 조건에 따른 메세지)
  • 비밀번호를 입력 받는 input은 View 로직이며 불일치 조건에 대한 로직은 비지니스 로직이다.

  • 이 두 로직을 함수나 컴포넌트로 잘 분리하는 것이 중요합니다.

  1. 로직 분리 방법 (중요)
  • 먼저 어느 시점에서 비지니스 로직을 통해 데이터를 가져오거나 가공할지 판단하고 해당 데이터를 View 로직의 상태에 담는다.

  • 비지니스 로직은 하위 컴포넌트의 변경에 영향을 받지 않는 페이지 수준에서 관리한다.

  1. 로직 분리 효과
  • 비지니스 상태에 따른 렌더링 흐름을 제어 가능

  • 관심사의 분리로 인한 커뮤니케이션 능률 향상

  • 로직 별 독립적인 테스트 가능

  • 로직 별 독립적인 일정 관리 가능

주요 내용

1. 무의식적 개발 습관

프론트엔드를 개발할 때 습관이 있습니다. 그 중 이 글의 주제와 관련되어있는 대표적인 습관은 상태 관리 입니다. 우린 큰 고민 없이 상태를 사용합니다.

큰 고민없이 사용하는 상태는 ‘유지보수 하기 어려운 코드’를 만들어 냅니다. 이 문제의 가장 근본적인 원인은 상태에 대한 이해에 있습니다.

흔히 리액트에서 다루는 상태란 View의 상태입니다. 즉, 값을 변경하면 View를 업데이트 하는 걸로 간주하여 렌더링을 하게 됩니다. 따라서 페이지를 구성하는 값들 중 상태는 무엇인지 잘 구분하고 관리하는 것만으로도 불필요한 렌더링을 줄이고, 코드를 상당히 개선할 수 있습니다.

2. View 로직과 비지니스 로직

이번 글에선 협업 관점에서 View 로직과 비지니스 로직을 알아보겠습니다.

우리가 생각하는 것보다 View 로직과 비지니스 로직은 명확하게 구분됩니다.

예를 들어, ‘추가 상품을 3개 이상 구매하면 최종 결제 금액에서 1,000원을 제(할인)한다. 보여줄 땐 할인 전 가격과 할인 후 가격을 노출하고, 할인 전 가격은 작고 흐릿하게, 할인 후 가격은 크고 굵게 노출한다.’라고 논의를 진행했다고 하면, 이 문장은 아래와 같이 두 로직으로 분리할 수 있습니다.

  • 비지니스 로직
    추가 상품을 3개 이상 구매하면 최종 결제 금액에서 1,000원을 제(할인)한다.

    = 다른 애플리케이션에서도 성립하는 사업 규칙

  • View 로직
    할인 전 가격과 할인 후 가격을 노출하고, 할인 전 가격은 작고 흐릿하게, 할인 후 가격은 크고 굵게 노출한다.

    = 특정 어플리케이션 자체. 사업 규칙에 강하게 의존하고 변경 가능성이 높다.

즉 우리가 만드는 서비스와 관련된 이야기를 할 때,어떻게 보여줄지 논의하는 것은 View 로직이며, 이를 제외하면 모두 비지니스 로직입니다.

3. 로직 예시 (input과 조건에 따른 메세지)

로직 분리와 관련해서 가장 많은 대화와 피드백을 주고 받은 건 input과 조건에 따른 메세지 입니다.

지금까지 내용을 토대로 input과 에러 메세지를 다루는 간단한 예시를 살펴보겠습니다.

비밀번호를 받는 간단한 input 컴포넌트가 있습니다.

export default function Page() {

return (
<>
<h1>어떤 페이지 입니다.</h1>
...
<form onSubmit={...}>
...
<InputPassword />
...
</form>
...
</>
);
}

export default function InputPassword() {
const [password, setPassword] = useState('');
const [isValid, setIsValid] = useState(false);

const onChangeHandler = (event) => {
setPassword(event.target.value);
setIsValid(event.target.value.length >= 8);
};

return (
<>
<label htmlFor="password">비밀번호</label>
<input
id="password"
type="password"
value={password}
onChange={onChangeHandler}
/>
<p>{isValid ? '' : '비밀번호는 8자 이상 입력해야 합니다.'}</p>
</>
);
}

여기서 ‘비밀번호가 8자 이상이어야 한다.’는 조건은 비지니스 로직 입니다. 그렇기 때문에 아래와 같이 분리하는 것이 좋습니다.

const isValidPassword = (password) => {
if (password.length < 8) {
return false;
}

return true;
};

export default function InputPassword() {
const [password, setPassword] = useState('');
const [isValid, setIsValid] = useState(false);

const onChangeHandler = (event) => {
setPassword(event.target.value);
setIsValid(isValidPassword(event.target.value));
};

return (
<>
<label htmlFor="password">비밀번호</label>
<input
id="password"
type="password"
value={password}
onChange={onChangeHandler}
/>
<p>{isValid ? '' : '비밀번호는 8자 이상 입력해야 합니다.'}</p>
</>
);
}

이제 isValidPassword의 인터페이스가 바뀌지 않는 이상, 비지니스 로직와 관련된 변경 사항은 isValidPassword만 수정하면 됩니다.

예를 들어, 비밀번호의 최소 자릿수가 8자리에서 12자리로 바뀐다면 아래와 같이 isValidPassword만 수정합니다.

const isValidPassword = (password) => {
if (password.length < 12) {
return false;
}
...
};

4. 로직 분리 방법

비지니스 로직은 어떤 수준에서 어떻게 관리되어야 할까요?

가장 먼저 컴포넌트 수준에서 사용되는 건 불가능 하진 않지만 어려운 점이 많습니다.

문제는 한 페이지에서 다루는 비지니스 로직은 컴포넌트 단위로 움직이지 않는다는 사실입니다. 만약 다른 컴포넌트에서 추가 구매와 관련된 비지니스 로직의 상태를 가져와야 한다면, 상위 컴포넌트를 통해 전달하고 전달 받는 방식이 되어야 합니다.

그렇기 때문에 비지니스 로직은 하위 컴포넌트의 변경에 영향을 받지 않는 페이지 수준에서 관리 되어야 합니다.

그렇다면 페이지 수준에서 비지니스 로직을 어떻게 다루면 좋을까요?

페이지의 세션이 유지되는 동안 비지니스 로직의 상태가 유지되도록 해야 합니다.

리액트의 경우 Context API를 활용한 커스텀 hook을 활용하는 방법이 있습니다.

const useMapCount = () => {
const businessLogic = React.useContext(BusinessLogicContext);
const [count, setCount] = React.useState(businessLogic.count);

const setCountIfEven = () => {
if (businessLogic.count % 2 === 0) {
setCount(businessLogic.count);
}
};

return {
count,
increase: businessLogic.increase,
setCountIfEven,
};
}

const Counter = () => {
const { count, increase, setCountIfEven } = useMapCount();

return (
<div>
<button type="button" onClick={() => {
increase();
setCountIfEven();
}}>
increase
</button>
<div>
<div>count in state : {count}</div>
</div>
</div>
);
};

이처럼 비지니스 로직과 View 로직을 분리하면 비지니스 상태에 따른 렌더링 흐름을 제어할 수 있습니다.

또한 비지니스 상태가 렌더링 흐름에 포함되어 있지 않기 때문에 비지니스 로직의 사용을 수정해도 View에 제한적인 영향을 줍니다. 이는 어떤 환경에서든 조금 더 장수할 수 있는 프로젝트를 만드는 데 도움을 줍니다.

5. 로직 분리 효과

  1. 로직 분리의 가장 큰 효과는 관심사의 분리 입니다.
  • 관심사를 잘 분리하면 서비스를 운영하는 구성원들과 소통할 때 커뮤니케이션이 명확해 집니다.
  1. 또 다른 효과는 효율적인 테스트 입니다.
  • 분리 하기 이전엔 테스트를 작성하다보면 View까지 테스트를 작성해야 했습니다. 하지만 이렇게 로직을 분리하면 View와 독립적으로 테스트를 작성할 수 있고, 참고할 좋은 레퍼런스가 충분히 많습니다.
  1. 또 다른 효과는 효율적인 일정 관리 입니다.
  • 로직을 분리하면 비지니스 로직 코드 작성 시간과 뷰 로직 코드 작성 시간을 분리하여 생각할 수 있습니다.