# 새로 만들기
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를 사용 시 제네릭
< T >
을 통해 해당 상태가 어떤 타입을 가지고 있는지 설정해야 한다.const [isSearch, setIsSearch] = useState<boolean>(false);
여기서 타입 지정을 해주지 않고, 초기값을 지정해주면 타입 추론이 발생한다.
undefined 타입
이 된다.
상태가 null일 수도 있고 아닐 수도 있을때 👉 초기화로 타입 추론을 하기 힘든 경우이다.
배열
빈 배열로 초기화
를 할 경우 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[]);
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 에 올려보면 알 수 있다.
setstate는 직접적으로 props로 전달하지 않는 것이 좋다.
1. 컴포넌트가 종속이 되면 분리한 의미가 없음
2. setstate가 여기저기로 흩어지면 디버깅이 어려움
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;
}
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;
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;
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는 우리가 리액트 컴포넌트에서 외부 라이브러리의 인스턴스 또는 DOM 을 특정 값 안에 담을 때 사용한다. 컴포넌트 내부에서 관리하고 있는 값을 관리할 때 유용하지만, useRef의 값은 렌더링과 관계가 없어야 한다.
interface MutableRefObject<T> {
current: T;
}
interface RefObject<T> {
readonly current: T | null;
}
useRef<T>(초기값)
에서 T와 초기값의 타입이 일치하는지 여부에 따라 달라지는 사용법을 useRef의 정의를 통해 소개한다.
MutableRefObject<T>
를 반환한다.MutableRefObject<T>
의 경우, 이름에서도 볼 수 있고 위의 정의에서도 확인할 수 있듯 current 프로퍼티 그 자체를 직접 변경할 수 있다.useRef<T>(initialValue: T): MutableRefObject<T>
RefObject<T>
를 반환한다.RefObject<T>
는 위에서 보았듯 current 프로퍼티를 직접 수정할 수 없다. useRef<T>(initialValue: T|null): RefObject<T>;
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을 직접 조작하기 위해 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 일 수 있는 상황에는, 해당 값이 유효한지 체킹하는 작업을 꼭 해주어야 자동완성도 잘 이루어지고, 오류도 사라집니다.
[React] Typescript React Hook 정복하기
타입스크립트로 리액트 Hooks 사용하기 (useState, useReducer, useRef)
항상 꼼꼼하게 구현하고 블로그에 정리까지 하는 멋진 개발자 ... 🌠 잘 읽어보고 갑니당