새로운 블로그로 이전 작업을 진행하고 있어 포스트가 새로 작성되고 있지 않습니다.

빠른 시일 내에 새로운 블로그로 인사드리겠습니다.

새로운 블로그 : https://unho.vercel.app/

본문 바로가기
공부 및 정리/프론트엔드

[React with Typescript] React에 Typescript 적용시키기 (1)

by 언호 2022. 1. 8.

개인 학습 과정에서 작성한 markdown 문서로 잘못된 내용이 존재할 수 있습니다.

잘못된 내용 또는 개선 방안을 말씀해주시면 수정하겠습니다.

 

 

📝 React에 TypeScript 적용시키기 (1)

본 글은 자세한 개념 정리가 아닌, 공식문서 및 여러 강의를 듣고 개인 학습 및 복습용으로 작성하였습니다.

 

 

리액트에 타입스크립트 추가하기

CRA (create-react-app) 을 통해서 간단하게 적용시킬 수 있습니다.

초기 새로운 프로젝트를 생성할때 적용시키기 위해서는 아래의 명령어를 사용하면 됩니다.

npx create-react-app my-app --template typescript

 

 

리액트 프로젝트 생성을 위해서는 --template 옵션이 들어가지 않아서 모르다가 타입스크립트를 추가하면서 처음 접하게 되었는데, 간단하게 생각하면 typescript 라는 이름을 가진 누군가 미리 잘 만들어놓은 기본 템플릿을 가져다 쓰게 도와주는 옵션이라고 생각하면 됩니다.

 

또는, 기존에 이미 존재하는 리액트 프로젝트에 타입스크립트를 추가하기 위해서는 아래의 명령어를 사용하여야 합니다.

npm install --save typescript @types/node @types/react @types/react-dom @types/jest

 

기본적으로 리액트에서 타입스크립트 파일은 .tsx 확장자를 사용합니다.

 

 

간단한 예제를 통한 기초 사용법

함수형 컴포넌트로 만든 간단한 더하기 예제

  • useRef 타입 명시
  • 변수 값이 null 이여서 오류 발생하는 경우, 해결 방법
  • 메서드를 위에 정의하여 사용시 타입 명시 방법
  • 엘리먼트에 바로 함수 사용시 타입 추론으로 명시할 필요 없음
  • null 로 추론되는 경우 회피 방법 (비권장)
import * as React from 'react';       // react 파일에는 export default 값이 없으므로 import * as ??? from 'react' 를 사용해야 합니다.

function App() {
  const [first, setFirst] = React.useState(0);            // state 초기화
  const [second, setSecond] = React.useState(0);
  const [result, setResult] = React.useState<number>(0);  // 제네릭으로 타입을 명시해줄수도 있음
  const inputRef = React.useRef<HTMLInputElement>(null);  // 초기 useRef null 값을 넣으나 어떠한 값을 가질지 모르기에 타입 추론이 불가능하여
                                                          // input 엘리멘트 값이 들어갈 것을 제네릭으로 타입을 지정해줍니다.

  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {   // 아래 엘리멘트에서 함수를 부르고, 위에서 정의하면 파라미터 값의 타입 추론이 안되어 지정해주어야 합니다.
    e.preventDefault();

    const input = inputRef.current;

    setResult((first + second));          // 결과값을 저장함
    setSecond(0);                         // 입력한 값을 저장함

    if (input) {                          // Ref가 렌더링 되어야 값을 잡는데, 초기값이 null 이기에 오류가 발생하여
      input.focus();                      // input에 null 이 아닌 엘리먼트가 들어왔을때 포커싱을 해주도록 설정합니다. 
    }                                     // 다른 방식으로 null 이 여도 실행가능한것은 변수명 뒤에 ! 붙일 수 있으나 엄청난 확신이 있을때만 사용해야합니다. (위험)
  }

  return (
    <>
      <h1>{first} + {second}</h1>
      <form onSubmit={onSubmit}>
        <input
          ref={inputRef}
          type="text"
          value={second}
          onChange={e => setSecond(parseInt(e.target.value))}     // 인라인으로 함수 내용을 작성하면 이벤트 객체가 타입이 추론되어 타입을 따로 지정해주지 않아도 됩니다.
        />
      </form>
      <h2>결과 : {result}</h2>
    </>
  );
}

export default App;

 

클래스형 컴포넌트로 만든 더하기 예제

  • 타입스크립트 인터페이스 사용법
  • React.Component 상속받을때 제네릭 이용한 props와 state 타입 명시
  • 클래스형에서 Ref 선언 및 사용법
import * as React from 'react';       // react 파일에는 export default 값이 없으므로 import * as ??? from 'react' 를 사용해야 합니다.


interface State {                   // 타입스크립트 인터페이스 이용하여 객체의 여러값을 한번에 타입 지정을 해둠
  first: number,
  second: number,
  result: number,
}


class App extends React.Component<{}, State> {    // 제네릭 <props, state, ...> 아래에 setState 에서 첫번째 파라미터인 이전 state 사용을 위해 타입 명시 필요
  state = {
    first: 0,
    second: 0,
    result: 0,
  }


  onSubmit = (e: React.FormEvent<HTMLFormElement>): void => {   // 아래 엘리멘트에서 함수를 부르고, 위에서 정의하면 파라미터 값의 타입 추론이 안되어 지정해주어야 합니다.
    e.preventDefault();

    this.setState(preState => {
      return {
        ...preState,
        second: 0,
        result: preState.first + preState.second,
      }
    })

    if (this.input) {                           
      this.input.focus();                       
    }                                           
  }

  onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    this.setState({ second: parseInt(e.target.value) });
  }

  input: HTMLInputElement | null = null;                      // 클래스에서 Ref 사용하는 방법 1, 타입이 두개 들어올 수 있으므로 | 사용하여 두개 타입을 허용시킴
  inputRef = (c: HTMLInputElement) => { this.input = c };

  render() {
    return (
      <>
        <h1>{this.state.first} + {this.state.second}</h1>
        <form onSubmit={this.onSubmit}>
          <input
            ref={this.inputRef}
            type="text"
            value={this.state.second}
            onChange={this.onChange}
          />
        </form>
        <h2>결과 : {this.state.result}</h2>
      </>
    );
  }
}

export default App;

 

 

특정 메서드 및 상황별 사용 방법

함수형 컴포넌트 props

부모 컴포넌트로부터 전달받은 데이터를 받을때도 타입을 명시할 필요가 있습니다.

// 부모 컴포넌트
const Parent = () => {
	return (
 		<>
        	<Child first={{ value:123, value2:'str' }} second={{ value3:456, value4:'test' }} />
        </>
    );
}



// 자식 컴포넌트
// 1.
interface First {
	value1: number,
    value2: string,
}

interface Second {
	value3: number,
    value4: string,
}

const Child: FunctionComponent<{ first: First, second: Second }> = ({ first, second }) => {
	return (
    	<>
        	{first.value1}
        </>
    );
}

// 2.
interface First {
	value1: number,
    value2: string,
}

interface Second {
	value3: number,
    value4: string,
}

const Child: FunctionComponent<{ first: First, second: Second }> = (props) => {
	return (
    	<>
        	{props.first.value1}
        </>
    );
}

// 3.
interface Props {
	first: {
    	value1: number,
        value2: string,
    },
    second: {
    	value3: number,
        value4: string,
    }
}

const Child: FunctionComponent<Props> = (props) => {
	return (
    	<>
        	{props.first.value1}
        </>
    );
}

 

클래스형 컴포넌트 props

함수형 컴포넌트의 props 와 조금 차이가 있습니다.

// 부모 컴포넌트
class Parent extends React.Component {
	render () {
    	return (
        	<>
        		<Child first={{ value:123, value2:'str' }} second={{ value3:456, value4:'test' }} />
        	</>
    	);
 	}
}



// 자식 컴포넌트, state 값이 있어서 타입을 명시해야 한다면 props 타입 명시 뒤에 state도 타입 명시를 해주어야 합니다.
interface First {
	value1: number,
    value2: string,
}

interface Second {
	value3: number,
    value4: string,
}

class Child extends React.Component<{ first: First, second: Second }> {
	render () {
    	return (
        	<>
        	</>
    	);
 	}
}

 

클래스형 컴포넌트 state에 비어있는 리스트 또는 유니온 타입일 경우

클래스형 컴포넌트에서 state 중 유니온 타입을 갖고 있거나 비어있는 리스트인 경우 추가적인 작업이 필요합니다.

// 오류 발생
interface State {
  preWord: "FIRST" | "SECOND" | "THIRD",
  value: String,
  numbers: number[],
}

class WordRelayClass extends React.Component<{}, State> {
  state = {
    preWord: '첫번째',
    value: '',
  }
  

// 1. 생성자 안에서 state 초기화 하기
interface State {
  preWord: "FIRST" | "SECOND" | "THIRD",
  value: String,
  numbers: number[],
}

class WordRelayClass extends React.Component<{}, State> {
  constructor(props: {}) {
    super(props);

    this.state = {
      preWord: "FIRST",
      value: '',
      numbers: []
    }
  }
  
  
// 2. state 옆에 타입 정의 한번 더 하기
interface State {
  preWord: "FIRST" | "SECOND" | "THIRD",
  value: String,
  numbers: number[],
}

class WordRelayClass extends React.Component<{}, State> {
  state: State = {
    preWord: 'FIRST',
    value: '',
    numbers: []
  }

 

고차함수

고차함수란 함수를 파라미터로 받거나 함수를 리턴하는 함수를 의미합니다.

그래서 함수 안에 함수를 사용하게 되는데, 아래 코드를 확인해보면 클릭스 number1 이라는 문자열을 파라미터로 넘기고 가장 위의 함수 파라미터로 전달 받고 그 안에 함수를 사용합니다.

그래서 화살표 함수가 두개가 연달아 사용이 됩니다.

const onClicked = (parameter: "number1" | "number2" | "number3") => () => {
	...
}

return (
	<>
    	<button onClick={onClicked('number1')}>number1</button>
        <button onClick={onClicked('number2')}>number2</button>
        <button onClick={onClicked('number3')}>number3</button>
    </>
)

 

useCallback

useCallback 은 함수를 기억하는 Hook 이라고 생각하면 된다. 특정 변수가 변경이 될때만 렌더링이 되도록 할 수 있습니다.

 

- 자바스크립트

useCallback 메서드의 첫번째 파라미터는 실행될 함수, 두번째 파라미터로는 배열이 들어오는데, 특정 변수를 집어 넣으면 그 변수의 값이 변할때만 렌더링을 시킵니다.

useCallback((e) => {}, []);

 

- 타입스크립트

T 부분에 정의하는 타입을 넣으면 됩니다.

// 1.
useCallback<(e: T) => T>((e) => {}, []);

// 2.
useCallback((e: T): T {}, []);

 

useRef

useRef 는 정의한 파일을 확인해보면 같은 이름의 3개의 메서드가 나옵니다.

3개의 메서드는 각 파라미터와 반환값이 다르게 되는데 이거를 확인하여 나의 상황에 맞는 타입을 지정해 주어야 합니다.

// 1
// 초기값이 데이터 타입 목록중 하나에 포함된다면, 속성값 중 current 를 변경 가능하다
// 제네릭 데이터 타입 > 초기 입력 값
function useRef<T>(initialValue: T): MutableRefObject<T>;

// 2
// 데이터 타입 목록 중 초기값이 포함되거나 초기값이 null 인데, 데이터 타입에 없는 경우, 속성값 중 current 가 readonly 로 변경이 불가능하다.
function useRef<T>(initialValue: T|null): RefObject<T>;

// 3
데이터 타입이 정해지지 않았고, 초기값이 없을때, 1번 사항과 같음
function useRef<T = undefined>(): MutableRefObject<T | undefined>;

 

createRef

React.createRef 는 클래스형 컴포넌트에서 Ref 를 이용하기 위해 사용합니다.

inputRef = createRef<T>();

 

useReducer

Reducer는 여러 컴포넌트에 흩어진 state를 하나로 모아서 관리하기 쉽게 해줍니다.

Redux 에서 자주 사용하여 비슷하다고 생각하면 됩니다.

// Reducer State 데이터 인터페이스 정의
interface IReducerState {
    first: 'one' | 'two',
    second: string,
    third: number,
    forth: string[][],
    fifth: [number, number],
}

// 초기 State
const initialState: IReducerState = {
    first: '',
    second: '',
    third: 123,
    forth: [['123'], ['456']],
    fifth: [1, 2],
}

// State 값을 변경할 Action 생성
export const SET_FIRST = 'SET_FIRST' as const;
export const CHANGE_THIRD = 'CHANGE_THIRD' as const;
export const UP_FIFTH = 'UP_FIFTH' as const;

// Action 데이터 타입 지정 (액션은 객체)
interface SetFirstAction {
	type: typeof SET_FIRST;
    First: 'one' | 'two';
}

// Action 행동(메서드) 정의
const setFirst = (first: 'one' | 'two'): SetFirstAction => {
    return { type: SET_FIRST, first };                           // 바로 위에 정의한 타입으로 반환
}

interface ChangeThirdAction {
	type: typeof CHANGE_THIRD;
    third: number;
}

const changeThird = (third: number): ChangeThirdAction => {
    return { type: CHANGE_THIRD, third };                           // 바로 위에 정의한 타입으로 반환
}

// 사용자로부터 어떠한 값을 입력 받는게 아니라면 값을 받는 함수가 필요없음 
interface UpFifthAction {
    type: typeof UP_FIFTH;
    fifth: [number, number];
}

// Action들 데이터 타입으로 모두 지정
type ReducerActions = SetFirstAction | ChangeThirdAction | UpFifthAction;
const reducer = (state: IReducerState,  action: ReducerActions): IReducerState => {
    switch (action.type) {
        case SET_FIRST:
            return {
                ...state,
                first: action.first,
            };
        case CHANGE_THIRD: {
            ...                // 어떤 다른 행동의 코드
            return {
                ...state,
                third: action.third
            }
        };
        case UP_FIFTH: {
            return {
                ...state,
                fifth: [state.fifth[0] + 1, state.fifth[1] + 1]
            }
        };
        default:
            return state;
    }
}


const App = () => {
    const [state, dispatch] = useReducer<React.Reducer<IReducerState, ReducerActions>>(reducer, initialState);
    
    const Clicked1 = useCallback(() => {
        dispatch(setFirst('one'));           // 버튼 클릭이되면 dispatch로 액션 실행
    }, []);
    
    return (
        <>
            <button onClick={Clicked1}>1</button>
        </>
    );
}

 

 

dispatch

useReducer 를 사용하여 자식 컴포넌트에 props 로 dispatch를 넘겨주는 경우에 props 에서 Dispath 타입을 명시해 주어야 합니다.

// 자식 컴포넌트
interface Props {
    first: string,
    dispatch: React.Dispatch<any>,      // any 대신에 부모컴포넌트에서 사용하던 action 들을 집어 넣으면 되는데, any도 크게 상관 없음
    onClick: () => void,
}

const Child: FC<Props> = ({ first, dispatch }) => {
    ...
}

 

setTimeout

Node.js 와 브라우저(인터넷)에서 사용하는 두가지 종류가 있어서, 반환값의 타입이 다른 경우가 있어 어디서 사용할지 명시할 필요가 있습니다.

//브라우저에서 사용하는 경우
window.setTimeout(() => {}, ??)

 

useState 에서 빈 배열을 사용하는 경우

useState 초기값으로 빈 배열을 사용하는 경우 변수는 타입 추론이 제대로 안됩니다.

never 로 타이핑이 되는데, never 는 일반저그로 함수의 리턴 타입으로 사용되는데 함수에서 항상 오류를 출력하거나 리턴값을 절대로 내보내지 않음을 의미합니다.

 

그래서 제네릭으로 타입을 명시해 줄 필요가 있습니다.

// 1.
const [value, setValue] = useState<string[]>([]);

// 2.
interface TypeInfo {
	first: number,
  	second: string,
}

const [values, setValues] = useState<TypeInfo[]>([]);

 

 

그 외 기타

객체에 명시한 타입을 그대로 가져와 사용하고 싶을때

const values = {
    value1: '100',
    value2: '200',
} as const				// 위의 값들은 하나로 고정되어 타입을 하나로 지정하고 싶을때, as const 를 사용하면 그 값들만 사용할수있게 바뀐다

type val = typeof values;                        // type val = { readonly value1: '100'; readonly value2: '200'; }
type val = keyof typeof values;                  // type val = 'value1' | 'value2'
type val = typeof values[keyof typeof values];   // type val = '100' | '200'

 

특정 메서드 (Object.keys) 반환 타입에 원하는 타입이 없을때

특정 메서드의 반환하는 타입이 원하는게 없는 경우가 있으면 강제로 형 변환이 필요합니다.

const values = {
    value1: '100',
    value2: '200',
    value3: '300',
} as const              // as const 를 사용하여 데이터 타입을 지정해준다

// 아래의 경우 Object.keys() 의 반환 데이터 타입이 string[] 인데, 위의 키는 value1, value2, value3 세개만 나와야 하므로 형을 자세히 지정할 필요가 있음
(Object.keys(values) as ['value1', 'value2', 'value3']).map(() => {});

참고 자료

- 공식 문서
- 인프런 강의

댓글