환불 기능이 유저들에게 공개가 되었다.
다행히 QA에서 치명적인 오류들을 거이 잡아내서 심각한 버그는 없었지만,
QA에서 발견하지 못했던 자잘한 오류들이 몇가지 있어 해당 기능을 수정했다.
환불 슬랫봇을 통해 환불에 관련한 요청들을 확인하며 뿌듯하기도 했지만,
아직 유저들도 완전히 익숙해지지 않아 전화를 통해, 환불이 간간히 들어오기도 했다.
버그가 없도록 만들 수는 없지만, 버그가 최대한 없도록 만드는 것이
우리 개발자가 할 일인 것 같다.
이번 스프린트부터 본격적으로 "파트너 어드민" 개발에 들어갔다.
지금 회사에 가용할 수 있는 개발자가 3명이 있는데,
CTO님은 개발 기획, 에러 모니터링과 SEO, 치명적인 오류 예방/개선을 주로 작업하시고,
다른 한 분은 보고되는 오류들과, 앱 개발을 주로 작업 작업하셔서,
새로 만들고 있는 기능 개발이 내 작업 업무가 된 것 같았다.
그래서 빠르게 개발하기 위해서, 고민하고 이를 응용하며
프론트엔드 컨퍼런스에서 세션을 진행하신 토스 개발자분의 "바퀴 대신 로켓 만들기"
강연이 자꾸 머리에 맴돌았던 스프린트였다.

5차 스프린트 기간: 24.08.30(금) ~ 24.09.20(금)
이전에 플랫폼 판매관리 페이지는 이전 셀러가 이용할 수 사이트에도 존재했다.
이번에 파트너 페이지에 새로 추가된 기능을 제외하고,
존재했던 기능들을 먼저 옮기는 작업이였는데,
그냥 옮기는 것은 의미가 없을 것 같아 기존 존재하던 것들을
개선한 뒤에 디자인을 입혀 옮기는 것을 목표로 잡고 갔다.
먼저 css 적용방식부터 다시 잡아나갔다.
기존에는 인라인 방식으로 모든 css가 구성되어 있어,
각 태그의 구조 파악이 한 눈에 들어오지 않았다.
때문에, Sass와 잘 맞는 BEM방법론을 채택하여,
각 태그의 역할 및 html의 역할과, css의 역할을 분리함으로서,
각 태그가 어떤 기능을 맡고 있는지 한 눈에 보이도록 개선했다.
그리고 처음 구현해보는 엑셀 다운로드 기능을 발견했다.
파트너 페이지에서는 파트너 분들이 데이터를 한눈에 확인할 수 있도록
데이터들을 엑셀로 볼 수 있는 기능을 제공하고 있었다.
이 엑셀 다운로드 기능은 각기 다양한 페이지에서 사용하고 있었는데,
어쩌면 해당 기능을 공통 컴포넌트로서 묶을 수 있을 것 같았다.
때문에, 먼저 해당 동작에 대해 정확하게 이해하는 작업에 돌입했다.
먼저 엑셀을 다운로드 하는 로직을 인라인에서 빼서 확인해보니
다음과 비슷한 형태였다.
// 1. 서버를 통해 데이터를 받아온다.
// 2. 브라우저나 IE인지 아닌지를 구별한 후, 다운로드 방식을 달리한다.
// - IE는 <a> 태그를 이용한 다운로드를 지원하지 않기 때문에, 따로 IE 전용 메소드인 msSaveOrOpenBlob를 사용한다.
// - 이외의 메소드는 <a> 태그의 download 속성을 이용하여 다운로드를 구현한다.
const handleExcelDownload = () => {
const blob = await api.getExcelFile(query);
// 파일명 짓는 로직
// ...
if (window.navigator?.msSaveOrOpenBlob) { // IE 브라우저인 경우
window.navigator.msSaveOrOpenBlob(blob.data, filename);
} else { // 그 외 브라우저인 경우
const url = window.URL.createObjectURL(blob.data);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
link.parentNode?.removeChild(link);
}
}
return (
<button onClick={() => handleExcelDownload()}>다운로드</button>
)
로직을 정확히 파악해보니, 아래의 코드에서 각 페이지마다 달라질 만한 것이
3가지 뿐이었고, 충분히 공통 컴포넌트로 분리가 가능할만한 것들이었다.
1. 엑셀 파일 이름
2. button태그의 label
3. blob을 받아올 API
때문에, 공통 컴포넌트로 만들어, 엑셀 다운로드에 대한 책임 분리를 명확히 하고,
분리하다 보니 엑셀의 파일 이름 형식이 각기 달라,
파트너 분들이 우리 서비스에서 다운로드한 파일들을 찾기 어려울 것을 예상되어,
파일의 접미사만을 받도록하여 이름을 명명짓도록 강제했다.
공톰 컴포넌트로 분리하게 되니, 책임 분리가 명확해졌고,
같은 기능임에도 각기 개발자만의 다른 성격(?)을 가져,
혼란이 올 수 있는 부분들과 실수들을 찾아 개선할 수 있었다.
터득한 Point.
- 같은 코드를 작성하더라도 이전 코드를 그대로 쓰지말고 개선하자.
- 책임 분리의 장단점, 이를 통한 분리 시점에 대한 감
- NestJS는 JSON으로 직렬화 한 뒤, return하기 때문에, JSON이외의 데이터들은 response를 통해 응답 헤더를 설정한 뒤 send 해야함.
3개의 페이지들에 대해서는 아직 명확한 디자인이 나오지 않아,
기본적으로 제공되어야할 기능과 확정된 기능들에 대해서만 우선 구현하였다.
공지사항에서는 새롭게 "중요" 공지사항이 생겼다.
때문에 중요 공지사항을 먼저 보여준 뒤 일반 공지사항들을 보여주도록
고차함수를 통해 데이터들을 가공한 뒤, 컴포넌트 데이터에 넣어줬다.
후기 작성에서는 작성한 유저의 정보와 내용을 제한적으로 보여주도록
데이터들을 가공하고, 평점에 대해서도 각 섹터별 점수에 대해서
평균값을 보내주도록했다.
정산 정보페이지에서는 파트너의 여러 정보를 수집하고자 이미지를 제출하고 있다.
하지만 해당 코드를 분석하다보니, 몇가지 문제들이 있었다.
그 이유는 FilesInterceptor('files')를 사용하고 있기 때문이었다.
각기 다른 이미지를 나타내는 파일들을 한 키로 담아 처리하고 있는 것을
다른 것을 의미하는 이미지라면 다른 키로 보내,
해당 이미지가 무엇인지 알 수 있도록 따로 처리할 수 있어야 한다고 생각했다.
이전에 NodeJS에서 multer 라이브러리를 이용해 달리 처리했던 경험이 떠올라
분명 있을 것 같아 NestJS 깃허브에 들어가 인터셉터 코드들을 뜯어보았다.
역시 존재했다!!!
// packages/platform-express/multer/interceptors
export function FileFieldsInterceptor(
uploadFields: MulterField[],
localOptions?: MulterOptions,
): Type<NestInterceptor> {
class MixinInterceptor implements NestInterceptor {
protected multer: MulterInstance;
constructor(
@Optional()
@Inject(MULTER_MODULE_OPTIONS)
options: MulterModuleOptions = {},
) {
this.multer = (multer as any)({
...options,
...localOptions,
});
}
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const ctx = context.switchToHttp();
await new Promise<void>((resolve, reject) =>
this.multer.fields(uploadFields)(
ctx.getRequest(),
ctx.getResponse(),
(err: any) => {
if (err) {
const error = transformException(err);
return reject(error);
}
resolve();
},
),
);
return next.handle();
}
}
해당 인터셉터를 통해 해당하는 이미지가 어떤 것을 의미하는지 알도록 설계했다.
FilesInterceptor('files')
FileFieldsInterceptor([{ name: 'bankAccountPhoto' }, { name: 'licensePhoto' }])
터득한 Point.
- 내가 쓰는 코드는 내 코드가 아니라 회사코드이므로, 모두가 알 수 있도록 설계하여 DX를 향상시키자.
- 예측 가능한 설계를 지향하자.
- 다양한 경험은 내가 볼 수 있는 시야를 넓혀준다.
공지사항을 작성하고나서,
상세 페이지에서 확인하면 일부 이미지가 깨지는 오류가 있었다.
확인해보니 상세페이지에 margin이 들어가고,
가로 세로 비율이 맞지 않은 상태로 작아지다 보니 깨지고 있었다.
현재 우리 서비스에서는 CKEditor 라이브러리를 채택하여.
공지사항을 작성하고 있었는데,
해당 라이브러리에서 자동으로 margin을 넣어주고 있어,
해당 라이브러리를 사용하는 다른 곳에서는 설정으로 margin을 0으로 바꿔주고 있었다.
처음에는 단순하게 다른 코드들처럼 설정으로 바꿔주려고 했는데,
해당 방식으로 하게 되면 각 태그들에 인라인으로 style이 들어가고,
해당 태그들을 DB에 그대로 저장해주고 있는데, 나중에 해당 style에 수정이 필요할 경우
DB를 직접 쿼리를 통해 수정하는 방법밖에 존재하지 않아 DB에 종속적이게 된다.
하지만 문제가 있는 것은 CSS이다. DB에 종속적인 것이 맞을까 하는 생각이 들어,
다른 방식으로 접근했다.
CKEditor를 통해 저장을 하는 방식은 그대로 두고,
설정을 통해 변경하는 것이 아닌, 공지사항 상세페이지의 css를 통해
className을 이용하면 DB에 종속되는 문제를 해결할 수 있을 것 같다는 생각이 들었다.
각 역할은 역할로서 존재할 때 가장 빛이나는 것 같다😎
팀원들과 문제 공유를 위해 문서화도 꼼꼼히 잊지 않기!

터득한 Point.
- 기존의 문제해결 방식이 정답이 아닐 수 있다. 끊임없이 의심하고 개선하자!
- 각 역할에 대한 책임 분리를 명확히 하자.
해당 오류는 기존 해결방식을 끊임없이 의심하고 개선하려다 발생한 오류였다.
어느 화창한 하루,
오늘도 어김없이 업무를 진행하며, 처음보는 회사코드를 분석하고 있었다.
그러다가 그냥 멈출 수 없게 만드는 코드를 발견했다.
if (paymentStatus===1) {
// 결제 완료된 데이터들 return
} else if (paymentStatus===2) {
// 부분 환불 데이터들 return
} else if (paymentStatus===3) {
} ...
클라이언트에서 특정 결제 상태인 것들만 보여주도록 필터 기능을 하는 곳에서,
각 필터마다 value을 1, 2, 3, 4, 5처럼 숫자를 보내줘서
서버에서 해당 숫자를 받고 그에 해당하는 데이터들을 가공하여 보내주고 있었다.
해당 코드를 작업하지 않은 작업자가 봤을 때는,
return 되는 로직을 확인하고 1의 값이 무엇인지 역으로 추론해야하는 상황이었다.
이전 회사코드를 React로 마이그레이션을 빠르게 진행하면서 생긴 상황인 것 같았다.
때문에 당연하게 고쳐야한다는 생각으로 해당 값을 Enum으로 전환하여 보내주도록,
환불 관련 작업을 하면서 클라이언트 코드와 서버코드를 고쳤다.
if (paymentStatus === PaymentStatus.PAYMENT) {
// 결제 완료된 데이터들 return
} else if (paymentStatus === PaymentStatus.PARTIAL) {
// 부분 환불된 데이터들 return
} ...
문제는 여기서 발생하게 되는데, 서버코드는 먼저 배포가 되었지만
클라이언트 코드는 QA 이후 운영에 배포 예정이었다.
때문에, 클라이언트는 아직 1, 2, 3, 4, 5를 보내고 있었고,
서버는 해당 값에 대한 로직이 없기 때문에 아무 값도 return 하지 않았다.
기존 로직과 새로 바뀌는 로직을 모두 수행할 수 있도록 짤 수 있었음에도,
거기까지 생각하지 못했다.
단지 개선에만 포커스를 두다가 생긴 오류였다.
때문에 이를 모두 수용할 수 있도록 코드를 고치고 난 후에야 정상화 되었다.
이전에 유저가 없던 프로젝트를 진행할 때는,
이러한 수정사항이 생길 때는 클라이언트를 고치고, 백엔드를 고쳐도 문제가 없었지만,
(당연히 문제가 없지! 아무도 안 쓰잖아!!)
현업에서는 기존로직과의 호환성이 그 무엇보다 중요했다.
이러한 상황들을 겪다보니, 왜 채용공고에서 경력직만 뽑는지 조금은 이해가 갔다.
이런 경험들은 직접 겪어봐야 몸에 체화가 더 빠르게 되고,
현업에서는 이러한 것들을 하나하나 가르쳐줄 시간이 없다.
서비스는 계속 변하고 코드들도 그에 맞춰 발빠르게 변화하도록 맞춰가야하기 때문이다.
터득한 Point.
- 기존 로직을 수정할 때는 이전 로직과의 호환성을 생각하자.
- 기존 로직을 수정할 때는 잠깐 멈추고 다시 한 번 생각해보자.