(TypeScript 공부 중)
어제는 강의자료로 주어진 ‘포켓몬 도감’ 코드에서
- 카드 뒷면에 해당 포켓몬의 다른 이미지 보여주기
- 포켓몬의 여러 다른 이미지 버전 중 선택할 수 있도록 배열로 만들어 놓음
- 원래 뒤죽박죽으로 나오던 것을 포켓몬 ID 1번부터 차례로 나열되도록 바꿈
과 같은 수정을 하였다.
오늘은 “왜 asycn/await fetch()로 문서 요소를 동적으로 추가하는 것이 완료되기 전에 document.querySelector 등이 호출되어 버리는 걸까”를 고민하다가 Core Web Vitals와 웹 페이지 성능, 로딩 프로세서 우선순위 조정하기 같은 프론트 개념들만 잔뜩 접하게 되었다. (타입스크립트 미안…)
심지어 DOMContentLoaded와 load 이벤트 리스너까지 걸어주었는데도 동적 요소가 다 만들어지기 전에 document.querySelectorAll을 호출하고 개수가 부족한 배열을 반환하고 끝내버린다. 실마리를 타고 타고 동적 렌더링과 puppeteer까지 왔는데 실험은 아직이다.
(에러 템플릿 추가)
에러 TimeoutError: Waiting failed: 50000ms exceeded - ✔️
에러 TimeoutError: Waiting failed: 50000ms exceeded - ✔️
발생 상황: puppeteer로 브라우저 페이지를 열어 내 로컬 파일에 접속하려고 한다.
// 크로미움으로 브라우저를 연다.
const browser = await puppeteer.launch();
// 페이지 열기
const page = await browser.newPage();
// 링크 이동
// await page.goto("http://127.0.0.1:5555");
const pokemonDictPathFull = "file:\\\\C:\\Users\\USER\\Desktop\\Sparta\\03.1_typescript_practice\\Pokemon\\06_index.html"
await page.goto(pokemonDictPathFull);
// .card 엘리먼트중에 값이 #100인 .card--id 엘리먼트가 생길때까지 기다림
await page.waitForFunction(
() =>
document.querySelector(".card:last-child .card--id").textContent ===
"#100",
{ timeout: 5000 }
);
에러 메세지 전문:
TimeoutError: Waiting failed: 50000ms exceeded
at Timeout.<anonymous> (C:\Users\USER\Desktop\Sparta\03.1_typescript_practice\node_modules\puppeteer-core\lib\cjs\puppeteer\common\WaitTask.js:64:32)
at listOnTimeout (node:internal/timers:564:17)
at process.processTimers (node:internal/timers:507:7)
TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received undefined
at Object.writeFile (node:fs:2168:5)
at C:\Users\USER\Desktop\Sparta\03.1_typescript_practice\Pokemon_nodeJs_to_typescript\07_pokemon_app.js:74:8 {
code: 'ERR_INVALID_ARG_TYPE'
}
원인: await page.goto()에 들어가는 URI 경로 부분에서 에러가 난다. ⇒ 아니다. page.waitForFunction 부분에서 타임아웃 에러가 난 것이다.
시도: await page.goto()에 들어가는 URI 경로를 여러가지로 바꿔 시도해보았다.
const pokemonDictURL3 = "http://localhost:63342" // 실패
const pokemonDictURL2 = "http://localhost:63342/03.1_typescript_practice/Pokemon/06_index.html?_ijt=66ccaeonhhf2cofel504p57d5m" //
const pokemonDictURL1 = "http://localhost:63342/03.1_typescript_practice/Pokemon/06_index.html?_ijt=h96cj73p98j09apg88e0nt10m4&_ij_reload=RELOAD_ON_SAVE"
- 시도 1: pokemonDictURL1, 2 ⇒ “Page “http://localhost:63342/…” requested without authorization” 메세지와 함께 실패
- 시도 2: pokemonDictURL3 ⇒ 이것은 인식을 못할 줄 알았다. 예상대로 실패.
- 시도 3: 그리고 검색해서 알게 된 방법인 ‘file://{파일_절대경로}’도 써보았지만 똑같이 인식을 못한다.
- 왜 안되는지 모르겠다. 다른 사람들은 이 방법으로 잘만 됐다던데… https://stackoverflow.com/questions/47587352/opening-local-html-file-using-puppeteer
- 시도 4: page.goto() 대신 page.setContent() 사용 ⇒ 실패
- 시도 5: 아예 유효하지 않은 URL이라면 ProtocolError가 난다.
(1/20 금 TIL에서 계속)
에러 ProtocolError: Protocol error (Page.navigate): Cannot navigate to invalid URL - (우회 해결)
에러 ProtocolError: Protocol error (Page.navigate): Cannot navigate to invalid URL - (우회 해결)
발생 상황: page.goto()에 유효하지 않은 url (localhost:63342 부분)을 인수로 제공함.
const pokemonDictPathFull = "file://localhost:63342/C:\\Users\\USER\\Desktop\\Sparta\\03.1_typescript_practice\\Pokemon\\06_index.html"
const pageMoved = await page.goto(pokemonDictPathFull);
에러 메세지 전문:
ProtocolError: Protocol error (Page.navigate): Cannot navigate to invalid URL
at C:\Users\USER\Desktop\Sparta\03.1_typescript_practice\node_modules\puppeteer-core\lib\cjs\puppeteer\common\Connection.js:329:24
at new Promise (<anonymous>)
at Frame.goto (C:\Users\USER\Desktop\Sparta\03.1_typescript_practice\node_modules\puppeteer-core\lib\cjs\puppeteer\common\Frame.js:207:13)
at CDPPage.goto (C:\Users\USER\Desktop\Sparta\03.1_typescript_practice\node_modules\puppeteer-core\lib\cjs\puppeteer\common\Page.js:439:91)
at scrape (C:\Users\USER\Desktop\Sparta\03.1_typescript_practice\Pokemon_nodeJs_to_typescript\07_pokemon_app.js:35:34)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
원인:
시도:
해결: :63342부분을 빼고 url을 주면 일단 이 에러는 잠잠해진다.
⇒ 로컬 파일에서 불러오는 방법은 냅두고 일단 서버를 켜서 인터넷으로 접근하는 방법을 사용하여 해결함.
에러 ReferenceError: document is not defined ✔️
에러 ReferenceError: document is not defined ✔️
발생 상황:
const pokemonDictURLTest = "https://pokeapi.co/api/v2/pokemon"
const pageMoved = await page.goto(pokemonDictURLTest);
console.log("3. after goto(): ", pageMoved) // HTTPResponse {}
console.log("3. page url: ", page.url()) // https://pokeapi.co/api/v2/pokemon
console.log(document.querySelector(".card:first-child .card--id").textContent)
에러 메세지 전문:
ReferenceError: document is not defined
at scrape (C:\Users\USER\Desktop\Sparta\03.1_typescript_practice\Pokemon_nodeJs_to_typescript\07_pokemon_app.js:39:17)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
원인: Node.js는 브라우저 환경이 아니고, document 변수가 소속된 DOM은 웹 브라우저에서 쓰이는 것이므로 그냥 백엔드 app.js파일에서 저렇게 쓰면 document 변수를 찾을 수 없는 게 당연한 것이었다.
힌트 조각모음:
- Next JS 같이 서버 사이드 렌더링을 사용하면 브라우저가 없으므로 window나 document 변수 또한 없다. ⇒ 동적 렌더링(dynamic rendering)으로 바꿔줘야 함
- Next JS ⇒ dynamic() 메소드 사용
- React ⇒ useEffect() 메소드 사용
- 다른 서버사이드 렌더링 라이브러리들 ⇒ componentDidMount 사용?
- DOM 이 완성되지 않은 상태더라도 document 글로벌 변수는 항상 존재한다고 함. 이 말에 따르면 document/window.addEventListner(’DOMContentLoaded’/’load’, …)로 document를 찍어봐도 … 아니다.
- puppeteer가 바로 동적 렌더리을 지원한다고 한다..!
시도:
- 시도 1: page.evaluate() 사용 ⇒ 실패.
await page.evaluate(() => { console.log(document) }); // => ReferenceError: document is not defined
해결: 1/20일자 TIL에서 해결함.
→ puppeteer를 사용하면 일단 headless ‘브라우저’를 그리게 되므로 document 변수 등이 존재하게 됨.
→ 페이지를 다 그리도록 기다렸다가 document 등을 불러와 쓸 수 있도록 도와주는 page.evaluate() 같은 메소드를 사용하면 document가 undefined 등이 되지 않을 수 있음.
⇒ 거기에 더해 처음 puppeteer.launch로 browser를 만들 때 옵션을 설정해주면 headless 브라우저에서만 존재하던 console.log 등을 내 백엔드 터미널로 가져올 수 있게 됨.
const browser = await puppeteer.launch({
dumpio: true // 이 설정이 없으면 page.on('console')을 등록해도 작동하지 않는다.
})
IntelliJ IDE에서 live-server 사용하기 초간단 버전
IntelliJ IDE에서 live-server 사용하기 초간단 버전
출처: https://developer-kade.tistory.com/136
npm 명령어로,
npm install -g live-server
이렇게 글로벌 설치를 해주고 아무 프로젝트 폴더로나 들어가서 다시
live-server
라고 입력해주기만 하면 끝!
- 루트 디렉토리에서 live-server 명령어 실행시 모습 :
- 브라우저에서 localhost:8080으로 접속한 모습
DOMContentLoaded와 onload 이벤트보다도 늦는 fetch() ? (스압주의)
DOMContentLoaded와 onload 이벤트보다도 늦는 fetch() ? (스압주의)
이것 외에는 저 두 이벤트 리스너 안에 찍은 document.querySelectorAll() 에 100개가 들어와야 하는 것이 아예 들어오지 않거나 1개, 5개씩 들쭉날쭉하게 들어오는 이유를 설명할 방법이 없다.
후….
브라우저의 동작 순서
- HTML을 읽기 시작한다.
- HTML을 파싱한다.
- DOM 트리를 생성한다.
- Render 트리(DOM tree + CSS의 CSSOM 트리 결합)가 생성되고
- Display에 표시한다
중에 onload 이벤트는 4번 이후에 격발되는 걸로 알고 있는데, 그렇다면 fetch()가 이보다도 늦어진다는 얘기가 된다.
// 06_pokemon_crawling.ts
...
const getPokemon = async (id: number): Promise<void> => {
const data: Response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const pokemon: any = await data.json();
...
}
바로 요 부분에서 fetch()가 CSS
Fetch 조사중…. fetch 예제:
// fetch() 예시:
fetch('http://example.com/movies.json')
.then((response) => response.json())
.then((data) => console.log('성공:', data));
.catch((error) => console.error('실패:', error));
크롬 개발자 도구의 “우선순위 힌트(Priority Hints)”란:
바로 이 부분을 말한다.
브라우저가 이미지, 스크립트, CSS 같은 리소스를 검색하고 다운로드할 때 이를 최적의 순서로 다운로드하기 위해 각 리소스에 부여하는 ‘우선순위’가 있다. 이렇게 우선순위를 찾은 순서에 따라(=동일한 우선순위라면 발견한 순서대로) 리소스들이 다운로드 된 것을 시각적으로 보여주는 게 바로 저 ‘폭포(Cascade, Priority Hints)’ 파트인 것이다.
이를 통해 어느 리소스가 가장 먼저 다운로드 되었는지 살펴볼 수 있다.
Core Web Vitals
: 구글에서 제공하는 웹페이지 성능 측정 ‘기준’.
- Largest Contentful Paint(최대 콘텐츠풀 페인트, LCP): 로딩 성능을 측정합니다. 우수한 사용자 경험을 제공하려면 페이지가 처음으로 로딩된 후 2.5초 이내에 LCP가 발생해야 합니다.
- ⇒ 페이지의 메인 콘텐츠가 로드된 시점을 식별하고 싶어서
- 가장 큰 요소가 렌더링된 시기를 측정한다.
= 페이지가 처음으로 로드를 시작한 시점 ~ 뷰포트 내에 있는 가장 큰 이미지 또는 텍스트 블록이 렌더링된 시점
- First Input Delay(최초 입력 지연, FID): 상호 작용을 측정합니다. 우수한 사용자 경험을 제공하려면 페이지의 FID가 100밀리초 이하여야 합니다.
- Cumulative Layout Shift(누적 레이아웃 시프트, CLS): 시각적 안정성을 측정합니다. 우수한 사용자 경험을 제공하려면 페이지에서 0.1 이하의 CLS를 유지해야 합니다.
페이지 로드의 75번째 백분위수가 위의 세 가지 메트릭 모두에서 권장 목표를 충족하면 Core Web Vitals 기준을 통과한다고 본다.
Core Web Vitals를 측정하는 방법에는 JavaScript에서 web-vitals 패키지를 사용하여 직접 작성하는 것과 이를 이용한 크롬 확장 프로그램 Web Vitals를 이용하는 방법 등이 있다.
더 자세한 내용은 아래 링크 참조:
<link rel="preload">
를 사용하여 로딩 프로세서의 속도를 높이는 방법
예시:
내가 개발자로서 저 Pacifico 폰트 파일이 중요한 리소스이고 로딩이 오래 걸림을 알고 있다면 얘를 브라우저가 발견할 때까지 기다리지 않고 미리 로딩하고 있으라고 명령하고 싶을 수 있다.
구현 방법:
<head>
<link rel="preload" as="script" href="critical.js">
</head>
- as 속성에 허용되는 값은
script
,style
,font
,image
등이다.
as
속성을 생략하거나 유효하지 않은 값을 갖는 것은 XHR 요청과 같다고 한다. 여기서 브라우저는 무엇을 가져오는지 알지 못하므로 올바른 우선순위를 판단할 수 없고, 스크립트와 같은 일부 리소스를 두 번 가져올 수도 있게 된다.
- 그 밖에 폰트나 CSS 파일을 preloading 할 때 주의점들이 있다.
- 브라우저는 사전 로드된 파일을 실행하지는 않는다. (스크립트를 ‘실행’하거나 스타일시트를 ‘적용’하지 않는다.)
⇒ 내가 원하는 건 스크립트 안의 fetch() 메소드를 더 일찍 ‘실행’시키는 것이므로 이 preload 기법은 무용하다.
- preload는 브라우저에게 강제성을 부여하므로, 최대한 적게 사용하는 것이 좋다. → 사용되지 않은 프리로드는
load
이벤트 후 약 3초 후에 Chrome에서 콘솔 경고를 트리거한다.
출처: https://web.dev/preload-critical-assets/#javascript-파일-사전-로딩
rel=prefetch
이건 뭘까?
fetch()를 미리 시작하게 만들어 주는 것 같지는 않다. 그냥 HTML 페이지를 미리 가져온다는 의미에서 prefetch라고 하는 듯.
우선순위 힌트로 리소스 로딩 최적화하기
이곳을 읽고 있다. 다시 읽는다면 웹 페이지 로딩 최적화에 대해 많은 기법과 개념을 배울 수 있을 것이다. 내가 원하는 “fetch()를 더 빨리 완료하거나 마지막 fetch()가 다 될 때까지 기다렸다가 document.addEventListener(”load”, …)등을 실행시키기” 같은 방법을 알려주지는 않는 것 같아 일단 지나간다.
Event Delegation
동적으로 추가된 element에 접근하기 위해서는 event delegation(이벤트 위임)이라는 걸 해야한다고 한다.
예를 들면 원하는 (동적으로 추가될) element에 어떤 클래스 속성을 붙여두고 그 클래스명을 가진 element가 클릭됐을 때에만 이벤트가 실행되도록 한다:
document.body.addEventListener('click', func);
const func = (e) => {
if (e.target.className === 'example') {
console.log('element accessed!'};
}
참고중: https://stackoverflow.com/questions/64854551/find-element-on-the-page-after-fetch-js
검색 키워드: “access to dom element after last fetch() method complete”
⇒ puppeteer가 동적 렌더링을 지원해줘서, ‘브라우저(페이지) 컨텍스트에서’ 쿼리셀렉터나 함수를 실행해준다… 는 부분까지 접근했다. 아직 실험은 안 해봤고, 미심쩍은 부분들이 남아있다.
puppeteer
: 구글에서 만든 노드 라이브러리. Headless Chrome 또는 Chrominum을 제어할 수 있다.
(여기 보는 중)
Uploaded by N2T