녕후킴

8월 4주차

0 views

📌 요소의 프로퍼티 가져오기

요소의 높이(clientHeight)나 스크롤 내 위치(offsetTop)에 접근해야만 하는 로직이 존재했다. 리액트에서 useRef로 DOM 요소의 높이 정보에 접근하기 전에, current의 초기값으로 null을 할당하기 때문에 null 체크도 해주어야 했다. 본인은 non null로 처리를 했는데, 팀장님께서 가급적 javascript로 처리를 하라고 피드백주셨다. (개인적으로 이럴때 만큼은 non null을 써도 되지 않나 싶은…)

그러면

useEffect(() => {
  const elementHeight = elementRef.current!.clientHeight
  setElementHeight(elementHeight)
}, [])

이랬던 코드가

useEffect(() => {
  const elementHeight = elementRef.current?.clientHeight

  if (elementHeight && elementHeight !== 0) {
    setElementHeight(elementHeight)
  }
}, [])

null 체크로 인해서 이렇게 길어지게 된다.

길어보이지 않을 수 있지만, 본인이 하고있는 프로젝트에서는 요소 5개의 clientHeight과 offsetTop을 구해야하기 떄문에 코드가 매우매우 길어지게된다. 그래서 코드 가독성을 조금이라도 개선시키고자 isNil을 추가했다.

useEffect(() => {
  const elementHeight = elementRef.current?.clientHeight

  if (isNil(elementHeight)) {
    setElementHeight(elementHeight)
  }
}, [])

위 코드는 원래같았으면 elementHeight에 에러가 발생해야하지만, isNil에 사용자 정의 타입 가드를 사용했기 때문에 에러가 발생하지 않는다. 코드로 본다면 다음과 같다.

type isNil = undefined | null
const isNil = <T>(param: T | isNil): param is T =>
  param !== undefined || param !== null

📌 곪아 터져버린 타입 폴더

페이지별로 *.d.ts로 타입 파일을 만들어 개발해왔다. 언젠가 타입 충돌이 일어날거라고 예상은 했었는데 오늘 터져버렸다. 동료가 개발하면서 선언한 타입이 내가 선언한 타입과 충돌이 일어난 것이다. husky도 없던터라 merge하기전에 문제를 잡지도 못했다.

어떻게 하면 이러한 상황을 예방할 수 있었을까 생각해봤는데, 현재 폴더 구조에서 100% 예방하는 것은 불가능했다. 물론 새로운 타입을 선언할 때마다, 왠지 충돌이 날것 같은 이름을 지을 때마다 한번쯤 확인할 수는 있지만 명확한 기준도 없고 이는 너무 피곤한 일이다.

다만 조금이라도 이러한 상황에 처하지 않기 위해서 FE 팀장님께 타입 폴더를 다음 세가지로 분류하는 것을 제안드렸다.

  1. libraries
  2. pages
  3. apis

그리고 어느 폴더에도 속하지 않는 common.d.ts를 두는 것이다. 여기에는 window 객체에 프로퍼티를 추가했을 때 필요한 타입들이 존재한다.

libraries 폴더의 경우, 현재 족히 5개 이상의 페이지에서 사용중인 구글 맵 라이브러리 혹은 mui 관련 타입을 관리하고, pages 폴더의 경우 클라이언트 상태 혹은 받아온 api에서 재가공된 타입들을 관리한다. 마지막으로 apis는 서버에서 내려주는 순수한 api 관련 타입들을 관리한다.

이렇게 구분한다고해서 타입 충돌을 100% 막을 수있는 것은 아니지만, 빈도는 낮출 수 있으리라 생각되고 FE 팀장님 역시도 흔쾌히 받아들여 주셨다.


📌 Property does not exist on type ‘never’ ts(2339)

최근 작업을 하면서 위 에러를 자주 마주쳤다. 기술 부재로 인해서 매번 타입 단언을 통해서 해결하며 넘어갔는데, 왜 특정 변수를 never 타입으로 인식하는지 궁금해서 찾아보았다.

우선 위 에러가 발생하는 코드는 두가지가 존재하는데, 그중 하나를 stackoverflow 에서 가져왔다.

interface Foo {
  a: 1
}

let instance: Foo | null = null

const mutate = () =>
  (instance = {
    a: 1,
  })

if (!instance) {
  console.log(null)
} else {
  console.log(instance.a) // 🔥 에러 발생 지점
}

첫 번째 예시는 왜 never 타입에 property가 존재하지 않는다고 표시하고 있을까? never 타입은 명시도 안했는데 어디서 튀어나온걸까? 타입스크립트 핸드북과 앞서 언급한 stackoverflow 답변들에서 해답을 얻을 수 있었다.

The never type represents th e type of values that never occur. For instance, never is the return type for a function expression or an arrow function expression that always throws an exception or one that never returns. Variables also acquire the type never when narrowed by any type guards that can never be true.

bold처리된 내용을 해석해보면,

never타입은 절대로 존재할 수 없는 값에 대한 타입이다. (…) 사실일 수 없는 타입 가드(else 문은 절대로 실행되지 않는다.)에 의해서 타입이 좁혀진 경우, 변수는 never타입을 갖게 된다.

else 문은 절대로 실행되지 않는다(사실일 수 없다). 그렇기 때문에 내부 변수가 never 타입을 갖게된다 정도로 해석이 가능한듯 싶다. 아래 두번째 예시가 동일한 에러를 발생하는 이유를 이 관점에서 똑같이 해석할 수 있다.

const instance = {
  bar: null,
}

console.log(instance.bar?.foo) // 🔥 Property 'foo' does not exist on type 'never'

instance.bar는 절대로 사실일 수 없는데 foo 프로퍼티에 접근하려고 하니, never에는 foo 프로퍼티가 존재하지 않는다 정도로 해석이 가능할듯 싶다.