Unsplash Random Gallery

nearworld·2022년 7월 20일
1

개발 일지

목록 보기
1/7

본 글은
client: https://github.com/NEARworld/gallery
proxy: https://github.com/NEARworld/gallery-proxy
프로젝트들을 개발하면서 겪은 문제와 해결방법들을 기록한 것입니다.

1: response.json() ✩

키워드: 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객체의 bodyJSON객체가 없었기 때문이다.

해결

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로 접근해서 이미지 데이터에 접근했다.


2: 환경 변수 undefined ✩✩✩✩

키워드: 환경변수, api키, 보안, bundle.js
발생 날짜: 2022/7/20
해결 날짜: 2022/7/20

배경 설명

웹 페이지에 랜덤 이미지 1개가 아닌 여러 장을 뿌리기 위해 unsplash-js 모듈을 사용하기로 결정

문제2-1: undefined

키워드
환경 변수, 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로 앱을 다시 시작해줬다.

문제2-2: type 에러

키워드
타입 추론, 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 || '';

문제2-3: 보안 이슈

키워드
보안, 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.htmllive server로 실행하여 브라우저에서 확인해 본 결과

accessKey 값이 그대로 노출되어 있는 것을 볼 수 있다.

보너스 학습

이번 문제를 겪으면서 알게 된 정보가 있다.
현재 프로젝트는 create-react-app을 사용해 세팅했기 때문에 webpack이 bundle파일을 만들때 source map 파일을 만들어준다. 풀어 말하면,
index.js.map같이 tsjs 코드간의 매핑을 위한 파일을 만들어준다.

source map은 ts 코드 디버깅을 위해서 존재한다.

이 경우에 단점이 존재한다.

  1. ts - js 매핑 파일이 존재하므로 소스 코드의 난독화가 무력화된다.
  2. 리액트 프로젝트의 규모가 크다면 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


3: random RGB값 ✩

키워드: rgb, Math.random()
발생 날짜: 2022/7/20
해결 날짜: 2022/7/20

배경 설명

웹 페이지를 랜덤한 배경색을 가진 정사각형 카드들로 꽉 채우려고 한다.

문제3-1: RGB 255

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>

4: 카드 8장 가로 길이 ✩✩✩

키워드: innerWidth
발생 날짜: 2020/7/20
해결 날짜: 2020/7/20

배경 설명

정사각형 카드들은 가로로 8개가 놓이고 각 카드 사이에 간격이 없어야한다.
정사각형 카드 8개의 가로 길이 총합은 웹페이지 가로 길이와 같아야한다.

문제4-1: innerWidth와 clientWidth

<div style={{
    backgroundColor: randomRGB(),
    width: window.innerWidth / 8,
    height: window.innerWidth / 8
  }}></div>

카드 가로 길이: window.innerWidth / 8
카드 8개의 가로 길이: window.innerWidth라고 생각했지만
innerWidth는 세로 scrollbarwidth를 포함한 길이라서 실제 보여지는 웹페이지 가로 길이보다 좀 더 길다.

해결

document.body.clientWidthscrollbarwidth를 포함하지 않아서 의도했던 길이에 딱 맞아 떨어졌다.

<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

문제4-2: CSS와 반응형

    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;
  }
}

이제 카드들이 반응형이 되었다. 윈도우 크기에 맞춰 동적으로 사이즈가 변한다.


5: Proxy 서버 구축 ✩✩✩✩✩

키워드: ts-node, node-fetch, unsplash-js
발생 날짜: 2020/7/21
해결 날짜: 2020/7/21

배경 설명

api의 액세스 키를 브라우저에서 노출시키지 않기 위해 클라이언트 대신에 unsplash api에 액세스 키를 가지고 접근할 proxy 서버를 구축해야한다.

문제5-1: 환경 변수 undefined

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_KEYundefined란 뜻이고 .env를 찾지 못하여 환경 변수들이 undefined 처리된 것 같았다.

해결

import 'dotenv/config';

dotenv.config() 호출없이 import문 만으로 프로젝트 디렉토리의 root 디렉토리에 있는 .env를 찾을 수 있었다.

문제5-2: type error

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 라이브러리에 의해 발생하는 것으로 보인다.

문제5-3: CommonJS 와 import()

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.jsoncompilerOptions 속성 중 하나인 module은 프로그램을 위해 모듈 시스템을 설정하는 기능을 한다.

참고자료

자세한 내용: https://www.typescriptlang.org/tsconfig#module

ts-node를 이용하여 타입스크립트 코드를 자바스크립트 코드로 변환할 때,
CommonJS 모듈로 변환하도록 설정되어있다. 이때 import 구문들은 모두 require()로 변환된다.
직접적으로 보고 이해하기 위해 코드를 챙겨왔다.

// app.ts
import nodeFetch from 'node-fetch';
// 컴파일 후
var nodeFetch = require('node-fetch');

이런식으로 코드가 변환된다. 그런데 node-fetch v3ES 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는 최신 자바스크립트 버전의 모듈 시스템을 사용하게된다. 그래서 특정 버전에 고정된게 아니라 새로운 자스 버전이 나올때마다 새 시스템 방식을 따르는 가변적인 설정이라 하겠다.

문제5-4: moduleResolution

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가 모듈을 탐색할때 쓰는 알고리즘을 통해 모듈들을 찾아낸다.

문제5-4: "type": "module"

(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 데이터를 받았다!


6: useState type ✩✩✩✩✩

키워드: type optional chaining unsplash-js
발생 날짜: 2020/7/21
해결 날짜: 2020/7/21

배경 설명

클라이언트단에서 Proxy서버를 통해 데이터를 받아오는 것을 성공했고 이미지를 backgroundImage 속성에 집어넣어 화면에 unsplash api로부터 받아온 이미지들을 뿌리려고 했다.

문제#6-1: object

타스 컴파일러가 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>
));

문제#6-2: never

data: objectdata로 바꿨더니 이번에는 아래의 에러가 발생했다.

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>
));

매개변수 dataimages라는 상태 변수에서 나오고 있는데 images 타입은 아래와 같이 지정되어 있기 때문이다.

const images: never[].
기본적으로 타입스크립트에서 타입의 개념은 가능한 모든 값의 집합이다.
예를 들어,

const str: string = 'hello';

이 경우에는 타입이 string이므로 가능한 모든 string타입 값들의 집합을 의미하고 hello문자열은 string 집합에 속하기 때문에 허용된다.
다시 never 타입을 보자면, never타입의 의미는 가능한 모든 값이 "절대 없는" 집합 정도로 볼 수 있을 것 같다. 수학의 0과 비슷한 의미를 포함하고 있다고 본다.

이제 코드를 보자.

const [images, setImages] = useState([]);

상태 변수 images빈 배열 []로 초기화된 상태다. 이 경우 빈 배열[]에는 어떤 타입의 요소가 들어갈지 지정되지 않은 상태다. 그리고 []안에 들어갈 요소들의 타입을 모르는 타스 컴파일러는 요소의 타입을 never라고 말하고 있다.

never? any? 의문점

여기서 생기는 의문..
디폴트 타입으로 never가 아니라 any여도 되지 않을까? 왜 useState()함수의 디폴트 타입은 굳이 never일까?

내 추측으로는 상태 변수의 기본 타입을 never로 설계한 사람이 상태 변수 사용시 엄격한 타입 지정을 강제하기 위해 타입 지정이 되어있지 않다면 never를 통해 어떠한 값도 집어넣지 못하게 하여 값을 집어넣으려고 할 시 에러를 발생시키고 개발자가 타입을 지정하는 행위를 하도록 강제했다고 생각한다. never[] 타입을 가지므로 images의 요소가 되는 images.map() 메서드의 매개변수 datanever 타입을 가지게 된다.
만약에 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([]);

리액트에서 함수 컴포넌트를 쓸 경우에는 배열 디스트럭처링을 이용해 상태 변수를 생성하므로 타스 컴파일러는 imagesnever[]타입으로 추론하게 된다.

참고자료

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

Generics 타입

이제 왜 imagesnever[]타입을 가지는 지 이해했으니 그럼 상태 변수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));
}, []);
  1. 서버단의 res.response 배열은 res.json(res.response)를 통해 클라이언트로 보내진다.
  2. res.response는 클라이언트에서 .then()메서드의 매개변수 data이다.
  3. setImages(data)로 인해 Random[] 타입을 가지는 res.response 값은 상태 변수images에 입력되었다.
images.map((data, index) => (
  <div
    key={index}
    className="card"
    style={{
      backgroundColor: randomRGB(),
      backgroundImage: `${data.urls.regular}`,
    }}
  ></div>
));
  1. map()메서드의 매개변수 dataRandom[]타입인 images의 요소이며 Random타입이다.
  2. unsplash-js 모듈에서 설계된 Random타입을 사용하여 data.urls.regular이 아무 에러없이 사용가능해진 상태가 된다.

optional chaining

images는 이제 에러를 발생시키지 않는다.
images타입이 지정되었어도 값이 주어지지 않는 상황에선 어떻게 될까?

const [images, setImages] = useState();

이 경우에 imagesundefined가 된다.
그럼 images.map()undefined.map()이므로 에러가 터져 앱이 충돌하게 된다.
이 경우에는 images?.map()으로 ? optional chianing을 써서 imagesundefined이거나 null일 경우에는 map()메서드를 호출하지 않도록 만들 수 있다.

그러므로 가장 안전한 코드는

const [images, setImages] = useState<Random[]>([]);
images?.map((data, index) => ...) 

가 아닐까싶다..


profile
깃허브: https://github.com/nearworld

0개의 댓글