완성한 포켓몬도감 배포 주소이다.
완성된 포켓몬도감
01. 타입 스크립트를 사용하기 위한 준비하기
typescript 설치
npm install -D typescript @types/react @types/react-dom
vite.config.js -> vite.config.ts 확장자 변경
tsconfig.json, tsconfig.node.json, vite-env.d.ts(/src/) 가져오기
vite로 typescript + 리액트 프로젝트 생성하면 기본으로 생성되는 파일을 가져온다.main.tsx 확장자 변경
- index.html에서 main.tsx로 변경
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( <App /> )
root에 as HTMLElement
를 추가
- allowJs true설정
import에러는 allowJS 설정을 해준다.
tsconfig.json에서"allowJs": true,
추가
02. 메인 페이지 타입 스크립트로 변경하기
- 파일 확장자 tsx로 변경
- 타입 폴더 생성
- 포켓몬 데이터를 위한 interface 생성
export interface PokemonData { conunt: number; next: string | null; previous: string | null; results: PokemonNameAndUrl[]; }
export interface PokemonNameAndUrl {
name: string;
url: string;
}
4. 메인페이지에 타입 주기 * state ```ts // 모든 포켓몬 데이터를 가지고 있는 State const [allPokemons, setAllPokemons] = useState<PokemonNameAndUrl[]>([]); // 실제로 리스트로 보여주는 포켓몬 데이터를 가지고 있는 State const [displayedPokemons, setDisplayedPokemons] = useState<PokemonNameAndUrl[]>([]);
함수 props
const filterDisplayedPokemonData = ( allPokemonsData: PokemonNameAndUrl[], displayedPokemons: PokemonNameAndUrl[] = [] ) => {...} axios.get(url)
const response = await axios.get<PokemonData>(url);
map()
displayedPokemons.map(({ url, name }: PokemonNameAndUrl, index) => ( <PokeCard key={index} url={url} name={name} /> ))
03. AutoComplete 컴포넌트를 타입스크립트로 변경하기
- 확장자변경
- AutoComplete props 타입지정
React Typescript Cheatsheet
여기에 각종 typescript작성법이 있는데 props 타입지정 관련 팁을 찾을 수 있다.
AutoComplete에서 props로 받는건 모든포켓몬정보, setState함수인데
setState: React.Dispatch<React.SetStateAction<number>>;
이렇게 작성가능하다는것을 참고하여 다음과같이 작성.
interface AutoCompleteProps { allPokemons: PokemonNameAndUrl[]; setDisplayedPokemons: React.Dispatch<React.SetStateAction<PokemonNameAndUrl[]>>; }
setDisplayedPokemons 정의된곳 보면const [displayedPokemons, setDisplayedPokemons] = useState<PokemonNameAndUrl[]>([]);
타입이 <PokemonNameAndUrl[]>
인것 확인
- 각 함수 props type 정의
(input: string)
(e: React.FormEvent<HTMLFormElement>)
04. PokeCard 컴포넌트를 타입스크립트로 변경하기
- tsx
- props 타입지정
({ url, name }: PokemonNameAndUrl)
- pokemon useState 타입지정
const [pokemon, setPokemon] = useState<PokeData>();
- formatPokeData에서 params 타입지정
콘솔에 찍어서 확인
이렇게 많은것을 타입 다 주기보단
변환하는 사이트가 있다.
타입변환사이트 quicktype
params를 먼저 stringify한다음 복사한다.console.log(JSON.stringify(params))
결과
결과를 복사하고 types폴더에 PokemonDetail.ts
파일 생성후 붙여넣어준다.
처음 인터페이스는 기본적으로 Welcome이라 되어있는데 이부분은 수정해서 사용하면된다.export interface Welcome {}
export interface PokemonDetail {}
돌아와서 params에 타입 지정까지 완료해준다.const formatPokeData = (params: PokemonDetail) => {...}
- LazyImage 컴포넌트 타입추가
interface LazyImageProps { src: string; alt: string; } const LazyImage = ({ src, alt }: LazyImageProps) => { const [isLoading, setIsLoading] = useState<boolean>(true); const [opacity, setOpacity] = useState<string>("opacity-0"); ...
05. 상세 페이지 타입 스크립트로 변경하기
가져온 데이터의 타입 인터페이스 생성을 해주자
formattedPokemonData
formattedPokemonData
를 JSON.stringify해서 콘솔에 찍고 복사-> quicktype에서 인터페이스 생성- FormattedPokemonData.ts 파일에 인터페이스 붙여넣기
- next와 previous에 undefined타입 추가
맨 처음 데이터와 마지막 데이터에는 next나 previous가 없을수 있으므로 undefined를 추가한다.const formattedPokemonData = { id, name, weight: weight / 10, height: height / 10, previous: nextAndPreviousPokemon.previous, next: nextAndPreviousPokemon.next, abilities: formatAbilities(abilities), stats: formatPokemonStats(stats), DamageRelations, types: types.map((type) => type.type.name), sprites: formatPokemonSprites(sprites), description: await getPokemonDescription(id), }; // console.log(JSON.stringify(formattedPokemonData));
PokemonDescription
getPokemonDescription 메서드에서 pokemonSpecies 콘솔에 찍고 interface생성
const getPokemonDescription = async (id) => { const url = `https://pokeapi.co/api/v2/pokemon-species/${id}/`; const {data: pokemonSpecies} = await axios.get(url); // console.log(JSON.stringify(pokemonSpecies)); // 한국어 description필터링 const descriptions = filterAndFormatDescription(pokemonSpecies.flavor_text_entries); return descriptions[Math.floor(Math.random() * descriptions.length)]; }
DamageRelations
const DamageRelations = await Promise.all( // 비동기 작업 처리하고 한꺼번에 리턴 types.map(async (i) => { const type = await axios.get(i.type.url); // console.log(JSON.stringify(type.data)) return type.data.damage_relations }) );
data는 항상 가져올때 response안에 data에 있으므로 주의한다!
any[]
부분이나 다른 타입들은 보고 수정해 사용하면된다...
06. 상세 페이지에 타입 적용하기
useState 타입지정
fetchPokemonData props 타입지정
const fetchPokemonData = async (id: string)
id에 undefined가 올 수 있다고 에러가 뜨는데
이건 params에서 id는 string만 온다고 타입단언을 해준다.const params = useParams() as {id: string}; // 타입단언
axios.get 타입지정
const {data: pokemonData} = await axios.get<PokemonDetail>(url);
formatPokemonStats
const formatPokemonStats = ([ // stats 배열을 구조분해할당 statHP, statAttack, statDEP, statSATK, statSDEP, statSPD, ]: Stat[]) => [ // 배열 리턴 { name: "Hit Points", baseStat: statHP.base_stat }, { name: "Attack", baseStat: statAttack.base_stat }, { name: "Defense", baseStat: statDEP.base_stat }, { name: "Special Attack", baseStat: statSATK.base_stat }, { name: "Special Defense", baseStat: statSDEP.base_stat }, { name: "Speed", baseStat: statSPD.base_stat }, ]; formatPokemonSprites
const formatPokemonSprites = (sprites: Sprites) => { const newSprites = {...sprites}; // console.log(Object.keys(newSprites)) // string이 아닌 값을 가졌다면 삭제하기(url 있는것만 남기기위해) ((Object.keys(newSprites) as (keyof typeof newSprites)[]).forEach(key => { // newSprites에 키값만 가져옴(오브젝트에 키값만 가져옴) if(typeof newSprites[key] !== 'string') { delete newSprites[key] } })); return Object.values(newSprites) as string[]; // url만 return } 반환타입지정
const filterAndFormatDescription = (flavorText: FlavorTextEntry[]): string [] => {
const getPokemonDescription = async (id: number): Promise<string> => { const url = `https://pokeapi.co/api/v2/pokemon-species/${id}/`; const {data: pokemonSpecies} = await axios.get<PokemonDescription>(url); // 한국어 description필터링 const descriptions = filterAndFormatDescription(pokemonSpecies.flavor_text_entries); return descriptions[Math.floor(Math.random() * descriptions.length)]; }
07. 에셋 컴포넌트들 타입 스크립트로 변경하기
ClassNameProps.ts
export interface ClassNameProps { className?: string; }
props가 있는 에셋컴포넌트에 타입지정export const Pokeball = ({ className: CN = "" }: ClassNameProps) => (...)
08. useRef 타입스크립트
BaseStat 컴포넌트
type guard
const ref = useRef<HTMLDivElement>(null); // ref 속성을 넣은 요소 useEffect(()=> { const setValueStat = ref.current; // 현재 요소에 접근 const calc = valueStat * (100 / 255); if(setValueStat) { // type guard setValueStat.style.width = calc + '%'; // null일때 할수없음 } }, [])
useRef
useRef는 변수로 사용하거나
직접 돔을 조작하기 위해 사용하는 훅이다.
useRef을 클릭해 Type Definition 확인
function useRef<T>(initialValue: T): MutableRefObject<T>;
매개 변수의 타입과 제네릭 타입이 T로 일치(current 프로퍼티 직접 변경가능)function useRef<T>(initialValue: T | null): RefObject<T>;
매개 변수의 타입이 null을 허용하는 경우 RefObject 반환
(RefObject current는 readonly이므로 프로퍼티 직접 수정 불가능)function useRef<T = undefined>(): MutableRefObject<T | undefined>;
타입을 제공하지 않아서 제네릭 타입이 undefined인 경우
(MutableRefObject 반환 => current 직접 변경가능)
- Mutable: 수정할 수 있는, 바꿀 수 있는
소스코드 예시
매개변수 타입 null일때(2번의 예시)
매개변수 타입이 null
일때current
는 readonly
라서 수정이 불가능한데,inputRef.current
가 null
아닐 때를 type guard로 if
절 작성하면
그 안에서는 타입에러가 나지 않고 수정도 된다.
const inputRef= useRef<HTMLInputElement>(null); const handleClick = () => { if(inputRef.current) { // type guard 있을때 current를 바꿔줄 수 있음 inputRef.current.value = ""; } } return ( <> <input ref={inputRef} /> <button onClick={handleClick}>clear</button> </> )
사실 current만 readonly인데 current의 프로퍼티인 value는 수정가능하다.
이건 얕게 동작한다는 의미이고 shallow하다고 표현한다.
매개변수 아무것도 넣지 않았을때const inputRef= useRef<HTMLInputElement>();
<input ref={inputRef} />
에서 에러가난다.
ref 속성에는 RefObject만 받기때문에!
반드시 null을 넣어주어야한다.
정리
useRef는
- 변수로 사용할때
const localVarRef = useRef<number>(0);
초기값을 제네릭 타입과 같은 타입의 값으로 넣어주기 - 직접 돔을 조작할때
const inputRef = useRef<HTMLInputElement>(null);
초기값을 null로 넣어주기
09. Damage Relations 컴포넌트 타입 스크립트로 변경하기
DamageModal
import { DamageRelations as DamageRelationsProps } from '../types/DamageRelationsOfPokemonTypes'; interface DamageModalProps { damages: DamageRelationsProps[]; setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>; }
DamageRelations란 컴포넌트가 이미 있으므로 DamageRelationsProps이름으로 import하여 사용한다.
setState prop은 React.Dispatch<React.SetStateAction<타입>>;
으로 지정하면된다.
*useRef()
를 사용할때는 위의 설명과 같이,
ref를 사용하는 엘리멘트 타입
과 초기값을 null
로 지정해야 DOM을 조작할수있다. *
const ref = useRef<HTMLDivElement>(null); return ( ... <div ref={ref} className="modal bg-white rounded-lg w-1/2"> ... );
DamageRelations
1. 컴포넌트 props에 타입
interface DamageModalProps { damages: DamageRelationsProps[]; } const DamageRelations = ({ damages }: DamageModalProps) => {
2. useState에 타입
먼저const [damagePokemonForm, setDamagePokemonForm] = useState();
여기서 damagePokemonForm의 데이터를 콘솔에 찍어봐야한다.
setDamagePokemonForm()
할때 안에 데이터를 JSON.stringify로 출력해본다.
그리고 복사해서 마찬가지로 https://app.quicktype.io/에서 interface를 생성해 타입파일을 만든다.
if (arrayDamage.length === 2) { // 합치는 부분 const obj = joinDamageRelations(arrayDamage); setDamagePokemonForm(reduceDuplicateValues(postDamageValue(obj.from))); } else { // console.log("@@@@", JSON.stringify(postDamageValue(arrayDamage[0].from))); // quick type setDamagePokemonForm(postDamageValue(arrayDamage[0].from)); // 첫번째 from }
export interface SeparateDamages { double_damage?: Damage[]; half_damage?: Damage[]; no_damage?: Damage[]; } export interface Damage { damageValue: string; name: string; url: string; }
SeparateDamages inerface를 import해서 쓰면된다.const [damagePokemonForm, setDamagePokemonForm] = useState<SeparateDamages>();
3. 각 함수 props와 리턴타입, key에 타입주기
separateObjectBetweenToAndFrom
const separateObjectBetweenToAndFrom = (damage: DamageRelationsProps): DamageFromAndTo => { const from = filterDamageRelations("_from", damage); const to = filterDamageRelations("_to", damage); // console.log("from", from) // console.log("to", to) return { from, to }; };
객체로 리턴하는데 이부분은 DamageFromAndTo
라는 interface를 새로 만들어서 리턴타입에 작성한다.
export interface DamageFromAndTo { // 직접작성 to: SeparateDamages; from: SeparateDamages; }
filterDamageRelations
const filterDamageRelations = (valueFilter: string, damage: DamageRelationsProps) => { const result: SeparateDamages = Object.entries(damage) .filter(([keyName, _]) => { return keyName.includes(valueFilter); // _to와 _from으로 나눔 }) .reduce((acc, [keyName, value]): SeparateDamages => { // reduce는 return해서 acc 메소드에 하나씩 쌓아줌 // console.log(acc, [keyName, value]); // _from, _to 삭제 const keyWithValueFilterRemove = keyName.replace(valueFilter, ""); return (acc = { [keyWithValueFilterRemove]: value, ...acc }); }, {}); // acc의 초기값 빈객체 return result; };
- props 타입주기
- result에
SeparateDamages
라고 타입 명시 - filter 안에 인자 안쓰는것
_
로 정리 - reduce 리턴타입
- reduce의 acc 초기값 빈객체는 되는데 빈배열은 안되는듯하다. *
postDamageValue
// 2-1. 한개일때: 데미지값 넣어주기 const postDamageValue = (props: SeparateDamages): SeparateDamages => { const result = Object.entries(props) .reduce((acc, [keyName, value]) => { const key = keyName as keyof typeof props; const valueOfKeyName = { double_damage: "2x", half_damage: "1/2x", no_damage: "0x", }; // 데미지값 각 객체마다 넣기 return (acc = { [keyName]: value.map((i: Info[]) => ({ damageValue: valueOfKeyName[key], ...i, })), ...acc, }); }, {}); // console.log(result); return result; };
- props 타입
- 리턴 타입
- reduce의 keyName 타입
const key = keyName as keyof typeof props;
- 리턴하는 acc 의 map의 인덱스?
joinDamageRelations
// 2-2. 두개일때: 합치기 const joinDamageRelations = (props: DamageFromAndTo[]): DamageFromAndTo => { return { to: joinObjects(props, "to"), from: joinObjects(props, "from"), }; };
joinObjects
// (합치기 메서드) const joinObjects = (props: DamageFromAndTo[], string: string) => { const key = string as keyof typeof props[0]; const firstArrayValue = props[0][key]; const secondArrayValue = props[1][key]; // firstArrayValue에 secondArrayValue를 합치기 const result = Object.entries(secondArrayValue).reduce( (acc, [keyName, value]: [string, Damage]) => { // console.log("ddd", acc, [keyName, value]); const key = keyName as keyof typeof firstArrayValue; const result = firstArrayValue[key]?.concat(value); // 첫번째 array value에 해당하는 데미지에 합침 return (acc = { [keyName]: result, ...acc }); }, {} ); return result; };
reduceDuplicateValues
// 중복 속성 데미지값 배로 처리 const reduceDuplicateValues = (props: SeparateDamages) => { const duplicateValues = { double_damage: "4x", half_damage: "1/4x", no_damage: "0x", }; // console.log(props) // console.log("object", Object.entries(props)); return Object.entries(props).reduce((acc, [keyName, value]) => { const key = keyName as keyof typeof props; // console.log("키밸류",[keyName, value]); const verifiedValue = filterForUniqueValues(value, duplicateValues[key]); return (acc = { [keyName]: verifiedValue, ...acc, }); }, {}); };
filterForUniqueValues
const filterForUniqueValues = (valueForFiltering: Damage[], damageValue: string) => { const initialArray:Damage[] = []; // 빈배열에 타입주기 return valueForFiltering.reduce((acc, currentValue) => { const { url, name } = currentValue; const filterACC = acc.filter((a) => a.name !== name); return filterACC.length === acc.length ? (acc = [currentValue, ...acc]) // 변경 없으면(중복없으면) currentvalue그대로 넣기 : (acc = [{ damageValue: damageValue, name, url }, ...filterACC]); // 같은것은 데미지밸류 변경 }, initialArray); // 초기값이 빈배열이면 에러가남 };
4. UI 출력부분(return) 타입
<> {Object.entries(damagePokemonForm).map(([keyName, value]: [string, Damage[]]) => { const key = keyName as keyof typeof damagePokemonForm; const valuesOfKeyName = { double_damage: "Weak", half_damage: "Resistant", no_damage: "Immune", }; return ( <div key={key}> <h3 className="capitalize font-medium text-sm md:text-base text-slate-500 text-center"> {valuesOfKeyName[key]} </h3> <div className="flex flex-wrap gap-1 justify-center"> {value.length > 0 ? ( value.map(({ name, url, damageValue }) => ( <Type key={url} type={name} damageValue={damageValue} /> )) ) : ( <Type key={"none"} type={"none"} /> )} </div> </div> ); })} </>
10. 로그인 페이지 및 기타파일 타입 스크립트로 변경하기
1. 로그인페이지 tsx로 변환
2. firebase.js -> firebase.ts
3. App.jsx -> App.tsx
4. NavBar.jsx -> NavBar.tsx
styled-component
TS관련 패키지 추가npm i --save-dev @types/styled-components
show 속성의 타입(단일)
const NavWrapper = styled.nav<{show: boolean}>` position: fixed; top: 0; left:0; right:0; display: flex; justify-content: space-between; align-items: center; padding: 8px 36px; letter-spacing: 16px; z-index: 100; background: ${props => props.show ? "#090b13": "transparent"}; `
여러개 prop일때는 interface를 써서 지정해주면된다.
interface Wapper extends 상속타입 { show: boolean; name: string; } const NavWrapper = styled.nav<Wapper>` position: fixed; top: 0; left:0; `
firebase User 타입
result.user를 찍어서 JSON 데이터로 interface를 생성해줘도되지만
더 정확한건
firebase에서 제공하는 User라는 인터페이스를 사용하면 된다.
import { User, getAuth, signInWithPopup, GoogleAuthProvider, onAuthStateChanged, signOut } from 'firebase/auth';
const [userData, setUserData] = useState<User | {}>(initialUserData);
그런데,
빈객체 {}
을 사용하는것보다null
을 사용하는것이 더 편하다.
null처리
const initialUserData = userDataFromStorage ? userDataFromStorage : null;
- sign-out
const handleSignOut = () => { signOut(auth) .then(() => { setUserData(null); }) .catch(error => { alert(error.massage); }) } - return UI
<SignOut> {userData?.photoURL && ( <UserImg src={userData.photoURL} alt="" /> )} <DropDown onClick={handleSignOut}>로그아웃</DropDown> </SignOut>
localStorage
string 혹은 null이 될 수 있다.
userDataFromStorage
변수에 미리 받아서 userDataFromStorage
가 null이 아닐때 userDataFromStorage
를 할당하면된다.userData
가 User혹은 null이므로
로컬스토리지에 데이터가 없을때 null을 준다.
const userDataFromStorage = localStorage.getItem("userData"); // const initialUserData = localStorage.getItem("userData") ? // JSON.parse(localStorage.getItem("userData")) : {}; const initialUserData = userDataFromStorage ? userDataFromStorage : null;
11. .d.ts 선언 파일 생성하기
어떠한 객체가 있는데 그 객체에 유니크한 식별자를 주기 위해서 uuid라는 라이브러리를 이용해서 유니크한 값을 만들어보겠다.
uuid 설치
npm i uuid
import
import하려고했을때 에러가난다.
js파일이기때문에... 타입이 any형식이 포함된다.
해당 항목이 있는 경우 'npm i--save-dev @types/uuid'를 시도하거나,
'declare module 'uuid';' 를 포함하는 새 선언(.d.ts) 파일 추가
uuid.d.ts
declare module 'uuid' { const v4: () => string; export { v4 }; }
.d.ts파일은 자동으로 찾기위해서는 해당 패키지이름으로 생성해야한다.
만일 다른 이름으로 생성하였다면 참조태그를 이용한다./// <reference paht="./main.d.ts" />
실제로 .d.ts파일이 필요할때
만들어서 사용하기보다는
만들어진 파일을 사용하는것이 좋다.
선언 모듈 설치
타입선언 모듈 저장소 이동
https://github.com/DefinitelyTyped/DefinitelyTyped
여기에 왠만한 타입 모듈이 저장되어있는데
골라서 사용하면된다.
설치할때는npm install -D @types/모듈이름
모듈을 설치하면 따로 dts 파일을 선언하지 않아도된다.
uuid : npm install -D @types/uuid
dts파일은 node_modules/@types 폴더 안에 들어있는데
ts-conif.json에서 명시를 할 수 있다.
"./node_modules/@types"는 기본값이기때문에 안써도 된다.
"moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "typeRoots": [ "./node_modules/@types" ],
12. localStorage wrapper 생성하기
localStorage Util 함수생성
const storage = { set: (key: string, value: any) => { localStorage.setItem(key, JSON.stringify(value)) }, get: <T>(key: string, defaultValue?: T): T => { const value = localStorage.getItem(key) return (value ? JSON.parse(value) : defaultValue) as T; }, remove: (key: string) => { localStorage.removeItem(key) } } export default storage;
NavBar 수정
Before
const userDataFromStorage = localStorage.getItem("userData"); const initialUserData = userDataFromStorage ? userDataFromStorage : null;
After
const initialUserData = storage.get<User>("userData"); ... // sign-in const handleAuth = () => { signInWithPopup(auth, provider) .then(result => { setUserData(result.user); storage.set("userData", result.user); }) .catch(error => { console.error(error); }) } // sign-out const handleSignOut = () => { signOut(auth) .then(() => { storage.remove("userData"); setUserData(null); }) .catch(error => { alert(error.massage); }) }
'Frontend > React' 카테고리의 다른 글
[React] 헷갈리는 리액트 이벤트 핸들러 작성법 (0) | 2023.12.08 |
---|---|
[Firebase] 파이어베이스를 이용해서 앱 배포하기 (1) | 2023.12.08 |
[React] 포켓몬도감 만들기7 (0) | 2023.12.08 |
[React] 포켓몬도감 만들기6 (1) | 2023.12.08 |
[React] 포켓몬도감 만들기5 (1) | 2023.12.08 |