TypeScript를 접한 지 3주가 되어간다. 처음에는 TypeScript가 큰 산으로 느껴졌다. 그런데 이번 주 들어서 왜인지 차츰 개념이 정리되는 느낌이다! 이 느낌을 최대한 글에 녹여 적지만 소중한 독자들의 TypeScript 공부에 도움이 되고 싶다.
이번에는 TypeScript 시리즈의 마지막으로 React & TypeScript 조합을 살펴본다. 기존에 React 개발 경험이 있다면, React.js와 React.ts의 차이점을 위주로 살펴보자!
본격적인 시작 전에 설명 순서를 간단히 적어보았다. 전체적인 그림을 머리에 딱 박아놓고 시작하자!
[TodoList 만들기 프로젝트]
1. 프로젝트 생성 > 2. 컴포넌트들 생성 > 3. Context-API(변수 전역 관리) > 4. 기능 구현
: 프로젝트 생성 이후, 컴포넌트 생성을 위해 styled-components 라이브러리도 같이 설치했다.
[프로젝트 생성]
-npx create-react-app ts-practice --template typescript
[프로젝트 실행]
-cd ts-practice
-yarn start
[styled-components 라이브러리 설치]
-yarn add styled-components @types/styled-components
차후에 context-api를 통해 todo 배열과 각 종 메서드들을 관리할 것이기 때문에, 여기서는 그 부분을 제외한 디자인 구성이 주를 이루었다. 디자인은 styled-components로 진행했다.
Screen.tsx는 최상단 컴포넌트인데 크게 볼 거 없다. 패스!
import React from 'react'
import styled from 'styled-components'
import ListView from './ListView';
const Container = styled.div`
width : 100%;
height : 100vh;
display : flex;
justify-content : center;
align-items :center;
background-color : #e9ecef;
`;
function Screen() {
return (
<Container>
<ListView/>
</Container>
)
}
export default Screen
해당 컴포넌트는 List.tsx를 담기 위한 컴포넌트이며, 이 후 context에서 받아올 todo들을 처리할 컴포넌트이기도 하다. 아직은 크게 볼 거 없다! 복붙!
import React from 'react'
import styled from 'styled-components'
import AddTodo from './AddTodo';
import List from './List';
const Container = styled.div`
width : 450px;
height : 650px;
border : 3px solid #868e96;
border-radius : 15px;
`;
const Header = styled.div`
h1 {
text-align : center;
color : #585c61;
}
`;
const Body = styled.div`
display : flex;
flex-direction : column;
align-items : center;
height : 470px;
`;
const Bottom = styled.div`
border-top : 3px solid #868e96;
`;
function ListView() {
return (
<Container>
<Header>
<h1>TO DO LIST</h1>
</Header>
<Body>
<List/>
<List/>
</Body>
<Bottom>
<AddTodo/>
</Bottom>
</Container>
)
}
export default ListView
해당 컴포넌트는 ListView.tsx의 todo 데이터를 받아와 처리하고, todo를 삭제하는 remove method가 실행될 컴포넌트이다.
하단에 handleRemove 함수를 주목하자
ex) const handleRemove = (e:React.MouseEvent< HTMLButtonElement>)=>{... ...}
TypeScript는 이렇게 event handle에 있어서 그 type을 밝혀야 한다. 얼핏 보면 e:React.MouseEvent< HTMLButtonElement>1) 이 부분이 어렵기는 하나, 코드에 onClick한 부분에 커서를 올리면 그대로 다 나온다. 그것을 복붙하면 어렵지 않다.
import React from 'react'
import styled from 'styled-components'
const Container = styled.div`
width : 96%;
height : 50px;
border : 2px solid #b0bac4;
border-radius : 15px;
margin-top: 5px;
cursor: pointer;
display : flex;
justify-content : center;
background-color : white;
p {
flex : 10;
font-weight : bold;
padding-left : 1em;
}
`;
const RemoveBtn = styled.button`
width : 40px;
height : 40px;
border-radius : 50%;
border:none;
margin-top : 5px;
margin-right : 7px;
font-weight: bold;
font-size: 1.2em;
color : #e9ecef;
background-color : #fb3838;
cursor: pointer;
border : 2px solid #868e96;
`;
function List() {
const handleRemove = (e:React.MouseEvent<HTMLButtonElement>)=>{
}
return (
<Container>
<p>hello</p>
<RemoveBtn onClick = {handleRemove}>X</RemoveBtn>
</Container>
)
}
export default List
화면 제일 하단에 todo를 추가하기 위한 버튼 구현부이다. 여기서 onSubmit event에서는 event type이 어떻게 다른 지 확인해보면 된다.
그리고 TypeScript에서는 useState를 사용할 시, state의 type을 generic의 형태로 밝힌다. 2) 타입추론이 되기 때문에 생략해도 무방하다. 하지만 null이나 undefinde 체크할 때 < boolean | null> 등으로 유용하게 쓰인다.
const [isWritten, setIsWritten] = useState< boolean>(false);
import React, { useState } from 'react'
import styled from 'styled-components';
const Container = styled.div`
display : flex;
justify-content : center;
align-items : center;
`;
const AddButton = styled.button`
width : 70px;
height : 70px;
border-radius : 50%;
border: 3px solid #868e96;
font-size : 3em;
color : white;
background-color : #46da0c;
margin-top : 11px;
cursor: pointer;
`;
const TodoInput = styled.input`
width : 350px;
height : 50px;
font-size : 1em;
margin-top : 20px;
padding : 0 1em;
`;
function AddTodo() {
const [isWritten, setIsWritten] = useState<boolean>(false); // useState에도 type밝힌다. 특히, null이나 undefined check할 때 좋다.
const handleClick = (e : React.MouseEvent<HTMLButtonElement>) =>{ // event handle에 있어서 event의 type을 밝혀줘야 한다.
setIsWritten(!isWritten);
console.log(isWritten)
}
const handleSubmit = (e : React.FormEvent<HTMLFormElement>)=>{
e.preventDefault();
console.log(e.target); // 추후 변경
setIsWritten(!isWritten);
}
return (
<Container>
{
!isWritten
? <AddButton onClick = {handleClick}>+</AddButton>
: <form onSubmit={handleSubmit}>
<TodoInput
type = "text"
placeholder = "write down your todoList here"
/>
</form>
}
</Container>
)
}
export default AddTodo
지금까지 view부분만 구현을 했다. 지금까지는 사실 크게 typeScript가 거의 안나왔기 때문에 복붙을 해도 무방하다고 생각한다.
Context-API에 대한 설명은 여기를 참고하면 전체 흐름에 대해서 파악할 수 있다.
src - Context - @types - index.d.ts를 만든다. 이는 index.d.ts에서 정의할 context 저장소의 타입을 전역적으로 쓰기 위함이다.
// index.d.ts
interface TodoContext {
todoList: todo[]
addTodo: (todo: string) => void
removeTodo: (id: number) => void
onComplete: (id: number) => void
}
src - Context - index.tsx에 하단의 코드를 기입한다.
import { createContext, useState } from "react"
// 1. create Context
const TodosContext = createContext<TodoContext>({
todoList: [],
addTodo: (todo: string) => {},
removeTodo: (id: number) => {},
onComplete: (id: number) => {},
})
// 2. create Provider function
type Props = {
children: JSX.Element | Array<JSX.Element>
}
type todo = {
id: number
text: string
did: boolean
}
const TodosProvider = ({ children }: Props) => {
const [todoList, setTodoList] = useState<Array<todo>>([]);
const addTodo = (text: string) => {
const nextId = [...todoList].length <=0 ? 1 : Math.max(...todoList.map((item) => item.id)) + 1
const newOne = [...todoList].concat({ id: nextId, text, did: false })
setTodoList(newOne)
}
const removeTodo = (id: number) => {
const newOne = [...todoList].filter((item) => item.id !== id)
setTodoList(newOne)
}
const onComplete = (id: number) => {
const updateOne = [...todoList].map((item) =>
item.id === id ? { ...item, did: !item.did } : item
)
setTodoList(updateOne)
}
return(
<TodosContext.Provider value={{
todoList, addTodo, removeTodo, onComplete
}}>{children}
</TodosContext.Provider>
)
}
export { TodosContext, TodosProvider }
전에 스크립팅했던 contextAPI에 더 자세하게 나와있다. 한 번 더 말하자면, ContextAPI 저장소로 해당 프로젝트의 가장 최상단 컴포넌트인 app.tsx를 감싸줌으로써 context 저장소에 저장된 데이터들을 전역적으로 관리할 수 있게 된다.
import React from 'react';
import './App.css';
import Screen from './components/Screen';
import { TodosProvider } from './Context';
function App() {
return (
<TodosProvider>
<Screen/>
</TodosProvider>
);
}
export default App;
context-api 구현이 완료되었기 때문에 데이터를 가지고 기능을 구현할 수 있다.
1) useContext()
contextAPI에 담겨있는 데이터들을 특정 컴포넌트로 가져올 때 쓰는 메서드이다.
const {todoList} = useContext< TodoContext>(TodosContext)
index.d.ts에서 정의해주었던 todosContext type을 useContext함수에서 generic으로 설정하고, 파라미터로써 TodosContext, context 저장소 자체를 가져온다.
import React, { useContext } from 'react'
import styled from 'styled-components'
import { TodosContext } from '../Context';
import AddTodo from './AddTodo';
import List from './List';
const Container = styled.div`
width : 450px;
height : 650px;
border : 3px solid #868e96;
border-radius : 15px;
`;
const Header = styled.div`
h1 {
text-align : center;
color : #585c61;
}
`;
const Body = styled.div`
display : flex;
flex-direction : column;
align-items : center;
height : 470px;
`;
const Bottom = styled.div`
border-top : 3px solid #868e96;
`;
function ListView() {
const {todoList} = useContext<TodoContext>(TodosContext)
console.log(todoList)
return (
<Container>
<Header>
<h1>TO DO LIST</h1>
</Header>
<Body>
{
todoList.map(todo =><List key = {todo.id} todo={todo}/>)
}
</Body>
<Bottom>
<AddTodo/>
</Bottom>
</Container>
)
}
export default ListView
ListView.tsx 컴포넌트를 하단에 두 컴포넌트를 보자
여기서 주목할 부분도 하나 있다.
type Props = {
todo : {
id : number;
text: string;
did : boolean;
}
}
function List({todo} : Props) {... ...}
jsx에서 props를 받아올 때, 따로 props의 타입을 밝힐 필요는 없었다. 하지만 typeScript의 백미는 에러 체크에 있다. 상위 컴포넌트가 props를 내려주기 전에, 특정 컴포넌트에서 자신이 받을 props의 type을 미리 지정해준다. 이로써 꼭 필요한 props들을 상위 컴포넌트에 요구하고, props가 충분치 않을 때 에러를 통해 개발자에게 요구할 수 있다!
방식은 위에서 보는 바와 같이 컴포넌트 위에다가 props의 type을 밝히고, 그것을 컴포넌트의 로직의 파라미터 자리에 넣어준다.이것이 tsx와 jsx의 가장 차이나는 생김새 중에 하나라고 생각한다.
import React, { useContext } from 'react'
import styled from 'styled-components'
import { TodosContext } from '../Context';
const RemoveBtn = styled.button`
width : 40px;
height : 40px;
border-radius : 50%;
border:none;
margin-top : 5px;
margin-right : 7px;
font-weight: bold;
font-size: 1.2em;
color : #e9ecef;
background-color : #fb3838;
cursor: pointer;
border : 2px solid #868e96;
`;
type Props = {
todo : {
id : number;
text: string;
did : boolean;
}
}
function List({todo} : Props) {
const {removeTodo, onComplete} = useContext<TodoContext>(TodosContext);
const handleRemove = (e:React.MouseEvent<HTMLButtonElement>)=>{
removeTodo(todo.id);
}
const handleClick = (e : React.MouseEvent<HTMLDivElement>)=>{
onComplete(todo.id)
}
const Container = styled.div`
width : 96%;
height : 50px;
border : 2px solid #b0bac4;
border-radius : 15px;
margin-top: 5px;
cursor: pointer;
display : flex;
justify-content : center;
background-color :${!todo.did ? "white": "#5f5f5f"} ;
div {
flex : 10;
font-weight : bold;
padding-left : 1em;
margin-top : 1em;
color : ${!todo.did ? "black": "white"}
}
`;
return (
<Container >
<div onClick = {handleClick}>{todo.text}</div>
<RemoveBtn onClick = {handleRemove}>X</RemoveBtn>
</Container>
)
}
export default List
import React, { useContext, useState } from 'react'
import styled from 'styled-components';
import { TodosContext } from '../Context';
const Container = styled.div`
display : flex;
justify-content : center;
align-items : center;
`;
const AddButton = styled.button`
width : 70px;
height : 70px;
border-radius : 50%;
border: 3px solid #868e96;
font-size : 3em;
color : white;
background-color : #46da0c;
margin-top : 11px;
cursor: pointer;
`;
const TodoInput = styled.input`
width : 350px;
height : 50px;
font-size : 1em;
margin-top : 20px;
padding : 0 1em;
`;
function AddTodo() {
const [isWritten, setIsWritten] = useState<boolean>(false); // useState에도 type밝힌다. 특히, null이나 undefined check할 때 좋다.
const [text, setText] = useState<string>("");
const {addTodo} = useContext<TodoContext>(TodosContext)
const handleClick = (e : React.MouseEvent<HTMLButtonElement>) =>{ // event handle에 있어서 event의 type을 밝혀줘야 한다.
setIsWritten(!isWritten);
}
const handleSubmit = (e : React.FormEvent<HTMLFormElement>)=>{
e.preventDefault();
addTodo(text);
setIsWritten(!isWritten);
setText("");
}
return (
<Container>
{
!isWritten
? <AddButton onClick = {handleClick}>+</AddButton>
: <form onSubmit={handleSubmit}>
<TodoInput
type = "text"
value = {text}
onChange = {(e : React.ChangeEvent<HTMLInputElement>)=>setText(e.target.value)}
placeholder = "write down your todoList here"
autoFocus
/>
</form>
}
</Container>
)
}
export default AddTodo
여태까지 TypeScript의 type부터 OOP, Generic, tsconfig.json 그리고 React.ts까지 살펴보았다. 나는 여태까지 React를 써왔지만 Typescript는 처음이여서 많이 낯설었다. 하지만 점점 써보면서 에러들을 미리 받아볼 수 있고, 그런 에러들을 선제적으로 처리하면서 더 나은 코드가 되고 있다는 느낌을 받을 수 있었다.
TypeScript를 공부하면서 이 언어는 귀찮지만 분명 개발자들에게 좋은 결과를 가져다 주는 친구라는 믿음이 생겼다. 그리고 블로그의 시작을 TypeScript로 해서 많이 부족한 부분도 많지만, 어떻게 써야 이해하기 쉽게 적을까를 고민하면서 내 나름대로 개념 정리가 잘 된 것 같다. 블로그로 개념을 정리하는 거는 완전 추천이다.
요즘 공부하는 React-Native의 공부가 얼추 정착되면, TypeScript & React를 통한 프로젝트도 시리즈로 포스팅해볼 예정이다!