카테고리 없음

[48일차]React-Task-App 2일차

PARKpatchnotes 2025. 11. 18. 16:37

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. onBluronMouseDown을 이용한 이벤트 충돌 해결

  • 문제 상황: 사용자가 게시판 이름을 입력하고 추가 버튼(FiCheck 아이콘)을 클릭하려고 하면, 입력창(input)이 포커스를 잃게 된다. 이때 onBlur 이벤트가 onClick 이벤트보다 먼저 발생하여, onBlur에 연결된 setIsFormOpen(false)가 실행되고 폼이 사라져 버린다. 결과적으로 onClick 이벤트는 실행되지 않는다.
  • 해결 방안: 마우스 이벤트의 생명주기에서 onMouseDownonBluronClick보다 먼저 발생한다. 따라서 아이콘의 클릭 이벤트를 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의 상태를 변경하는 작업을 수행한다.

  1. 입력값 확인: inputText가 비어있지 않은지 확인한다.
  2. addBoard 액션 디스패치:
    • uuid 라이브러리를 사용해 새로운 게시판의 고유 boardId를 생성한다.
    • dispatch 함수를 통해 addBoard 액션을 실행하고, payload로 새로운 게시판 객체를 전달한다.
    • boardsSliceaddBoard 리듀서는 이 payload를 받아 state.boardArray에 새로운 게시판을 추가(push)하여 상태를 업데이트한다.
  3. addLog 액션 디스패치:
    • 게시판이 등록되었다는 로그 메시지를 생성한다.
    • dispatch 함수를 통해 addLog 액션을 실행한다.
    • loggerSliceaddLog 리듀서는 이 로그를 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)를 조화롭게 사용하여, 타입 안전성과 성능, 유지보수성을 모두 고려한 모던 웹 애플리케이션의 구조를 구축할 수 있다.