처음 계획은 이미 제작한 투두 리스트를 타입스크립트로 리팩토링 할 예정이었지만 아직 익숙하지 않아 클론 코딩을 진행하였다.
프로젝트 소개
타입스크립트 사용이 목적이기 때문에 최대한 간단한 레이아웃과 기능을 구현한다. 할일을 작성하여 엔터 시 리스트에 할일이 생성되고 체크박스를 체크 했을때, 완료의 목적으로 내용에 줄이 그어지며 삭제할 수 있는 X버튼을 구현하여 목록을 삭제할 수 있게 한다.
리액트로 투두리스트를 구현하는것과 다른 점은 파라미터를 받을 때 타입을 설정해주어야 하는 것이다.
npx create-react-app tstodolist --template=typescript
//App.tsx
import React from 'react';
import '../styles/inputText.css';
interface InputTextProps { //인터페이스 사용
onChange(e: React.ChangeEvent<HTMLInputElement>): void; //아무것도 반환하지 않는 void타입
onKeyDown(e: React.KeyboardEvent<HTMLInputElement>): void; //엔터 시 텍스트 입력
inputText: string; //텍스트가 들어갈 것이므로 string 타입 지정
}
const InputText = ({
onChange,
onKeyDown,
inputText
}:InputTextProps) => {
return (
// <div className="inputTextContainer">
<input type="text"
className="inputText"
placeholder='Enter'
onChange={e=> onChange(e)}
onKeyDown={e => onKeyDown(e)}
value={inputText}
/>
// </div>
);
}
export default InputText;
initialState의 타입을 설정하기 위해 interface를 설정해준다.
// CheckBox.tsx
import React from 'react';
import '../styles/checkBox.css';
interface CheckBoxProps { //인터페이스 사용
checked?: boolean; //불리언 타입 지정
onClick?(): void;
}
const CheckBox = ({
checked,
onClick
}: CheckBoxProps) => { //체크 되었을때 표시 "O"
return (
<>
<div className="container" onClick={onClick}>
<div className="checkIcon">
{checked && "O"}
</div>
</div>
</>
);
}
export default CheckBox;
체크박스를 눌렀을때, O표시가 되고 색이 변한다.
//DeleteButton.tsx
import React from 'react';
import '../styles/deleteButton.css'
interface DeleteButtonProps { //인터페이스 사용
onClick? (): void;
}
const DeleteButton = ({
onClick
}:DeleteButtonProps) => {
return (
<>
<div className="deleteButtonContainer" onClick={onClick}>
<div className="checkIcon">
{'X'} // X표시
</div>
</div>
</>
);
}
export default DeleteButton;
//InputText.tsx
import React from 'react';
import '../styles/inputText.css';
interface InputTextProps { //인터페이스 사용
onChange(e: React.ChangeEvent<HTMLInputElement>): void;
onKeyDown(e: React.KeyboardEvent<HTMLInputElement>): void;
inputText: string;
}
const InputText = ({
onChange,
onKeyDown,
inputText
}:InputTextProps) => {
return (
<input type="text"
className="inputText"
placeholder='Enter'
onChange={e=> onChange(e)}
onKeyDown={e => onKeyDown(e)}
value={inputText}
/>
);
}
export default InputText;
import React from 'react';
import '../styles/item.css';
import CheckBox from './CheckBox';
import DeleteButton from './DeleteButton';
import Text from './Text';
interface ItemProps { //인터페이스 사용
onClickCheckBox(id: number): void; //id값을 추가하여 체크박스를 구분한다
onClickDeleteButton(id: number): void;
completed?: boolean;
text: string;
id: number;
}
const Item = ({
onClickCheckBox,
onClickDeleteButton,
completed,
text,
id,
}:ItemProps) => {
return (
<>
<div className="itemContainer">
<CheckBox
checked={completed}
onClick={() => onClickCheckBox(id)}
/>
<Text completed={completed}>
{text}
</Text>
<DeleteButton
onClick={() => onClickDeleteButton(id)}
/>
</div>
</>
);
}
export default Item;
//Text.tsx
import React from 'react';
import '../styles/text.css';
interface TextProps { //인터페이스 사용
completed?: boolean; //불리언 타입 지정
children: React.ReactNode;
}
const Text = ({
completed,
children
}: TextProps) => {
return (
<>
<div className={`text ${completed ? 'completedText' : ''}`}>
{children}
</div>
</>
);
}
export default Text;
children은 주로 재사용을 위해서 사용되며 children 자리에 원하는 구조의 JSX를 넣을 수 있다는 것도 편한것 같다. 하지만 유지보수나 구조가 복잡해질때를 대비하여 사용을 지양해야 할 것 같다.
text-decoration: line-through;
체크된 목록의 텍스트에 줄 긋기는 CSS단에 코드를 작성한다.
//TodoList.tsx
import React, {useRef, useState} from 'react';
import InputText from './InputText';
import Item from './Item';
import '../styles/todoList.css';
interface TList {
id: number; //구분을 위해 id값을 작성하고 타입 지정
text: string;
completed: boolean;
}
const TodoList = () => {
const [inputText, setInputText] = useState('');
const [tasks, setTasks] = useState<TList[]>([
]);
const nextId = useRef(4);
// 체크박스 핸들러
const handleClickCheckBox = (id: number) => { //id를 가진 컴포넌트의 상태변경
setTasks(tasks.map(task =>
task.id === id ? {...task, completed: !task.completed} : task
));
}
// 삭제버튼 핸들러
const handleClickDeleteButton = (id: number) => {
setTasks(tasks.filter( task => task.id !== id)); //해당 id를 가진 컴포넌트 삭제
}
// 입력값 변경 핸들러
const handleInputTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputText(e.target.value);
}
// 입력값 엔터 핸들러
const handleInputTextKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && e.nativeEvent.isComposing === false){ //ENTER 입력이 되면 이벤트 실행
const newList: TList = {
id: nextId.current,
text: inputText,
completed: false,
}
setTasks(tasks.concat(newList));
setInputText('');
nextId.current += 1;
}
}
console.log(tasks);
return (
<div className='appContainer'>
<div className="todoListContainer">
<div className="todoList">
{tasks.map( task =>
<Item
key={`${task.id}task`}
id = {task.id}
text = {task.text}
completed={task.completed}
onClickCheckBox={handleClickCheckBox}
onClickDeleteButton={handleClickDeleteButton}
/>
)}
</div>
<InputText
onChange={handleInputTextChange}
onKeyDown={handleInputTextKeyDown}
inputText={inputText}
/>
</div>
</div>
);
}
export default TodoList;