React & TypeScript/React

[React / TypeScript] styled-components와 함께 사용하는 TypeScript에 대한 고찰 (feat.react)

yoonjong Park 2022. 11. 28.

0. 설치간 오류

JS로 만들어진 패키지, TS type이 정의된 패키지를 모두 설치해야 TS에서 인식할 수 있다.

어찌보면.. 당연. 패키지의 타입을 정의해서 가져다 줘야 해석을 하던가 말던가 하지..

보통 이런 것들은 @types/~패키지명  으로 정의되어 있음.

 

@types/~* 라고 정의되어 있는 경우, 모두 type 정의한 옵션 패키지 구나 정도로 생각하면 된다.

만약 @types/~패키지명을 설치 안하면 다음과 같이 display된다.

Module not found: Error: Can't resolve 'styled-components'

패키지명은 예시

tip. 만약 본인이 패키지를 설치했는데 type이 없는 패키지일 경우, @types/설치한 패키지명 으로 install을 해보면 꽤 유명한 것들은 다 설치가 될 것이다.

 

간단하지만, 그동안 package설치가 어떻게 이루어지는지 명확하게 정리가 안되었는데,

뭔가 좀 더 정확하게 이해된 느낌이다.

 

1. TypeScript는 왜 사용하나?

타입정의로 인한 에러 방지

타입을 정의함으로써 좀 더 명료한 코드 작성 및 동작 가능 -> 막는다.
number로 받기로하면, number만 입력 & 실행이 가능하다.
JavaScript는 그렇게 안된다. undefined를 return 해버린다. 그래서 문제가 될 수 있다.

TypeScript 컴파일러의 강력한 기능

정적타입검사 및 오류 확인 기능이 굉장히 뛰어나다.
그리고, 이 것들을 Browser로 실행하기 전에 알 수 있다는 점이 굉장히 매력적이다.

 

2-1. 사용하는 예

import styled from "styled-components";

interface CircleProps {
  bgColor: string;
}

const Container = styled.div<CircleProps>`  // CircleProps 타입을 사용
  width: 200px;
  height: 200px;
  background-color: ${props => props.bgColor};
  border-radius: 50%;
`;

const Circle = ({ bgColor }: CircleProps) => {  // CircleProps 내부의 bgColor객체의 Property(속성)을 Props로 사용
  return <Container bgColor={bgColor}>박윤종</Container>;
};

// const Circle = (props : CircleProps) => {  // 이런 식으로도 사용 가능
//  return <Container bgColor={props.bgColor}>박윤종</Container>;
// };

export default Circle;

위의 코드는 Styled-Components를 사용하면서, Type을 정의해준 것이다.

코드 보면, 대략 이해를 할 수 있기 때문에,  나머지는 직접 타이핑 하면서 숙달해나가면 될 것 같다.

타입을 정의에 따른 차이

type CircleProps {}
interface CircleProps {}

위와 같은 방식으로 할 수 있는데,결론부터 얘기하면 interface 만 사용하면 된다.

여러가지 측면에서 interface가 더 유리하다. 관련글

가장 좋은 점을 설명하자면, Interface는 미리 정의한 Type Object의 shape과 비교해서 TypeScript가 미리 알려준다.
Prop Types는 브라우저 실행 후에 알게 된다. (안그럴 떄도 있지만 주로 그러하다)

 

2-2 Optional Props

렌더링 된 결과 화면

import styled from "styled-components";

interface CircleProps {
  bgColor: string;
  borderColor?: string; // optional = " ?: ", not required
  text?: string;
}

const Container = styled.div<CircleProps>`
  width: 200px;
  height: 200px;
  background-color: ${props => props.bgColor};
  border-radius: 50%;
  border: 4px solid ${props => props.borderColor};
`;

const Circle = (props: CircleProps) => {
  return (
    <Container
      bgColor={props.bgColor}
      borderColor={props.borderColor ?? "white"}>
      {props.text ?? "default text"}
    </Container>
  );
};

// 아래와 같은 방식도 가능하다. 위 방식보다 아래 쪽 처럼 하는 것이 나중에 테스트 시 용이해질 가능성이 크다. Mock Data를 넣을 수 있기 때문이다.
// const Circle = ({ bgColor, borderColor, text = "default text" }: CircleProps) => {
//   return (
//     <Container bgColor={bgColor} borderColor={borderColor ?? "white"}>
//       {text ?? "default text"}
//     </Container>
//   );
// };

export default Circle;

optional은 ?: 로 표기해서 처리한다.

import Circle from "./Circle";

function App() {
  return (
    <div style={{ background: "purple" }}>
      <Circle bgColor="teal" borderColor="black" />
      <Circle bgColor="tomato" text="예제텍스트" />
    </div>
  );
}

export default App;

text props가 있는 경우와 없는 경우는 "default text"가 적용된 것을 알 수 있다.

 

2-3. State & Type

타입스크립트는 똑똑하다. setState 시, 초기값을 입력하면 초기값에 맞춰서 타입을 자동으로 설정해준다.

없을 경우에만 undefined로 정해질 뿐이다.

useState<number>(0)  이런식으로 <number> 부분을 추가해서 타입을 적어줄 수도 있고, 초기값이 있는 경우는 굳이 안적어줘도 무방하다.

위 코드와 같이 두가지 타입을 갖는 경우는 굉장히 드물고, 보통 1가지 타입만들 넣어서 작업하게 된다.

 

2-4 Forms

import { useState } from "react";

const App = () => {
  const [value, setValue] = useState("");
  const onChange = (event: React.FormEvent<HTMLInputElement>) => {
    const {
      currentTarget: { value }, // value 라는 변수를 만들면서, event.currentTarget 안에 동일한 변수의 값을 넣어준다.
    } = event;
    // const value = event.currentTarget.value; 과 같은 기능을 하긴 함.
    // 객체 분해 할당을 하는 이유는 여러개의 변수를 생성할 때 유용하기 때문.
    // 관련자료 구조분해할당 https://mzl.la/3FeCpXp
    setValue(value);
  };

  const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    console.log("hello,", value);
  };

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          value={value}
          onChange={onChange}
          type="text"
          placeholder="username"
        />
        <button>Log in</button>
      </form>
    </div>
  );
};

export default App;

 

3. styled-components 의 확장

styled-components안에 types가 있는데, 타입스크립트에서 추가적으로 파일을 생성하며 선언병합(Declaration Merging)을 활용해서 확장(extend)해나가는 것이 가능하다. (참고 : 공식문서)

  "devDependencies": {
    "@types/styled-components": "^5.1.26"   // in package.json
  },

 

3-1 styled.d.ts 파일 만들기

// import original module declarations
import "styled-components";

// and extend them!
declare module "styled-components" {
  export interface DefaultTheme {  // DefaultTheme Spelling 잘 보자...한참 찾은 기억이 있다ㅠ
    textColor: string;
    bgColor: string;
    btnColor: string;
  }
}

먼저, src 폴더에 styled.d.ts 파일을 만들어준다. (참고 : 공식문서)

이렇게 함으로써, 타입을 재정의하고 사용할 수 있게 된다.

 

3-2 theme.ts 파일 만들기

import { DefaultTheme } from "styled-components";

export const lightTheme: DefaultTheme = {
  bgColor: "white",
  textColor: "black",
  btnColor: "tomato",
};

export const darkTheme: DefaultTheme = {
  bgColor: "black",
  textColor: "white",
  btnColor: "teal",
};

/src 에 theme.ts 파일을 만들고 위와 같이 작성한다.

3-3. 실제 사용하기

// Index.tsx

import React from "react";
import ReactDOM from "react-dom";
import { ThemeProvider } from "styled-components";
import App from "./App";
import { lightTheme } from "./theme";

ReactDOM.render(
  <React.StrictMode>
    <ThemeProvider theme={lightTheme}>
      <App />
    </ThemeProvider>
  </React.StrictMode>,
  document.getElementById("root")
);

위와 같이 App 컴포넌트를 ThemeProvider로 감싸고 theme를 전달해준다. (theme변경은 나중에 추가설명)

// App.tsx

import styled from "styled-components";

const Container = styled.div`
  background-color: ${props => props.theme.bgColor};
`;
const H1 = styled.h1`
  color: ${props => props.theme.textColor};
`;

const App = () => {
  return (
    <Container>
      <H1>Protected</H1>
    </Container>
  );
};

export default App;

그러고나면, App 컴포넌트 내부에서 props를 통해서 즉각 사용이 가능해진다.

왜냐하면 3-1 과정을 통해서 (styled.d.ts 생성) 

styled-components 원본 패키지의 extend(확장)이 이미 완료되었기 떄문이다. 

 

이제 자유롭게 사용하면 된다ㅎㅎLet go

 

이벤트에 관련된 Type 좀 더 자세한 설명은 다른 글로 추가 설명하겠다.

간단히만 설명하면 이런 것을 말한다.

export default const App = () => {
  const onClick = (event: React.FormEvent<HTMLButtonElement>) => {} 
  // 이 부분 처럼 각종 React Component에 관한 것도 Type 으로 정의해주고
  // 어떤 element가 동작하는것인지 정의어야 한다. 
  // 무작정 any같은 타입을 사용하지 말자.
  return (
  <form>
    <button onClick={onClick}>click me</button>
  </form>
  );
};


// Form 내부 이벤트가 아니라면 아래와 같이 정의되어진다.
export default const App = () => {
  const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {} 
  return (
  	<button onClick={onClick}>click me</button>
  );
};

React.MouseEvent<HTMLButtonElement>  

React.MouseEvent 라는 것은 이벤트가 일어날 타입을 의미하고, <HTMLButtonElement>는 이벤트가 일어나는 대상 인자의 타입을 말한다. 

이 부분만 제대로 이해하면 기본적으로 TypeScript에 대한 이해는 모두 마쳐진 것으로 봐야할 것 같다. 나머지는 TypeScript 문법을 자세히 보면서 실력을 늘려나가면 되겠다.

리액트 이벤트 타입에 관련해서 정의된 글은 다음과 같다.

SyntheticEvent https://ko.reactjs.org/docs/events.html

 

 

참고

노마드코더, React JS 마스터클래스 #3 TypeScript

타입스크립트 type과 interface의 공통점과 차이점

구조분해할당 https://mzl.la/3FeCpXp

styled-components : 타입스크립트로 사용하 (공식문서)  https://styled-components.com/docs/api#typescript

TypeScript Declaration Merging (선언 병합) https://www.typescriptlang.org/docs/handbook/declaration-merging.html

SyntheticEvent (합성 이벤트) https://ko.reactjs.org/docs/events.html

댓글