Map을 포함한 Object JSON stringify

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
interface MyType {
id: number;
cover: string;
outComes: Map<number, string>;
questions: Map<number, Map<number, string>>;
}

const stringify = (object: any) => {
for (const eachIdx in object) {
if (object[eachIdx] instanceof Map) {
object[eachIdx] = Array.from(object[eachIdx]);
stringify(object);
} else if (typeof object[eachIdx] == 'object') stringify(object[eachIdx]);
}
return JSON.stringify(object, null, 2);
};

const jsonString2ObjectWithMap = <ReturnType>(
jsonString: string
): ReturnType => {
const object = JSON.parse(jsonString);
console.log(`-------------변환전`);
console.log(object);

const jsonstringToObject = (object) => {
for (const eachIdx in object) {
if (
object[eachIdx] instanceof Array &&
object[eachIdx].length > 0 &&
object[eachIdx][0].constructor === Array
) {
object[eachIdx] = new Map(object[eachIdx]);
jsonstringToObject(object);
} else if (typeof object[eachIdx] == 'object')
jsonstringToObject(object[eachIdx]);
}

return object;
};

console.log(`-------------변환후`);
const result = jsonstringToObject(object);

console.log(result);

return result;
};
실행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const myObject: MyType = {
id: 30,
cover: 'cover',
outComes: new Map([
[0, 'o1'],
[1, 'o2'],
]),
questions: new Map([
[
0,
new Map([
[1, 'answer1'],
[2, 'ansewr2'],
]),
],
[
1,
new Map([
[1, 'ansewr1'],
[2, 'ansewr2'],
]),
],
[2, new Map([])],
]),
};

console.log(`-----------------map을 포함한 오브젝트 json stringify`);
const rst = stringify(myObject);
console.log(rst);
console.log(typeof rst);

console.log(`-----------------다시 오브젝트화`);
const result = jsonString2ObjectWithMap<MyType>(rst);
output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
-----------------map을 포함한 오브젝트 json stringify
{
"id": 30,
"cover": "cover",
"outComes": [
[
0,
"o1"
],
[
1,
"o2"
]
],
"questions": [
[
0,
[
[
1,
"answer1"
],
[
2,
"ansewr2"
]
]
],
[
1,
[
[
1,
"ansewr1"
],
[
2,
"ansewr2"
]
]
],
[
2,
[]
]
]
}
string
-----------------다시 오브젝트화
-------------변환전
{
id: 30,
cover: 'cover',
outComes: [ [ 0, 'o1' ], [ 1, 'o2' ] ],
questions: [ [ 0, [Array] ], [ 1, [Array] ], [ 2, [] ] ]
}
-------------변환후
{
id: 30,
cover: 'cover',
outComes: Map { 0 => 'o1', 1 => 'o2' },
questions: Map { 0 => [ [Array], [Array] ], 1 => [ [Array], [Array] ], 2 => [] }
}

상황

  • 서버로 json 형태로 값을 전달하고 싶은데…
  • Map은 json stringify로 변환되지 않는다
  • 변환하기 위해서는 한번 Array로 변환한 다음에야 가능했다
  • 어떤 객체에 맵이 중첩으로 사용된 경우 수동으로 바꿔주는 게 귀찮다

해결

stringify.ts
1
2
3
4
5
6
7
8
9
const stringify = (object: any) => {
for (const eachIdx in object) {
if (object[eachIdx] instanceof Map) {
object[eachIdx] = Array.from(object[eachIdx]);
stringify(object);
} else if (typeof object[eachIdx] == 'object') stringify(object[eachIdx]);
}
return JSON.stringify(object, null, 2);
};
  • 객체 안 멤버들을 하나씩 돌아가면서 Map이거나 오브젝트인지 확인한다
  • Map이면은 JSON stringify 할 수 있는 Array로 변환한다
  • 오브젝트이면 중첩되어있는 Map을 찾기 위해 재귀적으로 반복한다

사용법

  • myObject와 같은 중첩 map을 포함하고 있고,
  • 포함되어있는 Map이 Map<number,string> 일 경우에만 때만 테스트해보아서, 더 다양한 경우에도 동작할지는 모르겠다
  • input : { a:… } 처럼 오브젝트를 넣어야 한다
  • console.log 찍어봤을 때 { }로 묶여있는…
  • 만약에 그냥 Map인 경우에는 이런 식으로 { a: new Map() } 한번 감싸주면 된다

repo

react에서 Map을 state로 사용할 때

맞는 예
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const [state, setState] = React.useState(new Map());

const add = (key, value) => {
setState((prev) => new Map([...prev, [key, value]]));
};


const upsert = (key, value) => {
setState((prev) => new Map(prev).set(key, value));
}

const delete = (key) => {
setState((prev) => {
const newState = new Map(prev);
newState.delete(key);
return newState;
});
}

const clear = () => {
setState((prev) => new Map(prev.clear()));
}
틀린 예
1
2
3
4
5
6
7
8
const [state, setState] = React.useState(new Map());

const add = (key, value) => {
state.set(key, value);
setState(state);
};

// ...
  • 원시 타입이 아닌 객체를 state로 다룰 때 실수하기 쉽다
  • 완전히 새로운 레퍼런스?를 넣어줘야 상태가 변경되었다고 판단하고 리렌더링 된다

배열

array-state-example.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// work
const add = (value) => {
setState((prev) => {
return [...prev, value];
});
};

// not work
const add = (value) => {
setState((prev) => {
prev.push(value);
return prev;
});
};

참고

js bind

  • typescript 코드다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface myType {
name: string;
age: number;
}

const me: myType = {
name: 'name',
age: 99,
};

[1, 2, 3, 4, 5].forEach(function (this: myType, value, index) {
console.log(this);
console.log(value, index);
}, me);
// 0 1 { name: 'name', age: 11 }
// 1 2 { name: 'name', age: 11 }
// 2 3 { name: 'name', age: 11 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class MyClass {
me: Person;

constructor(person: Person) {
this.me = person;
}

someFunction() {
[1, 2, 3].forEach(function (value) {
console.log(value, this);
});
}

someFunctionBindThis() {
[1, 2, 3, 4].forEach(function (this: MyClass, value) {
console.log(value, this);
}, this);
}

someFunctionWithArrow() {
[1, 2, 3].forEach((value) => {
console.log(value, this);
});
}
}

const myclass = new MyClass(me);
myclass.someFunction();
// 1 undefined
// 2 undefined
// 3 undefined

myclass.someFunctionBindThis();
// 1 MyClass { me: { name: 'name', age: 11 } }
// 2 MyClass { me: { name: 'name', age: 11 } }
// 3 MyClass { me: { name: 'name', age: 11 } }

myclass.someFunctionWithArrow();
// 1 MyClass { me: { name: 'name', age: 11 } }
// 2 MyClass { me: { name: 'name', age: 11 } }
// 3 MyClass { me: { name: 'name', age: 11 } }
  • bind 함수를 배워보았다
  • bind를 배우면서 functionarrow function 차이를 실감하게 되었다
  • arrow function은 this를 바인딩하지 않는다
  • 고로 arrow function은 bind를 사용할 수 없다
  • 이 함수 표현은 메서드 함수가 아닌 곳에 적합하다

참고

typescript에서 mathjs 사용해서 소수점 정확하게 계산하기

  • 부동소수점으로 인한 계산 오류가 있다
  • mathjs 라이브러리로 해결해보자

mathjs 설치

1
2
yarn add mathjs
yarn add @types/mathjs -D
  • mathjs만 설치하면 타입 정의가 없기 때문에
  • @types/mathjs 보조 라이브러리까지 설치

mathjs 임포트

example1.ts
1
import * as math from 'mathjs';
  • 그냥 기본값으로 사용하려면 import * as math from 'mathjs'로 불러와 사용
  • default export가 없어서 import math from 'mathjs'이런 식으로 사용불가
    • Attempted import error: "mathjs" does not contain a default export
example2.ts
1
2
3
import { create, all } from 'mathjs';

const mathF = create(all, { number: 'Fraction' }) as math.MathJsStatic;
  • 지정 config를 적용해 사용하려면 create 메서드 사용
  • create(all, { number: 'Fraction' }) as math.MathJsStatic
  • as 키워드로 타입을 명시하지 않으면 undefined 에러 발생
    • Cannot invoke an object which is possibly 'undefined'

소수점 계산

calc.ts
1
2
3
4
5
6
7
8
9
import { create, all } from 'mathjs';

const mathF = create(all, { number: 'Fraction' }) as math.MathJsStatic;

const mathjsCalc = (expr: string) => {
return mathF.number(mathF.evaluate(expr));
};

const result = mathjsCalc(`${value1} * ${value2} * 0.01`);
  • evaluate메서드를 사용해서 수식 string을 주면 알아서 계산하도록 했다
  • 이때 math ConfigOptions에서 number 옵션이 기본값(number)인 경우에 일반적인 계산이 된다
  • number 옵션에 Fraction을 주고 생성하게 되면 evaluate 메서드에서 숫자를 Fraction 타입으로 파싱 해서 계산하게 되어 우리가 기대하는 결과를 얻을 수 있다

참고

react로 만들어본 퇴근시간 계산기

기능

메인 페이지

  • 목표 시각까지 남은 시간 계산
  • url param을 통해 목표 시각 세팅
  • 목표 시각이 지나면 퇴근하라는 메시지를 뛰움

about 페이지

  • 링크 공유 기능
  • 퇴근 시간 설정 기능
  • 자동 클립보드 복사

깃허브 페이지 spa 세팅

배경이미지 출처

해결한 문제들

gsap 적용안됨

Share.tsx
1
2
3
4
5
import { Power3, TimelineLite } from 'gsap';
import CSSPlugin from 'gsap/CSSPlugin'; // 추가

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const C = CSSPlugin; // 추가
  • 로컬에서는 애니메이션이 잘 실행되었는데, 빌드 후 애니메이션이 안 나오는 문제 해결

후기

  • 어느 정도 리액트에 익숙해진 것 같다
  • 주로 사용하는 패턴이 생긴 것 같다
  • 엄청 간단해 보이지만 이것저것 신경 쓴 것 같은데, 기록을 안 해놨다 ㅠㅠ
  • 나는 코드에 조금의 변화가 생겨도 커밋을 했었는데,,
  • 이번에는 커밋을 안 하고 한 번에 몰아서 마지막에 파일별로 커밋을 했다
  • 그래서 해결한 문제들이 기억이 잘 안 난다.. ㅠㅠ

신경 썼던 것들

input element

  • about 페이지에서는 링크를 생성하는 부분이 있다
  • 처음에는 input width가 좁았었다. 그래서 고민했던 것이…
  • input 넓이보다 안의 내용이 더 길면은 끝부분이 감춰진다

  • input에서 포커스를 해제하면 위 그림처럼,
  • 계속 앞쪽으로 포커스가 자동으로 이동했다

  • 나는 input 내용의 가장 뒤쪽,
  • url param이 변하는 것에 포커스가 가도록 만들려고 이것저것 해봤는데…
  • 결국엔 그냥 input width를 늘려버리고 끝을 냈다…

github pages SPA

  • 깃허브 페이지는 기본적으로 SPA를 지원하지 않는다
  • react-router-dom 으로 여러 경로들을 만들어 두면 index 말고는 404 페이지로 리디렉트 된다
  • 그래서 찾아보니까,,.!
404.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var segmentCount = 1;

var l = window.location;
l.replace(
l.protocol +
'//' +
l.hostname +
(l.port ? ':' + l.port : '') +
l.pathname
.split('/')
.slice(0, 1 + segmentCount)
.join('/') +
'/?p=/' +
l.pathname
.slice(1)
.split('/')
.slice(segmentCount)
.join('/')
.replace(/&/g, '~and~') +
(l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
l.hash
);
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(function (l) {
if (l.search) {
var q = {};
l.search
.slice(1)
.split('&')
.forEach(function (v) {
var a = v.split('=');
q[a[0]] = a.slice(1).join('=').replace(/~and~/g, '&');
});
if (q.p !== undefined) {
window.history.replaceState(
null,
null,
l.pathname.slice(0, -1) + (q.p || '') + (q.q ? '?' + q.q : '') + l.hash
);
}
}
})(window.location);
  • 원리는 index 경로 외에 나머지 경로로 들어오면 404페이지를 반환하는데,
  • 커스텀 404페이지에서 스크립트를 통해서 index페이지로 리디렉트 하게 한다
  • 이때, url params 및 query string을 가공한다
  • index.html에서는 404.html로부터 넘겨받은 값으로 라우팅이 적용된 화면을 보여준다
  • 이걸 만든 사람은 정말 대단한 것 같다. ㄷㄷ;

소스코드

참고

github pages spa

type vs interface

1
2
3
4
5
6
7
8
9
10
// X
type MyFile extends File = {}

// O
interface MyFile extends File {
preview: string;
}

// O
type MyFile = {preview: string} & File
  • type은 extends, implements가 안됨
  • 대신 extends의 경우는 & 키워드를 사용하여 대체할 수 있다
  • interface와 type은 거의 같은 역할을 한다
  • 그래서 둘중 하나만 사용하여 일관적인 스타일을 유지하는 게 좋다고 들었다

참고

  • 나는 typescript를 공부하면서 처음 접한게 type이라서 type을 쭉 사용해왔다
  • 그런데 상속을 해야하는 경우에 마추쳤을 때, 불가피하게 interface 키워드를 사용했고,
  • type을 사용할 수 없을까 찾아보다가 type vs interface 에 대한 좋은 링크를 찾았다

react url params 사용하기

example.tsx
1
2
3
4
5
// 전
res = await myFetch(`get/${location.pathname.split('/')[2]}`);

// 후
res = await myFetch(`get/${id}`);
  • 처음에는 url params에 어떻게 접근할까하다가 location객체에서 수동으로 파싱해서 썼었는데

  • ‘react-router-dom’ 에서 useRouteMatch라는 메서드를 통해서 자동으로 파싱하고 필요한 차일드에 넘겨주었다

parent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
// 최상위, 컴포넌트 밖
type MatchParams = {
id: string;
};

// 컴포넌트 안 최상위
const match = useRouteMatch<MatchParams>('/start/:id');

// 렌더 부분, 컴포넌트 안 return 부분
<Route path="/start/:id">
<Start id={match?.params.id || ''} />
</Route>;
child.tsx
1
2
3
4
5
6
7
8
9
// 최상위, 컴포넌트 밖
type Props = {
id: string;
};

// 컴포넌트 선언부
function Start({ id }: Props) {
// ...
}
  • 차일드에서 useRouteMatch를 사용해보았는데, useEffect에서 무한루프에 걸려서
  • 부모에서 전달해주었다

참고

Cannot use JSX unless the '--jsx' flag is provided.ts(17004)

  • react typescript에서 tsconfig가 계속 자동수정되는 문제 해결하기
오류 메시지 모음
1
2
3
4
5
6
Cannot use JSX unless the '--jsx' flag is provided.ts(17004)

Specify JSX code generation: 'preserve', 'react', 'react-jsx', 'react-jsxdev' or'react-native'. Requires TypeScript version 2.2 or later.

The following changes are being made to your tsconfig.json file:
- compilerOptions.jsx must be react-jsx (to support the new JSX transform in React 17)

  • .ts, .tsx 확장자의 아무 파일이나 열고,
  • F1 > TypeScript 검색
  • TypeScript: Select TypeScript Version... 선택
  • 주의 : (타입스크립트 관련 파일이여야 위 명령이 검색된다)

  • Use Workspace Version 을 선택해준다.
.vscode/settings.json
1
2
3
4
{
// (...)
"typescript.tsdk": "node_modules\\typescript\\lib"
}
  • .vscode/settings.json 가보면 이렇게 업데이트 된것을 확인할 수 있다

TMI

상황

  • react typescript eslint airbnb rule 로 개발하는데
  • yarn start로 시작을 하면 자동으로 tsconfig.json의
  • compilerOptions > jsx 가 자동으로 “react-jsx”로 업데이트 됬다
  • 그런데 나는 “react-jsx”일 때 빨간줄이 마구 그어졌다
  • 그래서 yarn start 이후에 수동으로 “react”로 변경해 주었었다…

해결

  • 워크스페이스의 typescript가 사용되지 않아서 발생한 문제였다
  • compilerOptions > jsx 가 “react-jsx” 여도 빨간줄이 안생긴다!

참고

react typescirpt eslint prettier airbnb style guide 적용하기

방법 바로 보기

react typescirpt eslint prettier airbnb style guide 적용 방법은 여기로…
https://github.com/chinsun9/typescript-react-eslint-prettier-practice

TMI

상황

  • 진짜 여러 튜토리얼을 따라해도 뭔가 잘 안됬다
  • 차근차근 따라하기보다 세팅을 빨리 끝마치고 싶은 생각에 복붙을 시전했다
  • 일단 뭔가 쫌 되야 이렇구나 생각할텐데 잘안되서 머리가 아팠다

문제 & 원인

vscode setting.json

  • eslint없이 prettier 를 사용하면서 설정해둔 설정값
  • format on save 이것때문에, 린트 이후에 prettier가 다시 코드를 포맷팅하는 문제
.vscode/setting.json
1
2
3
4
5
6
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": false
}
  • 처음에는 이거를 둘다켜놨엇다
  • 기존에 eslint에서 fix한거 formatOnSave가 다시 prettier 포맷터로 다시 포맷해서 생겼었다

.eslintrc.js

Parsing error: “parserOptions.project” has been set for @typescript-eslint/parser. The file does not match your project config: .eslintrc.js. The file must be included in at least one of the projects provided.

  • 이유를 모르겠으나. .js 방식으로하면 위 오류가 떠서 안됬다
  • 그래서 단순하게 .json방식으로 바꾸었다. 그랬더니 해결됬다…

알게된 것

  • eslint-config-prettier 를 통해 별도 .prettierrc 파일없이
  • eslintrc에서 prettier 설정이 가능하다
  • reportWebVitals.ts 이런 자동생성된 파일은 린트를 적용시킬 필요가없다
  • 파일 최상단에 /* eslint-disable */를 적어 무시해주자
  • 빨간줄이 생기면 희소식이다. 빨간줄 조차 안생기면 eslintrc 파일에 문제가 생긴거다
  • vscode도 재시작해보자
  • “source.fixAll.eslint”: true 설정으로 eslint에서 포맷팅을 실행할 수 있다
  • 쓸모없다고 생각하는 룰은 그냥 꺼버리자
  • 마우스를 올려서 어떤 룰인지 확인하고 .eslintrc > rules 에 해당 룰을 적고 0으로 세팅한다

세팅법 - 여기까지 리액트 프로젝트

https://github.com/chinsun9/typescript-react-eslint-prettier-practice

  • readme.md에 설정하는 방법을 정리해두었다
  • 커밋내역을 보면 명령어 입력 순서가 나와있으니 참고하면 좋을 것 같다

참고

react typescript Prettier 적용하기, Prettier로 여러 파일 한번에 포맷팅하기

settings.json
1
2
3
4
5
6
7
8
9
10
{
// ...
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
  • settings.json에 추가시켜준다
  • 저장할 때마다 자동으로 포맷팅되는 것을 확인할 수 있다

일괄적으로 포맷팅하기

  • prettier 없이 개발했다가 코드 포맷팅을 일괄적으로 하고싶을 때가 있다

.prettierrc
1
2
3
4
5
6
7
{
"singleQuote": true,
"parser": "typescript",
"semi": true,
"printWidth": 120,
"tabWidth": 2
}
  • .prettierrc 파일을 생성한다

npx prettier -w **/*.tsx

  • terminal에 위 명령을 치면
  • prettier가 일회성으로 설치되면서 지정한 확장자에 대해 포맷팅을 일괄적으로 실행해준다
  • .prettierrc prettier config file을 참고해서 실행되니
  • 원하는 디렉터리만, 또는 무시할 디렉터리, 파일들을 설정해서 사용할 수 있다

참고