셀메이트와 엑셀의 상관관계

셀메이트에서 개발을 하다보면 셀메이트의 ‘셀’이 ‘셀러’를 의미하기도 하지만 ‘엑셀’도 포함하는 게 아닌가 싶을 정도로 엑셀 파일을 자주 접하게 된다. 업체에서 셀메이트에 데이터를 저장, 수정할 때 주로 사용하는 양식이 엑셀 형태이기 때문이다. 이전 프로젝트에서 엑셀 열 설정 개선을 하며 ‘한동안 엑셀이나 열 같은 단어 그만 보고 싶다’고 생각했는데, 어림 없지! 다시 돌아온 주문서 열 설정 3차! 이번 프로젝트에서 내가 맡은 업무 중 하나는 업체가 엑셀 파일을 업로드한 후 제목행을 선택하면 해당 행의 내용을 주문서 항목으로 넣을 수 있도록 구축하는 것이다. 말로 설명하려니 어렵다. 백문이불여일견이라고 밑에서 사진과 함께 보는 편이 낫겠다.

SheetJS 라이브러리를 접하다

이전 프로젝트를 진행하며 2개의 라이브러리를 적용했던 경험이 있다. D&D 기능을 위해 vuedraggable을 사용하고, element 외부 클릭 이벤트를 제어하기 위해 v-click-outside를 썼다. 라이브러리를 사용하는 기분은 마치 치트키를 장착한 플레이어 같은? 머리 아프게 로직을 짜고 긴 코드를 쓰지 않아도, 라이브러리에서 제공해주는 속성을 가져와 적용하기만 하면 원하는 기능을 쉽게 만들어낼 수 있었다. 하지만 마음 한 켠에는 ‘이렇게 날로 먹어도 되나?’ 싶은 미묘한 죄책감이 있었다. 아직도 이 부분은 해결하지 못한 딜레마다. 개발자로서 기존의 누군가가 만든 라이브러리를 잘 활용하는 것이 좋은지, 아니면 어렵고 힘들더라고 내가 코드 한 줄 한 줄 다 짜는 것이 좋은지. 이런 고민은 개인의 것으로 돌리고, 일은 해야하니까 엑셀 다운로드에 최적화된 라이브러리를 찾았다. 역시 세상은 넓고 똑똑한 개발자는 많다. 오픈소스 최고.

SheetJS

https://github.com/SheetJS/sheetjs SheetJS는 Javascript로 클라이언트 단에서 엑셀 다운로드를 구현하는 라이브러리다. SheetJS를 사용해 JSON, HTML(table), 배열 등 다양한 형태의 데이터로 엑셀 파일을 다운로드할 수 있다.

yarn add xlsx 또는 npm install xlsx로 해당 라이브러리를 설치하면 끝이다. 참 쉽죠?

SheetJS 설치 후,

  1. 엑셀 workbook을 생성하고
  2. 데이터(JSON, 배열, HTML 등)를 가져와 sheet를 만들고
  3. workbook에 만든 시트를 추가하고
  4. 엑셀 파일을 생성해서
  5. 다운로드 가능하도록 처리하면 된다

XLSX.utils 제공 기능

  • XLSX.utils.sheet_to_csv: generates CSV
  • XLSX.utils.sheet_to_txt: generates UTF16 Formatted Text
  • XLSX.utils.sheet_to_html: generates HTML
  • XLSX.utils.sheet_to_json: generates an array of objects

설치는 내가 할게 적용은 누가 할래

xlsx.utils로 다양한 형태를 제공하나 내가 필요한 건 JSON 형태였기 때문에 sheet_to_json을 사용했다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function reloadRow() {
  if (excelFile) {
    excelFile.SheetNames.forEach((sheetName) => {
      const roa = XLSX.utils.sheet_to_json(excelFile.Sheets[sheetName], { range: titleRowNum.value - 1 });
      if (roa.length) tmpResult[sheetName] = roa;
    });
  }
}

function displayExcelRow(excel) {
  if (excelFile) {
    excelData.value = excel;
    excelArray = [Object.keys(excelData.value[0])];
    emit('selectedExcelRow', excelArray);
  }
}

function importExcel(event) {
  const file = event.target.files[0];
  const reader = new FileReader();
  reader.onload = (e) => {
    let data = e.target.result;
    data = new Uint8Array(data);
    excelFile = XLSX.read(data, { type: 'array' });
    const fileData = XLSX.utils.sheet_to_json(excelFile.Sheets[excelFile.SheetNames[0]]);
    tmpResult = [];
    emit('countRow', fileData.length);
    reloadRow();
    displayExcelRow(tmpResult[Object.keys(tmpResult)[0]]);
  };
  reader.readAsArrayBuffer(file);
}

importExcel() 함수로 엑셀 파일을 불러온 뒤 JSON 형태로 변환한다. reloadRow() 함수는 엑셀 파일이 바뀌거나 제목행이 바뀔 경우 재사용되어야 하므로 별도로 분리했다. 이후 displayExcelRow() 함수를 통해 화면에 원하는 내용을 뿌릴 수 있다. 이번 프로젝트에서 중요한 부분은 사용자가 제목행을 선택하면 해당 행이 key값으로 바뀌어야 한다는 것 이었다. 기본적으로 1행을 key값으로 받기 때문에 sheet_to_json 내에 range 속성을 넣어 사용자가 선택한 행이 key값이 될 수 있도록 커스터마이징했다. 또한 사용자가 3행을 제목행으로 저장하면 향후에 따로 세팅하지 않아도 3행에 있는 값을 제목행으로 불러오도록 했다.

그 외에 제목행은 다른 component에 있어서 props와 watch로 제목행이 바뀌는 걸 감시하도록 했다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
watch(() => props.titleRow,
  newVal => {
    titleRowNum.value = newVal;
    reloadRow();
    displayExcelRow(tmpResult[Object.keys(tmpResult)[0]]);
  });

watch(() => props.formData,
  () => {
    titleRowNum.value = parseInt(props.formData.rowIndex || 1, 10);
  });

코드가 길어지니 슬슬 읽기 싫어진다. 바로 결과물로 간다.

그래서 결과는요?

주문서 열 설정 진입 시 주문서 열 설정 페이지 진입 시 보여지는 창이다. 초기 템플릿으로 할 경우 기본값은 1행으로 되어 있다. 사용자는 가운데에 있는 필드에 설정값을 하나씩 기재해야 하는데 너무 귀찮다. 50개 가까이 되는데 언제 다 치나. 그래서 자신이 가지고 있는 엑셀 파일을 업로드한다.

설명이 있는 경우 하필 이 업체는 1행에 설명이 적혀있어서 원하는 제목행을 가져올 수 없다. 그렇다면 2행으로 가본다.

내용 기입 2행에 원하는 제목행이 있다는 것을 발견한 사용자는 신이 난다. 가운데 필드를 클릭하고 선택 가능한 항목을 클릭하면 값이 담긴다. 여러개 항목도 담길 수 있다. 이 경우 |, @ 등의 특수문자를 값 사이에 기재해줘야 한다. 원하는 내용을 모두 넣고 저장하면!

저장한 다음 사용자가 다시 페이지로 왔을 때 저장된 제목행과 주문서 열 설정 내역이 보이는 것을 확인할 수 있다.

또 오겠지 주문서 열 설정

아쉽게도 이번 3차 때도 FAIL이 떴다. 요구하는 기능이 모두 동작하고 기획과 디자인 QA를 마무리했음에도 테스트가 부족해 배포가 어렵다고 판단된 부분이어서 더욱 아쉽다. 개인적으로 FAIL이라는 단어의 어감이 좋지 않아 다른 단어로 대체했으면 한다. ‘다음 기회에’ 혹은 ‘또 만나요’ 같은.