Showing posts with label Typescript. Show all posts
Showing posts with label Typescript. Show all posts

expo custom plugin 좌충우돌 typescript 에러

 expo 프로젝트에서 android manifest 파일을 수정해야할 일이 있었다.

그래서 custom plugin 을 만들어보기로 했다.


plugin.ts를 만들어 app.json plugins에 넣으면 에러가 난다. typescript를 제대로 인식하지 못하는 것이다.

expo 기본 프로젝트에서 생성되는 tsconfig.json 파일은 expo/tsconfig.base 이 파일을 상속하는데,

"compilerOptions" 에 "module": "preserve"라고 되어 있다.

이것때문에 루트에 존재하는 app.json에서 해당 ts 파일을 불러올수가 없다.

이것을 "commonjs"로 변경하면 잘 되긴 하는데, 영 찝찝하다.

그래서 prebuild만을 위한 tsconfig.plugin.json 파일을 따로 생성해주고

app.config.ts를 생성해주고 그 상단에서 해당 파일은 tsconfig.plugin.json을 사용하도록 처리해주면 된다.


tsconfig.plugin.json

{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"allowJs": true,
"customConditions": null
},
"include": ["app.config.ts", "plugins/**/*.ts"]
}


app.config.ts

import { register } from "ts-node";
register({ project: "./tsconfig.plugin.json" });

import { ExpoConfig } from "expo/config";

export default ({ config }: { config: ExpoConfig }) => {
config.plugins?.push("./path/to/plugin/ts");
return config;
};


이렇게 해주면 해결된다.



** 문제점 발견

이렇게 설정을 해주면 expo install을 할때 문제가 발생한다. 아마 expo install을 하는 과정에서 app.json을 건드리게 되는데 이게 app.config.ts까지 타게 되는데 라이브러리를 설치할때는 commonjs가 아니라 기존의 preserve 로 동작해야하나보다. 


그때는 귀찮지만, app.config.ts의 파일이름을 잠깐 다른걸로 변경해두면 된다.


좀더 엘레강스한 해결법은 못찾았다.

루프문 최적화 (역행루프)

```
for (var i = 0; i < array.length; i++) {}
```

보다

```
var length = array.length
for (var i = length; i >=0; i--) {}
```

가 더 빠르다고 한다.


첫번째이유
매번 array.length를 계산하지 않는다. (지역변수에 할당)

두번째 이유
i를 0 (false)랑 비교하기 때문에 속성검색을 최소화 할 수 있다.

[React-Query] useInfinteQuery 사용시 주의사항 (무한스크롤)

리액트 무한 스크롤 구현

  1. 무한 스크롤 구현방법에는 2가지가 있다.

    1. scroll 이벤트를 통해 페이지 마지막부분으로 scroll이 되었을경우 새로운 page를 fetching하는 방법.

    2. 페이지 마지막 부분에 div태그를 두고 Intersection Observer를 통해 해당 div가 화면에 보일때 새로운 page를 fetching 하는 방법

    두가지 방법의 차이점과 어느게 더 좋은지에 관한것은 다른 블로그에도 많으니 생략하겠다.



  2. 리액트에서 Intersection Observer를 편하게 사용하기 위한 라이브러리가 있다.

    react-intersection-observer를 사용해보자.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import React from 'react';
    import { useInView } from 'react-intersection-observer';
     
    const Component = () => {
      const { ref, inView, entry } = useInView({
        /* Optional options */
        // threshold: 0,
        // rootMargin:"50px"
      });
     
      return (
        <div>
            <ul>
                .......
            </ul>
            <div ref={ref}>
                <h2>{`Header inside viewport ${inView}.`}</h2>
            </div>
        </div>    
      );
    };
    cs

    ref를 페이지 마지막 div에 넣고 해당 div가 뷰포트 안에 들어오면 inView 값이 true가 되는 방식이다.



  3. useInfiniteQuery를 사용해보자.

    이제 inView값이 true 일때 useInfiniteQuery가 다음 page를 Fetching 하게 되면 된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    ...
    import { useInfiniteQuery } from "@tanstack/react-query";
     
     
    const Component = () => {
        ....
     
        const { isFetchingNextPage, fetchNextPage } = useInfiniteQuery(['query-key'], fetchFc, {
            enabled: false// 자동으로 fetch가 되는것을 방지하고 오직 fetchNextPage함수에 의해서만 fetch 되도록 하기 위해
            getNextPageParams: (lastPage, allPages) => lastPage.page < lastPage.totalPage ? lastPage.page + 1 : undefined,
            // 마지막 페이지가 아닐경우 다음 페이지 넘버를 반환하고 마지막 페이지일 경우 undefined를 반환한다.
        });    
     
        useEffect(() => {
            if (inView && !isFetchingNextPage) {
                fetchNextPage()
            }
        }, [inView, isFetchingNextPage])
     
        return ...
    }
    cs

    fetchFn에 pageParam이 전달된다.

    1
    2
    3
    4
    const fetchFn = ({ pageParam = 1}) => {
        return fetch(`/api/getsomething?page=${pageParam}`)
            .then(response => response.json())
    }
    cs

    하지만 가장처음 호출될때는 undefined가 전달된다. 한번도 호출된적이 없기 때문에 getNextPageParam에서 사용될 lastPage가 없기 때문이다. 그래서 default값으로 1을 넣어준다. 참고

    이 방법에는 문제점이 있다.

    실제로 fetchNextPage가 실행되는 순간 isFetchingNextPage의 값이 true가 되기전에 바로 fetchNextPage가 또한번 더 실행된다. fetchNextPage가 비동기함수로써 실행되기까지 시간이 걸리기 때문이다. (isFetchingNextPage 값이 true가 되기까지 시간이 걸린다. 그전에 또다시 fetchNextPage가 실행된다)



  4. useState를 통해 상태를 관리해보자.

    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
    ...
    import { useState} from 'react';
    import { useInfiniteQuery } from "@tanstack/react-query";
     
     
    const Component = () => {
        ....
        const [data, setData] = useState([]);
     
        const [isLoading, setIsLoading] = useState(false);
     
        const { fetchNextPage } = useInfiniteQuery(['query-key'], fetchFc, {
            ... 
            onSuccess: (newData) => {
                setData(newData.pages.map(pages.results).flat());
                setIsLoading(false);    // 다시 fetchNextPage가 가능할 수 있도록 해준다.
            }
        });    
     
        useEffect(() => {
            if (inView && !isLoading) {
                setIsLoading(true);
                fetchNextPage();
            }
        }, [inView, isLoading])
     
        return ...
    }
    cs

    위의 경우에는 fetchNextPage가 동시에 여러번 수행되지는 않는다. 하지만 fetch가 끝나고나서 setIsLoading(false)가 호출되는 순간 실제로 화면에 다시 렌더링되면서 화면에 새로운 li 엘레멘트들이 paint되기전에 inView가 트리거되어 또한번 fetchNextPage가 호출된다. 즉, 의도치 않은 fetching이 또 일어난다.



  5. useRef를 통해 상태관리를 해보자.

    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
    ...
    import { useState} from 'react';
    import { useInfiniteQuery } from "@tanstack/react-query";
     
     
    const Component = () => {
        ....
     
        const isLoading = useRef(false);
     
        const { fetchNextPage } = useInfiniteQuery(['query-key'], fetchFc, {
            ... 
            onSuccess: (newData) => {
                ...
                isLoading.current = false;    // 다시 fetchNextPage가 가능할 수 있도록 해준다.
            }
        });    
     
        useEffect(() => {
            if (inView && !isLoading.current) {
                isLoading.current = true;
                fetchNextPage();
            }
        }, [inView, isLoading])
     
        return ...
    }
    cs

    이제 깔끔하다. 원하는대로 infinite scroll이 구현되었다.

[JS, TS] self-compare 자기자신비교(?)

리액트에서 불변성에 대해 알아보다가,

shallowEqual 코드를 살펴보고 있었다.

https://github.com/facebook/react/blob/v16.8.6/packages/shared/shallowEqual.js


그중 is라는 함수를 좀더 살펴봤는데

https://github.com/facebook/react/blob/v16.8.6/packages/shared/objectIs.js


그중에 눈에 띄는 코드가 있었다.

1
(x !== x && y !== y) // eslint-disable-line no-self-compare
cs


x !== x 라는 코드는 항상 false라고 생각했기 때문에 

if 조건절안에서 의미없는 코드라고 생각했다.

사실 거의 대부분 false이다.


https://eslint.org/docs/latest/rules/no-self-compare

위 docs를 살펴보니,

값이 NaN일 경우엔

1
2
3
let a = NaN
=== a // false
!== a // true
cs

이렇다고 한다.


하지만 가독성을 위해선 아래와 같이 더 좋은방법이라고 한다.

1
typeof x === 'number' && isNaN(x)
cs


[nestJS] DTO 클래스와 interface 인터페이스 차이

 nestJS를 하다보면 파라미터 타입지정을 DTO 클래스로 한다.

1
2
3
4
5
6
7
8
9
10
11
12
export class MoviesController {
 
  ...
 
  @Post()
  create(@Body() movieData: CreateMovieDto) {
      return this.moviesService.create(movieData);
  }
 
  ...
 
}
cs

여기서 CreateMovieDto 부분을 살펴보면 아래와 같다.

1
2
3
4
5
export class CreateMovieDto {
  readonly title: string;
  readonly year: number;
  readonly genres: string[];
}
cs


문득 드는 생각은 그냥 interface를 사용하면 안되나? 였다.

1
2
3
4
5
export interface ICreateMovie {
    title: string;
    year: number;
    genres: string[];
}
cs


결론부터 말하면 사용하는건 문제는 없다.

하지만 아래 두가지 이유로 DTO 클래스를 사용한다.

  1. Typescript는 실제로 컴파일될때 ES6 Javascript로 변환되는데 그때 interface는 사라진다. interface는 코딩하는 과정에서 도움을 줄 뿐 실제로 동작하는데는 영향을 미치지 않는다. 

  2. 파이프를 이용한 데이터 validation을 하기 위해서는 DTO 클래스를 써야한다.


실제로 github에서 이와 같은 논쟁이 있다.

https://github.com/nestjs/nest/issues/1228

공식홈페이지에서도 DTO 사용을 추천한다.