BDD는 유저행동 기반 개발의 약자로 프론트앤드 개발자입장에서는 기획의 요소와 개발하는 코드의 싱크가 맞을수록 BDD 철학을 지키는 효과를 가집니다. 그래서 기획문서를 면밀히 살펴보고 설계에서 기획적 요소를 최대한 그대로 사용하려고 노력하는데요.
이번 포스팅에서는 제가 어떻게 기획적 요소를 코드로 일맥상통하게 표현하였고 그로 인해 얻은 가독성 향상 효과를 소개하고자 합니다
코드를 작성하기 전에 기획 문서를 보면, 기획적 요소를 어떻게 코드에 담을지 고민할 때가 많았습니다. 특히 도메인 특성상 여러 타입과 상태에 따라 ui와 동작의 분기가 복잡하고 다양해야 하는 기획이 있다보면, if else로 범벅이 된 코드가 되기 쉽고 이는 유지보수성을 떨어트리는 요인이 되곤 합니다.
해당 표는 제가 근무하는 넥스트유니콘의 스프린스 기획문서 중 일부를 가져온 것입니다.(실제는 더욱 복잡한 조건이 존재합니다.ㅠㅠ) 표에서는 회원의 타입과, 각 회원의 인증타입에 맞게 복잡한 요구사항을 표로 제공하고 있습니다.
이전에는 해당 표를 보고 각 조건에 맞게 if문을 적절하게 코드를 작성해왔습니다.
const _유니콘라이브_모달_계산 = (roleAuthState) => {
const is_전문투자자_인증_유도 = () => roleAuthState.role === '일반회원' && roleAuthState.status === '인증대기';
const is_전문투자자만_이용_가능 = () => {
return (roleAuthState.role === '스타트업대표' && roleAuthState.status === '인증대기')
|| (roleAuthState.role === '스타트업대표' && roleAuthState.status === '인증완료')
|| (roleAuthState.role === '스타트업팀원' && roleAuthState.status === '인증대기')
|| (roleAuthState.role === '스타트업팀원' && roleAuthState.status === '인증완료');
};
const is_전문투자자_인증_대기중 = () => (roleAuthState.role === '전문투자자' && roleAuthState.status === '인증대기');
const is_활성화_됨 = () => (roleAuthState.role === '전문투자자' && roleAuthState.status === '인증완료');
return {
is_전문투자자_인증_유도,
is_전문투자자_인증_대기중,
is_전문투자자만_이용_가능,
is_활성화_됨,
};
};
[코드1] 평소의 기획요소를 반영한 코드
위의 코드를 어떻게 바라보시나요? 기본적으로 가독성이 크게 떨어지지는 않지만 뭔가 불편합니다. 중복되는 코드도 많고, 사소한 오타 하나로 버그를 유발하기 딱 좋은 코드입니다.심지어 다음과 같은 변경이 있다고 가정한다면 더 그렇겠죠
다음은 해당 변경사항을 반영한 코드입니다.
const _유니콘라이브_모달_계산 = (roleAuthState) => {
const is_전문투자자_인증_유도 = () => roleAuthState.role === '일반회원' && roleAuthState.status === '인증대기';
// const is_전문투자자만_이용_가능 = () => {
// return (roleAuthState.role === '스타트업대표' && roleAuthState.status === '인증대기')
// || (roleAuthState.role === '스타트업대표' && roleAuthState.status === '인증완료')
// || (roleAuthState.role === '스타트업팀원' && roleAuthState.status === '인증대기')
// || (roleAuthState.role === '스타트업팀원' && roleAuthState.status === '인증완료');
// };
// 수정됨
const is_전문투자자만_이용_가능 = () => {
return (roleAuthState.role === '스타트업대표' && roleAuthState.status === '인증완료')
|| (roleAuthState.role === '스타트업팀원' && roleAuthState.status === '인증대기')
|| (roleAuthState.role === '스타트업팀원' && roleAuthState.status === '미인증');
};
const is_전문투자자_인증_대기중 = () => (roleAuthState.role === '전문투자자' && roleAuthState.status === '인증대기');
const is_활성화_됨 = () => (roleAuthState.role === '전문투자자' && roleAuthState.status === '인증완료');
return {
is_전문투자자_인증_유도,
is_전문투자자_인증_대기중,
is_전문투자자만_이용_가능,
is_활성화_됨,
};
};
[코드2] 기획이 수정되었을 때, 일반적인 대응
위의 코드를 보시면 기존의 is_전문투자자만_이용_가능
에서 일부 코드가 빠지고 추가된 것을 확인할 수 있습니다.해당코드를 변경할 때 표의 행과 열을 확인하고, 각 조건에 맞는 bool값으로 계산하는 과정에서 충분히 실수를 할 수 있습니다.
저는 이러한 잠정적 문제를 해결하기 위해 다음과 같이 조건문을 표로 표현하는 방식을 차용했습니다.
const COLUMNS = {
미인증: 0, // 미인증
인증대기: 1, // 인증대기
인증완료: 2, // 인증완료
} as const;
const ROWS = {
일반회원: 0, // 일반회원
스타트업_대표: 1, // 스타트업 대표
스타트업_팀원: 2, // 스타트업 팀원
전문투자자: 3, // 전문투자자
} as const;
const _모달_종류 = {
_: '_',
_전문투자자_인증_유도_모달: '_전문투자자_인증_유도_모달',
_전문투자자만_이용_가능_모달: '_전문투자자만_이용_가능_모달',
_인증_대기_중_모달: '_인증_대기_중_모달',
_활성화: '_활성화',
} as const;
const _조건_테이블 = [
// 미인증, 인증대기, 인증완료
[_모달_종류._전문투자자_인증_유도_모달, _모달_종류._, _모달_종류._], // 일반회원
[_모달_종류._, _모달_종류._전문투자자만_이용_가능_모달, _모달_종류._전문투자자만_이용_가능_모달], // 스타트업 대표
[_모달_종류._, _모달_종류._전문투자자만_이용_가능_모달, _모달_종류._전문투자자만_이용_가능_모달], // 스타트업 팀원
[_모달_종류._, _모달_종류._인증_대기_중_모달, _모달_종류._활성화], // 전문투자자
];
export const _유니콘라이브_모달_계산: InvestorCalcFn = (roleAuthState) => {
const res = _조건_테이블[ROWS[roleAuthState.role]][COLUMNS[roleAuthState.status]];
const is_전문투자자_인증_유도 = () => res === _모달_종류._전문투자자_인증_유도_모달;
const is_전문투자자만_이용_가능 = () => res === _모달_종류._전문투자자만_이용_가능_모달;
const is_전문투자자_인증_대기중 = () => res === _모달_종류._인증_대기_중_모달;
const is_활성화_됨 = () => res === _모달_종류._활성화;
return {
is_전문투자자_인증_유도,
is_전문투자자_인증_대기중,
is_전문투자자만_이용_가능,
is_활성화_됨,
};
};
[코드3] 표를 이용한 조건문 표현
COLUMNS, ROWS 상수는 각각의 테이블에 해당하는 값과 인덱스를 맵핑해주기 위해 존재하고, const res = _조건_테이블[ROWS[roleAuthState.role]][COLUMNS[roleAuthState.status]];
코드를 통해 해당 조건을 한번에 매핑해주는 방식입니다.
코드3에서 표현하는 방식은 [그림1]과 동일한 형식이므로 한눈에 이해하기 쉽습니다. 특히 변경점을 파악할 때 이점이 있습니다.
const _조건_테이블 = [
// 미인증, 인증대기, 인증완료
[_모달_종류._전문투자자_인증_유도_모달, _모달_종류._, _모달_종류._], // 일반회원
[_모달_종류._, _모달_종류._전문투자자만_이용_가능_모달, _모달_종류._전문투자자만_이용_가능_모달], // 스타트업 대표
[_모달_종류._, _모달_종류._전문투자자만_이용_가능_모달, _모달_종류._전문투자자만_이용_가능_모달], // 스타트업 팀원
[_모달_종류._, _모달_종류._인증_대기_중_모달, _모달_종류._활성화], // 전문투자자
];
// 에서 다음으로 변경
const _조건_테이블 = [
// 미인증, 인증대기, 인증완료
[_모달_종류._전문투자자_인증_유도_모달, _모달_종류._, _모달_종류._], // 일반회원
[_모달_종류._전문투자자만_이용_가능_모달, _모달_종류._, _모달_종류._전문투자자만_이용_가능_모달], // 스타트업 대표
[_모달_종류._, _모달_종류._전문투자자만_이용_가능_모달, _모달_종류._], // 스타트업 팀원
[_모달_종류._, _모달_종류._인증_대기_중_모달, _모달_종류._활성화], // 전문투자자
];
//_유니콘라이브_모달_계산 함수는 변경사항 없음
[코드4] 기획의 변경이 있을 때
[코드4]에서 표현하는 방식처럼 조건 테이블을 기획문서와 동일한 형태로 바꾸기만 하면 추가적인 비즈니스로직 변경을 하지 않아도 됩니다. 비즈니스 로직을 변경하지 않아도 된다는 것은 사람이할 수 있는 실수를 방지한다고 생각합니다.
이러한 표현방식은 선언적 프로그래밍이라고도 말하는데 가독성을 높이는데 선언적 표현이 주는 장점은 익히 들어보셨을것이라 생각하고 넘어가겠습니다 :)
이러한 방법은 3차원, 4차원 테이블로 확장할 수 있습니다. 조건이 복잡할수록 가독성 측면과 유지보수성에서 큰 이득을 볼것이라 기대합니다.
조건테이블은 진리표의 역할을 하기도 하므로 체계적인 사고를 하도록 도와줄 수도 있습니다. 프로그래밍을 하면서 사소한 조건을 고려하는것을 빠트려 크고 작은 버그를 만드신 경험은 다들 있을 것입니다. 이때 조건을 간과하지 않기 위해 진리표를 이용할겁니다. 진리표 자체가 비즈니스로직으로 역할을 할 수 있는 방식은 제3자가 코드를 이해할 때, if else 로 범벅이 된 코드를 통해 동작을 이해하는 것 보다, 표를 보고 개발자의 의도를 빠르게 이해할 수 있을것 입니다.
한글 변수와 기획문서가 코드로 자연스럽게 표현 되었다는 부분이 색다르네요 👍 협업할 때 코드 가독성과 유지 보수 측면에서도 위 코드로 작성하게 되면 많은 도움을 줄 수 있을 것 같네요! 좋은 글 써주셔서 감사합니다!