[ KISA ] WEB TECH SEMINAR
Next.js를 활용한 검색 최적화 대응 웹사이트 구축하기
한국인터넷진흥원에서 진행하는 웹 개발 교육 과정중 '리액트' 기반의 프레임워크인 Next.js 강좌가 있어 신청하였고,
배움의 기회가 주어져 수강하고 왔다. 나의 부족한 배경지식으로 수업에 대한 이해도가 부족했지만
감사하게도 녹화본을 공유해주셔서 복습겸 이렇게 정리할 수 있는 시간을 마련해보았다.
Next.js란
Next.js는 웹 어플리케이션 풀 스택(프론트엔드와 백엔드)을 만들기위한 React framework(리액트 프레임워크)다.
Next.js는 리액트 기반의 프레임워크다. 사용자 인터페이스를 만들기 위해 리액트 컴포넌트를 사용하고,
추가적인 기능과 최적화를 위해 Next.js를 사용합니다.
Next.js 사용 환경
Next.js는 Node.js 환경에서 작동한다.
Node.js가 없다면 학습차원에서는 stackblitz 사이트 이용만으로 가능하다.
JSX란
Javascript와 XML 두 개가 혼합된 형태, 리액트에서 사용하는 기본 문법이자 파일 형식이다.
자바스크립트에서는 파일 확장자명이 .js .jsx 두 가지를 사용하는데 보편적으로 .js로 통합되는 분위기고,
타입스크립트에서는 .ts .tsx가 있는데 .tsx만 사용한다.
JSX는 클래스 컴포넌트, 함수 컴포넌트로 나뉘어진다.신규로 작성하는 경우 거의 함수 컴포넌트로 작성한다.
함수 컴포넌트의 JSX파일은 두 개의 파트로 나눌 수 있다.자바스크립트 파트와 XML파트, 함수의 반환 값으로(return ();) XML 파트가 만들어진다.반환되는 XML자체를 JSX Node라고 부른다. 해당 함수 컴포넌트가 호출되면 XML 파트에 작성한 것이 JSX Node로 반환되어 html의 일부로 구성되거나 html 자체가 되는 것 같다. 그리고 html에서 class라는 속성은 XML 파트에서 className으로 작성해야 한다. 자바스크립트 파트내에서 class라는 예약어와의 충돌을 피하기 위해 XML 파트에서 class를 className이라고 사용하게 된 것. 웹 어플리케이션 한정으로 XML 파트는 html이 작성되는 곳이라고 생각해도 될 듯. 또 XML 파트에서 코드 작성에 유의해야할 부분은 태그다. 태그는 반드시 닫는 태그와 함께 사용되어야 한다. XML 파트에서 중괄호{}를 사용한다면 자바스크립트 언어가 작성된다는 것이다. 중괄호{}를 이용해서 자바스크립트 표현을 나타내는 이유는 자바스크립트 파트에서 선언한 상수 또는 연산된 값을 XML 파트에서 출력하기 위해서다. XML 파트 안에 중괄호{} 기호를 사용함으로써 자바스크립트를 표현할 수 있기 때문에 JSX라고 할 수 있는 것.
코드 작성할 때
JSX에서 인라인 style을 작성할 때 문자열로 이루어진 값을 지원하지 않는다.
반드시 object(객체) 형식으로 작성해야 한다. 예를 들면
// 자바스크립트 파트
const inlineStyle = {textDecoration: 'underline'};
// XML 파트
return (
<main
className={'flex min-h-screen flex-col items-center justify-between p-24'}
// XML 파트에서 중괄호{} 기호 내부는 자바스크립트식으로 작성할 수 있는 곳
// 인라인 스타일을 작성할 때는 반드시 객체로 값을 작성해야 한다.
// 자바스크립트식으로 표현된 인라인 스타일 값{} 그리고 그 내부의 중괄호는 객체를 의미
// style={{textDecoration: 'underline'}}
style={inlineStyle}
>
Hello, Next.js
{key}
</main>
);
순수하게 콘텐츠나 스타일을 작성할 때는 XML 파트에서 진행, 상태 등 동적인 요소를 다룰 때는 자바스크립트 파트에 작성한다.
Next.js를 사용하는 가장 큰 장점은 서버 사이드 렌더링에 대한 부분
리액트로 만들어진 웹 페이지의 렌더링은 프론트단에서만 진행되기 때문에
화면에 보여지는 내용이 소스코드상에 반영되지 않아 검색엔진이 페이지의 정보를 수집하기 어렵다.
Next.js로 만들어진 웹 페이지는 서버단에서 렌더링을 한 뒤에 프론트단으로 전달하기 때문에
검색엔진 최적화에 불리한 리액트의 단점이 보완된다.
RSC(React Server Component) 리액트 서버 컴포넌트
- (리액트) 서버 컴포넌트: 서버에서만 처리되는 컴포넌트, 서버에서만 동작을 하고, 클라이언트단에서는 단순하게 HTML로 표현된다.
- 클라이언트 컴포넌트: 서버에서 브라우저단으로 정보들이 전송되고, 브라우저에서 자바스크립트가 동작할 때 그 때 동작하는 컴포넌트
Next.js에서는 모든 컴포넌트를 기본적으로 서버 컴포넌트로 동작하게 만들었다.
클라이언트 컴포넌트는 시작줄에 "use client" 선언으로 구분한다.
서버 컴포넌트와 클라이언트 컴포넌트의 가장 큰 차이는
서버 컴포넌트는 async function로 선언할 수 있지만
클라이언트 컴포넌트는 async function로 선언할 수 없다.
그래서 기본적으로 서버 컴포넌트는 async로 선언하고,
클라이언트 컴포넌트는 일반 함수로 선언하여 구분하고 있다.
서버 컴포넌트는 정적인 컨텐츠를 담거나 서버에서 수행해야 하는 작업들을 담고,
클라이언트 컴포넌트는 사용자 인터랙션이 필요한 내용을 담는다.
***유의할 규칙***
1. 서버 컴포넌트 안에 클라이언트 컴포넌트가 들어갈 수 있지만
2. 클라이언트 컴포넌트 안에 서버 컴포넌트가 들어갈 수 없다.
코드 실습
import { NewComponent } from './NewComponent';
// 서버 컴포넌트는 async function으로 선언할 수 있어
// async 키워드로 클라이언트 컴포넌트와 서버 컴포넌트를 구분한다.
export default async function Home() {
// 자바스크립트 파트
// 서버 컴포넌트이기에 터미널에 콘솔이 출력된다.
// 웹 브라우저의 콘솔창에는 출력되지 않는다.
console.log('Home Component');
const inlineStyle = { textDecoration: 'underline' };
// XML 파트
return (
<main
className={'flex flex-col min-h-screen items-center justify-between p-24'}
style={inlineStyle}
>
<div>접속 시각: { new Date().toISOString()}</div>
<NewComponent />
</main>
); // JSX Node
}
'use client'; // 클라이언트 컴포넌트임을 선언
export function NewComponent() {
// 클라이언트 컴포넌트이기 때문에 웹 브라우저의 콘솔창에 출력된다.
console.log('NewComponent');
const [date, setDate] = useState(new Date());
useEffect(() => {
setInterval(()=>{
setDate(new Date());
}, 500);
}, []);
return <>현재 시각: {date.toISOString()}</>
}
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
export default function Template({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
useEffect(() => {
// 클라이언트 단에서 앱 초기화 시점에 실행해야 할 내용
// axios.defaults
}, []);
return <>{children}</>;
}
코드에서 정리해야할 부분
1. Date 객체: 자바스크립트 내장 객체(자바스크립트에서 Date 객체는 빌트인으로 존재한다)
Date 객체는 new 연산자와 Date() 생성자의 조합으로 Date 객체가 생성된다 → new Date();
***Date() 생성자에 전달하는 인자가 없다면 현재 날짜와 시간이 저장된 Date객체가 반환된다.
→ new Date().toString(); // Tue Nov 12 2024 09:51:19 GMT+0900 (한국 표준시)
→ new Date().toISOString(); // 2024-11-12T00:50:51.999Z
생성된 Date객체는 날짜를 저장할 수 있고, 날짜와 관련된 메서드도 제공해준다.
Date객체는 1) 현재 날짜를 출력하는 용도 2) 생성 및 수정시간을 저장 3) 시간 측정 등에 활용된다.
new Date( milliseconds )
new Date(0); 이 코드는 UTC 기준 1970년 1월 1일 0시 0분 0초에서
milliseconds 밀리초(1/1000 초) 후의 시점이 저장된 Date 객체가 반환된다고 한다.
웹 브라우저상의 콘솔창에 new Date(0); 을 작성해봤는데 'Thu Jan 01 1970 09:00:00 GMT+0900 (한국 표준시)' 이 값이 반환됐다.
내 브라우저에서 왜 0시 0분 0초가 아닌 9시 0분 0초가 반환될까?
각 컴퓨터가(서버와 클라이언트) 위치한 시간대(timezone)가 다르기 때문
자바스크립트에서 Date객체가 반환하는 값이 달라지는 경우
자바스크립트의 Date는 같은 날짜와 시간을 '어떤 형식으로 입력하느냐'에 따라 반환되는 결과가 달라진다.
그리고 컴퓨터 자체의 시간대 설정에 따라 그 결과가 달라지기도 한다.
지구는 둥글고 지역에 따라 경도가 다르기 때문에 적용되는 시간도 달라진다.
그래서 표준으로 삼는 시간대가 필요했고, 시간대의 종류에는 GMT, UTC, KST 등이 있다.
- GMT(영국에 위치한 그리니치 천문대를 기준으로 경도를 나누는 시간대)
웹 브라우저의 콘솔 창에 new Date(); 를 입력하면 날짜와 시간 뒤에 GMT +0900 (한국 표준시) 가 출력되는 것을 볼 수 있다.
플러스가 적힌 시간대가 더 시간이 이른 것, 한국이라는 지역이 GMT보다 9시간 더 앞서 있다는 것을 의미한다.
만약 영국 시간이 12PM(정오) 라면 한국 시간은 9:00 PM이다.
그런데 GMT는 현재 국제 표준으로 삼고 있는 시간대가 아니다.
초라는 시간단위의 정의가 1967년 바뀐 후 바뀐 초의 정의를 기반으로 시간 체계를 만들었는데
그 과정을 기반으로 UTC(Universal Time Coordinated) 라는 시간대가 나옴
초의(second) 정의가 바뀌면서 현재 국제 표준 시간대인 UTC가 나옴
UTC는 전 세계 어디에서 측정해도 같은 시간을 나타낸다.
웹 브라우저상의 콘솔창에 'GMT'가 출력되는데 출력되는 것만 GMT라고 표시되는 것 같다.
Date MDN을 보니 GMT가 아닌 UTC(협정 세계시)를 기준으로,
Date 객체는 1970년 1월 1일 UTC 자정과의 시간 차이를 밀리초로 나타내는 정수 값을 담는다고 적혀있다.
Date 객체의 중심을 구성하는 시간 값은 UTC가 기준이라고 적혀있다.
Javascript에서 GMT라고 출력되지만 UTC를 기준으로 Date객체가 구성된다.
Date객체 사용시 유의할 점
Date 객체의 일부 메서드는 UTC 기반의 값을 반환하고, 다른 메서드는 현지 시각 기반의 값을 반환한다.
UTC 기반의 값을 반환하는 메서드 ex. Date객체.toISOString();
현지(지금 컴퓨터가 위치하는 곳) 시각 기반의 값을 반환하는 메서드 ex. Date객체.toString();
new Date('1970-01-01'); 의 반환 값이 0시 0분 0초가 아닌 Thu Jan 01 1970 09:00:00 GMT+0900 (한국 표준시) 로
출력된 이유는 출력에서 별도의 메서드를 쓰지 않았기 때문에 모두 현지(지금 컴퓨터가 위치한 곳 → 한국) 시각 기반으로 출력된다.
new Date() 생성자에 YYY-MM-DD 형태의 ISO 8601 문자열을 인자로 전달했기 때문에
이를 UTC로 해석하게 되고, 한국 기준으로 오전 9시가 출력되게 되는 것이다.
유닉스 타임(Unix time)
어떤 시간을 기준으로 지금까지 흐른 시간을 초 단위로 표현한 것을 유닉스 타임이라고 한다.
Date 역시 유닉스 타임을 기반으로 시간을 표현한다. 다만 초 단위가 아닌 천 분의 1초 단위(밀리초 단위)다.
new Date().toISOString();------------------------
자바스크립트에서 new Date()가 호출되면
현재 시스템 시간을 가져와서 Date객체를 생성한다.
이 시간은 기본적으로 로컬 시간으로(한국) 저장되며
내부적으로는 UTC 타임존 기준으로 밀리초 단위로 변환된다.
new Date(); → Tue Nov 12 2024 11:28:59 GMT+0900 (한국 표준시)
그리고 생성된 Date객체에서 toISOString() 메서드를 호출하면
해당 날짜와 시간이 ISO 8601 형식의 문자열로 변환된다.
날짜와 시간이 YYYY-MM-DDTHH:mm:ss.sssZ와 같은 형태로 변환된다.
toISOString()은 항상 UTC(협정 세계시) 기준으로 날짜와 시간을 출력하므로,
로컬 시간이 아닌 UTC 시간을(위치는 영국?) 기준으로 결과가 반환된다.
2. useState()와 useEffect()
- useState()는 리액트 훅으로 컴포넌트에 상태 변수를(a state variable) 추가할 수 있게 해준다.
const [ state, setState ]= useState(initialState);
컴포넌트에서 상태변수를 선언하기 위해서 컴포넌트 내부에서 가장 위에 useState를 호출한다.
useState훅을 사용할 때 배열 디스트럭처링을 사용하여 [ state, setState ] 이런 형식으로
상태변수와 그 상태변수의 값을 변경하는 함수의 이름을 작명하는 것이 관례다.
useState()는 배열을 반환한다. useState()의 인자로 초기 값을 전달하고,
해당 초기 값을 반환하는 배열의 0번째 인덱스 요소가 갖게 된다.
그리고 배열의 1번 인덱스는 해당 상태 값을 변경할 때 사용하는 메소드다.
state는 useState()의 인자로 전달한 initialState값을 갖게 된다 → state = initialState;
그리고 state의 값인 initialState를 변경하려고 할 때 setState() 메소드를 호출한다.
state에 저장하려고 하는 값을 setState() 함수의 인자로 전달하여 state의 값을 변경하는 것이다.
set 함수는 반환 값이 없다. 단지 state의 값을 변경할(업데이트할) 뿐이다.
- useEffect()
useEffect()를 사용하면 함수 컴포넌트에서 side effect를 수행할 수 있다.
useEffect()는 외부 시스템과 컴포넌트를 동기화하는 리액트 훅이다.
useEffcet(setup, dependencies?); useEffect는 undefined를 반환한다.
Effect를 선언하기 위해여 함수 컴포넌트 내부에서 최상위 레벨에서(at the top level of you component) useEffect를 호출한다.
setup은 Effect의 로직이 작성된 함수다. setup 함수는 cleanup 함수를 선택적으로 반환할 수 있다.
해당 컴포넌트가 DOM에 추가되면 리액트는 setup 함수를 실행한다.
변경된 dependencies와 함께 재렌더링된 후에 리액트는 클린업 함수가 있는 경우 이전 값으로(with the old values)
클린업 함수를 실행한 다음 새로운 값으로 setup 함수를 실행한다. 컴포넌트가 DOM에서 제거되면 리액트는 마지막으로
클린업 함수를 실행한다.
dependencies는 setup 함수에 작성된 코드 내에서 참조된 모든 반응형의 값들의 목록이다.
반응형 값들에는 prop, state, 컴포넌트 본문 내부에서 직접 선언한 모든 변수와 함수들을 포함한다.
dependency의 목록은 반드시 고정된 수의 항목(아이템)이어야 하며 [dep1, dep2, dep3] 와 같이 인라인으로 작성되어야 한다.
리액트는 각 dependency에 대해 Object.is()를 사용하여 이전의 값과 비교한다.
dependency를 지정하지 않으면 컴포넌트가 다시 렌더링될 때마다 Effect를 실행할 것이다.
dependency의 자리에 빈 배열이 작성되었을 때와 값이 있을 때의 차이
쉽게 말해 useEffect는 컴포넌트가 렌더링된 이후에 어떤 일을 수행할지 정해주는 것이다.
페이지가 처음 렌더링되고 난 이후에 useEffect는 무조건 한 번 실행된다.
만약 dependency 위치에 값이 존재한다면 dependency의 값이 변경될 때마다 useEffect가 실행된다.
useState, useEffect-----------------------------------------------------------------------------------------------------------------
const [ date, setDate ] = useState(new Date());
new Date()로 생성된 Date객체가 date변수의 초기 값이 된다.
그리고 date가 갖고 있는 상태 값은 setDate 함수를 통해 변경될 수 있다.
```
'use client'; // 클라이언트 컴포넌트임을 선언
import { useState, useEffect } from 'react';
export function NewComponent() {
// date 상태변수를 선언하기 위해 useState 훅 사용
// NewComponent가 렌더링된 이후에 date 변수에 new Date()로 생성된 객체가 초기화된다.
// 상태변수 date의 값을 바꾸려면 setDate 함수를 사용한다.
const [ date, setDate ] = useState(new Date());
useEffect(() => {
// NewComponent 컴포넌트가 렌더링될 때마다 실행된다.
// dependency가 빈 배열이므로 컴포넌트가 렌더링될 때 최초로, 단 한 번 실행된다.
// 컴포넌트가 렌더링될 때마다, 최초로 한 번 실행되는데, 실행될 작업을(함수) 작성한다.
// 500밀리초 간격으로 새로운 Date객체를 생성해서 상태변수의 값을 생성한 Date객체로 설정한다.
setInterval(() => {
setDate(new Date());
}, 500);
}, []);
return <>현재 시각: { date.toISOString() }</>
}
```
NewComponent가 렌더링된 이후에 작업 순서
1. NewComponent 컴포넌트가 렌더링된 이후에 useState훅이 호출되어 date의 값이 초기화된다.
현재시각을 나타내는 Date객체가 date의 초기값이 된다.
그리고 date의 값인 Date()객체에서 toISOString() 메서드를 호출하여 반환된 값을 화면에 렌더링하게 된다.
예를 들면, 현재 시각: 2024-11-12T06:55:34.740Z 의 형태로 화면에 출력된다.
2. useEffect 훅이 실행된다.
빈 배열([])을 의존성으로 설정, NewComponent가 렌더링된 이후에 useEffect()가 실행되는 기준이 dependency고,
이 dependency의 기준이 빈 배열이니 렌더링된 이후에 최초로 한 번만 실행되는 것이다. dependency가 빈 배열인 경우, useEffect는 컴포넌트가 마운트된 직후에 한 번만 실행된다. 즉 처음 렌더링된 이후에 한 번만 실행된다.
useEffect 내에서 setInterval 함수가 설정되는 이 함수는 500밀리세컨즈마다(즉 0.5초마다) setDate(new Date())를 호출한다.
매 0.5초마다 date상태가 갱신된다.
3. setInterval에 의한 date값 갱신(상태 갱신)
setInterval은 0.5초마다 setDate(new Date()); 함수를 호출함으로써
date의 값이 갱신되고, 상태 갱신으로 인해 리액트는 해당 상태가 변경되었음을 감지하고, 컴포넌트를 재렌더링한다.
컴포넌트 재렌더링: date 상태가 변경될 때마다 리액트는 컴포넌트를 다시 렌더링하여 새로운 date.toISOString() 값을 화면에 표시한다.
date 상태가 갱신되면 해당 상태는 새롭게 렌더링된 컴포넌트로 전달된다.
상태 갱신으로 (setDate 호출) 리액트는 상태가 변경되었음을 감지하고,
해당 상태를(date) 사용하는 컴포넌트로 재렌더링한다.
컴포넌트가 재렌더링될 때 React는 가장 최신 상태의 값을 컴포넌트에 전달한다.
리액트는 상태가 변경되면 자동으로 그 상태가 사용하는 컴포넌트를 다시 렌더링하고,
컴포넌트 함수가 실행될 때마다 최신 상태의 값을 함수 내부에 전달하는 리액트의 상태 관리 방식
*** setInterval(code, delay);
setInterval() 메서드는 지정한 시간(delay)을 간격으로 code를 호출한다.
delay는 밀리초(1/1000초)단위의 시간으로 작성해야 한다.
setInterval() 메서드의 interval ID를 반환한다.
반환된 interval ID는 타이머를 식별하는 0이 아닌 숫자 값이다.
clearInterval() 함수를 호출하여 해당 interval ID를 제거할 수 있다.
첫 번째 실행은 delay(밀리초단위) 후에 code(함수) 실행한다.
delay를 지정하지 않으면 기본 값은 0으로 함수가 계속 실행된다.
3. 라우팅
Next.js에서는 파일 기반의 라우팅을 제공하고 있다.
Next.js에서는 웹 페이지를 찾을 때 디렉토리 내부의 page.tsx를 찾는다.
예를 들어 주소창에 '도메인/디렉토리명' 이렇게 작성하면
해당 디렉토리 내부에 page.tsx 파일을 찾아 보여준다.
주소는 반드시 page.tsx 파일이 있는 디렉토리명만 해당한다 → 디렉토리 구조가 주소가 된다.
단, app이라는 디렉토리명 안에 존재하는 디렉토리명만이 주소로 사용이 된다.
app이라는 디렉토리 밖에 위치하는 디렉토리들은 라우팅과 무관하다.
app 디렉토리안에 위치한 디렉토리 내부의 page.tsx 파일은 해당 디렉토리 경로를
주소창에 입력했을 때 보여지는 대표적인 view 페이지다.
Next.js에서 page.tsx 파일과 같이 약속되어 있는 규칙의 파일이 몇 가지 있는데
대표적으로 layout.tsx, 템플릿 파일 등이 있다.
***app디렉토리 안에 존재하는 디렉토리의 이름이 ()괄호 안에 작성되면 해당 디렉토리는 주소에 반영되지 않는다.
()괄호가 작성된 디렉토리 안에 layout.tsx 파일을 작성할 수 있는데 특정 그룹별로 layout을 적용할 수 있다.
4. layout.tsx와 template.tsx
template.tsx 파일은 클라이언트쪽에서 사용이 된다. 현재 접속한 주소에 따라서 활성화되는 메뉴를 다르게 만들고 싶을 때 usePathname() 훅을 사용하는데, use로 시작하는 리액트 훅 시리즈는 클라이언트에서만 사용된다.
import { usePathname } from 'next/navigation';
서버 컴포넌트는 클라이언트의 컴포넌트는 포함할 수 있고,
클라이언트 컴포넌트는 서버 컴포넌트를 포함할 수 없다는 원칙하에
클라이언트 컴포넌트에 속하는 template.tsx 파일은 layout.tsx 파일 내부에서 작동한다고 보면된다.
template.tsx 파일에서 클라이언트 단에서 앱 초기화 시점에 실행해야할 내용, axios.defaults 등의 내용을 작성하게 된다.
노드(Node.js)버전 확인하는 법
터미널상에서 node -v 명령어 작성후 엔터치면 버전이 출력된다.
Next.js를 사용하기 위한 Node.js 버전 요구사항
Next.js를 사용하려면 최소 Node.js 18.17 버전 이상이 필요하다.
Next.js 프로젝트 생성시 디렉토리 구조
ver.1 ------------------------------------------
- / 루트
- / app
- / page-2
- / depth-2
- / components
- / utils
- / ...
ver.2 ------------------------------------------
- / src
- / app
- / components
- / utils
- / ...
유의사항
- Next.js 13버전 부터 서버 사이드 컴포넌트 이용이 가능하다
Next.js로 서버 사이드 컴포넌트를 만들려면 Next.js 13버전을 사용할 것
Next.js의 버전 확인은 package.json 에서 dependencies에서 next의 값을 확인할 것
다음글에 이어서...
참고
- Date객체 관련 내용: https://yozm.wishket.com/magazine/detail/1695/
- setInterval() mdn: https://developer.mozilla.org/ko/docs/Web/API/Window/setInterval
- 터미널에서 Next.js 프로젝트 만들기: https://nextjs.org/docs/app/getting-started/installation
'공부기록용' 카테고리의 다른 글
[ javascript ] 자바스크립트에서 네트워크 통신하는 법 (XMLHttpRequest) (9) | 2024.11.14 |
---|---|
[ KISA ] Next.js를 활용한 검색 최적화 대응 웹사이트 구축하기(2) (4) | 2024.11.13 |
[ javascript ] 두서없는 공부 기록 (0) | 2024.11.11 |
[ javascript ] nullish 병합 연산자(??), 쿵쿵따 게임 실습 (2) | 2024.11.09 |
[ javascript ] 순서도 정리 및 복습, 옵셔널 체이닝(?.) (0) | 2024.11.06 |