안녕하세요, 오픈소스컨설팅의 Frontend 개발자 노은지입니다.
얼마 전, Table 안에 있는 TableToolbar 공통 컴포넌트를 수정하는 이슈를 진행하고 코드 리뷰를 하면서 팀의 시니어 개발자분께서 코멘트를 달아주셨습니다.
수정 전 TableToolbar의 모습은 아래와 같았습니다.
키워드를 이용해 table 내의 정보를 검색하고, table 내에서 사용자가 선택한 row가 몇 개인지 search input 우측에 표시되도록 하며, 우측 끝에 tool 들이 배치되도록 구성되어 있는데요.
새롭게 요구된 디자인은 아래와 같습니다. 1줄이었던 TableToolbar가 2줄로 표현이 되는 스타일 변경이 있었습니다.
특수한 페이지에서만 변경된 디자인이 적용 되도록 해야 했고, 선택된 row의 개수를 표시하는 UI가 조건부로 렌더링 될 수 있도록 해야 했는데요. 요구한 디자인을 반영하는 것 자체는 어려운 일이 아니었지만, 문제는 그 조건을 설정하는 과정에서 비즈니스 로직이 섞이게 된 것이었습니다.
“비즈니스 로직이 섞이지 않게”
개발을 시작하면서 많이 보고 들었던 낯익은 문장입니다. 글로만 보면 무엇인지 알 것만 같은 비즈니스 로직이란게 뭔지, 그 실체가 저에게 실제로 뚜렷하게 와 닿지는 않았던 것 같습니다.
그래서 오늘은 이 비즈니스 로직이란 것이 뭔지 정리해 보고, 제가 실무에서 겪었던 경험을 공유해 보려 합니다.
비즈니스 로직이란?
프론트엔드에서의 비즈니스 로직이란 애플리케이션 또는 웹사이트의 사용자 인터페이스(UI)와 관련된 비즈니스 규칙이나 일련의 처리 과정을 뜻합니다. 일반적으로 비즈니스 로직은 사용자의 입력에 따라서 데이터를 처리하고 조작하며, 애플리케이션의 주요 기능과 작업을 수행하는 것입니다.
프론트엔드 개발에서 대표적인 비즈니스 로직은 다음과 같습니다
1. 데이터 유효성 검사: 사용자가 제출한 데이터를 확인하고 유효성을 검사합니다. 예를 들면, 사용자가 양식을 제출할 때 필수로 요구되는 필드들이 모두 채워져 있는지 확인하거나, 데이터 형식이 올바른지 등을 확인하는 작업을 수행합니다.
2. 데이터 조작, 가공: 사용자의 요청에 따라서 데이터를 조작하고 가공합니다. 사용자가 특정 기간의 쇼핑 내역을 조회했을 때, 그에 해당하는 데이터를 데이터베이스에서 가져와 필터링하고 정렬하는 작업 등을 의미합니다.
3. 비즈니스 규칙 적용: 애플리케이션에서의 비즈니스 규칙 적용이란 쉽게 말해 사용자의 결제가 성공한 경우에만 주문을 처리한다거나, 특정 상황에서 할인을 적용하는 등의 규칙을 적용하는 것 등을 이야기합니다.
4. 상태관리: 애플리케이션 상태를 관리하는 것 또한 비즈니스 로직입니다. 사용자가 장바구니에 물품을 추가하거나 제거할 때와 같이 변화된 상태를 업데이트하고 UI에 반영하는 작업을 수행합니다.
비즈니스 로직이 섞이지 않게 개발하는 것은 어떤 것을 의미하는 걸까요?
관심사 분리라는 말을 많이 들어본 적이 있습니다. 이 관심사 분리와 비즈니스 로직 사이에는 밀접한 관계가 있는데요. 관심사 분리란 소프트웨어의 구성 요소를 서로 다른 책임 영역으로 나누는 원칙을 의미합니다.
관심사 분리: Seperation of Concerns
여러 곳에서 사용될 컴포넌트의 입장으로 생각했을 때, 비즈니스 로직은 주로 복잡하고 다양한 조건을 포함하기 때문에 이 비즈니스 로직이 섞이지 않게 함으로써 코드의 가독성을 높이고, 컴포넌트의 주요 역할을 더욱 명확하게 할 수 있습니다. 또한 비즈니스 로직의 분리를 통해서 코드를 수정, 추가, 삭제하는데 다른 기능들과의 의존성이 줄어들고 유지보수가 쉬워지며, 재사용성이 향상됩니다.
또한 단위테스트, 통합 테스트 등을 더욱 효과적으로 수행할 수 있습니다.
이렇게 글로 읽으면 대충 감은 잡히는데요, 간단한 To do list 어플리케이션을 예로 든 블로그 글들을 찾아봐도 비즈니스 로직이 정확히 뭘 뜻하는지 명쾌한 해답을 얻기는 조금 힘들었습니다.
제가 피드백을 받았던 코드를 간단히 보면서 이해해볼까요?
type TableToolbar<T extends object> = {
// 다른 property들...
is특정페이지Table?: boolean;
};
export function TableToolbar<T extends object>({
// 다른 property들...
is특정페이지Table = false,
}: PropsWithChildren<TableToolbar<T>>): ReactElement | null {
// 다른 logics...
return (
<Toolbar className={cx(is특정페이지Table ? classes.특정페이지Toolbar : classes.toolbar)}>
<section className={cx(is특정페이지Table ? classes.특정페이지Search : 'toolbar-header-left-section')}>
<div className={styles['search']}>
<InputText {/*프로퍼티들*/}/>
{searchKeyword && (
<IconButton {/*프로퍼티들*/}>
<IconUploadClose />
</IconButton>
)}
</div>
{!!selectedRowCount && (
<span className={styles['typo-selected-rows']}>{t('{{count}} Selected', { count: selectedRowCount })}</span>
)}
</section>
/* 생략 */
</Toolbar>
);
}
위 코드는 TableToolbar라는 공통 컴포넌트의 일부입니다.
저는 디자인 요구사항에 따라 코드를 수정하기 위해 is특정페이지Table 이란 property를 추가로 받아와서 boolean 값에 따라서 스타일 적용을 달리하도록 설계했습니다. 여기서 문제는 is특정페이지Table이라는 property 명에 있었습니다. 이 이름으로 유추해 볼 때, ‘특정페이지’가 렌더링 되는 Table 일 때만 스타일이 조건부로 달라지게 될 거라는 예상을 하게 될 겁니다.
TableToolbar 컴포넌트의 입장에서 이 property의 이름은 관심사 밖에 해당된다고 할 수 있습니다. 공통 컴포넌트는 어느 곳이든 상관없이 제 기능을 수행해야 하고, ‘특정페이지’가 렌더링 되는 Table 외에도 TableToolbar가 두 줄로 표현되어야 하는 상황은 얼마든지 발생할 수 있으니까요.
그래서 코드는 다음과 같이 변경되었습니다.
type TableToolbar<T extends object> = {
// 다른 property들...
isDoubleRowsToolbar?: boolean;
};
export function TableToolbar<T extends object>({
// 다른 property들...
isDoubleRowsToolbar = false,
}: PropsWithChildren<TableToolbar<T>>): ReactElement | null {
// logics...
return (
<Toolbar className={cx(isDoubleRowsToolbar ? classes.doubleRowsToolbar : classes.toolbar)}>
<section className={cx(isDoubleRowsToolbar ? classes.doubleRowsSearch : 'toolbar-header-left-section')}>
<div className={styles['search']}>
<InputText {/*프로퍼티들*/}/>
{searchKeyword && (
<IconButton {/*프로퍼티들*/}>
<IconUploadClose />
</IconButton>
)}
</div>
</section>
{!!selectedRowCount && (
<span
className={isDoubleRowsToolbar ? styles['double-toolbar-typo-selected-rows'] : styles['typo-selected-rows']}
>
{t('{{count}} Selected', { count: selectedRowCount })}
</span>
)}
/* 생략 */
</Toolbar>
);
}
is특정페이지Table이라는 property 명은 isDoubleRowsToolbar로 바뀌었습니다. 이 이름이라면 TableToolbar가 2줄로 표현되어야 하는 곳은 어디든 이 property 명을 사용할 때 어색하지 않을 것 같습니다.
변경된 디자인 요구사항에 따르면 위 이미지와 같이, ‘7개 선택됨’이라는 텍스트는 search bar의 아랫줄에 표시되어야 합니다.
바로 위의 코드를 보시면 selectedRowCount라는 변수에 선택된 row 개수가 state로 저장되며 search input과 section 태그로 함께 묶여 있습니다.
그래서 저는 처음에 ‘특정 페이지일 때 선택된 row 개수 표시가 아래 줄에 표시되게 해야겠다’라고 생각했습니다.
// 다른 import문들...
import { InventoryTypeCode } from 'common/const/enums';
type TableToolbar<T extends object> = {
// 다른 property들...
isDoubleRowsToolbar?: boolean;
inventoryType?: InventoryTypeCode;
};
export function TableToolbar<T extends object>({
// 다른 property들...
isDoubleRowsToolbar = false,
inventoryType = InventoryTypeCode.A,
}: PropsWithChildren<TableToolbar<T>>): ReactElement | null {
// logics...
return (
<Toolbar className={cx(isDoubleRowsToolbar ? classes.doubleRowsToolbar : classes.toolbar)}>
<section className={cx(isDoubleRowsToolbar ? classes.doubleRowsSearch : 'toolbar-header-left-section')}>
<div className={styles['search']}>
<InputText {/*프로퍼티들*/}/>
{searchKeyword && (
<IconButton {/*프로퍼티들*/}>
<IconUploadClose />
</IconButton>
)}
</div>
{!!selectedRowCount && !isDoubleRowsToolbar && (
<span className={styles['typo-selected-rows']}>{t('{{count}} Selected', { count: selectedRowCount })}</span>
)}
</section>
<div
className={
isDoubleRowsToolbar && inventoryType !== InventoryTypeCode.B
? styles['second-row-in-double-rows']
: styles['app-second-row-in-double-rows']
}
>
{!!selectedRowCount && isDoubleRowsToolbar && inventoryType !== InventoryTypeCode.B && (
<span className={styles['double-toolbar-typo-selected-rows']}>
{t('{{count}} Selected', { count: selectedRowCount })}
</span>
)}
/* 생략 */
</div>
</Toolbar>
);
}
enum type인 InventoryTypeCode를 이용해 B화면이 아닐 때, section 안에 있는 선택된 row의 개수가 보이도록 합니다.
그리고 B화면일 때는 두 번째 줄에 표시되도록 했습니다. 여기서 문제는 중복된 코드가 있었고, B화면이 아닌 다른 화면에서 TableToolbar가 2줄로 표시되어야 하는 상황이 추가로 생기면 해당 화면에 해당하는 property를 계속 추가해야 하는 문제가 생깁니다.
이렇게 상황에 따라 다르게 표시되는 UI 요소를 처리하는 로직을 공통 컴포넌트 내에 추가하는 것은 비즈니스 로직이 컴포넌트에 혼재되었다고 볼 수 있습니다. 컴포넌트는 일반적으로 UI를 렌더링하고 상태를 관리하는 역할을 해야 하며, 특정 화면의 로직에 대한 결정을 내리는 것은 비즈니스 로직에 해당합니다.
따라서 row의 개수가 section 태그 밖으로 빠지도록 element 구조를 변경하고, TableToolbar가 1줄인지, 2줄인지에 따라서 선택된 row 개수의 위치를 다르게 표시 되도록 조건부 스타일링 하는 것이 낫겠다는 판단을 내리고 아래와 같이 코드 수정을 진행했고 코드리뷰에서 approve 받을 수 있었습니다.
type TableToolbar<T extends object> = {
// 다른 property들...
isDoubleRowsToolbar?: boolean;
};
export function TableToolbar<T extends object>({
// 다른 property들
isDoubleRowsToolbar = false,
}: PropsWithChildren<TableToolbar<T>>): ReactElement | null {
// logics...
return (
<Toolbar className={cx(isDoubleRowsToolbar ? classes.doubleRowsToolbar : classes.toolbar)}>
<section className={cx(isDoubleRowsToolbar ? classes.doubleRowsSearch : 'toolbar-header-left-section')}>
<div className={styles['search']}>
<InputText {/*프로퍼티들*/}/>
{searchKeyword && (
<IconButton {/*프로퍼티들*/}>
<IconUploadClose />
</IconButton>
)}
</div>
</section>
{!!selectedRowCount && (
<span
className={isDoubleRowsToolbar ? styles['double-toolbar-typo-selected-rows'] : styles['typo-selected-rows']}
>
{t('{{count}} Selected', { count: selectedRowCount })}
</span>
)}
/* 생략 */
</Toolbar>
);
}
공통 컴포넌트는 범용적이게, 재사용 가능하게 설계하기
공통 컴포넌트에서 특정 화면에 따라 스타일을 변경하는 property를 추가하는 것은 언제든지 발생할 수 있는 상황입니다. 이러한 상황에서 주의해야 할 점은 컴포넌트가 가능한 한 범용적이고 재사용 가능하도록 설계하는 것입니다. 컴포넌트를 가능한 한 범용적으로 유지하려면 몇 가지 고려해야 할 사항이 있습니다.
- 프로퍼티 이름과 설명: 프로퍼티의 이름과 설명을 명확하게 작성하여 다른 개발자들이 컴포넌트를 사용할 때 이해하기 쉽도록 합니다. 프로퍼티 이름이나 설명이 모호하면 오해를 불러일으킬 수 있습니다.
- Default value 설정: 가능한 한 Default value를 설정하여 대부분의 상황에서 컴포넌트가 예상대로 동작하도록 하면 다른 화면에 해당하는 프로퍼티를 추가해야 하는 경우를 최소화할 수 있습니다.
- 확장성 고려: 컴포넌트를 설계할 때, 특정 화면에 대한 스타일링 설정을 추가하는 것이 아니라, 더 범용적인 확장 가능한 방식으로 설계하려고 노력하는 것입니다. 예를 들어, 다양한 스타일링 요구사항을 처리할 수 있는 커스텀 CSS 클래스를 사용하거나, 스타일을 외부에서 주입할 수 있는 방법을 고려할 수 있습니다.
- 컴포넌트 분리: 특정 화면에 해당하는 스타일링 설정이 많이 필요한 경우, 이를 별도의 컴포넌트로 분리하고 컴포넌트 간에 필요한 프로퍼티를 전달하는 방식을 고려할 수 있습니다. 이렇게 하면 코드의 가독성과 유지 보수성을 높일 수 있습니다.
컴포넌트를 범용적으로 유지하고 비즈니스 로직과 스타일링 설정을 적절하게 다루면 코드의 재사용성과 유지 보수성을 향상시킬 수 있습니다. 하지만 프로젝트의 복잡성과 요구사항에 따라서는 어느 정도의 커스텀화가 필요할 수 있으며, 이를 위해 프로퍼티를 추가하는 것은 합리적인 선택일 수 있습니다. 단, 이러한 커스텀화가 과도하지 않도록 주의해야 합니다.
마치며
이번 이슈로 비즈니스 로직이 무엇인지, 관심사라는 게 뭔지 곰곰이 생각해 보고 여러 레퍼런스들을 찾아보기도 했습니다. 이번에 제 경험을 다시 한번 글로 정리하면서 복기해 보니 개념이 좀 더 명확해졌습니다.
저와 같은 고민을 하셨던 분들이 이 글을 읽고 감을 잡는데 조금이라도 도움이 된다면 좋을 것 같습니다.
읽어주셔서 감사합니다! 피드백은 언제나 환영입니다!
참고
One reply on “관심사 분리, Separation of concerns”
오호~