본문 바로가기

카테고리 없음

엑셀 기반 상품 대량 등록/수정 기능 개발기(+ zod 런타임 유효성 검사)

도입 배경

영업결과 중 하나로 야채 가게는 싯가를 반영하여 상품 가격을 매일 변경해야 하는 특성이 있다. 기존에는 상품 상세 페이지에 일일이 접근하여 가격을 수정해야 했고, 이는 상당한 시간과 노력이 소요되는 작업이라 판단하였다.

예상 사용자들이 엑셀 작업에 익숙하다는 점을 고려했을 때, 엑셀 파일을 통한 대량 수정 기능을 제공한다면 업무 효율을 크게 개선할 수 있고, 청과상 같이 가격 변동이 잦은 업종에 종사하는 영업대상자들에게 세일즈 포인트중 하나로 작용할 수 있다고 판단하였다.

사용자 플로우

1. 신규 상품 등록시

  1. 엑셀 양식 다운로드
  2. 양식에 맞게 상품 데이터 작성
  3. 엑셀 파일 업로드
  4. 유효성 검사 후 상품 대량 등록

2. 기존 상품 일괄 수정시

  1. 엑셀 양식 다운로드 (기존 상품 데이터 포함)
  2. 수정할 정보 변경
  3. 엑셀 파일 업로드
  4. 유효성 검사 후 상품 대량 수정

라이브러리 선정

엑셀 관련 라이브러리는 총 3가지를 검토했다.

  • xlsx
  • xlsx-js-style
  • excelJS

생성된 엑셀 파일에는 안내 문구와 함께 스타일, 헤더 스티키 등의 편의 기능을 제공해야 했기 때문에 기본 xlsx는 제외되었고, xlsx-js-styleexcelJS 중에서 선택해야 했다.

성능 측정

측정 환경:

  • 상품 데이터: 10,000개
  • 측정 도구: Web Performance API(performance.now())
  • 측정 범위: 데이터 변환부터 파일 다운로드 완료까지(백엔드 api 제외)
  • 측정 방법: 각 라이브러리별 10회 반복 측정 후 평균값 산출
const downloadExcel = () => {
  const start = performance.now();
  
  // 엑셀 생성 로직
  
  const duration = performance.now() - start;
  console.log(`처리시간: ${duration}ms`);
}

다운로드 성능 비교

xlsx-js-style 라이브러리

  • 측정값: 531, 455, 453, 460, 467, 458, 461, 470, 455, 511ms
  • 평균: 472.1ms

excelJS 라이브러리

  • 측정값: 680, 701, 690, 563, 663, 692, 690, 563, 579, 697ms
  • 평균: 651.8ms

업로드 성능 비교

xlsx-js-style 라이브러리

  • 측정값: 485, 466, 457, 446, 463, 538, 468, 494, 483, 463ms
  • 평균: 476ms

excelJS 라이브러리

  • 측정값: 494, 494, 481, 470, 491, 460, 481, 455, 481, 507ms
  • 평균: 481ms

커뮤니티 지표

npm 다운로드 수

  • excelJS가 압도적으로 높음

GitHub Stars

  • excelJS: 13.6k
  • xlsx-js-style: 473

-> excelJS가 압도적으로 높음

최종 선택: excelJS

다운로드 시 xlsx-js-style이 약 180ms 빠르지만, 다음과 같은 이유로 excelJS를 선택하였다.

- 헤더 행 고정

- 셀 유효성 검사

- 특정 열 숨기기(상품 ID와 같은 식별자)

- 활발한 커뮤니티와 유지보수

구현 방식 소개

다운로드 기능

  1. 상품 데이터 GET API 호출
  2. 엑셀 형식에 맞게 데이터 변환
  3. 엑셀 템플릿에 데이터 삽입
  4. 상품 데이터 엑셀에 삽입
  5. 안내 문구 삽입 및 시트 환경 설정
    1. 헤더 행 고정
    2. 필수 입력 열 표시
    3. 상품 ID열 숨김 처리

업로드 기능

  1. 각 cell별 zod 스키마 정의
  2. row별 순회하면서 각 cell 유효성 검사
  3. 에러 o ->  사용자에게 행, cell, 입력값 정보 모달로 노출
  4. 에러 x -> 상품 등록 POST API 호출
export const validateExcelData = (rawData: ExcelRow[]): ValidationResult => {
  const validProducts: ValidatedProduct[] = [];
  const validationErrors: ValidationResult["errors"] = [];

  rawData.forEach((row, index) => {
    const productData = {
     // ... 상품 데이터 정보
    };
		
		// zod의 parse가 아닌, safeParse함수로 유효성 검사 진행
    const result = productSchema.safeParse(productData);

		// 유효성 검사 에러 발생시
    if (!result.success) {
      validationErrors.push({
        row: index + 5, // 데이터는 5행부터 시작함(1~4행은 안내문구)
        issues: result.error.issues.map((issue) => {
          const columnName = issue.path[0] as keyof ExcelRow;
          const inputValue = row[columnName];
          
          // 1.컬럼정보, 2.입력값, 3.오류메세지 저장
          return {
            column: columnHeaderMap[columnName],
            inputValue,
            message: issue.message,
          };
        }),
      });
    } else {
      validProducts.push({
				// ... 유효성 검사 통과한 row push.
      });
    }
  });

		// 유효성 통과 row, 실패 row 반환
  return {
    validProducts,
    errors: validationErrors,
  };
};

 

유효성 검사시 parse가 아닌, safeParse를 선택하게된 이유

zod는 두 가지 유효성 검증 방식을 제공

1. parse: 에러를 throw

try {
  const result = schema.parse(data);
  // 성공 로직
} catch (error) {
  // 에러 처리
}

 

2. safeParse:에러를 결과 객체에 포함

const result = schema.safeParse(data);

if (result.success) {
  // 성공 로직
} else {
  // 에러 처리
}

 

safeParse를 선택한 이유는 다음과같다.

  1. 예상 가능한 에러
    • 엑셀 업로드 시 유효성 검사 실패는 충분히 예상 가능한 상황임
    • 따라서 예상하지 못한 에러를 처리하는 try-catch문을통해 제어해야하는 parse보단 safeParse 선택
  2. 가독성
    • try-catch블록없이 if-else분기로 처리함으로써, 불필요한 인덴트 줄임
  3. 명시적 타입 내로잉 코드 추가
    • try-catch블록을 사용하게 될시, catch문에서의 error타입이 zod에러로 내로잉이 되지 않기 때문에 instanceof를통해 명시적으로 타입 내로잉관련 코드를 추가해줘야하는 불편함이 존재

엑셀 시트를 다운로드 받을때 유효성 검사를  각 셀에 적용한채로 내려주면 되는데 굳이 zod 런타임 유효성 검사를 진행하는 이유가 있나?

-> 필요하다 판단.

유효성 검사 우회 가능성이 존재한다.

cell별로 유효성 검사를 지정해놓아도, 사용자가 복사,붙혀넣기로 유효성 검사를 우회할 수 있음

  • ex) 면세여부를 결정하는 필드를 입력받을때 "Y", "N"만 허용하도록 설정해도 "YES", "NO" 입력 가능

성과 및 기대 효과

 

  • 업무 효율 향상: 상품 상세 페이지를 일일이 접근할 필요 없이 엑셀로 일괄 관리
  • 사용자 친화적: 엑셀에 익숙한 사용자들이 학습 곡선 없이 사용 가능
  • 세일즈 포인트 확보: 청과상과 같이 가격 변동이 잦은 업종에 어필 가능

실제 유저의 사용률체크 방식은 ga를통한 페이지 접근여부, 체류시간, 사용자와의 지속적인 소통을 통해 확인할 예정이다.