프로젝트/Task 어플리케이션

1118 Board List 생성, Side Form 생성, Style 생성 - vanilla extract

thinktank911 2025. 11. 18. 22:59

Board List 생성

이 부분이 Board List임

 

App.tsx

게시물 목록 중 활성화된 게시물 페이지를 표시하기 위해 활성화된 id를 식별하여 props로 전달해준다.

  • activeBoardId 생성, props로 전달
  • 초기값은 첫번째로 오는 게시물의 id인 'board-0'으로 셋팅
// App.tsx
function App() {
  const [activeBoardId, setActiveBoardId] = useState('board-0');

  return (
    <div className={appContainer}>
      <BoardList 
        activeBoardId={activeBoardId} 
        setActiveBoardId={setActiveBoardId} 
        />

 

BoardList.tsx

useSelector 로 boards에 있는 상태 데이터 가져오기

  • BoardList의 props 타입 정의
  • useSelector 쓰기
  • boardsSlice에 정의된 initialState 중 boardArray 가져온다. 
  • boardArray를 map으로 돌려서 UI 생성
type TBoardListProps = {
  activeBoardId : string;
  setActiveBoardId : React.Dispatch<React.SetStateAction<string>>
}
const BoardList = ({
    activeBoardId,
    setActiveBoardId
} : TBoardListProps) => {


const { boardArray } = useTypedSelector(state => state.boards);

...
{boardArray.map((board, index)=> (
    <div key={board.boardId}
      onClick={()=> setActiveBoardId(boardArray[index].boardId)}
      className={
        clsx(
          {
            [boardItemActive]:
            boardArray.findIndex(b => b.boardId === activeBoardId) === index,
          },
          {
            [boardItem]:
            boardArray.findIndex(b => b.boardId === activeBoardId) !== index,
          }
        )
      }
    >
    <div>
        {board.boardName}
      </div>
    </div>
))}

 

 

※ clsx

clsx는 조건부로 클래스 이름을 동적으로 결합하는 데 사용되는 JavaScript 라이브러리

문자열, 객체, 배열을 인자로 넘겨주어 클래스를 조합하며, 

`const className = clsx('foo', isActive && 'bar', { active: isActive })`와 같은 형식으로 사용

 

기본 사용: 문자열을 인자로 넘겨주면 공백으로 구분된 하나의 문자열로 결합합니다.

clsx('foo', 'bar') → 'foo bar'

 


조건부 클래스 사용: 객체 형태로 조건을 전달하여 조건이 true일 때만 해당 클래스를 포함시킵니다.

const isActive = true; 
clsx('foo', { 'bar': isActive }) → 'foo bar'


배열 사용: 배열을 인자로 넘겨주고, 배열 안에서 조건부 로직을 사용하여 클래스를 결합할 수 있습니다.

clsx(['foo', isActive && 'bar']) → 'foo bar'


다양한 조합: 위 세 가지 방법을 자유롭게 섞어서 사용할 수 있습니다.

const className = 
clsx('foo', isActive && 'bar', { active: isActive, disabled: false }) → 'foo bar active'

 

Board List Style 생성하기

 

Vanilla Extract 라이브러리 사용하여 css style 생성

2025.11.18 - [프레임워크\라이브러리/CSS] - [라이브러리] Vanilla Extract란?

 

[라이브러리] Vanilla Extract란?

Vanilla Extract란?Zero-runtime CSS in JS 라이브러리. CSS-in-JS는 말 그대로 "JavaScript 안에서 CSS를 작성하는 방식"이다.즉, 스타일을 별도의 CSS 파일이 아니라 JavaScript 코드 안에서 작성하는 것.대표적인 CSS-

thinktank911.tistory.com

 

Side Form 생성

이 부분이 Side Form임

  • board List 우측 + 버튼 누르면 나오는 입력 폼이 Side Form임

BoardList.tsx

  • form이 열려있는지 유무 상태 값을 받는 isFormOpen useState 선언
  • isFormOpen 이 true인 경우 sideForm을, false인 경우 'react-icons/fi' 모듈에서 받아온 FiPlusCircle 컴포넌트 셋팅
  • SideForm 컴포넌트에 setIsFormOpen props 넘겨주기
const [isFormOpen, setIsFormOpen] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);
  const handleClick = () => {
    setIsFormOpen(!isFormOpen);
    setTimeout(() => {
      inputRef.current?.focus();
    },0);
  }
  
 return (
 	<div className={addSection}>
      {
        isFormOpen ?
        <SideForm inputRef={inputRef} setIsFormOpen={setIsFormOpen} />
        : 
        <FiPlusCircle className={addButton} onClick={handleClick} />
      }
    </div>
 )

 

SideForm.tsx

  • props 타입 설정
  • useState로 inputText 셋팅
  • onChange 함수에 setInputText 정의해 inputText 데이터 변경
  • onBlur(포커스 해제) 함수에 setIsFormOpen(false); 정의해 입력 폼 닫기
// props 타입 지정
type TSideFormProps = {
  setIsFormOpen : React.Dispatch<React.SetStateAction<boolean>>,
  inputRef : React.RefObject<HTMLInputElement>
}

const SideForm: FC<TSideFormProps> = ({
  setIsFormOpen,
  inputRef
}) => {

  // 입력텍스트 useState 선언 
  const [inputText, setinputText] = useState('');
  // 입력텍스트 값 변경
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setinputText(e.target.value);
  }
  // 포커스 해제 시 입력 폼 닫기
  const handleOnBlur = () => {
    setIsFormOpen(false);
  }
  
  return (
    <div className={sideForm}>
      <input 
        className={input}
        type='text'
        placeholder='새로운 게시판 등록하기'
        value={inputText}
        onChange={handleChange}
        onBlur={handleOnBlur}
      />
      <FiCheck className={icon} onMouseDown={handleClick}/>
    </div>
  )

 

입력부 포커스 주기

  • useRef 사용
    • useRef는 HTML 엘리먼트에 접근 및 제어하기 위해 사용한다.
    • ref={inputRef}로 props 넘겨주면 current 속성 이용해 해당 input 엘리먼트 제어가 가능
// BoardList.tsx
const inputRef = useRef<HTMLInputElement>(null);
const handleClick = () => {
    setIsFormOpen(!isFormOpen);	// 입력폼 열리고나서
    setTimeout(() => {
      inputRef.current?.focus();	// 포커스주기
    },0);
}
  
{
isFormOpen ?
<SideForm inputRef={inputRef} setIsFormOpen={setIsFormOpen} />
: 
<FiPlusCircle className={addButton} onClick={handleClick} />
}
  
// SideForm.tsx
<input 
    ref={inputRef}
    className={input}
  • autoFocus 사용
    • 사실 ref 대신 autoFocus 선언해주면 간단함
<div className={sideForm}>
  <input 
    // ref={inputRef}
    autoFocus
    className={input}

 

게시물 추가 기능 구현

  • boardSlice 리듀서 작성
  • payload 의미 : dispatch(action)로 보내주는 action 함수의 데이터 값

boardsSlice.ts

const boardsSlice = createSlice({
    name: 'boards', // state를 식별해주는 이름
    initialState,
    reducers: {
    	// state는 초기값, payload는 dispatch에서 보내준 값
        addBoard: (state, {payload} : PayloadAction<TAddBoardAction>) => {
            state.boardArray.push(payload.board)	// boardArray에 board 추가
        }
    }
})

export const {addBoard} = boardsSlice.actions;
export const boardsReducer = boardsSlice.reducer;

 

SideForm.tsx

const handleClick = () => {
    if(inputText){
    // 게시물 추가 데이터 전달
      dispatch(
        addBoard({
          board: {
            boardId: uuidv4(), 
            boardName: inputText,
            lists: []
          }
        })
      )

	// 로그 추가 데이터 전달
      dispatch(
        addLog({
          logId: uuidv4(),
          logMessage: `게시판 등록 ${inputText}`,
          logAuthor: 'admin',
          logTimestamp: new Date().toISOString()
        })
      )
    }
  }
  
  return (
    <div className={sideForm}>
      <input 
        // ref={inputRef}
        autoFocus
        className={input}
        type='text'
        placeholder='새로운 게시판 등록하기'
        value={inputText}
        onChange={handleChange}
        onBlur={handleOnBlur}
      />
      {/* onClick 대신 onMouseDown을 사용하여 onBlur보다 먼저 이벤트를 처리한다. */}
      <FiCheck className={icon} onMouseDown={handleClick}/>
    </div>
  )

 

[이슈]

  • 문제발생 : handleClick을 onClick 이벤트로 선언했더니 제대로 게시물 추가가 안되었다.
  • 원인파악 : onClick 보다 onBlur가 먼저 발생해 handleOnBlur 안에서 SetIsFormOpen으로 입력폼이 닫히면
    추가할 inputText가 사라져 onClick이 실행되지 않는다.
  • 해결방안 : onBlur보다 먼저 실행되는 onMouseDown로 대신해서 선언한다.
※ 마우스 이벤트 발생 순서
onMouseDown ➡️ onBlur ➡️ onMouseUp ➡️ onClick 순

 


컴포넌트의 로컬 상태(useState)와 전역 상태(Redux), 그리고 빌드 타임 CSS(@vanilla-extract/css)를 조화롭게 사용하여, 타입 안전성과 성능, 유지보수성을 고려한 어플리케이션 구조를 적용할 수 있었다.