일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 29 | 30 | 31 |
- next Link
- JS
- 자바스크립트
- 프론트엔드
- 타입스크립트
- 백준 스택
- TS
- 파이썬
- C++
- Android
- CSS
- 안드로이드
- 백준 스택 시간초과 python
- 스택
- HTML
- javascript
- 파이어베이스
- k for k
- 리액트
- firebase
- 알고리즘
- kotlin
- nodejs
- react
- NPM
- typescript
- stdin vs input
- 최적화
- 코딩테스트
- Python
- Today
- Total
sooleeandtomas
1.5. next v.12 Routing, 팀버너스리의 a태그의 진화 본문
a와 Link태그의 차이에 대해 알아보겠습니다!
a태그는 href 특성을 통해 다른 페이지나 같은 페이지의 어느 위치, 파일, 이메일 주소와 그 외 다른 URL로 연결할 수 있는 하이퍼링크를 만듭니다. 하이퍼링크란 주소와 주소가 비선형적으로 엮인 인터페이스를 의미합니다. 주소와 주소 사이를 중간 과정 없이 옮겨다닐 수 있는 특징이 있습니다. download, href, hreflang, ping, referrerpolicy, rel, target, type을 프롭으로 가집니다.
next의 Link태그는 prefetching, client-side navigation을 위해 a태그를 확장한 리액트 컴포넌트입니다.
a태그를 확장했다는 말은, next에서 <Link> 를 사용하게 되면 <a> 태그가 생성된다는 것입니다. a의 사용성을 가져가고, 추가로 기능을 붙였다는 말입니다.
Link컴포넌트의 props는 아래와 같습니다.
1. href (required) href는 필수prop 입니다.
href는 SEO에 아주 중요합니다. 검색엔진이나 크롤링 봇은 일반적으로 a 태그를 타고 웹사이트를 크롤링합니다. 따라서 a 태그가 구조적으로 설정이 되어있다면 크롤링 봇이 웹사이트의 모든 페이지의 정보를 가져갈 수 있습니다.
예를들어, 아래의 내용은 앵커 텍스트가 "공유창고 다락"이고, 링크는 "https://www.dalock.co.kr"로 되어있습니다. 검색엔진은 앵커텍스트와 링크를 연결합니다. 이 경우, "공유창고 다락"이라는 검색어에 https://www.dalock.co.kr 가 나올 가능성이 높아집니다.
<a href="https://www.dalock.co.kr">공유창고 다락</a>
만약에 anchor text에 p태그를 넣으면 어떻게 될까요?
GPT
일반적으로 검색 엔진은 `<a>` 태그의 anchor text를 직접 추출하여 해당 링크의 내용과 연결합니다. `<a>` 태그는 하이퍼링크를 정의하는 데 사용되며, 링크의 텍스트 내용이 검색 엔진의 색인에 포함되기 때문에 검색 결과에 영향을 줍니다.
하지만 `<p>` 태그로 `<a>` 태그를 감싸는 경우, 검색 엔진은 기본적으로 두 가지 사항을 고려합니다.
1. **텍스트의 구조적 의미**: `<p>` 태그는 단락을 나타내는데 사용되기 때문에, 그 안에 있는 텍스트들은 문단의 일부로 간주됩니다. 따라서 `<a>` 태그를 `<p>` 태그로 감싸면 해당 링크의 텍스트도 문단의 일부로 간주되며, 문단의 의미와 연관되어 색인화될 수 있습니다.
2. **링크의 중요도**: 검색 엔진은 텍스트의 위치와 주변 문맥을 고려하여 링크의 중요도를 결정합니다. `<a>` 태그를 `<p>` 태그로 감싸면 해당 링크의 텍스트가 문단 내에서 다른 텍스트와 함께 나타나기 때문에 링크의 중요도는 약간 감소할 수 있습니다.
따라서 `<a>` 태그의 anchor text를 `<p>` 태그로 감싸면 일반적으로 검색 엔진이 해당 텍스트를 추출하여 색인화할 수 있지만, 링크의 중요도는 약간 감소할 수 있습니다. 검색 엔진은 다양한 요소를 고려하여 페이지의 내용을 색인화하므로 이러한 마크업 변경이 검색 결과에 어떤 영향을 미칠지는 페이지의 전반적인 구조와 내용에 따라 다를 수 있습니다.
href는 shift + click, ctr + click 기능을 가능케 합니다. (사용성)
따라서, Link에 style 속성을 넣기위해 children으로 styled-component를 넣는 경우, 꼭 passhref를 해서 하위 자식에게 href를 넘겨줘야합니다.
<Link href="hi">
<DIV_HI/>
</Link>
//결과 : <a class="css-qurnw5"></a>
<Link href="hi" passHref>
<DIV_HI/>
</Link>
//결과 : <a class="css-qurnw5" href="hi"></a>
-passhref
위에서 설명한대로 하위 자식에게 href를 넘기는 기능입니다. 단, 하위자식이 a태그여야 합니다.
- as
다이나믹 라우트를 사용할 때 as를 사용합니다. analytics, tracking webpage.. 처럼 url의 쿼리부분을 지정할 수 있습니다. 아래와같이 as를 사용하면 products/[id]의 모든 페이지는 ?utm=1234값을 갖게 됩니다.
<Link
href="/products/[id]"
as={`products/${id}/?utm=1234`}
key={key}
>
<a>
<p>tracking</p>
</a>
</Link>
-scroll (default:true)
페이지 이동 시 무조건 scroll을 최상단으로 올립니다.
- prefetch (default:true)
next의 Link컴포넌트는 href의 주소를 읽어와서 해당 페이지 리소스를 미리 받아놓습니다. 특히! viewport에 있는 Link만 미리 받아옵니다. 이는 페이지 이동 시, 빠른 이동을 할 수 있게 합니다. 만약 이렇게 미리 받아놓는게 싫으면 prefetch={false}로 설정하면 됩니다.
하지만, link를 호버했을 때는 여전히 prefetch를 하게된다는 점 주의해주세요.
When prefetch is set to false, prefetching will still occur on hover.
prefetch가 next의 Link에서 중요한 개념입니다.
이에 앞서, next의 근본인 React의 router를 먼저 이해해 봅시다. React 와 같은 Single Page Application에서는 앱 내의 주소가 변경될 때 브라우저의 navigate 이벤트 핸들링을 막고, 한 페이지 내에서 리렌더링만 발생시킵니다. 이 동작원리는 이와 같습니다. App의 최상단에는 RouterContext.Provider가 있습니다. RouterContext.Provider에 match, location, history객체가 담겨있습니다. Router컴포넌트를 통해 받아온 path가 RouterContext의 location과 일치한지 확인한 후, Router컴포넌트의 prop인 component를 매칭시켜 렌더링하는 방식입니다. *React는 build시점에 모든 Router의 Link href링크의 리소스들을 가져옵니다.
next의 경우에는 리소스를 가져오는 시점과 양이 리액트와 다릅니다. 최초 빌드된 파일을 보면, staticGeneration인 경우 Html파일이 로드되고, serverSideRender일 경우, 서버에서 런타임 중에 돌아갈 js파일이 만들어집니다. 그 후, 각 페이지를 요청할 때, 파일이 로드되는 것이 기본 방식입니다. 보통 next의 페이지 진입 후의 일련의 과정은 이렇습니다. next는 html을 모두 로드한 후, js까지 모두 다운로드 하면, 그 후 react dom을 구성해 보여줍니다. 이렇게 보면, next는 페이지 이동 시마다 html로드,js로드,react-dom구성의 과정을 거치니 매우 느릴 것 같은 느낌이듭니다. 하지만, prefetch라는 기능 덕분에, SPA와 비슷한 빠른 페이지전환이 가능합니다.
next Link에서 prefetch 코드가 실제 어떻게 작성되었는지 한번 보면 좋을 것 같습니다. github 소스를 보는게 아직은 어려워서 heechan님의 블로그 이분의 블로그를 따라가봤습니다. 저는 12버전 코드를 살펴봤습니다.
//타입명이 인간적이네요
type LinkPropsReal = React.PropsWithChildren<
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> &
LinkProps
>
const Link = React.forwardRef<HTMLAnchorElement, LinkPropsReal>(props, forwardedRef) => {
//useRouter훅으로 router를 가져옵니다.
const router = useRouter()
//intersectionObserver로 뷰포트에 보이는지 확인하는 코드입니다.
const [setIntersectionRef, isVisible, resetVisible] = useIntersection({
rootMargin: '200px',
})
//굳이 p로 props.prefetch !== false값을 넣어줍니다.
const p = prefetchProp !== false
React.useEffect(() => {
const shouldPrefetch = isVisible && p && isLocalURL(href)
// locale라는 props을 넣어주면 여기서 활용됩니다.
const curLocale =
typeof locale !== 'undefined' ? locale : router && router.locale
// 이미 prefecthed된 이력이 있는지 확인합니다.
const isPrefetched =
prefetched[href + '%' + as + (curLocale ? '%' + curLocale : '')]
if (shouldPrefetch && !isPrefetched) {
prefetch(router, href, as, {
locale: curLocale,
})
}
}, [as, href, isVisible, locale, p, router])
}
//위의 코드에서 prefetched된 이력을 확인했습니다. 여기는 prefetched 객체의 선언 코드입니다.
//cachekey가 Link컴포넌트에서 만들어진 href + '%' + as + (curLocale ? '%' + curLocale : '') 입니다.
const prefetched: { [cacheKey: string]: boolean } = {}
function prefetch(
router: NextRouter,
href: string,
as: string,
options?: PrefetchOptions
): void {
if (typeof window === 'undefined' || !router) return
if (!isLocalURL(href)) return
// Prefetch the JSON page if asked (only in the client)
// We need to handle a prefetch error here since we may be
// loading with priority which can reject but we don't
// want to force navigation since this is only a prefetch
// 궁극적으로는 router.prefetch를 실행하는 것이니, router.prefetch 코드로 이동하겠습니다.
router.prefetch(href, as, options).catch((err) => {
if (process.env.NODE_ENV !== 'production') {
// rethrow to show invalid URL errors
throw err
}
})
const curLocale =
options && typeof options.locale !== 'undefined'
? options.locale
: router && router.locale
// Join on an invalid URI character
prefetched[href + '%' + as + (curLocale ? '%' + curLocale : '')] = true
}
router.prefetch가 정의되어있는 next/client/router-loader.ts 의 아래 prefetch함수를 살펴보겠습니다.
prefetch(route: string): Promise<void> {
// https://github.com/GoogleChromeLabs/quicklink/blob/453a661fa1fa940e2d2e044452398e38c67a98fb/src/index.mjs#L115-L118
// License: Apache 2.0
let cn
// navigator : 유저의 디바이스 등의 정보를 알려줍니다. networkInformation
if ((cn = (navigator as any).connection)) {
// Don't prefetch if using 2G or if Save-Data is enabled.
if (cn.saveData || /2g/.test(cn.effectiveType)) return Promise.resolve()
}
//대략적인 내용은 ..dev,production에따라 다른 경로를 통해 각각 [build파일].js을 가져옵니다.
return getFilesForRoute(assetPrefix, route)
.then((output) =>
Promise.all(
canPrefetch
? output.scripts.map((script) =>
//아래 코드블럭에서 설명해보겠습니다.
prefetchViaDom(script.toString(), 'script')
)
: []
)
)
.then(() => {
//prefetchViaDom이 완료되면 this.loadRoute를 실행합니다. 아래 코드블럭에서 볼 수 있습니다.
requestIdleCallback(() => this.loadRoute(route, true).catch(() => {}))
})
.catch(
// swallow prefetch errors
() => {}
)
},
최후 파싱된 링크로 Dom을 직접생성합니다. document.head에 넣어줍니다.
function prefetchViaDom(
href: string,
as: string,
link?: HTMLLinkElement
): Promise<any> {
return new Promise<void>((res, rej) => {
const selector = `
link[rel="prefetch"][href^="${href}"],
link[rel="preload"][href^="${href}"],
script[src^="${href}"]`
if (document.querySelector(selector)) {
return res()
}
link = document.createElement('link')
// The order of property assignment here is intentional:
if (as) link!.as = as
link!.rel = `prefetch`
link!.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
link!.onload = res as any
link!.onerror = rej
// `href` should always be last:
link!.href = href
//document.head에 <link> 태그를 넣습니다. 왜 굳이 head에 넣어야 하는건지는 모르겠음.
document.head.appendChild(link)
})
}
//route를 load하는 함수
loadRoute(route: string, prefetch?: boolean) {
return withFuture<RouteLoaderEntry>(route, routes, () => {
return resolvePromiseWithTimeout(
getFilesForRoute(assetPrefix, route)
.then(({ scripts, css }) => {
return Promise.all([
entrypoints.has(route)
? []
: Promise.all(scripts.map(maybeExecuteScript)),
Promise.all(css.map(fetchStyleSheet)),
] as const)
})
.then((res) => {
return this.whenEntrypoint(route).then((entrypoint) => ({
entrypoint,
styles: res[1],
}))
}),
MS_MAX_IDLE_DELAY,
markAssetError(new Error(`Route did not complete loading: ${route}`))
)
.then(({ entrypoint, styles }) => {
const res: RouteLoaderEntry = Object.assign<
{ styles: RouteStyleSheet[] },
RouteEntrypoint
>({ styles: styles! }, entrypoint)
return 'error' in entrypoint ? entrypoint : res
})
.catch((err) => {
if (prefetch) {
// we don't want to cache errors during prefetch
throw err
}
return { error: err }
})
.finally(() => devBuildPromiseResolve?.())
})
},
음.. 설명해보려했지만 어렵네요 .. ^^ ;
이렇게 nextLink의 일련의 과정을 살펴봤습니다.
결론
1. 검색엔진을 위해 Link href를 잘 쓰자
2. prefetch를 통해 렌더링을 최적화해주는 Link 컴포넌트를 잘사용하자.
- prefetch는 캐시를 통해 한번 prefetch된 이상, 한번 더 prefetch하지 않는다.
- viewport에 들어오거나 hover 시 prefetch를 해준다.
참고자료
heechan님의 블로그 (Next.js Link와 Prefetch과정 파헤쳐보기) **레포지토리 보는 부분 엄청 많이 참고했습니다
'코딩 공부 노트 > next js' 카테고리의 다른 글
webfont 최적화로 FCP 줄이기 (0) | 2023.12.12 |
---|---|
2. next v.12 getServerSideProps 스마트하게 사용하기 (0) | 2023.06.30 |
1. next v.12 Routing, 팀버너스리의 a태그의 진화 (0) | 2023.06.28 |