Typescript를 공부하면서 React 프로젝트를 Typescript로 업데이트 하고 싶어졌다. Typescript를 얹어 보자!
Vite + React + TS로 생성했던 프로젝트를 참고했다.
typescript를 비롯해 react와 react-dom 패키지들의 타입을 설치해준다.
npm i typescript @type/node @type/react @type/react-dom @type/react-router-dom
tsc --init 명령어로 tsconfig.json을 생성할 수 있다.
tsc --init
Typescript를 추가하려는 프로젝트도 Vite로 생성했어서 tsconfig.json > references > path에 tsconfig.node.json을 추가하고 tsconfig.node.json도 만들어줬다.
No inputs were found in config file '내 파일 디렉터리'.... include는 ["src"] exclude는 []다. ...
이런 에러가 뜬다면 일단 에디터를 껐다 켜본다. 나는 껐다 켰더니 해결됐다.
tsconfig.node.json 참고 1
tsconfig.node.json 참고 2
firebase key 때문에 .env를 사용했는데 빨간 줄이 떴다. Vite는 환경변수를 import.meta.env.VITE_변수명으로 접근한다. tsconfig.json에 types를 추가해 주면 된다.
{
"compilerOptions": {
...
"module": "ESNext",
...
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
{
"compilerOptions": {
...
"module": "ESNext",
"types": ["vite/client"],
...
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
build시에 Typescript를 Javascript로 변환해주어야 하기때문에 tsc 명령어를 추가한다.
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
js는 ts로 jsx는 tsx로 파일 확장자를 변경한다.
.js=>.ts
.jsx=>.tsx
확장자를 변경했으니 index.html에서 src의 파일 확장자를 변경해준다.
<!--기존-->
<script type="module" src="/src/main.jsx"></script>
<!--변경-->
<script type="module" src="/src/main.tsx"></script>
.tsx로 변경한 파일부터 타입 스크립트를 얹어준다. interface를 선언하여 props에 type을 정해주거나, generic을 사용하여 에러들을 해결해준다.
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Router />
</React.StrictMode>
);
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Router />
</React.StrictMode>
);
jsx를 tsx로 확장자를 변경하면 e와 ref.current에 빨간 줄이 생긴다.
...
export default function CreateWord() {
const days = useFetch("http://localhost:3001/days");
const history = useHistory();
const [isLoading, setIsLoading] = useState(false);
function onSubmit(e) {
e.preventDefault();
if (!isLoading) {
setIsLoading(true);
const day = dayRef.current.value;
const eng = engRef.current.value;
const kor = korRef.current.value;
fetch(`http://localhost:3001/words/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
day,
eng,
kor,
isDone: false,
}),
}).then(res => {
if (res.ok) {
alert("생성이 완료 되었습니다");
history.push(`/day/${day}`);
setIsLoading(false);
}
});
}
}
const engRef = useRef(null);
const korRef = useRef(null);
const dayRef = useRef(null);
useRef를 선언하는 부분에서 <HTMLElement>를 generic으로 넣어준다. (html element에 ref를 사용했으므로)
isLoading이 끝났을 때 사용하는 ref 값이 null일수도 있기 때문에 에러가 뜬다. 그래서 조건부 렌더링을 같이 걸어준다. 이미 검증되어서 값이 들어오기 때문에 안전하게 사용할 수 있다.
ref.current를 사용할 때는 렌더링이 끝난 뒤에도 null일 수 있기 때문에 값을 체크해주는 것이 좋다. (해당 요소가 조건에 따라 보일 수도 있고 안보일 수도 있기 때문에)
export default function CreateWord() {
const days: IDay[] = useFetch("http://localhost:3001/days");
const history = useHistory();
const [isLoading, setIsLoading] = useState(false);
function onSubmit(e: React.FormEvent) {
e.preventDefault();
if (!isLoading && dayRef.current && engRef.current && korRef.current) {
setIsLoading(true);
const day = dayRef.current.value;
const eng = engRef.current.value;
const kor = korRef.current.value;
fetch(`http://localhost:3001/words/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
day,
eng,
kor,
isDone: false,
}),
}).then(res => {
if (res.ok) {
alert("생성이 완료 되었습니다");
history.push(`/day/${day}`);
setIsLoading(false);
}
});
}
}
const engRef = useRef<HTMLInputElement>(null);
const korRef = useRef<HTMLInputElement>(null);
const dayRef = useRef<HTMLSelectElement>(null);
마지막 인자인 params는 파라미터값을 받아오므로 undefined이 될 수도 있다. 하지만 doc의 세 번째 파라미터는 string만 들어올 수 있어 허용될 수 없다.
그래서 params이 undefined일 경우에 대신 넣을 문자열을 정해줬다.
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const docRef = doc(db, 'rolling-paper', params);
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const docRef = doc(db, 'rolling-paper', !!params ? params : '홍길동');
이 문제는 type이 없어 생긴 문제가 아니었다
기존에 작성해둔 코드는 콘솔에 데이터를 찍어보면서 타고 타고 데이터에 접근했는데 미리 선언된 type에 _document가 존재하지 않아서 에러가 뜬 것이었다. 이 때도 코드에 접근하면서 왜 이렇게 복잡하게 만들었을까 생각했던 기억이 나는데 내 접근 방법이 잘못된 것이었다.
데이터를 반환하는 메서드를 사용하면 에러가 생기지 않는다. exists()라는 메서드로 데이터가 있는지 여부를 확인한 다음 data() 메서드로 데이터를 가져오면 된다. 그리고 필요하다면 type들을 정해주면 된다.
에러 해결을 못해서 letters.을 치고 고민하고 있었는데 데이터를 반환하는 메서드가 자동 완성으로 떠서 "😳!!!" 하며 해결할 수 있었다. 타입스크립트의 자동 완성 기능이 편리하다고 느낀 경험이었다.
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const docRef = doc(db, 'rolling-paper', params);
const letters = await getDoc(docRef);
if (letters._document.data.value.mapValue.fields !== undefined) {
let dataList = Object.entries(
letters._document.data.value.mapValue.fields
);
dataList = dataList.map((message) => {
return message.map((el, i) => {
return i === 1 ? el.stringValue : el;
});
});
setList(dataList);
} else {
setList([]);
}
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const docRef = doc(db, 'rolling-paper', !!params ? params : '홍길동');
const letters = await getDoc(docRef);
if (letters.exists()) {
let dataList: [string, any][] = Object.entries(letters.data());
setList(dataList);
} else {
setList([]);
}