스프레드시트는 현대 컴퓨팅의 필수 요소가 되었습니다. 사용자들이 데이터를 탭 형식으로 정리, 조작 및 분석할 수 있도록 합니다. Google Sheets와 같은 애플리케이션은 강력하고 대화식인 스프레드시트의 표준을 제시했습니다.
이 블로그 포스트에서는 JavaScript를 사용하여 스프레드시트 애플리케이션을 구축하는 과정을 안내하겠습니다. 주요 프로그래밍 개념에 초점을 맞추고 JavaScript 기능을 탐색하며 설명과 함께 자세한 코드 스니펫을 포함할 것입니다.
전체 소스 코드는 여기에서 내 Codepen에서 이용 가능합니다.
구글 스프레드시트란?
구글 스프레드시트는 사용자가 온라인에서 스프레드시트를 만들고 편집하고 협업할 수 있는 웹 기반 애플리케이션입니다. 수식, 데이터 유효성 검사, 차트 및 조건부 서식 지원 기능을 제공합니다.
우리 프로젝트는 구글 스프레드시트의 핵심 기능을 모방하며 다음을 중점으로 합니다:
- 편집 가능한 셀.
- 수식 구문 분석 및 평가.
- Pub/Sub 모델을 통한 실시간 업데이트.
- 키보드 탐색 및 셀 선택.
- 셀 간 동적 종속성 평가.
이 프로젝트의 기능
- 편집 가능한 셀: 사용자가 텍스트 또는 수식을 셀에 입력할 수 있습니다.
- 수식 지원:
=
로 시작하는 수식을 처리하고 식을 평가합니다. - 실시간 업데이트: 종속 셀의 변경으로 Pub/Sub 모델을 사용하여 업데이트가 트리거됩니다.
- 키보드 탐색: 화살표 키를 사용하여 셀 간 이동을 가능하게 합니다.
- 동적 평가: 다른 셀에 종속된 수식의 실시간 업데이트를 보장합니다.
- 에러 처리: 잘못된 입력이나 순환 종속성에 대한 의미 있는 오류 메시지를 제공합니다.
- 확장 가능한 디자인: 더 많은 행, 열 또는 기능을 추가하기 쉽도록 확장이 가능합니다.
애플리케이션의 주요 구성 요소
1. 모드 관리
const Mode = {
EDIT: 'edit',
DEFAULT: 'default'
};
이 열거형은 두 가지 모드를 정의합니다:
- EDIT: 선택된 셀을 편집할 수 있도록 합니다.
- DEFAULT: 편집 없이 탐색 및 상호 작용이 가능합니다.
모드 사용 이유?
모드는 UI 상태의 관리를 간소화합니다. 예를 들어 DEFAULT 모드에서는 키 입력으로 셀 간 이동이 가능하며, EDIT 모드에서는 입력이 셀 내용을 수정합니다.
2. Pub/Sub 클래스
Pub/Sub 모델은 구독과 실시간 업데이트를 처리합니다. 셀은 다른 셀을 구독하고 종속성이 변경될 때 동적으로 업데이트할 수 있습니다.
class PubSub {
constructor() {
this.map = {};
}
get(source) {
let result = [];
let queue = [ (this.map[source] || [])];
while (queue.length) {
let next = queue.shift();
result.push(next.toUpperCase());
if (this.map[next]) queue.unshift(this.map[next]);
}
return result;
}
subscribeAll(sources, destination) {
sources.forEach((source) => {
this.map[source] = this.map[source] || [];
this.map[source].push(destination);
});
}
}
주요 기능:
- 동적 종속성 관리: 셀 간의 종속성을 추적합니다.
- 업데이트 전파: 소스 셀이 변경될 때 종속 셀을 업데이트합니다.
- 너비 우선 탐색: 모든 종속 노드를 추적하여 무한 루프를 피합니다.
사용 예시:
let ps = new PubSub();
ps.subscribeAll(['A1'], 'B1');
ps.subscribeAll(['B1'], 'C1');
console.log(ps.get('A1')); // Output: ['B1', 'C1']
3. 행 및 셀 생성
class Cell {
constructor(cell, row, col) {
cell.id = `${String.fromCharCode(col + 65)}${row}`;
cell.setAttribute('data-eq', '');
cell.setAttribute('data-value', '');
if (row > 0 && col > -1) cell.classList.add('editable');
cell.textContent = col === -1 ? row : '';
}
}
class Row {
constructor(row, r) {
for (let c = -1; c < 13; c++) {
new Cell(row.insertCell(), r, c);
}
}
}
주요 기능:
- 동적 테이블 생성: 프로그래밍 방식으로 행 및 열 추가 가능.
- 셀 식별: 위치에 따라 ID 생성 (예: A1, B2).
- 편집 가능한 셀: 유효한 경우에만 셀을 편집할 수 있음(헤더가 아닌 행/열).
동적 행 및 셀 사용 이유
이 접근 방식을 통해 테이블 크기를 확장 가능하고 유연하게 유지하여 행이나 열을 추가할 때 구조를 변경하지 않아도 됨.
4. 상호작용을 위한 이벤트 처리
addEventListeners() {
this.table.addEventListener('click', this.onCellClick.bind(this));
this.table.addEventListener('dblclick', this.onCellDoubleClick.bind(this));
window.addEventListener('keydown', this.onKeyDown.bind(this));
}
주요 기능:
- 클릭 이벤트: 셀 선택 또는 편집.
- 더블 클릭 이벤트: 수식 편집 활성화.
- 키다운 이벤트: 화살표 키로 탐색 지원.
5. 수식 구문 분석 및 평가
function calcCell(expression) {
if (!expression) return 0;
return expression.split('+').reduce((sum, term) => {
let value = isNaN(term) ? getCellValue(term) : Number(term);
if (value === null) throw new Error(`Invalid cell: ${term}`);
return sum + Number(value);
}, 0);
}
주요 기능:
- 동적 계산: 다른 셀을 참조하는 수식 계산.
- 재귀적 평가: 중첩 의존성 해결.
- 오류 처리: 잘못된 참조 및 순환 종속성 식별.
6. 사용자 입력 오류 처리
function isValidCell(str) {
let regex = /^[A-Z]{1}[0-9]+$/;
return regex.test(str);
}
주요 기능:
- 유효성 검사: 입력이 유효한 셀 ID를 참조하는지 확인.
- 확장성: 유효성 검사를 해치지 않고 동적 테이블 확장 지원.
JavaScript 주제 다루기
1. 이벤트 처리
클릭 및 키 누름과 같은 상호 작용을 관리합니다.
window.addEventListener('keydown', this.onKeyDown.bind(this));
2. DOM 조작
DOM 요소를 동적으로 생성하고 수정합니다.
let cell = document.createElement('td'); cell.appendChild(document.createTextNode('A1'));
3. 재귀
의존성을 동적으로 처리합니다.
function calcCell(str) { if (isNaN(str)) { return calcCell(getCellValue(str)); } }
4. 오류 처리
유효하지 않은 셀과 순환 종속성을 감지합니다.
if (!isValidCell(p)) throw new Error(`invalid cell ${p}`);
결론
이 프로젝트는 JavaScript를 사용하여 강력한 스프레드시트를 보여줍니다. 이는 이벤트 처리, 재귀 및 Pub/Sub 패턴을 활용하여 복잡한 웹 애플리케이션의 기초를 마련합니다. 데이터 내보내기, 차트, 또는 형식 규칙과 같은 기능을 추가하여 확장할 수 있습니다.
참고
Source:
https://dzone.com/articles/spreadsheet-application-javascript-guide