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 생성

- 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)를 조화롭게 사용하여, 타입 안전성과 성능, 유지보수성을 고려한 어플리케이션 구조를 적용할 수 있었다.
'프로젝트 > Task 어플리케이션' 카테고리의 다른 글
| 1120 Firebase로 배포하기 (0) | 2025.11.21 |
|---|---|
| 1120 드래그 앤 드롭 기능 만들기 (0) | 2025.11.20 |
| 1119 EditModal, LoggerModal, LogItem 컴포넌트 생성, 게시판 삭제 기능 (0) | 2025.11.19 |
| 1117 리액트를 이용한 태스크 정리 앱 만들기 - Vite, Redux 셋팅, Redux Hooks 타입 지정, 전역 스타일 생성 (0) | 2025.11.17 |