[ TS ] React Hooks

Happhee·2022년 5월 13일
12

📘 TypeScript

목록 보기
10/10
post-thumbnail

✨ TS + React 시작하기

# 새로 만들기
npx create-react-app [폴더이름] --template typescript
# or
yarn create react-app [폴더이름] --template typescript

# 기존 react
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
# or
yarn add typescript @types/node @types/react @types/react-dom @types/jest

✨ useState

타입 지정

useState를 사용 시 제네릭 < T >을 통해 해당 상태가 어떤 타입을 가지고 있는지 설정해야 한다.

const [isSearch, setIsSearch] = useState<boolean>(false);

여기서 타입 지정을 해주지 않고, 초기값을 지정해주면 타입 추론이 발생한다.

❗️ 하지만 타입 지정 & 초기값 지정이 모두 안되어 있다면 ❗️

undefined 타입이 된다.

1️⃣ 여러 타입이 들어올 수 있을 때

상태가 null일 수도 있고 아닐 수도 있을때 👉 초기화로 타입 추론을 하기 힘든 경우이다.

2️⃣ 상태의 타입이 까다로운 구조를 가진 객체이거나 배열일 때

  • 배열
    빈 배열로 초기화를 할 경우 never 타입의 배열로 추론한다.

  • 객체
    빈 객체로 초기화 해도 할당 가능하다.
    하지만 오류를 방지하기 위해 타입 지정을 해주는 게 좋다.

type Result = {
  address_name: string;
  category_group_code: string;
  category_group_name: string;
  category_name: string;
  distance: string;
  id: string;
  phone: string;
  place_name: string;
  place_url: string;
  road_address_name: string;
  x: string;
  y: string;
}
const [results, setResults] = useState<Result[]>([]);

const [results, setResults] = useState([] as Result[]);

🖥 useState

import React, { useState } from 'react';
import styled, { css } from 'styled-components';

import choiceBread from '../assets/images/choiceBread.png';
import noChoiceBread from '../assets/images/noChoiceBread.png';

interface Coordinates {
  longitude: number;
  latitude: number;
}

function Header() {
  const [input, setInput] = useState<string>('');
  
  const [isSearch, setIsSearch] = useState<boolean>(false);
  const [isLocation, setIsLocation] = useState<boolean>(false);
  
  const handleInputDisabled = () => {
    setIsLocation(prev => !prev);
  };
  // input 값 핸들링
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInput(e.target.value);
  };
  // 검색 버튼 핸들링
  const handleSearchButton = (e: React.MouseEvent<HTMLButtonElement>) => {
    setIsSearch(prev => !prev);
  };
  return (
    <HeaderContainer>
      <h1>🍰 빵수니가 져아 🍰 </h1>
      <HeaderWrapper>
        <SearchButton isChoice={isLocation} onClick={handleInputDisabled}>
          현위치
        </SearchButton>
        <SearchLabel>우리 동네 </SearchLabel>
        <SearchInput type="text" onChange={handleInputChange} value={input} placeholder="지역을 입력해주세요" />
        <SearchButton type="submit" isChoice={isLocation}  onClick={handleSearchButton}>
          검색
        </SearchButton>
      </HeaderWrapper>
    </HeaderContainer>
  );
}
export default Header;

여기서 e 객체의 타입이 무엇일지, 타입스크립트를 처음 쓰는 사람이라면 잘 모를 수 있다!
e 객체의 타입이 무엇인지 외우실 필요도, 구글에 "TypeScript react onChange event" 라고 검색하실 필요도 없다! 그냥 커서를 onChange 에 올려보면 알 수 있다.

🖥 useState + Props

setstate는 직접적으로 props로 전달하지 않는 것이 좋다.
1. 컴포넌트가 종속이 되면 분리한 의미가 없음
2. setstate가 여기저기로 흩어지면 디버깅이 어려움

  • src/core/resultsType.ts
export interface Result {
  address_name: string;
  category_group_code: string;
  category_group_name: string;
  category_name: string;
  distance: string;
  id: string;
  phone: string;
  place_name: string;
  place_url: string;
  road_address_name: string;
  x: string;
  y: string;
}
export interface Params {
  query: string;
}
  • src/pages/Main.tsx
import Header from 'components/Header';
import ResultsSection from 'components/ResultsSection';
import React, { useState } from 'react';
import { Result } from 'core/resultsType';

function Main() {
  const [results, setResults] = useState<Result[]>([]);
  const [isSearch, setIsSearch] = useState<boolean>(false);

  const handleIsSearch = (newIsSearch: boolean) => {
    setIsSearch(newIsSearch);
  };
  const handleResults = (newResults: Result[]) => {
    setResults(newResults);
  };

  return (
    <StyledWrapper>
      <Header handleIsSearch={handleIsSearch} handleResults={handleResults} />
      <StlyedSectionContainer>
        <ResultsSection isSearch={isSearch} results={results} />
      </StlyedSectionContainer>
    </StyledWrapper>
  );
}

export default Main;
  • src/components/Header.tsx
import { storeSearch } from 'libs/api';
import React, { useRef, useState } from 'react';
import styled, { css } from 'styled-components';

interface HeaderProps {
  handleIsSearch: (newIsSearch: boolean) => void;

  handleResults: (newResuls: Result[]) => void;
}
interface Coordinates {
  longitude: number;
  latitude: number;
}
function Header(props: HeaderProps) {
  const { handleIsSearch, handleResults } = props;
  const position = useRef<Coordinates>({ longitude: 0, latitude: 0 });
  const searchRef = useRef<HTMLInputElement>(null);
  const [input, setInput] = useState<string>('');
  const [isLocation, setIsLocation] = useState<boolean>(false);

  const storeSearchHttpHandler = async (params: Params) => {
    const { data } = await storeSearch(params);

    handleIsSearch(false);
    handleResults(data.documents);
  };

  const handleMyLocation = () => {
    if (!isLocation) {
      new Promise(resolve => {
        navigator.geolocation.getCurrentPosition(currentPosition => {
          position.current = currentPosition.coords;
          const params = {
            y: position.current.latitude,
            x: position.current.longitude,
            radius: 1000,
            query: '베이커리',
          };
          storeSearchHttpHandler(params);
        });
      });
    } else {
      const params = {
        y: position.current.latitude,
        x: position.current.longitude,
        radius: 1000,
        query: '베이커리',
      };
      storeSearchHttpHandler(params);
    }
  };

  const handleInputDisabled = () => {
    if (null !== searchRef.current) {
      searchRef.current.disabled = !searchRef.current.disabled;
      setIsLocation(prev => !prev);
    }
  };

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInput(e.target.value);
  };
  const handleSearchButton = (e: React.MouseEvent<HTMLButtonElement>) => {
    handleIsSearch(true);
    e.preventDefault();
    if (null !== searchRef.current) {
      if (!searchRef.current.disabled) {
        const params: Params = {
          query: input + ' ' + '베이커리',
        };
        storeSearchHttpHandler(params);
      } else {
        handleMyLocation();
      }
    }
  };
  return (
    <HeaderContainer>
      <h1>🍰 빵수니가 져아 🍰 </h1>
      <HeaderWrapper>
        <SearchButton isChoice={isLocation} onClick={handleInputDisabled}>
          현위치
        </SearchButton>
        <SearchLabel>우리 동네 </SearchLabel>
        <SearchInput
          ref={searchRef}
          type="text"
          onChange={handleInputChange}
          value={input}
          placeholder="지역을 입력해주세요"
        />
        <SearchButton isChoice={isLocation} type="submit" onClick={handleSearchButton}>
          검색
        </SearchButton>
      </HeaderWrapper>
    </HeaderContainer>
  );
}
export default Header;

✨ useReducer

🖥 useReducer

import React, { useReducer } from "react";

type Color = "red" | "orange" | "yellow";

type State = {
  count: number;
  text: string;
  color: Color;
  isGood: boolean;
};

type Action =
  | { type: "SET_COUNT"; count: number }
  | { type: "SET_TEXT"; text: string }
  | { type: "SET_COLOR"; color: Color }
  | { type: "TOGGLE_GOOD" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "SET_COUNT":
      return {
        ...state,
        count: state.count + action.count // count가 자동완성되며, number 타입인걸 알 수 있습니다.
      };
    case "SET_TEXT":
      return {
        ...state,
        text: action.text // text가 자동완성되며, string 타입인걸 알 수 있습니다.
      };
    case "SET_COLOR":
      return {
        ...state,
        color: action.color // color 가 자동완성되며 color 가 Color 타입인걸 알 수 있습니다.
      };
    case "TOGGLE_GOOD":
      return {
        ...state,
        isGood: !state.isGood
      };
    default:
      throw new Error("Unhandled action");
  }
}

function ReducerSample() {
  const [state, dispatch] = useReducer(reducer, {
    count: 0,
    text: "hello",
    color: "red",
    isGood: true
  });

  const setCount = () => dispatch({ type: "SET_COUNT", count: 5 }); // count 를 넣지 않으면 에러발생
  const setText = () => dispatch({ type: "SET_TEXT", text: "bye" }); // text 를 넣지 않으면 에러 발생
  const setColor = () => dispatch({ type: "SET_COLOR", color: "orange" }); // orange 를 넣지 않으면 에러 발생
  const toggleGood = () => dispatch({ type: "TOGGLE_GOOD" });

  return (
    <div>
      <p>
        <code>count: </code> {state.count}
      </p>
      <p>
        <code>text: </code> {state.text}
      </p>
      <p>
        <code>color: </code> {state.color}
      </p>
      <p>
        <code>isGood: </code> {state.isGood ? "true" : "false"}
      </p>
      <div>
        <button onClick={setCount}>SET_COUNT</button>
        <button onClick={setText}>SET_TEXT</button>
        <button onClick={setColor}>SET_COLOR</button>
        <button onClick={toggleGood}>TOGGLE_GOOD</button>
      </div>
    </div>
  );
}

export default ReducerSample;

상태값이 객체로 이루어져 있고 안에 여러 타입의 값이 들어 있다면 State 라는 타입을 준비하는 것이 좋다.

또한, 액션들은 type값만 있는 것이 아니라 count, text, color 같은 추가적인 값이 있기에 Action 이라는 타입스크립트 타입을 정의하게 되면

리듀서에서 자동완성이 되어 개발에 편의성을 더해주고, 액션을 디스패치하게 될 때에도 액션에 대한 타입검사가 이루어지므로 사소한 실수를 사전에 방지 할 수도 있다.


✨ useRef

useRef는 우리가 리액트 컴포넌트에서 외부 라이브러리의 인스턴스 또는 DOM 을 특정 값 안에 담을 때 사용한다. 컴포넌트 내부에서 관리하고 있는 값을 관리할 때 유용하지만, useRef의 값은 렌더링과 관계가 없어야 한다.

반환타입

interface MutableRefObject<T> {
    current: T;
}

interface RefObject<T> {
    readonly current: T | null;
}

useRef<T>(초기값) 에서 T와 초기값의 타입이 일치하는지 여부에 따라 달라지는 사용법을 useRef의 정의를 통해 소개한다.

  • ✅ 인자의 타입과 제네릭의 타입이 T로 일치하는 경우, MutableRefObject<T>를 반환한다.
    MutableRefObject<T>의 경우, 이름에서도 볼 수 있고 위의 정의에서도 확인할 수 있듯 current 프로퍼티 그 자체를 직접 변경할 수 있다.
useRef<T>(initialValue: T): MutableRefObject<T>
  • ✅ 인자의 타입이 null을 허용하는 경우, RefObject<T>를 반환한다.
    RefObject<T>는 위에서 보았듯 current 프로퍼티를 직접 수정할 수 없다.
useRef<T>(initialValue: T|null): RefObject<T>;
  • ✅ 제네릭의 타입이 undefined인 경우(타입을 제공하지 않은 경우), MutableRefObject<T | undefined>를 반환한다.
useRef<T = undefined>(): MutableRefObject<T | undefined>;

본질적으로 useRef는 .current 프로퍼티에 변경 가능한 값을 담고 있는 상자이다.
따라서 로컬 변수 용도로 사용할 수 있다.

변수 값 관리

useRef 를 쓸땐 아래와 같은 코드처럼 Generic 을 통해 ~.current 의 값을 추론한다.

interface Coordinates {
  longitude: number;
  latitude: number;
}
const position = useRef<Coordinates>({ longitude: 0, latitude: 0 });
position.current = currentPosition.coords;

DOM 관리

DOM을 직접 조작하기 위해 property로 useRef 객체를 사용할 경우, RefObject<T>를 사용해야 하므로 초기 값으로 null을 설정해야 한다.

const searchRef = useRef<HTMLInputElement>(null);
<SearchInput
          ref={searchRef}
          type="text"
          onChange={handleInputChange}
          value={input}
          placeholder="지역을 입력해주세요"
        />

Generic 으로 HTMLInputElement 타입을 넣어주었다.
어떤 타입을 사용해야 할 지 모르겠다면 커서를 DOM 위에 올려보자.

~.current 의 값을 추론하는 것을 예외처리 없이 DOM에서 사용해보면,,?

 const handleInputDisabled = () => {
   searchRef.current.disabled = !searchRef.current.disabled;
   setIsLocation(prev => !prev);
 };


null 체킹에 대한 검사가 필요함을 알 수 있다.

따라서 DOM에서 searchRef.current 안의 값을 사용 하려면 null 체킹을 통해 특정 값이 정말 유효한지 유효하지 않은지 체크해야 한다.

 const handleInputDisabled = () => {
    if (null !== searchRef.current) {
      searchRef.current.disabled = !searchRef.current.disabled;
      setIsLocation(prev => !prev);
    }
  };

타입스크립트에서 만약 어떤 타입이 undefined 이거나 null 일 수 있는 상황에는, 해당 값이 유효한지 체킹하는 작업을 꼭 해주어야 자동완성도 잘 이루어지고, 오류도 사라집니다.

  • useRef, 사용하는 것이 바람직한가? 일반적인 data flow에서 벗어나 DOM element를 조작하는 일이므로, React스럽지 못하다.
    보통 높은 component 계층으로 state를 끌어올리는 것이 해결책이 될 수 있으므로,
    다음과 같은 상황에 사용하는 것이 바람직하다.
    1. 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때.
    2. 애니메이션을 직접적으로 실행시킬 때.
    3. 서드 파티 DOM 라이브러리를 React와 같이 사용할 때

[React] Typescript React Hook 정복하기
타입스크립트로 리액트 Hooks 사용하기 (useState, useReducer, useRef)

profile
즐기면서 정확하게 나아가는 웹프론트엔드 개발자 https://happhee-dev.tistory.com/ 로 이전하였습니다

2개의 댓글

comment-user-thumbnail
2022년 5월 15일

항상 꼼꼼하게 구현하고 블로그에 정리까지 하는 멋진 개발자 ... 🌠 잘 읽어보고 갑니당

1개의 답글