1. 개요
이전 통계 자동화 페이지에서 통계 엑셀 파일 미리보기 기능을 개발했었다.
토이 프로젝트로 해당 기능을 좀 더 발전시켜서 엑셀 파일을 웹에서 편집할 수 있는 에디터를 제작해봤다.
2. 구조
type 관리
공통적으로 사용하는 type들은 루트 디렉토리의 types
디렉토리의 editor.d.ts
에서 관리했다.
// editor.d.ts
declare module 'editor' {
export interface IFile {/* 생략.. */}
export interface ISheet {/* 생략.. */}
/// 생략..
}
각 컴포넌트의 prop
, return
등의 타입 정보는 해당 컴포넌트 소스코드에서 관리했다.
// ToggleButton.tsx
interface ToggleButtonProps {
/* 생략.. */
}
export const ToggleButton: React.FC<ToggleButtonProps> = ({ value, valueIfActive, propertyName, icon }) => {
/* 생략.. */
return <Button icon={icon} className={isActive ? 'button-active' : ''} onClick={toggle} />
}
상태 관리
데이터는
- File: 각 시트, 현재 활성화된 시트, 파일 제목등의 데이터
- Sheet: 제목, 시트 전체 cell 정보, 선택 cell, 시트별 history 등의 데이터
- Cell: cell 정보 - 값, width, height, font정보, color정보 등의 데이터
로 나누어 관리했다.
// editor.d.ts
export interface IFile {
title: string
lastEditTime: string | null
sheets: ISheet[]
currentSheetIdx: number
}
export interface ISheet {
title: string
cells: ICell[][]
historyInfo: HistoryInfo
scrollPosition: ScrollPosition
selectedCell: SelectedCell
selectedArea: SelectedArea
}
export interface ICell {
value: string
width: number
height: number
fontSize?: number
/// 생략..
}
상태관리를 위해 recoil
과 context
를 사용했다. 각 시트의 데이터들과 현재 편집중인 시트 정보등을 fileState
란 atom
에 저장했다.
atomEffect
를 활용해서 해당 데이터가 변경될 때마다 localStorage에 저장되도록 했다.
// store.ts
const localStorageEffect: (key: string) => AtomEffect<IFile> =
key =>
({ setSelf, onSet }) => {
const savedValue = localStorage.getItem(key)
if (savedValue != null) {
setSelf(JSON.parse(savedValue))
}
onSet((newValue, _, isReset) => {
isReset ? localStorage.removeItem(key) : localStorage.setItem(key, JSON.stringify(newValue))
})
}
export const fileState = atom<IFile>({
key: 'file',
default: {
title: '',
lastEditTime: null,
sheets: [getEmptySheet(1), getEmptySheet(2)],
currentSheetIdx: 0,
},
effects: [localStorageEffect('file')],
})
화면 하단의 탭의 시트 제목을 클릭해 해당 시트를 편집할 수 있다. 편집 시트의 전환은 recoil
과 context
를 활용하여 구현했다.
탭 클릭시 fileState
의 currentSheetIdx
가 변경되고
useEffect
를 통해 currentSheetIdx
가 변경되면 cells
(각 셀 정보), historyInfo
(시트별 history 스택)등 사용자에 보여지는 시트의 데이터가 변경된다.
이때 각 cells
, historyInfo
등은 App 전체를 감싸고 있는 EditorContext
에 의해 각 컴포넌트에서 바로 가져다 쓸 수 있다.
// App.tsx
<RecoilRoot>
<EditorProvider>
<div className="App">
<Header />
<Editor />
<Footer />
</div>
</EditorProvider>
</RecoilRoot>
컴포넌트 관리
디자인시스템으로 antd
를 사용했고, 해당 시스템에서 에디터 관련해서 굉장히 유용한 컴포넌트들을 많이 제공해줬다.
따라서 공통 컴포넌트는 많이 만들지 않았다.
MemoizedButton
, ToggleButton
등 memo
를 활용하는 부분만 따로 추상화해서 공통 컴포넌트로 활용해줬다.
// MemoizedButton.tsx
export const MemoizedButton = memo((buttonProps: ButtonProps) => {
return <Button {...buttonProps} />
})
앱의 각 부분을 컴포넌트로 나눠서 개발했다.
hook 관리
똑같은 상태 변경 로직을 툴박스나 시트 오른쪽 클릭 메뉴 - contextMenu 등 여러 컴포넌트에서 사용했다.
이러한 로직들은 추상화해서 hooks 폴더에 따로 모아두고 사용했다.
// useToggle.ts
export const useToggle = (initialValue: boolean): [boolean, () => void] => {
const [state, setState] = useState(initialValue)
const toggle = () => {
setState(prev => !prev)
}
return [state, toggle]
}
css 관리
css-in-js
인 emotion
을 활용했다.
로딩속도는 일반 css 방식에 비해 느리지만, 동적으로 css 주기 편하고, jsx와 한 파일에 있어서 보기도 편한 장점이 있었다.
css는 각 컴포넌트와 같은 파일에 위치하되 동적으로 변경되지 않는 부분은 함수형 컴포넌트 외부에서 선언해 둠으로써 css
함수가 매 렌더링마다 호출되지 않게 했다.
공통 style 변수들은 루트 디렉토리의 data 디렉토리의 variables.style.ts
파일에서 관리했다.
// Functionbar.tsx
export function Functionbar() {
/* ..생략 */
return (
<div css={functionBarCss}>
</div>
)
}
const functionBarCss = css`
padding: 4px 0;
border-bottom: ${border.basic};
background-color: ${color['main-bg']};
height: ${height.functionbar};
`
// variables.style.ts
export const color = {
'main-bg': '#F5F5F5',
'selected-cell': 'rgba(0,0,0,0.2)',
active: 'rgb(22, 119, 255)',
}
'프로젝트 회고 > 엑셀 편집기 개발' 카테고리의 다른 글
5. 정리 - 엑셀 편집기 (0) | 2023.09.13 |
---|---|
4. 성능 측정, 최적화 (0) | 2023.09.12 |
3. 테스트 자동화 (0) | 2023.09.11 |
2. 개발 시 어려웠던 점들 (0) | 2023.09.11 |