1. @vanilla-extract/css 라이브러리 설명 및 활용
1.1. @vanilla-extract/css란 무엇인가?
@vanilla-extract/css는 'Zero-runtime CSS-in-JS' 라이브러리이다. 이는 스타일을 TypeScript(또는 JavaScript) 코드로 작성하지만, 빌드 시점에 정적인 CSS 파일로 완벽하게 추출된다는 의미이다. 다른 CSS-in-JS 라이브러리(예: styled-components)가 런타임에 스타일을 생성하는 것과 달리, @vanilla-extract/css는 런타임에 스타일 관련 코드를 남기지 않아 성능상 이점을 가진다.
- 주요 특징:
- 타입 안전성: 모든 스타일이 TypeScript로 작성되므로, 변수나 속성 이름에서 오타가 발생하면 빌드 시점에 오류를 잡을 수 있다.
- 지역 스코프: 생성된 모든 클래스 이름은 고유한 해시값을 가지므로, 스타일 충돌 문제에서 자유롭다.
- 성능: 런타임 오버헤드가 없어 기존 CSS 파일을 사용하는 것과 유사한 성능을 보인다.
- 테마 시스템:
createGlobalTheme과 같은 함수를 통해 전역적인 디자인 시스템 변수(색상, 폰트 크기 등)를 쉽게 만들고 공유할 수 있다.
1.2. 코드에서의 활용 분석 (BoardList.css.ts)
제공된 BoardList.css.ts 파일은 @vanilla-extract/css를 사용하여 BoardList 컴포넌트의 스타일을 정의한다.
import { style } from "@vanilla-extract/css";
import { vars } from "../../App.css";
// 'container'라는 클래스를 생성한다.
export const container = style({
display: "flex",
flexDirection: "row",
// vars.color.mainDarker와 같이 전역 테마 변수를 사용하여 스타일을 일관되게 유지한다.
backgroundColor: vars.color.mainDarker,
});
// 'boardItem' 클래스를 생성한다.
export const boardItem = style({
// ...스타일 속성
cursor: "pointer",
// ':hover'와 같은 가상 클래스(pseudo-class)를 객체 키로 사용하여 인터랙션을 정의한다.
":hover": {
opacity: 0.8,
transform: "scale(1.03)",
},
});
style함수를 호출하여 CSS 클래스를 생성하고, 이를export하여 다른 컴포넌트 파일에서import하여 사용할 수 있게 한다.vars객체는App.css.ts파일에서createGlobalTheme으로 정의된 전역 스타일 변수들이다. 이를 통해 프로젝트 전체의 색상, 간격, 폰트 크기 등을 중앙에서 관리하고 일관성을 유지한다.
1.3. 컴포넌트에서의 적용 (BoardList.tsx)
BoardList.tsx 컴포넌트에서는 이렇게 생성된 스타일을 import하여 JSX 요소의 className 속성에 적용한다.
import {
// ... 다른 스타일들
boardItem,
boardItemActive,
container,
} from "./BoardList.css";
import clsx from "clsx";
// ...
return (
// 'container' 스타일을 div에 적용한다.
<div className={container}>
{boardArray.map((board, idx) => (
<div
// 'clsx' 라이브러리를 사용하여 조건부 스타일링을 적용한다.
className={clsx(
// activeBoardId와 현재 board의 id가 일치하면 'boardItemActive' 스타일을 적용한다.
{
[boardItemActive]:
boardArray.findIndex((b) => b.boardId == activeBoardId) === idx,
},
// 일치하지 않으면 'boardItem' 스타일을 적용한다.
{
[boardItem]:
boardArray.findIndex((b) => b.boardId === activeBoardId) !== idx,
}
)}
>
<div>{board.boardName}</div>
</div>
))}
</div>
);
import된 스타일 객체(container,boardItem등)는 실제로는 빌드 시점에 생성된 고유한 클래스 이름 문자열이다.clsx라이브러리는 여러 개의 클래스를 조건에 따라 조합할 때 유용하며, 객체의 키가 클래스 이름, 값이true일 경우 해당 클래스를 적용한다.
2. SideForm 컴포넌트 기능 및 이벤트 처리 분석
SideForm 컴포넌트는 새로운 게시판을 추가하는 입력 폼의 역할을 한다. 여기서 주목할 점은 이벤트 처리 순서와 상태 관리 로직이다.
2.1. 자동 포커스 기능
사용자가 게시판 추가 버튼을 클릭하면 SideForm이 나타나고, 사용자는 즉시 게시판 이름을 입력할 수 있어야 한다. 이는 <input> 태그의 autoFocus 속성을 통해 구현된다.
// SideForm.tsx
return (
<div className={sideForm}>
<input
autoFocus // 이 속성으로 인해 컴포넌트가 렌더링될 때 자동으로 포커스가 잡힌다.
className={input}
type="text"
// ...
/>
// ...
</div>
);
2.2. onBlur와 onMouseDown을 이용한 이벤트 충돌 해결
- 문제 상황: 사용자가 게시판 이름을 입력하고 추가 버튼(
FiCheck아이콘)을 클릭하려고 하면, 입력창(input)이 포커스를 잃게 된다. 이때onBlur이벤트가onClick이벤트보다 먼저 발생하여,onBlur에 연결된setIsFormOpen(false)가 실행되고 폼이 사라져 버린다. 결과적으로onClick이벤트는 실행되지 않는다. - 해결 방안: 마우스 이벤트의 생명주기에서
onMouseDown은onBlur나onClick보다 먼저 발생한다. 따라서 아이콘의 클릭 이벤트를onClick대신onMouseDown으로 처리하면,onBlur가 발생하기 전에handleClick함수를 실행시킬 수 있다.
// SideForm.tsx
const handleOnBlur = () => {
setIsFormOpen(false); // 포커스를 잃으면 폼을 닫는다.
};
const handleClick = () => {
// 게시판 추가 로직
};
return (
<div className={sideForm}>
<input
// ...
onBlur={handleOnBlur}
/>
{/* onClick 대신 onMouseDown을 사용하여 onBlur보다 먼저 이벤트를 처리한다. */}
<FiCheck className={icon} onMouseDown={handleClick} />
</div>
);
2.3. 게시판 추가 로직 (Redux 상태 변경)
게시판 추가 버튼을 클릭하면 handleClick 함수가 실행된다. 이 함수는 Redux의 상태를 변경하는 작업을 수행한다.
- 입력값 확인:
inputText가 비어있지 않은지 확인한다. addBoard액션 디스패치:uuid라이브러리를 사용해 새로운 게시판의 고유boardId를 생성한다.dispatch함수를 통해addBoard액션을 실행하고,payload로 새로운 게시판 객체를 전달한다.boardsSlice의addBoard리듀서는 이payload를 받아state.boardArray에 새로운 게시판을 추가(push)하여 상태를 업데이트한다.
addLog액션 디스패치:- 게시판이 등록되었다는 로그 메시지를 생성한다.
dispatch함수를 통해addLog액션을 실행한다.loggerSlice의addLog리듀서는 이 로그를state.logArray에 추가한다.
// SideForm.tsx
const handleClick = () => {
if (inputText) {
// 1. addBoard 액션을 디스패치한다.
dispatch(
addBoard({
board: { boardId: uuidv4(), boardName: inputText, lists: [] },
})
);
// 2. addLog 액션을 디스패치한다.
dispatch(
addLog({
logId: uuidv4(),
logMessage: `게시판 등록: ${inputText}`,
logAuthor: "User",
logTimestamp: String(Date.now()),
})
);
}
};
// boardsSlice.ts
// ...
reducers: {
addBoard: (state, { payload }: PayloadAction<TAddBoardAction>) => {
state.boardArray.push(payload.board);
},
// ...
}
// loggerSlice.ts
// ...
reducers: {
addLog: (state, { payload }: PayloadAction<ILogItem>) => {
state.logArray.push(payload);
},
}
컴포넌트의 로컬 상태(useState)와 전역 상태(Redux), 그리고 빌드 타임 CSS(@vanilla-extract/css)를 조화롭게 사용하여, 타입 안전성과 성능, 유지보수성을 모두 고려한 모던 웹 애플리케이션의 구조를 구축할 수 있다.