우선 script component딥다이브를 하기전에 개인적으로 자바스크립트 로드 방법과 실행순서가 궁금해서 간단히 알아보고 시작하도록 하겠습니다.
자바스크립트 로드 방법과 실행순서
요약: 웹 브라우저가 HTML문서를 파싱하다가 (구문을 분석할 때) <script src=""></src>태그를 만날 때 script태그에 어떤 속성이 존재하냐에 따라서 동작하는 방식이 달라지게 됩니다. 이는 문서를 로딩하는 시간에 영향을 주기 때문에 개발자는 이 차이를 이해 할 필요가 있습니다.
아무 속성도 없을 때
HTML파싱 시작 -> script태그 파싱 -> script 태그의 src=""에 해당하는 스크립트 파일 다운로드 시작 -> HTML파싱 중단 -> 자바스크립트 실행 -> 자바스크립트 실행 종료 -> 나머지 HTML파싱 시작
Async 속성이 있을 때
HTML 파싱 시작 -> script 태그 파싱 -> script 태그의 src=""에 해당하는 스크립트 파일 다운로드 시작
Case 1) 스크립트 다운로드가 먼저 끝
스크립트 다운로드 완료 -> HTML 파싱 중단 -> 자바스크립트 실행 -> 자바스크립트 실행 종료 -> HTML파싱 재개
Case 2) HTML 파싱이 먼저 끝
HTML 파싱 완료 -> 스크립트 다운로드 완료 -> 스크립트 실행 시작
Case1, Case2에 나열된 것 처럼 네트워크 성능에 따라서 자바스크립트 실행 순서를 보장할 수 없다는 점을 기억해야 합니다.
"async"는 한가지 주의해야 할 점이 있습니다.
비동기로 로딩되기 떄문에 자바스크립트 파일의 링크 순서에 상관없이 로딩됩니다.
뒤에 있는 자바스크립트 파일이 먼저 로딩되어 실행될 수 있습니다.
상호 의존적이거나 연관 관계가 있는 자바스크립트 파일이나 코드들은 "async"를 사용하면 안되는 이유중에 하나입니다.
어떤 경우에는 잘 실행되겠지만, 어떤 경우에는 참조 에러가 나면서 자바스크립트 코드 실행이 멈출 수도 있습니다.
같은 이유로 HTML페이지 상단에서 비동기(async)로 로딩되어 실행되는 자바스크립트 코드는 HTML페이지 파싱이 끝나기 전에 실행될 가능성도 있습니다. 그래서 테스트 및 개발을 할 때는 잘 실행되다, 막상 실 서비스에 적용하면 어떤 떄는 실행되고, 어떤 때는 에러가 나는 이상한 현상이 발생하기도 합니다.
비동기로 가져오는 자바스크립트 코드는 2번의 DOM생성 체크를 하는 코드가 필수적으로 필요하며, 실행 순서에 주의해서 상호 의존적인 실행 코드들은 하나의 파일에 모아서 가져오는 방식을 사용해야 합니다.
Defer 속성이 있을 때
HTML 파싱 시작 -> script 태그 파싱 -> script 태그의 src=""에 해당하는 스크립트 파일 다운로드 시작 -> 스크립트 파일 다운로드 시작 및 완료 -> HTML 파싱 완료 -> 스크립트 실행
Defer속성이 있을 경우 스크립트 파일을 백그라운드(비동기)로 내려받지만 HTML파싱이 완료되기 전에 스크립트 구문을 실행하지 않는다는 점을 기억해야 합니다.
"async"와의 가장 중요한 차이점이 "순서대로"입니다. 스크립트 태그가 위치한 순서대로 차례대로 실행됩니다. 더 중요한 차이는 "defer"속성으로 정의한 자바스크립트는 DOM이 생성된 후에 실행됩니다. 따라서 2번의 DOM생성 체크 이벤트 핸덜러 처리를 할 필요가 없습니다.
Script Component
이제 스크립트는, 예를 들어서 페이스북 SDK를 불러온다고 생각하면 됩니다. 아니면 구글 애널리틱스 JS파일을 불러올 수도 있고, 카카오 SDK가 될 수도 있습니다. 이런 외부 JS파일을 불러올 때, Next는 최적화를 해줍니다.
Next의 _document.tsx는 계속 말했지만, 앱 전체의 뼈대와 같습니다. 따라서 만약 앱이 가지고 있는 모든 페이지의 Head에 넣고 싶은 게 있다면, 여기 넣으면 되는 것입니다.
보통 프로젝트를 끝내고 나면 이제 다른 웹사이트하고 상호작용을 해아합니다. GA를 사용한다거나, 페이스북 SDK를 사용한다거나,... 가 될 수 있습니다. 가장 쉬운방법은 그냥 _app.tsx에 <script>태그를 추가하는 방법이 있을 수 있겠습니다. 이게 가장 쉬운 방법이고 문제없이 작동할 것입니다.
하지만 NextJS가 이런 스크립트의 최적화를 도와준다는 점입니다. 추가하는 스크립트 중에 페이지가 열리자마자 불러와야 하는 스크립트가 포함되어 있을 수도 있습니다. 대부분의 웹사이트가 그렇지 않지만 예를 들어 JQuery를 사용하고 있다고 생각해 봅시다. 그리고 JQuery를 스크립트에서 불러온다고 해 봅시다. 만약 JQuery를 사용 중이라면 JQuery를 최우선 순위로 불러와야 합니다.
반면에 GA를 사용한다면 페이지를 다 불러온 다음에 GA를 불러와도 됩니다. 이렇게 한다면 웹 페이지를 더 빨리 불러올 수 있게 되는 것입니다. 필요없는 스크립트를 다운로드 받지 않아도 되니까 말이죠.
스크립트를 언제 어떻게 불러올지 정하려면 일반 script태그를 사용하는 게 아니라 NextJS에서 제공하는 Script컴포넌트를 사용해야 합니다.
Script태그에는 strategy속성을 줄 수 있습니다. 속성의 종류는 아래 4개가 될 수 있습니다.
beforeInteractive는 페이지를 다 불러와서 상호작용을 하기 전에 스크립트를 불러오는 전략입니다. 내가 정말 유저가 페이지하고 상호작용하기 전에 꼭 스크립트를 불러와야만 한다면 beforeInteractive를 사용하면 됩니다. 다만 사용하는 대부분의 스크립트는 페이지를 불러오기 전에 불러올 필요가 없습니다.
afterInteractive는 NextJS에서 제공하는 기본 strategy입니다. 이는 페이지를 먼저 다 불러온 다음에 스크립트를 불러옵니다.
lazyOnLoad는 스크립트를 불러오긴 하는데 스크립트를 불러오는 게 최우선 순위는 아닙니다. 이는 다른 모든 데이터나 소스들을 불러오고 나서야 스크립트를 불러오는 전략입니다. 예를 들어 사이트에 좋아요가 몇 개인지 알기 위해 페이스북 SDK를 불러오려고 한다면 이게 lazyOnLoad를 사용하기에 좋은 예시가 될 수 있을 것입니다. 페이지를 다 불러올 때까지는 별로 필요한 기능이 아닙니다.
우선 이를 적용시켜보기위해 _app.tsx에 Script태그를 통해서 kakao SDK, facebook SDK for JavaScript를 불러와 보도록 하겠습니다.
https://developers.kakao.com/docs/latest/ko/getting-started/sdk-js#download
https://developers.facebook.com/docs/javascript/quickstart
/pages/_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { SWRConfig } from "swr";
import useUser from "@libs/client/useUser";
import { useRouter } from "next/router";
import Script from "next/script";
function MyApp({ Component, pageProps }: AppProps) {
const { pathname } = useRouter();
const { user } = useUser(pathname);
console.log("APP IS RUNNING");
return (
<SWRConfig
value={{
fetcher: (url: string) =>
fetch(url).then((response) => response.json()),
}}
>
<div className="mx-auto w-full max-w-xl">
<Component {...pageProps} />
</div>
<Script
src="https://developers.kakao.com/sdk/js/kakao.js"
strategy="lazyOnload"
/>
<Script
src="https://connect.facebook.net/en_US/sdk.js"
onLoad={() => {
// @ts-ignore
window.fbAsyncInit = function () {
// @ts-ignore
FB.init({
appId: "your-app-id",
autoLogAppEvents: true,
xfbml: true,
version: "v14.0",
});
};
}}
/>
</SWRConfig>
);
}
export default MyApp;
여기서 facebook SDK를 불러올 때, OnLoad를 통해서 이 스크립트가 다 불러와지고 실행할 함수를 지정해서 간단히 함수를 실행해 줄 수 있습니다. 스크립트를 정확히 언제 불러올지, 그리고 그 스크립트를 불러왔다는 걸 확인할 수 있는건 엄청난 기능이라고 생각합니다. 왜냐하면 일반 script태그를 사용하고 그 다음에 UseEffect를 이용해서 이벤트를 기다리고 window가 바뀌는 걸 기다리는건 정말 번거로웠기 때문입니다.
그리고 SEO최적화를 해보도록 하겠습니다. 그냥 /product/[id]로 들어가서 title을 주고 싶다고 해 봅시다. 그럼 layout.tsx를 아래와 같이 바꾸어 주겠습니다.
/pages/Components/layout.tsx
import React, { useCallback } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { cls } from "@libs/client/utils";
import Head from "next/head";
interface LayoutProps {
title?: string;
canGoBack?: boolean;
hasTabBar?: boolean;
children: React.ReactNode;
seoTitle: string;
}
const Layout = ({
title,
canGoBack,
hasTabBar,
children,
seoTitle,
}: LayoutProps) => {
const router = useRouter();
const onClick = useCallback(() => router.back(), [router]);
return (
<div>
<Head>
<title>{seoTitle} | Carrot Market</title>
</Head>
<div
className={cls(
!canGoBack ? "justify-center" : "",
"fixed top-0 left-0 flex w-full items-center border-b bg-white px-10 py-3 text-lg font-medium text-gray-700"
)}
>
{canGoBack ? (
<button onClick={onClick}>
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 19l-7-7 7-7"
></path>
</svg>
</button>
) : null}
{title ? <span>{title}</span> : null}
</div>
<div
className={cls("pt-16", hasTabBar ? "pb-[6rem]" : "")}
>
{children}
</div>
{hasTabBar ? (
<nav className="fixed bottom-0 left-0 flex w-full items-center justify-around border-2 border-t bg-white pb-4 pt-4 text-xs text-gray-800">
<Link href="/">
<a className="flex flex-col items-center space-y-2">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
></path>
</svg>
<span>홈</span>
</a>
</Link>
<Link href="/community">
<a className="flex flex-col items-center space-y-2">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
></path>
</svg>
<span>동네생활</span>
</a>
</Link>
<Link href="/chats">
<a className="flex flex-col items-center space-y-2">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
></path>
</svg>
<span>채팅</span>
</a>
</Link>
<Link href="/streams">
<a className="flex flex-col items-center space-y-2">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"
></path>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"
></path>
</svg>
<span>라이브</span>
</a>
</Link>
<Link href="/profile">
<a className="flex flex-col items-center space-y-2">
<svg
className="h-6 w-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
></path>
</svg>
<span>나의 계정</span>
</a>
</Link>
</nav>
) : null}
</div>
);
};
export default Layout;
/pages/products/[id].tsx
import type { NextPage } from "next";
import Layout from "@components/layout";
import Button from "@components/button";
import { useRouter } from "next/router";
import useSWR, { useSWRConfig } from "swr";
import Link from "next/link";
import { Product, User } from "@prisma/client";
import { Backdrop, CircularProgress } from "@mui/material";
import { useState } from "react";
import useMutation from "@libs/client/useMutation";
import { cls } from "@libs/client/utils";
import useUser from "@libs/client/useUser";
import Image from "next/image";
interface ProductWithUser extends Product {
user: User;
}
interface ItemDetailResponse {
ok: boolean;
product: ProductWithUser;
relatedProducts: Product[];
isLiked: boolean;
}
const ItemDetail: NextPage = () => {
const { user, isLoading } = useUser();
const router = useRouter();
// const { mutate } = useSWRConfig();
const { data, mutate: boundMutate } =
useSWR<ItemDetailResponse>(
router.query.id ? `/api/products/${router.query.id}` : null
);
const [toggleFav] = useMutation(
`/api/products/${router.query.id}/fav`
);
const onFavClick = () => {
// SWR boundMutate를 활용해 캐시로 바로 UI에 업데이터 하고, Revalidate는 안하고
// 실제로 mutate를 DB에 주어서 ( toggleFav )로 값에 변화는 주어야 함.
toggleFav({});
if (!data) return;
boundMutate(
(prev) => prev && { ...prev, isLiked: !prev.isLiked },
false
);
// mutate(
// "/api/users/me",
// (prev: any) => {
// console.log(prev);
// return { ok: !prev.ok };
// },
// false
// );
};
return (
<Layout canGoBack seoTitle="Product Detail">
<Backdrop
sx={{
color: "#fff",
zIndex: (theme) => theme.zIndex.drawer + 1,
}}
open={data === undefined}
>
<CircularProgress color="inherit" />
</Backdrop>
<div className="px-4 py-3">
<div className="mb-8">
<div className="relative pb-64">
<Image
src={`https://imagedelivery.net/_SMYXsMOOEvTYhYAAKoRCQ/${data?.product?.image}/public`}
className="bg-slate-300 object-cover"
alt=""
layout="fill"
/>
</div>
<div className="mt-1 flex items-center space-x-3 border-t border-b py-3">
<Image
width={48}
height={48}
src={`https://imagedelivery.net/_SMYXsMOOEvTYhYAAKoRCQ/${data?.product?.user?.avatar}/avatar`}
className="h-12 w-12 rounded-full bg-slate-300"
alt=""
/>
<div>
<p className="text-sm font-medium text-gray-700">
{data?.product?.user?.name}
</p>
<Link
href={`/users/profiles/${data?.product?.user?.id}`}
>
<a className="cursor-pointer text-xs font-medium text-gray-500">
View profile →
</a>
</Link>
</div>
</div>
<div className="mt-5">
<h1 className="text-3xl font-bold text-gray-900">
{data?.product?.name}
</h1>
<span className="mt-3 block text-3xl text-gray-900">
${data?.product?.price}
</span>
<p className="my-6 text-base text-gray-700">
{data?.product?.description}
</p>
<div className="flex items-center justify-between space-x-2">
<Button text="Talk to seller" large />
<button
onClick={onFavClick}
className={cls(
"flex items-center justify-center rounded-md p-3 hover:bg-gray-100",
data?.isLiked
? "text-red-400 hover:text-red-500"
: "text-gray-400 hover:text-gray-500"
)}
>
{data?.isLiked ? (
<svg
className="h-6 w-6"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
clipRule="evenodd"
></path>
</svg>
) : (
<svg
className="h-6 w-6 "
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
)}
</button>
</div>
</div>
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">
Similar items
</h2>
<div className="mt-6 grid grid-cols-2 gap-4">
{data?.relatedProducts?.map(
({ id, name, price }) => (
<Link href={`/products/${id}`} key={id}>
<a>
<div className="mb-4 h-56 w-full bg-slate-300" />
<h3 className=" -mb-1 text-gray-700">
{name}
</h3>
<span className="text-sm font-medium text-gray-900">
${price}
</span>
</a>
</Link>
)
)}
</div>
</div>
</div>
</Layout>
);
};
export default ItemDetail;
잘 적용된것을 확인해 볼 수 있습니다.
'Web > NextJs' 카테고리의 다른 글
[ Next.js - DeepDive ] - SSR + SWR (0) | 2022.08.06 |
---|---|
[ Next.js - DeepDive ] - getServerSideProps (0) | 2022.08.06 |
[ Next.js - DeepDive ] - _document and Fonts (0) | 2022.08.04 |
[ Next.js - DeepDive ] - Lazy-load Imports (0) | 2022.08.04 |
[ Next.js - DeepDive ] - Dynamic Import (0) | 2022.08.04 |