본 글은
client
: https://github.com/NEARworld/gallery
proxy
: https://github.com/NEARworld/gallery-proxy
프로젝트들을 개발하면서 겪은 문제와 해결방법들을 기록한 것입니다.
키워드:
response.json()
발생 날짜: 2022/7/20
해결 날짜: 2022/7/20
새 프론트엔드 포트폴리오를 만들고자 Unsplash api를 이용하여 랜덤 이미지들을 다운받고자 했다.
useEffect(() => {
fetch('https://source.unsplash.com/random/200x200'
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.log(err));
}, [])
위 코드를 실행시, SyntaxError: Unexpected token � in JSON at position 0
에러 발생
json()
메서드는 response
객체의 body
에 담긴 JSON
객체를 파싱하여 자바스크립트 object
로 만드는데 response
객체의 body
에 JSON
객체가 없었기 때문이다.
useEffect(() => {
fetch('https://source.unsplash.com/random/200x200'
.then(res => console.log(res.url))
.catch(err => console.log(err));
}, [])
Unsplash api는 response
객체에 url: "https://..."
형태로 이미지 데이터를 보내주기 때문에 res.url
로 접근해서 이미지 데이터에 접근했다.
키워드:
환경변수
,api키
,보안
,bundle.js
발생 날짜: 2022/7/20
해결 날짜: 2022/7/20
웹 페이지에 랜덤 이미지 1개가 아닌 여러 장을 뿌리기 위해 unsplash-js
모듈을 사용하기로 결정
키워드
환경 변수
,undefined
,npm start
import {createApi} from 'unsplash-api';
const unsplash = createApi({accessKey: process.env.REACT_APP_ACCESS_KEY})
REACT_APP_ACCESS_KEY
환경 변수가 undefined
환경 변수 생성 시 해당 환경 변수가 앱 빌드 후에 생성 된 것이므로 앱에 반영이 안된 상태
npm start
로 앱을 다시 시작해줬다.
키워드
타입 추론
,type narrowing
,config
에러
Argument of type '{ accessKey: string | undefined; }' is not assignable to parameter of type 'InitParams'.
타스 컴파일러가 process.env.REACT_APP_ACCESS_KEY
를 타입 추론했다.
string | undefined
로 추론했는데 환경 변수가 undefined일수도 있다고 보는데 accessKey
타입은 string
이기 때문에 not assignable
에러를 발생시켰다.
type narrowing
을 적용했다.
if (process.env.REACT_APP_ACCESS_KEY)
unsplash = createApi({accessKey: process.env.REACT_APP_ACCESS_KEY});
문제를 해결 중에 타입스크립트 기초 스터디에 같이 참여 중이신 개발자분이 config
폴더를 생성하고 그 안에 환경 변수들을 따로 관리해두는 것이 좋다고 알려주셨다.
export function getUnsplashAccessKey(): string {
return process.env.REACT_APP_ACCESS_KEY || '';
}
함수가 아닌 변수로도 가능하다.
export const unsplashAccessKey: string = process.env.REACT_APP_ACCESS_KEY || '';
키워드
보안
,proxy
,api키
,source탭
,bundle.js
,npm run build
,*.js.map
,sourceMap
npm start
브라우저 source
탭에서 bundle.js
를 확인해 보니 소스코드가 위 사진처럼 그대로 오픈되어있다. process.env.REACT_APP_KEY
환경 변수의 값도 그대로 노출되어 api key
를 누구나 획득할 수 있는 상태가 되었다. 보안이 엉망인 상태다.
이번에는 npm run build
를 해보았다.
생성된 build
폴더 안의 index.html
을 live server
로 실행하여 브라우저에서 확인해 본 결과
accessKey
값이 그대로 노출되어 있는 것을 볼 수 있다.
이번 문제를 겪으면서 알게 된 정보가 있다.
현재 프로젝트는 create-react-app
을 사용해 세팅했기 때문에 webpack
이 bundle파일을 만들때 source map
파일을 만들어준다. 풀어 말하면,
index.js.map
같이 ts
와 js
코드간의 매핑을 위한 파일을 만들어준다.
source map은 ts 코드 디버깅을 위해서 존재한다.
이 경우에 단점이 존재한다.
ts
-js
매핑 파일이 존재하므로 소스 코드의 난독화가 무력화된다.- 리액트 프로젝트의 규모가 크다면
map
파일 생성으로 인해 빌드 시 메모리를 어느정도 잡아 먹게 될 수 있다.
source map
을 비활성화 시키기 위해서 아래의 작업을 해준다.
// package.json
{
"build": "set \"GENERATE_SOURCEMAP=false\" && react-scripts build"
}
위는 windows
운영체제에서만 먹히는 커맨드이므로 크로스 플랫폼, 맥OS 등 다른 운영체제에서 먹히도록 하려면 아래의 링크를 참고하자.
React 깃헙 이슈
: https://github.com/facebook/create-react-app/issues/8340
package.json
을 위처럼 변경해줬다면 npm run build
를 해서 프로젝트 디렉토리의 build/static/js
경로를 확인해주자.
map
파일이 생성되지 않았음을 확인할 수 있다. 성공했다!
다만, 우측에서 accessKey
값이 환경 변수로 세팅했음에도 여전히 노출되어 있기 때문에 프론트 단에서는 절대로 중요한 보안 키를 사용하지 말자. 무조건 백엔드 단에서 사용해야한다.
다만, 예외 경우가 있다.
카카오 api처럼 특정 도메인만 허용하는 기능을 지원하면 프론트에서 키 값을 사용해도 무방하다.
proxy 서버 이용해야 한다. ( 문제#5 참고)
보안 이슈 때문에 클라이언트측에서 api에 직접 접근할 수 없으므로 api에 대신 접근해줄 대리 서버를 사용하여 대리 서버가 api에 접근 후 api에서 받은 데이터를 클라이언트에 뿌려줄 수 있다.
클라이언트 <-> 서버(Proxy) <-> api
Proxy서버를 이용한 작업 절차는 아래와 같다.
1. 클라이언트에서 proxy 서버로 요청 전송
2. proxy 서버에서 api 키를 api에 보내는 요청에 담아 전송
3. api가 api키를 통한 인증 확인 후 데이터를 담은 응답 전송
4. 서버는 응답을 전송받고 데이터를 추출
5. 추출한 데이터를 클라이언트에 전송
Proxy 서버 개념
https://blog.naver.com/PostView.naver?blogId=dktmrorl&logNo=222410286839
키워드:
rgb
,Math.random()
발생 날짜: 2022/7/20
해결 날짜: 2022/7/20
웹 페이지를 랜덤한 배경색을 가진 정사각형 카드들로 꽉 채우려고 한다.
Math.random()
은 0 <= x <= 1
범위 내의 값을 가진다.
rgb
의 각 r
, g
, b
값은 0 <= x <= 255
범위 내의 값을 가질 수 있다.
Math.random() * 255
위의 수식을 사용하게 돼면 Math.random()
의 범위는 0 * 255 <= x <= 1 * 255
가 된다.
즉, 0 <= x <= 255
의 범위를 가진다.
소수점 자리까지 나오므로 깔끔하게 정수로 표현하기 위해 아래의 코드를 사용할 수 있다.
Math.floor(Math.random() * 255)
function randomRGB(): string {
return `rgb(
${Math.floor(Math.random() * 255},
${Math.floor(Math.random() * 255},
${Math.floor(Math.random() * 255}
)`
}
<div style={{backgroundColor: randomRGB()}}></div>
키워드:
innerWidth
발생 날짜: 2020/7/20
해결 날짜: 2020/7/20
정사각형 카드들은 가로로 8개가 놓이고 각 카드 사이에 간격이 없어야한다.
정사각형 카드 8개의 가로 길이 총합은 웹페이지 가로 길이와 같아야한다.
<div style={{
backgroundColor: randomRGB(),
width: window.innerWidth / 8,
height: window.innerWidth / 8
}}></div>
카드 가로 길이: window.innerWidth / 8
카드 8개의 가로 길이: window.innerWidth
라고 생각했지만
innerWidth
는 세로 scrollbar
의 width
를 포함한 길이라서 실제 보여지는 웹페이지 가로 길이보다 좀 더 길다.
document.body.clientWidth
는 scrollbar
의 width
를 포함하지 않아서 의도했던 길이에 딱 맞아 떨어졌다.
<div style={{
backgroundColor: randomRGB(),
width: document.body.clientWidth / 8,
height: document.body.clientWidth / 8
}}></div>
MDN
문서를 참조했다.
MDN
문서 출처: https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth
return (
<div className="container">
{
Array.from({ length: 40 }).map((item, index) => (
<div key={index} className="card" style={{
backgroundColor: randomRGB(),
width: document.body.clientWidth / 8,
height: document.body.clientWidth / 8
}}></div>
))
}
</div>
)
의도했던 조건들을 충족하는 코드다. 그러나 코드가 지저분한 것이 좀 그렇다..
javascript로만 문제를 해결하려 했으나 발견된 문제가 있다.
브라우저 창 크기를 줄이니 카드들의 크기가 반응적이지 않다..
javascript
코드로 크기를 지정해둔 터라 윈도우 크기가 변화하더라도 렌더링이 발생하지 않는다면 그냥 초기 clientWidth
값을 갖기 때문이다.
그래서 반응형으로 만들기 위해 css
grid
레이아웃을 쓰기로 했다.
.container {
display: grid;
grid-template-columns: repeat(8, 1fr);
.card {
width: 100%;
aspect-ratio: 1;
}
}
이제 카드들이 반응형이 되었다. 윈도우 크기에 맞춰 동적으로 사이즈가 변한다.
키워드:
ts-node
,node-fetch
,unsplash-js
발생 날짜: 2020/7/21
해결 날짜: 2020/7/21
api
의 액세스 키를 브라우저에서 노출시키지 않기 위해 클라이언트 대신에 unsplash api
에 액세스 키를 가지고 접근할 proxy 서버
를 구축해야한다.
import dotenv from 'dotenv';
import * config from './config';
dotenv.config();
console.log(config.accessKey);
dotenv
를 설치하여 config()
메서드를 호출했음에도 환경 변수 process.env.API_ACCESS_KEY
의 값이 할당된 config.accessKey
의 값이 undefined
가 되었다. 이 말은 process.env.API_ACCESS_KEY
가 undefined
란 뜻이고 .env
를 찾지 못하여 환경 변수들이 undefined
처리된 것 같았다.
import 'dotenv/config';
dotenv.config()
호출없이 import
문 만으로 프로젝트 디렉토리의 root
디렉토리에 있는 .env
를 찾을 수 있었다.
Type '(url: RequestInfo, init?: RequestInit | undefined) => Promise<Response>' is not assignable to type '(input: RequestInfo | URL, init?: RequestInit | undefined) => Promise<Response>'.
Types of parameters 'url' and 'input' are incompatible.
Type 'RequestInfo | URL' is not assignable to type 'RequestInfo'.
Type 'Request' is not assignable to type 'RequestInfo'.
Type 'Request' is missing the following properties from type 'Request': size, bufferts(2322)
request.d.ts(25, 5): The expected type comes from property 'fetch' which is declared here on type 'InitParams'
unsplash-js
깃헙 저장소에서 똑같은 오류에 관한 이슈를 찾았다.
https://github.com/unsplash/unsplash-js/issues/186
해당 에러는 DOM
라이브러리에 의해 발생하는 것으로 보인다.
C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\dist\index.js:851
return old(m, filename);
^
Error [ERR_REQUIRE_ESM]: require() of ES Module C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\node-fetch\src\index.js from C:\Users\1\Desktop\portfolios\gallery\proxy\src\app.ts not supported.
Instead change the require of index.js in C:\Users\1\Desktop\portfolios\gallery\proxy\src\app.ts to a dynamic import() which is available in all CommonJS modules.
at Object.require.extensions.<computed> [as .js] (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\dist\index.js:851:20)
at Object.<anonymous> (C:\Users\1\Desktop\portfolios\gallery\proxy\src\app.ts:32:38)
at Module.m._compile (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\dist\index.js:857:29)
at Object.require.extensions.<computed> [as .ts] (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\dist\index.js:859:16)
at phase4 (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\dist\bin.js:466:20)
at bootstrap (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\dist\bin.js:54:12)
at main (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\dist\bin.js:33:12)
at Object.<anonymous> (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\dist\bin.js:579:5) {
code: 'ERR_REQUIRE_ESM'
}
정확한 에러의 원인은 모르겠지만 에러 내용에 CommonJS
가 언급되는걸로 봐서는 module
부분에 관한 에러인 것 같다.
현재 사용중인 node-fetch
버전은 v3
.
node-fetch v3
에서는 require()
를 사용하지 않고 import()
를 사용한다고 한다.
좀 더 깊이 파보면,
// tsconfig.json
"module": "CommonJS"
위 설정이 현재 에러에 관련되어있다.
tsconfig.json
의 compilerOptions
속성 중 하나인 module
은 프로그램을 위해 모듈 시스템을 설정하는 기능을 한다.
ts-node
를 이용하여 타입스크립트 코드를 자바스크립트 코드로 변환할 때,
CommonJS
모듈로 변환하도록 설정되어있다. 이때 import
구문들은 모두 require()
로 변환된다.
직접적으로 보고 이해하기 위해 코드를 챙겨왔다.
// app.ts
import nodeFetch from 'node-fetch';
// 컴파일 후
var nodeFetch = require('node-fetch');
이런식으로 코드가 변환된다. 그런데 node-fetch v3
이 ES module
이라 require('node-fetch')
를 이용해 불러오는 것이 불가능하다.
require() of ES Module ... not supported
그렇게 하면 위 에러가 발생한다.
에러를 해결하기 위한 해결책이 여러 개있다.
//tsconfig.json
"module": "ES2015"
//tsconfig.json
"module": "ES6"
ES2015
는 다른 표현으로 ES6
이므로 같은 설정이다. 그냥 다른 표현들이 쓰이니 지원해준 것 같다. 위 자바스크립트 표준 버전이 tsconfig
에서 지원하는 가장 오래된 ES
버전이고 import
를 지원한다.
//tsconfig.json
"module": "ESNext"
ESNext
는 최신 자바스크립트 버전의 모듈 시스템을 사용하게된다. 그래서 특정 버전에 고정된게 아니라 새로운 자스 버전이 나올때마다 새 시스템 방식을 따르는 가변적인 설정이라 하겠다.
C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\src\index.ts:859
return new TSError(diagnosticText, diagnosticCodes, diagnostics);
^
TSError: ⨯ Unable to compile TypeScript:
src/app.ts:3:27 - error TS2792: Cannot find module 'unsplash-js'.
Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the
'paths' option?
문제가 해결되나 싶었지만 또 다른 에러를 마주했다.. 다시 tsconfig.json
에 관련된 에러다.
모듈 시스템을 변경해줬지만 타입스크립트 컴파일러가 import한 모듈들을 찾지 못하고 있다.
그래서 직접 설정을 통해 컴파일러에게 모듈들을 찾는 방법을 알려줘야한다.
//tsconfig.json
"moduleResolution": "node"
moduleResolution
은 타스 컴파일러가 어떻게 모듈을 찾아야할지를 설정해주는 속성이다.
node
값을 설정해주면 타스 컴파일러는 node.js
가 모듈을 탐색할때 쓰는 알고리즘을 통해 모듈들을 찾아낸다.
(node:25748) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
C:\Users\1\Desktop\portfolios\gallery\proxy\src\app.ts:1
import "dotenv/config";
^^^^^^
SyntaxError: Cannot use import statement outside a module
at Object.compileFunction (node:vm:352:18)
at wrapSafe (node:internal/modules/cjs/loader:1031:15)
at Module._compile (node:internal/modules/cjs/loader:1065:27)
at Module.m._compile (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\src\index.ts:1618:23)
at Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
at Object.require.extensions.<computed> [as .ts] (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\src\index.ts:1621:12)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:822:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at phase4 (C:\Users\1\Desktop\portfolios\gallery\proxy\node_modules\ts-node\src\bin.ts:649:14)
해결했나 싶은데 에러가 또 발생했다. 이번에는 tsconfig.json
에 관한 에러는 아니다.
Cannot use import statement outside a module
.
import
문이 app.ts
에 써져있는데 app.ts
는 모듈이 아니기때문에 import
문을 쓸 수없다는 에러다. 그래서 프로젝트를 모듈화시켜서 app.ts
에서 import
를 사용해야한다.
// package.json
"dependencies": {
...
},
"type": "module"
"type": "module"
를 package.json
에 설정하여 프로젝트를 모듈화한다.
그럼 이제 모듈화가된 프로젝트내에 있는 app.ts
에서 import
를 사용할 수 있게 된다.
또! 에러가 터졌다!!
TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for C:\Users\1\Desktop\portfolios\gallery\proxy\src\app.ts
at new NodeError (node:internal/errors:371:5)
at Object.file: (node:internal/modules/esm/get_format:72:15)
at defaultGetFormat (node:internal/modules/esm/get_format:85:38)
at defaultLoad (node:internal/modules/esm/load:13:42)
at ESMLoader.load (node:internal/modules/esm/loader:303:26)
at ESMLoader.moduleProvider (node:internal/modules/esm/loader:230:58)
at new ModuleJob (node:internal/modules/esm/module_job:63:26)
at ESMLoader.getModuleJob (node:internal/modules/esm/loader:244:11)
at async Promise.all (index 0)
at async ESMLoader.import (node:internal/modules/esm/loader:281:24) {
code: 'ERR_UNKNOWN_FILE_EXTENSION'
}
// tsconfig.json
"ts-node": {
"esm": true
}
안타깝게도 ts-node
를 사용하여 노드 서버를 production
레벨에서 쓰기에는 리스크가 있다고 한다.
proxy
서버가 api
로부터 json
데이터를 받았다!
키워드:
type
optional chaining
unsplash-js
발생 날짜: 2020/7/21
해결 날짜: 2020/7/21
클라이언트단에서 Proxy
서버를 통해 데이터를 받아오는 것을 성공했고 이미지를 backgroundImage
속성에 집어넣어 화면에 unsplash api
로부터 받아온 이미지들을 뿌리려고 했다.
타스 컴파일러가 Property 'urls' does not exist on type 'object
에러를 발생시켰다.
map
메서드의 첫번째 인자로 들어간 data: object
때문인데 매개변수 data
의 타입을 object
로 지정했지만 object
타입은 urls
에 대한 정보를 알 턱이 없다.. 타입스크립트의 타입에 대해 잘 모르는 상태에서 쓴 코드이다. 그러므로 data: object
는 말이 되질 않는다.
images.map((data, index) => (
<div
key={index}
className="card"
style={{
backgroundColor: randomRGB(),
backgroundImage: `${data.urls.regular}`,
}}
></div>
));
data: object
를 data
로 바꿨더니 이번에는 아래의 에러가 발생했다.
Property 'urls' does not exist on type 'never'
에러.
const [images, setImages] = useState([]);
images.map((data, index) => (
<div
key={index}
className="card"
style={{
backgroundColor: randomRGB(),
backgroundImage: `${data.urls.regular}`,
}}
></div>
));
매개변수 data
는 images
라는 상태 변수에서 나오고 있는데 images
타입은 아래와 같이 지정되어 있기 때문이다.
const images: never[]
.
기본적으로 타입스크립트에서 타입
의 개념은 가능한 모든 값의 집합
이다.
예를 들어,
const str: string = 'hello';
이 경우에는 타입이 string
이므로 가능한 모든 string
타입 값들의 집합을 의미하고 hello
문자열은 string
집합에 속하기 때문에 허용된다.
다시 never
타입을 보자면, never
타입의 의미는 가능한 모든 값이 "절대 없는" 집합
정도로 볼 수 있을 것 같다. 수학의 0
과 비슷한 의미를 포함하고 있다고 본다.
이제 코드를 보자.
const [images, setImages] = useState([]);
상태 변수
images
는 빈 배열
[]
로 초기화된 상태다. 이 경우 빈 배열[]
에는 어떤 타입의 요소가 들어갈지 지정되지 않은 상태다. 그리고 []
안에 들어갈 요소들의 타입을 모르는 타스 컴파일러는 요소의 타입을 never
라고 말하고 있다.
여기서 생기는 의문..
디폴트 타입으로never
가 아니라any
여도 되지 않을까? 왜useState()
함수의 디폴트 타입은 굳이never
일까?
내 추측으로는 상태 변수
의 기본 타입을 never
로 설계한 사람이 상태 변수
사용시 엄격한 타입 지정을 강제하기 위해 타입 지정이 되어있지 않다면 never
를 통해 어떠한 값도 집어넣지 못하게 하여 값을 집어넣으려고 할 시 에러를 발생시키고 개발자가 타입을 지정하는 행위를 하도록 강제했다고 생각한다. never[]
타입을 가지므로 images
의 요소가 되는 images.map()
메서드의 매개변수 data
는 never
타입을 가지게 된다.
만약에 any[]
였다면 프로그램 실행시 프로그램이 설계 의도대로 작동하지 않을 가능성이 생기게 된다. 그러므로, never
타입으로 코드 설계 단계에서 "엄격히" 타입을 지정하도록 만들어둔게 아닌가 싶다.
는 망상이었고.. 실제로는
타스 컴파일러가 빈 배열[]
에 대해 never[]
로 타입 추론하는 상황들
// 그냥 빈 배열만 할당할 때
let arr = []
// arr: any[]
이면 타입 추론으로 any[]
타입을 지정하지만
// 빈 배열이 오브젝트 프로퍼티일 때
let obj = {
arr: []
}
// arr: never[]
// 빈 배열 arr1, arr2가 배열 arr의 요소가 되고 배열 arr을 디스트럭처링할때
let arr = [[], []]
let [arr1, arr2] = arr
// arr1: never[]
// arr2: never[]
이제 never[]
가 어찌 추론되는지 알았으므로 useState
코드를 다시 보면
const [images, setImages] = useState([]);
리액트에서 함수 컴포넌트
를 쓸 경우에는 배열 디스트럭처링
을 이용해 상태 변수
를 생성하므로 타스 컴파일러는 images
를 never[]
타입으로 추론하게 된다.
useState
작동 방식
https://www.netlify.com/blog/2019/03/11/deep-dive-how-do-react-hooks-really-work/
never
타입 설명
https://yceffort.kr/2022/03/understanding-typescript-never
이제 왜 images
가 never[]
타입을 가지는 지 이해했으니 그럼 상태 변수images
의 타입을 어떻게 지정할 수 있는지 알아야한다.
const [images, setImages] = useState([]);
const [images, setImages] = useState<Random[]>([]);
두번째 코드를 보면 <Random[]>
부분이 추가된 것을 볼 수 있다. 우선, <>
은 타입스크립트에서 지원하는 제네릭 (Generics)
타입을 지정할때 쓴다.
제네릭
타입은 무엇일까?
// any 타입 리턴 함수
function example(arg: any): any {
return arg;
}
const val = example(1)
// no error
val.split('');
// 제네릭 타입 리턴 함수
function example2<T>(arg: T): T{
return arg;
}
const val2 = example2(1);
// error
val2.split('');
위 코드를 보면 제네릭 타입을 쓴 example2<T>
는 아무 값이나 다 허용해주는 개방성을 any
타입처럼 갖고 있지만 사용할때는 그 값의 타입에 따라 작동하는 유연성도 가지고 있다.
비유를 들어any
타입이 그야말로 현관문을 활짝 열어놓고 누가 들어오든 신경을 안쓰는 것이라면 제네릭
타입은 현관문을 통해 들어오게 하되 누구인지는 파악하는 것이라고 이해했다.
이제 제네릭
타입이 무엇인지 이해했으니 다시 useState
코드를 확인하자
const [images, setImages] = useState<Random[]>([]);
제네릭 타입의 심볼인 <>
안에 Random[]
타입을 넣게 되면 images
는 빈 배열을 받아도 더이상 never[]
타입이 아닌 Random[]
타입을 가지게 된다.
제네릭(Generics)
타입 설명
https://velog.io/@mokyoungg/TS-%EC%A0%9C%EB%84%88%EB%A6%ADGeneric
// proxy 서버 코드
import nodeFetch from 'node-fetch';
import {createApi} from 'unsplash-js';
const unsplash = createApi({
accessKey: process.env.ACCESS_KEY,
fetch: nodeFetch as unknown as fetch
})
app.get("/", (req: Request, res: Response, next: NextFunction) => {
unsplash.photos
.getRandom({
count: 30,
})
.then((res) => {
res.json(res.response);
})
.catch((err) => console.log(err));
console.log("get request to /");
});
위 proxy
서버쪽 코드를 보자. getRandom()
메서드의 반환 값에서 추출된 res.response
는 오브젝트들을 요소로 가지는 배열인데 이 response
배열이 Random[]
타입을 가진다.
다시 클라이언트단 코드를 보자.
const [images, setImages] = useState<Random[]>([]);
useEffect(() => {
fetch("http://localhost:8080")
.then((res) => res.json())
.then((data) => {
setImages(data);
console.log(data);
})
.catch((err) => console.log(err));
}, []);
res.response
배열은 res.json(res.response)
를 통해 클라이언트로 보내진다.res.response
는 클라이언트에서 .then()
메서드의 매개변수 data
이다.setImages(data)
로 인해 Random[]
타입을 가지는 res.response
값은 상태 변수images
에 입력되었다.images.map((data, index) => (
<div
key={index}
className="card"
style={{
backgroundColor: randomRGB(),
backgroundImage: `${data.urls.regular}`,
}}
></div>
));
map()
메서드의 매개변수 data
는 Random[]
타입인 images
의 요소이며 Random
타입이다.unsplash-js
모듈에서 설계된 Random
타입을 사용하여 data.urls.regular
이 아무 에러없이 사용가능해진 상태가 된다.images
는 이제 에러를 발생시키지 않는다.
images
타입이 지정되었어도 값이 주어지지 않는 상황에선 어떻게 될까?
const [images, setImages] = useState();
이 경우에 images
는 undefined
가 된다.
그럼 images.map()
은 undefined.map()
이므로 에러가 터져 앱이 충돌하게 된다.
이 경우에는 images?.map()
으로 ?
optional chianing
을 써서 images
가 undefined
이거나 null
일 경우에는 map()
메서드를 호출하지 않도록 만들 수 있다.
그러므로 가장 안전한 코드는
const [images, setImages] = useState<Random[]>([]);
images?.map((data, index) => ...)
가 아닐까싶다..