또 이 둘은 어떻게 구분해야할까?
A sphere of knowledge, influence, or activity. The subject area to which the user applies a program is the domain of the software. — Eric Evans³
“우리가 만드는 서비스와 관련된 이야기를 할 때, 어떻게 보여줄지 논의하는 걸 제외하면 모두 도메인, 즉 비지니스 로직이다.”
이 글을 작성하는데 참고한 글에서는 위와같이 설명한다.
엽떡을 방문포장할 경우 최종 결제 금액에서 3,000원을 할인한다.
앱에서 할인 전 가격과 할인 후 가격을 노출하되
할인 전 가격은 작고 흐릿하게, 할인 후 가격은 크고 굵게 표시한다.
위의 조건은 아래와 같이 두 가지로 분리할 수 있다.
B: 방문포장할 경우 최종 결제 금액에서 3,000원을 할일한다.
V: 할인 전 가격과 할인 후 가격을 노출하고, 할인 전 가격은 작고 흐릿하게, 할인 후 가격은 크고 굵게 노출한다.
B는 우리가 만드는 애플리케이션이 사용되는 영역으로, 사업 규칙(비지니스로직)이다.
V(View로직)은 애플리케이션 그 자체로, 사업규칙에 의존하고 변경될 가능성이 높다.
또한 사업규칙을 변경하는 것 보다 보여주는 방법을 변경하는 것이 쉽다.
로직 분리, 값을 다루는데 어려움을 많이 겪는 케이스의 대표적인 예제로 input과 에러메시지가 있다.
다음은 간단한 비밀번호를 받는 input이다.
export default function Page() {
const [password, setPassword] = useState('');
const [isValid, setIsValid] = useState(false);
const handleChange = (e) => {
setPassword(e.target.value);
setIsValid(e.target.value.length >= 6);
};
return (
<>
<h1>어떤 페이지 입니다.</h1>
...
<label htmlFor="password">비밀번호</label>
<input
id="password"
type="password"
value={password}
onChange={handleChange}
/>
<p>{isValid ? '' : '비밀번호는 6자 이상 입력해야 합니다.'}</p>
...
</>
);
}
보통 하나의 input으로 하나의 값을 받는다고 하면 위와 같이 작성했을 것이다.
그런데 아이디, 이메일, 이름 등 input의 갯수가 많아진다면
하나의 상태가 업데이트되면 페이지를 전체를 렌더링 하면서 퍼포먼스가 떨어지게된다.
그래서 다음과 같이 별도의 컴포넌트로 분리하게된다.
export default function Page() {
const [password, setPassword] = useState('');
const [isValid, setIsValid] = useState(false);
const handleChange = (e) => {
setPassword(e.target.value);
setIsValid(e.target.value.length >= 6);
};
return (
<>
<h1>어떤 페이지 입니다.</h1>
...
<form onSubmit={...}>
...
<InputPassword />
...
</form>
...
</>
);
}
export default function InputPassword() {
const [isValid, setIsValid] = useState(false);
const handleChange = (event) => {
setIsValid(event.target.value.length >= 6);
};
return (
<>
<label htmlFor="password">비밀번호</label>
<input
id="password"
type="password"
onChange={handleChange}
/>
<p>{isValid ? '' : '비밀번호는 6자 이상 입력해야 합니다.'}</p>
</>
);
}
이렇게 분리해야 password가 업데이트 되어도
const isValidPassword = (password) => {
if (password.length < 6) {
return false;
}
return true;
};
export default function InputPassword() {
const [isValid, setIsValid] = useState(false);
const handleChange = (e) => {
setIsValid(isValidPassword(e.target.value));
};
...
}
isValidPassword
만 수정하면된다.const PasswordValidator = {
VALIDATIONS: {
NOT_VALID_EMPTY: { isValid: false, message: '비밀번호를 입력해주세요.' },
NOT_VALID_LENGTH: { isValid: false, message: '비밀번호는 12자 이상 입력해야 합니다.' },
VALID: { isValid: true }
},
validate: function(password) {
if (password.length === 0) {
return this.VALIDATIONS.NOT_VALID_EMPTY;
}
if (password.length < 12) {
return this.VALIDATIONS.NOT_VALID_LENGTH;
}
return this.VALIDATIONS.VALID;
},
};
export default function InputPassword() {
const [isValid, setIsValid] = useState(PasswordValidator.VALIDATIONS.NOT_VALID_EMPTY);
const handleChange = (event) => {
setIsValid(PasswordValidator.validate(event.target.value));
};
return (
<>
<label htmlFor="password">비밀번호</label>
<input
id="password"
type="password"
onChange={handleChange}
/>
<p>{isValid.message || ''}</p>
</>
);
}
비즈니스 로직은 어떤 수준에서 관리되어야할까?
컴포넌트 수준에서 사용되는 건 불가능하지 않지만 어려운 점이 많다.
한 페이지에서 다루는 비즈니스 로직은 컴포넌트 단위로 움직이지 않는다.
예를들어, 방문포장 구매내역이 컴포넌트A - 최종결제 금액은 컴포넌트 B에서 다룬다.개발자가 어떤 이유로 두 비즈니스 로직을 컴포넌트 수준에서 관리하지만 비즈니스 로직은 뷰를 관리하는 방법과 다른 맥락을 가지기 때문에 문제가 발생할 수 있다는 점이다.
만약 다른 컴포넌트에서 방문포장 구매와 관련된 로직의 상태를 가져와야한다면 상위 컴포넌트를 통해 전달 받는 방식으로 되어야하고, 이렇게 되면 페이지의 코드가 상당부분 바뀔 수 있다.
따라서 비즈니스 로직은 하위 컴포넌트의 변경에 영향을 받지 않는 페이지 수준에서 관리되어야한다.
Context API
가 있다.class BusinessLogic {
count;
constructor() {
this.count = 0;
}
increase() {
this.count = this.count + 1;
}
...
}
const BusinessLogicContext = React.createContext(new Logic());
// 페이지 컴포넌트
const Page = () => {
const logic = React.useMemo(() => new Logic(), []);
return (
<Context.Provider value={logic}>
<Counter></Counter>
</Context.Provider>
);
};
// 페이지를 구성하는 하위 컴포넌트
const Counter = () => {
const businessLogic = React.useContext(BusinessLogicContext);
const [count, setCount] = React.useState(counter.count);
return (
<div>
<button type="button" onClick={() => {
businessLogic.increase();
if (businessLogic.count % 2 === 0) {
setCount(businessLogic.count);
}
}}>
increase
</button>
<div>
<div>count in context : {businessLogic.count}</div>
<div>count in state : {count}</div>
</div>
</div>
);
};
// 비즈니스 로직과 관계있는 훅을 관리할 때 좋은 예시 코드
const useMapCount = () => {
const businessLogic = React.useContext(BusinessLogicContext);
if (!businessLogic) {
throw new Error('useMapCount 훅은 PageLogic Context API 환경에서 사용되어야 합니다.');
}
...
}
// 비즈니스 로직을 주입받는 방법
const useMapCount = (businessLogic) => {
if (!businessLogic) {
throw new Error('useMapCount 훅은 PageLogic Context API 환경에서 사용되어야 합니다.');
}
...
}
// interface를 PageLogic과 함께 여기에서 관리하는 건 생각보다 더 중요할 수 있습니다 !
interface BusinessLogicR1 {
selectExtraProduct: (productId: number) => void;
...
}
class PageLogic {
constructor(
businessLogicR1: BusinessLogicR1,
businessLogicR2: BusinessLogicR2,
) {
...
}
selectExtraProduct(productId: number) {
businessLogicR1.selectExtraProduct(productId);
}
...
}
...
페이지 렌더링
로직의 분리는 환경을 구분하지 않는다!
분리를 하는 것은 리액트 프로젝트를 잘 운영하는 것보다 더 근본적인 이유를 갖는다.
💡 리액트를 처음 공부할때 다른 사람의 코드를 보며 어떤 기준으로 컴포넌트를 분리하는지, 컴포넌트 밖에 정의하는 함수는 어떤 기준으로 작성되는지 등등 궁금점이 있었다. 이 글을 작성하면서 그동안 궁금했던 부분도 같이 풀어진 것 같다. 아직은 기능구현에 급급한 코드를 짜고 있다면 앞으로는 성능을 고려하며 비즈니스 로직과 뷰 로직을 분리하는 연습을 진행해봐야겠다고 생각했다.