이번엔 엑셀 다운로드.. 열심히 CSV 다운로드 만들었더니 이게 아니랍니다.. ㅖㅖㅖ??????
엑셀 이용해서 시트 별로 데이터 정리해달라고 해서 다시 구현하는 와타시..
라이브러리는 아래 두 개 사용합니다.
npm i exceljs
npm i file-saver --save
실질적으로 excel 파일 형식으로 구현하는건 exceljs
이고 그걸 파일로 내보내고 저장할 수 있게 하는게 file-saver
이다. excel 라이브러리는 많은데 그 중 exceljs를 사용한 이유는 무료이고 스타일링 등 사용할 수 있는 기능이 많기 때문이다. 적당한 선에서 엑셀 다운로드를 구현하는 것이면 exceljs 추천한다.
import ExcelJS from 'exceljs'
const workbook = new ExcelJS.Workbook()
workbook.creator = '루루의 개발일지'
제일 먼저 엑셀을 만들어 넣을 workbook 을 만들고 프로퍼티를 설정한다. 나는 creator만 넣었지만 필요하다면 아래 링크를 참고하자.
https://github.com/exceljs/exceljs?tab=readme-ov-file#set-workbook-properties
그리고 시트에 데이터를 넣을 건데 여기서 주의할 점! 엑셀은 행별로 넣거나 개별로 넣어야한다. 그말은 즉, 데이터는 이중배열로 만들더라도 한번에 넣으면 안되고 forEach로 한줄씩 넣어줘야한다는 뜻.. 왜냐면? 해봤는데 안됐다.(ㅋㅋ)
데이터를 만들때 표의 header은 header, key, width 세개의 속성을 가진 객체로 만든다. 사실 그냥 문자열 배열로 만들어도 되긴하지만 표의 header이 제목 길이에 따라 width가 커지길 원해서 해당속성을 추가했다. 이 속성이 있으면 엑셀 생성시 width만큼 해당 열의 크기가 잡힌다. 단, 이 단위는 px단위가 아니라 엑셀 표 움직이면 보이는 그 너비임.
그리고 data는 각자의 형식에 맞게 문자열 배열로 만든다. 만약 표 형식이라고 하면 화면에 나오길 바라는 형태의 문자열 이중 배열로 만들면 된다. 아래 예시는 제목과 데이터로만 이루어진 [2][n] 표 만드는 예제이다.
const makeSheetHeader = (title: string) => {
return { header: title, key: title, width: title.length * 2 }
}
...
const headers: { header: string; key: string; width: number }[] = []
const data: string[] = []
rowData.forEach(({ title, unit, value }) => {
headers.push(makeSheetHeader(title))
data.push(`${value} ${unit}`)
})
그리고 data의 유무에 따라 sheet를 생성하거나 하지않는다. 일단 다 생성하고 나중에 sheet 삭제해도 되지만 귀찮기 때문에 애초에 data가 있을 때 생성하겠다. 새로운 시트를 생성하려면 addWorksheet(시트이름)
을 하면된다. 그럼 이제부터 해당 시트에 모든 데이터가 들어갈거임
const sheet = workbook.addWorksheet('요약')
만들어둔 헤더는 아래와 같이 추가한다. 이렇게 해야 header에 넣어두었던 width 속성이 읽힌다. 그리고 데이터를 addRow
를 통해 넣어준다. 첫 열을 띄고 표 만드는게 이뻐보여서 빈문자열 넣어놨음..ㅎ
sheet.columns = ['', ...headers]
sheet.addRow(['', ...data])
만약에 data가 이중배열이면 아래처럼 넣으면 된다.
data.forEach(item => {
sheet.addRow(item)
})
그리고 이건 취향차이인데 표 헤더부분에 스타일을 주고 싶다면 아래처럼한다. 참고로 표는 index 첫 시작이 1이기 때문에 headerStartIndex를 2로 줬다.(아까 맨 처음에 빈문자열 넣어서 실질적인 스타트는 2부터임) 각각 cell에 스타일 주는 로직이니 필요하다면 수정해서 사용하도록.
const headerStartIndex = 2
addHeaderStyle(sheet, headerStartIndex,headers.length)
...
const addHeaderStyle = (sheet: ExcelJS.Worksheet, startIndex: number, length: number) => {
for (let i = startIndex; i < startIndex + length; i++) {
const headerEachCell = sheet.getCell(`${String.fromCharCode(i + 64)}1`)
headerEachCell.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'cce6ff' },
}
headerEachCell.border = {
left: { style: 'thin', color: { argb: 'bfbfbf' } },
right: { style: 'thin', color: { argb: 'bfbfbf' } },
}
headerEachCell.font = {
size: 12,
bold: true,
}
headerEachCell.alignment = { horizontal: 'center' }
}
}
여기까지 하면 아래와 같은 화면이 나온다.
나는 시트를 나눠두긴 해도 한눈에 어떤 데이터인지 알 수 있게 해주고 싶어서 시트 내에도 타이틀을 달고자했다.
필요하다면 사용하자
첫번째 행을 두개의 행으로 나누겠단 뜻이다. 그리고 B1칸에 타이틀을 넣고 스타일링한다.
sheet.spliceRows(1, 0, [], [])
addSheetTitle(sheet, '요약')
...
const addSheetTitle = (sheet: ExcelJS.Worksheet, title: string) => {
const titleCell = sheet.getCell('B1')
titleCell.value = title
titleCell.font = {
size: 14,
bold: true,
}
}
그럼 아래와 같은 결과 완성!
근데 여기까지는 엑셀 내보내기를 위한 준비를 한거고 이제 만든걸 excel로 다운받을 수 있도록 해야한다. 여기에서 file-saver를 사용할건데 saveAs에 이 엑셀을 어떤 이름으로 저장할건지 작성해주면 된다. 이미 해당이름으로 저장된 파일이 있다면 알아서 뒤에 카운트 해줌 ex) example(1)
import { saveAs } from 'file-saver'
const fileData = await workbook.xlsx.writeBuffer()
const blob = new Blob([fileData], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
saveAs(blob, '파일 저장할 이름')
const generateExcel = useCallback(async () => {
const isAllEmpty = [gData, cData].every(data => isEmpty(data))
if (isAllEmpty) {
return alert('다운로드 할 데이터가 없습니다.')
}
const workbook = new ExcelJS.Workbook()
workbook.creator = '루루의 개발일지'
const sheets = {
card: {
data: cData,
generate: () => generateCardExcelData(...),
},
graph: {
data: gData,
generate: () => generateGraphExcelData(...),
},
}
Object.values(sheets)
.filter(({ data }) => !isEmpty(data))
.forEach(({ generate }) => generate())
const fileData = await workbook.xlsx.writeBuffer()
const blob = new Blob([fileData], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
saveAs(blob, 'example')
return workbook
}, [gData, cData])
...
export const generateCardExcelData = (workbook: ExcelJS.Workbook, cdata: CardExcel[]) => {
const headers: { header: string; key: string; width: number }[] = []
const data: string[] = []
cdata.forEach(({ title, unit, value }) => {
headers.push(makeSheetHeader(title))
data.push(`${value} ${unit}`)
})
if (!isEmpty(card)) {
const sheet = workbook.addWorksheet('요약')
sheet.columns = ['', ...headers]
sheet.addRow(['', ...data])
const headerStartIndex = 2
addHeaderStyle(sheet, headerStartIndex, headers.length)
sheet.spliceRows(1, 0, [], [])
addSheetTitle(sheet, '요약')
}
}
엑셀은 생각보다 다운로드가 금방돼서 연달아 클릭하면 와다다다 다운받아진다. 그래서 1초에 한번 다운로드 되게 디바운스를 걸어보자. 다운로드 중인 상태를 표시하고 싶어서 나는 isDownloading을 추가함.
const [isDownloading, setIsDownloading] = useState(false)
const debouncedGenerateExcel = useCallback(
debounce(async () => {
await generateExcel()
setIsDownloading(false)
}, 1000),
[generateExcel]
)
const handleClick = useCallback(() => {
if (isDownloading) return
setIsDownloading(true)
debouncedGenerateExcel()
}, [isDownloading, debouncedGenerateExcel])
이걸 두번 래핑한 이유는한번만 래핑하면 isDownloading 상태 변경하는게 예상대로 작동하지 않고 비동기로 돌아갔다. 찾아보니까 debounce 걸거면 한번더 래핑해야 동기로 진행된다고 해서 했더니 됨. ㅇㅁㅇ!
이제 저 handleClick함수를 다운로드 버튼에 연결하면 끝이다~!