[타입스크립트 (7)] TypeScript & React

SeHoony·2021년 9월 27일
1

TypeScript

목록 보기
7/7
post-thumbnail

TypeScript를 접한 지 3주가 되어간다. 처음에는 TypeScript가 큰 산으로 느껴졌다. 그런데 이번 주 들어서 왜인지 차츰 개념이 정리되는 느낌이다! 이 느낌을 최대한 글에 녹여 적지만 소중한 독자들의 TypeScript 공부에 도움이 되고 싶다.
이번에는 TypeScript 시리즈의 마지막으로 React & TypeScript 조합을 살펴본다. 기존에 React 개발 경험이 있다면, React.js와 React.ts의 차이점을 위주로 살펴보자!

1. 개괄

본격적인 시작 전에 설명 순서를 간단히 적어보았다. 전체적인 그림을 머리에 딱 박아놓고 시작하자!

[TodoList 만들기 프로젝트]
1. 프로젝트 생성 > 2. 컴포넌트들 생성 > 3. Context-API(변수 전역 관리) > 4. 기능 구현

2. 프로젝트 생성

2-1 ) 터미널 명령어

: 프로젝트 생성 이후, 컴포넌트 생성을 위해 styled-components 라이브러리도 같이 설치했다.

[프로젝트 생성]
-npx create-react-app ts-practice --template typescript
[프로젝트 실행]
-cd ts-practice
-yarn start
[styled-components 라이브러리 설치]
-yarn add styled-components @types/styled-components

3. 컴포넌트 생성

차후에 context-api를 통해 todo 배열과 각 종 메서드들을 관리할 것이기 때문에, 여기서는 그 부분을 제외한 디자인 구성이 주를 이루었다. 디자인은 styled-components로 진행했다.

3-1 ) 파일 구성

  • view만 구현한 파일구성이며, components 디렉토리를 하나 만들고, 거기에 하단의 components들을 생성한다. 최상단 컴포넌트 순으로 index.tsx - app.tsx - Screen.tsx - AddTodo.tsx | List.tsx | ListView.tsx이다.
  • View 완성해놓은 지금까지의 모습이다. 여기서 기능들을 추가해보겠다.

3-2) 스크립트 분석

3-2-1. Screen.tsx

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

3-2-2. ListView.tsx

해당 컴포넌트는 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

3-2-3. List.tsx

해당 컴포넌트는 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

3-2-4. addTodo.tsx

화면 제일 하단에 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가 거의 안나왔기 때문에 복붙을 해도 무방하다고 생각한다.

4. context-API 변수 전역 관리

Context-API에 대한 설명은 여기를 참고하면 전체 흐름에 대해서 파악할 수 있다.

1. index.d.ts 생성 : context type 결정

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
}

2. index.tsx 생성 : context-api 구현

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 }

3. app.tsx 수정 : provider 적용

전에 스크립팅했던 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;

5. 기능 구현

context-api 구현이 완료되었기 때문에 데이터를 가지고 기능을 구현할 수 있다.

5-1. ListView.tsx 수정

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 컴포넌트를 하단에 두 컴포넌트를 보자

5-2. List.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

5-3. AddTodo

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

6. Conclusion!

여태까지 TypeScript의 type부터 OOP, Generic, tsconfig.json 그리고 React.ts까지 살펴보았다. 나는 여태까지 React를 써왔지만 Typescript는 처음이여서 많이 낯설었다. 하지만 점점 써보면서 에러들을 미리 받아볼 수 있고, 그런 에러들을 선제적으로 처리하면서 더 나은 코드가 되고 있다는 느낌을 받을 수 있었다.
TypeScript를 공부하면서 이 언어는 귀찮지만 분명 개발자들에게 좋은 결과를 가져다 주는 친구라는 믿음이 생겼다. 그리고 블로그의 시작을 TypeScript로 해서 많이 부족한 부분도 많지만, 어떻게 써야 이해하기 쉽게 적을까를 고민하면서 내 나름대로 개념 정리가 잘 된 것 같다. 블로그로 개념을 정리하는 거는 완전 추천이다.
요즘 공부하는 React-Native의 공부가 얼추 정착되면, TypeScript & React를 통한 프로젝트도 시리즈로 포스팅해볼 예정이다!

 
profile
두 발로 매일 정진하는 두발자, 강세훈입니다. 저는 '두 발'이라는 이 단어를 참 좋아합니다. 이 말이 주는 건강, 정직 그리고 성실의 느낌이 제가 주는 분위기가 되었으면 좋겠습니다.

0개의 댓글