이런 경우를 생각해보자.
상단의 버튼으로 배 이름을 클릭할 때마다 새로운 데이터를 가져온다.
이미지 및 함대 데이터는Suspese
되어 있는 ShipDetails
컴포넌트에서 직접 호출하고
getShip
은 캐시의 역할을 하고 실질적으로 데이터를 요청하는 부분은 getShipImp
다.
만약 유저의 네트워크가 느리다고 가정하자. (월말에 데이터를 다써서 3G로 쓰는 경우가 많은데 네트워크가 느린 경우, 페이지의 기본기가 적나라하게 드러난다고 느꼈다.)
새로운 데이터를 응답받았지만, 잠시동안 이전 img가 보이는 것을 확인할 수 있다.
이유인즉슨 브라우저는 새로운 이미지가 완전히 로드될 때까지 이전 이미지를 유지하기 때문이다.
물론 브라우저 캐시가 잡히면 이런 현상은 사라진다.
아무튼 이런 식으로 새로운 이미지가 다운로드 되기 전까지 Suspense
에 걸리게 하여 opacity를 사용하면 아직 이미지 로드가 덜 됐다는 것을 명확히 보여줄 수 있을 것 같다.
(opacity와 pending 관련해서는 spin-delay
패키지를 사용했다.)
그럼 이를 해결하기 위해 suspense
를 사용하자.
먼저 Img
컴포넌트를 하나 만든다.
Img
컴포넌트 내부에 imgSrc(src)
는 promise
를 반환하는 함수다.
이미지 캐시가 있다면 이미지 캐시를 반환하고 없다면 preloadImage
함수를 호출한다
preloadImage
는 브라우저가 이미지를 실제로 렌더링하기 전에 네트워크 요청을 통해 해당 이미지를 캐싱하는 방식이다.
즉, imgSrc(src)
에서 반환된 promise 이미지 객체를 use(imgSrc(src))
에서 사용하게 되는데, 이는 suspense
의 트리거로 작동한다.
다시 확인해보면 버튼 클릭시 이미지가 완료되기 전까지는 suspense에 걸리는 것을 확인할 수 있다.
loading UI를 보여주는 것보다 suspense를 활용하여 데이터를 보여주는 것도 좋은 방법!
ErrorBoundary
를 ShipDetails
전체를 감싸게 되면서 에러가 발생하면 위와 같은 에러 UI가 렌더링된다.
ErrorBoundary
의 장점은 특정 컴포넌트만 에러 바운더리를 지정할 수 있다는 것이다
가령, img 요청이 잘못되어 Img
컴포넌트에만 에러가 발생하고 나머지 데이터는 잘 들어왔다고 가정하자
그렇다면 전체 에러 UI를 띄울 필요는 없을 것이다(물론 이건 프로덕트 정책에 따라 다를 것이다)
그렇다면 부분적으로 ErrorBoundary
적용해보자
기존의 <Img/>
컴포넌트를 맵핑하는 ShipImg
컴포넌트를 만든다.
그리고 ShipImg
를 사용하기만 하면 끝!
이제 이미지 로드에 실패해도 이미지를 제외한 나머지 데이터는 화면에 잘 나온다!
최상단 버튼을 눌러서 새로운 전함(?) 데이터를 가져온다고 하자 (느린 네트워크 환경에서)
우리는 여기서 waterfall을 발견할 수 있다. 전함 데이터는 모두 다운로드가 되었는데도 이미지가 아직 다운로드 중이라 Suspense가 pending 상황이 된 것이다.
(영상이 너무 빠르다면 사진으로 설명)
get-ship
이 모두 다운 받고도 webp
가 다운로드 중이라 화면은 여전히 로딩 상태인 것! 이것은 크나큰 낭비다!
데이터가 다 받아졌다면 데이터를 먼저 보여주고, 차례대로 이미지를 보여준다면 더 낫지 않을까?
Suspense
에 key
를 주면서 해결이 가능하다.
먼저 ShipImg
에 Supense를 추가한다. 하지만 변화는 없을 것이다. 왜냐하면 추가한 Suspense
내부에 <Img/>
는 여전히 부모의 서스펜딩에 속하기 때문이다.
이제 key
를 통해 새로운 바운더리를 만들어줘야 한다.
이제 props.src
가 바뀔 때마다 기존의 에러 바운더리를 제거하고 에러 바운더리에 속한 모든 자식들을 새로운 에러 바운더리에 넣는다.
이제 원하는대로 get-ship
api가 끝나면 Detail에 정보가 먼저 나오고 Img
는 fallback이 된다.
그리고 img 다운로드가 끝나면 이미지가 제대로 나오게 된다.
참고 : epic-react