$ npx create-react-app [프로젝트 폴더명] --template typescript
$ cd [프로젝트 폴더명]
$ yarn add redux react-redux @types/react-redux
o
x
react-redux
대안
@types/
를앞에 붙여서 설치
라이브러리의 타입스크립트 지원 여부 확인 방법
index.d.ts
파일 유무 확인@types
써드파티 라이브러리 (라이브러리에 타입스크립트 지원 가능하도록 추가)
라이브러리의 써드 파티 타입스크립트 지원 여부 확인 방법
npm 에서 @types/라이브러리명
을 입력
TypeSearch 에서 라이브러리명을 검색
→ 액션타입, 액션생성함수, 리듀서를 모두 한 파일에 작성
// 액션 타입 선언
const INCREASE = "counter/INCREASE" as const;
const DECREASE = "counter/DECREASE" as const;
const INCREASE_BY = "counter/INCREASE_BY" as const;
// 액션 생성 함수 선언
export const increase = () => ({
type: INCREASE,
});
export const decrease = () => ({
type: DECREASE,
});
export const increaseBy = (diff: number) => ({
type: INCREASE_BY,
payload: diff,
});
// 모든 액션 객체들에 대한 타입 준비
type CounterAction =
| ReturnType<typeof increase>
| ReturnType<typeof decrease>
| ReturnType<typeof increaseBy>;
// 이 리덕스 모듈에서 관리할 상태의 타입 선언
type CounterState = {
count: number;
};
// 초기 상태 선언
const initialState: CounterState = {
count: 0,
};
// 리듀서 작성
function counter(
state: CounterState = initialState,
action: CounterAction
): CounterState {
switch (action.type) {
case INCREASE: // case 입력 후, Ctrl + Space 를 누르면 어떤 종류의 action.type들이 있는지 확인 가능
return { count: state.count + 1 };
case DECREASE:
return { count: state.count - 1 };
case INCREASE_BY:
return { count: state.count + action.payload };
default:
return state;
}
}
export default counter;
action.type
이 실제 문자열로 추론 되도록 함.
추후 액션 객체 만들 때, action.type
의 값을 추론하는 과정에서
action.type
이 string
이 아닌, 실제 문자열(ex. "counter/INCREASE"
)로 추론 되도록 함.
액션에 부가적으로 필요한 값
FSA 규칙 :
이 규칙을 적용하면 액션들이 모두 비슷한 구조로 이루어지게 됨.
→ 추후 다룰 때 편함 + 읽기 쉬움 + (액션 구조 일반화 → 액션 관련 라이브러리 사용 가능)
무조건 따를 필요 x
ReturnType<typeof _____>
특정 함수의 반환값을 추론
주의
상단부의 액션 타입 선언 시, as const
를 하지 않으면 제대로 작동 x
리듀서
state와 함수의 반환값이 일치하도록 작성
액션
CounterAction
을 타입으로 설정
import { combineReducers } from "redux";
import counter from "./counter";
const rootReducer = combineReducers({
counter,
});
// 루트 리듀서 내보내기
export default rootReducer;
// 루트 리듀서의 반환값 유추
// 추후 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야 하므로 내보내줌.
export type RootState = ReturnType<typeof rootReducer>;
index.tsx
에서 스토어 제작- 스토어를 프로젝트에 적용 (
Provider
컴포넌트 사용)
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './modules';
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
주의
리액트 컴포넌트 작성 시 → .tsx
확장자 사용
src/components/Counter.tsx
import React from 'react';
type CounterProps {
count: number;
onIncrease: () => void;
onDecrease: () => void;
onIncreaseBy: (diff: number) => void;
}
function Counter({
count,
onIncrease,
onDecrease,
onIncreaseBy
}: CounterProps) {
return (
<div>
<h1>{count}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
<button onClick={() => onIncreaseBy(5)}>+5</button>
</div>
);
}
export default Counter;
리덕스의 값 불러와서 사용, 액션도 디스패치함.
src/containers/CounterContainer.tsx
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../modules";
import { increase, decrease, increaseBy } from "../modules/counter";
import Counter from "../components/Counter";
function CounterContainer() {
// 상태 조회 (상태 조회 시, state의 타입을 RootState로 지정)
const count = useSelector((state: RootState) => state.counter.count);
const dispatch = useDispatch(); // 디스패치 함수 가져옴
// 각 액션들을 디스패치하는 함수 제작
const onIncrease = () => {
dispatch(increase());
};
const onDecrease = () => {
dispatch(decrease());
};
const onIncreaseBy = (diff: number) => {
dispatch(increaseBy(diff));
};
return (
<Counter
count={count}
onIncrease={onIncrease}
onDecrease={onDecrease}
onIncreaseBy={onIncreaseBy}
/>
);
}
export default CounterContainer;
count
값의 타입
useSelector
가 (알아서) 유추
→ 굳이 :number
라고 타입을 설정 할 필요 x
App
에서CounterContainer
렌더링 (App.tsx
)
import React from "react";
import CounterContainer from "./containers/CounterContainer";
const App: React.FC = () => {
return <CounterContainer />;
};
export default App;
- 개발 서버 구동 (
yarn start
)
// **src/modules/todos.ts**
// 액션 타입 선언
const ADD_TODO = "todos/ADD_TODO" as const;
const TOGGLE_TODO = "todos/TOGGLE_TODO" as const;
const REMOVE_TODO = "todos/REMOVE_TODO" as const;
// 새로운 항목을 추가 할 때 사용 할 고유 ID 값
let todosId = 0;
// 액션 생성 함수
export const addTodo = (text: string) => ({
type: ADD_TODO,
payload: {
id: todosId++,
text,
},
});
export const toggleTodo = (id: number) => ({
type: TOGGLE_TODO,
payload: id,
});
export const removeTodo = (id: number) => ({
type: REMOVE_TODO,
payload: id,
});
// 모든 액션 객체들에 대한 타입
type TodosAction =
| ReturnType<typeof addTodo>
| ReturnType<typeof toggleTodo>
| ReturnType<typeof removeTodo>;
// 투두 타입
export type Todo = {
id: number;
text: string;
done: boolean;
};
// 상태 타입 (전체 투두)
export type TodosState = Todo[];
// 초기 상태
const initialState: TodosState = [];
// 리듀서
function todos(
state: TodosState = initialState,
action: TodosAction
): TodosState {
switch (action.type) {
case ADD_TODO:
return state.concat({
// action.payload 객체 안의 값이 모두 유추됨.
id: action.payload.id,
text: action.payload.text,
done: false,
});
case TOGGLE_TODO:
return state.map((todo) =>
// payload 가 number 인 것이 유추됨.
todo.id === action.payload ? { ...todo, done: !todo.done } : todo
);
case REMOVE_TODO:
// payload 가 number 인 것이 유추됨.
return state.filter((todo) => todo.id !== action.payload);
default:
return state;
}
}
export default todos;
todos
리듀서 등록// modules/index.ts
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos
});
// 루트 리듀서를 내보내주세요.
export default rootReducer;
// 루트 리듀서의 반환값를 유추해줍니다
// 추후 이 타입을 컨테이너 컴포넌트에서 불러와서 사용해야 하므로 내보내줍니다.
export type RootState = ReturnType<typeof rootReducer>;
TodoInsert
: 새 항목 등록용TodoItem
: 할 일 정보를 보여주는 용TodoList
: 여러 개의 TodoItem
을 렌더링하는 용src/components/TodoInsert.tsx
onInsert
이 props
(함수) 를 받아와 호출하여 새 항목 추가
input
의 상태
컴포넌트 내부에서 로컬 상태로 관리
import React, { ChangeEvent, FormEvent, useState } from "react";
type TodoInsertProps = {
onInsert: (text: string) => void;
};
function TodoInsert({ onInsert }: TodoInsertProps) {
const [value, setValue] = useState("");
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const onSubmit = (e: FormEvent) => {
e.preventDefault();
onInsert(value);
setValue("");
};
return (
<form onSubmit={onSubmit}>
<input
placeholder="할 일을 입력하세요."
value={value}
onChange={onChange}
/>
<button type="submit">등록</button>
</form>
);
}
export default TodoInsert;
src/components/TodoItem.tsx
각 할 일 항목에 대한 정보를 보여주는 컴포넌트
done
값이 바뀜props
로 받아오는 것todo
: 할 일 정보onToggle
& onRemove
: 상태 토글 및 삭제를 해주는 함수CSSProperties
이란?
style 객체의 타입
import React, { CSSProperties } from "react";
import { Todo } from "../modules/todos";
type TodoItemProps = {
todo: Todo;
onToggle: (id: number) => void;
onRemove: (id: number) => void;
};
function TodoItem({ todo, onToggle, onRemove }: TodoItemProps) {
const textStyle: CSSProperties = {
textDecoration: todo.done ? "line-through" : "none",
};
const removeStyle: CSSProperties = {
marginLeft: 8,
color: "red",
};
const handleToggle = () => {
onToggle(todo.id);
};
const handleRemove = () => {
onRemove(todo.id);
};
return (
<li>
<span onClick={handleToggle} style={textStyle}>
{todo.text}
</span>
<span onClick={handleRemove} style={removeStyle}>
(X)
</span>
</li>
);
}
export default TodoItem;
src/components/TodoList.tsx
여러 개의
TodoItem
컴포넌트를 렌더링
props
로 받아오는 것
todo
: 할 일 정보
onToggle
& onRemove
: TodoItem
컴포넌트들에게 전달
코드
import React from 'react';
import { Todo } from '../modules/todos';
import TodoItem from './TodoItem';
type TodoListProps = {
todos: Todo[];
onToggle: (id: number) => void;
onRemove: (id: number) => void;
};
function TodoList({ todos, onToggle, onRemove }: TodoListProps) {
if (todos.length === 0) return <p>등록된 항목이 없습니다.</p>;
return (
<ul>
{todos.map(todo => (
<TodoItem
todo={todo}
onToggle={onToggle}
onRemove={onRemove}
key={todo.id}
/>
))}
</ul>
);
}
export default TodoList;
// src/containers/TodoApp.tsx
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../modules";
import { toggleTodo, removeTodo, addTodo } from "../modules/todos";
import TodoInsert from "../components/TodoInsert";
import TodoList from "../components/TodoList";
function TodoApp() {
const todos = useSelector((state: RootState) => state.todos);
const dispatch = useDispatch();
const onInsert = (text: string) => {
dispatch(addTodo(text));
};
const onToggle = (id: number) => {
dispatch(toggleTodo(id));
};
const onRemove = (id: number) => {
dispatch(removeTodo(id));
};
return (
<>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onToggle={onToggle} onRemove={onRemove} />
</>
);
}
export default TodoApp;
// src/App.tsx
import React from 'react';
import TodoApp from './containers/TodoApp';
const App: React.FC = () => {
return <TodoApp />;
};
export default App;
참고