이번에는 프로젝트 전체 컴포넌트들에 시맨틱 태그들이 적절히 적용되었나 점검하고 필요한 부분은 적절한 시맨틱 태그로 변경해보겠습니다.
semantic은 의미론적인이라는 뜻을 가지고 있습니다.
프로그래밍에서, 시맨틱은 코드 조각의 '의미'를 나타냅니다.
그렇다면 시맨틱 태그란 특정한 의미 혹은 목적을 가진 태그라 할 수 있습니다.
몇 가지 HTML 태그를 살펴봤을 때,h1
은 heading으로 머릿말을 나타내는데 사용됩니다.
header
는 레이아웃 혹은 페이지의 상단 부분을 나타나는데 사용되는 태그입니다.
main
은 주요 콘텐츠 영역을 나타나는데 사용됩니다.
시맨틱 태그,각 태그는 특정한 의미를 가지고 있는데요.
특정 의미에 따라 사용되는 것이 권장됩니다.
이유는 다음과 같습니다.
검색 엔진은 시맨틱 태그를 적절히 사용하는 것이 페이지의 검색 순위에 영향을 줄 수 있는 중요한 요소로 간주합니다.
때문에 의도에 맞게 적절한 태그를 사용하면 본인의 웹페이지의 검색 순위를 높일 수 있습니다.
각 마크업을 적절한 태그로 설정하면 개발자가 HTML 코드를 보았을 때, 어떤 내용인지 파악하기 쉽습니다.
아래 태그를 확인해보시죠.
이전
<div >
<PrimaryLink
href={"/"}
color={"none"}
className={"text-primary-normal text-title1 font-bold"}
>
코아
</PrimaryLink>
</div>
위 태그만 보았을 때, 어떤 내용인지 단번에 파악하기 쉽지 않습니다.
하지만 다음과 같이 변경해보면 어떨까요?
이후
<header >
<PrimaryLink
href={"/"}
color={"none"}
className={"text-primary-normal text-title1 font-bold"}
>
코아
</PrimaryLink>
</header>
div
를 header
로 변경하면서 이제 위 태그가 페이지의 헤더라는 것을 파악할 수 있죠.
이처럼 적절한 태그로 설정하는 것은 가독성을 높여주어 개발자의 경험을 높여줄 수 있습니다.
적절한 시맨틱 마크업을 하는 것은 스크린 리더가 화면을 해석하는데에 도움을 줍니다.
스크린 리더
스크린 리더(Screen Reader)는 시각 장애인이나 시력이 약한 사용자를 위해 컴퓨터 화면에 표시된 내용을 음성으로 읽어주거나, 점자 디스플레이를 통해 전달해주는 보조 기술(Assistive Technology)입니다.
스크린 리더는 웹 페이지의 구조와 의미를 HTML 시맨틱 태그를 통해 해석합니다.
예를 들어
<h1>~<h6>
: 제목의 계층 구조를 이해하고, 중요도에 따라 사용자가 탐색할 수 있도록 도와줍니다.
<nav>
: 내비게이션 영역을 알려주어 빠르게 이동할 수 있게 합니다.
<main>
: 주요 콘텐츠로 바로 이동할 수 있도록 지원합니다.
<button>
: 클릭 가능한 버튼임을 명확히 전달합니다.
시맨틱 태그를 잘 사용하면 스크린 리더가 웹 페이지를 더 정확하고 효율적으로 해석할 수 있어 접근성이 높아집니다.
위에서 알아봤듯이,시맨틱 태그를 적절히 설정하는 것은 DX,SEO,웹 접근성 향상에 도움을 준다는 것을 알아봤습니다.
그렇다면 저의 프로젝트에 적절히 시맨틱 태그들이 설정되어있는지 검토를 하고 적절치 않은 부분이 있다면 변경해보도록 하겠습니다.
저의 프로젝트는 Nextjs를 사용하고 있습니다.
최상단에서 항상 노출되고 있는 layout 컴포넌트입니다.
모든 페이지에 적용되는 컴포넌트죠.
코드는 다음과 같습니다.
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ViewTransitions>
<html lang="en">
<body
className={`${geistMono.className} antialiased bg-gray-100`}
>
{/*웹 성능 측정*/}
<WebVitals/>
{/*헤더*/}
<Header/>
<main
className={"w-full lg:h-[calc(100vh-80px)] md:h-[calc(100vh-60px)] sm:h-[calc(100vh-60px)] lg:flex lg:justify-center lg:items-center md:flex md:justify-center md:items-center sm:px-[10px] "}>
<ModalProvider>
{children}
</ModalProvider>
</main>
</body>
</html>
</ViewTransitions>
);
}
"use client"
import PrimaryLink from "@/app/_components/link/primaryLink";
import {usePathname} from "next/navigation";
import React from 'react';
/**
* 헤더
*/
const Header = () => {
const pathname = usePathname();
// 네비게이션 메뉴
const NAVMENU = [
{
title: "퀴즈",
link: "quiz",
},
]
// 현재 페이지에 맞는 클래스명
function getActiveClass(link:string){
if(pathname===`/${link}`){
return "bg-primary-normal text-black hover:bg-primary-dark "
}
}
return (
<header className={"w-full h-[80px] md:h-[60px] sm:h-[60px] bg-headerBackground flex justify-between items-center lg:px-container md:px-10 sm:px-10"}>
{/*로고,메뉴*/}
<div className={"flex gap-10 items-center"}>
<PrimaryLink
href={"/"}
color={"none"}
className={"text-primary-normal text-title1 font-bold"}
>
코아
</PrimaryLink>
<nav>
<ul className={"flex gap-3 text-title3Normal text-primary-normal"}>
{NAVMENU.map((item, index) => (
<li key={index} className={`cursor-pointer px-[12px] flex justify-center items-center w-[100px] h-[32px] rounded-[8px] hover:bg-primary-dark hover:text-black ${getActiveClass(item.link)}`}>
<PrimaryLink
color={"none"}
href={`/${item.link}`}
>{item.title}</PrimaryLink>
</li>
))}
</ul>
</nav>
</div>
</header>
);
};
export default Header;
위 컴포넌트는 헤더 컴포넌트입니다.
적절히 설정된 부분과 아닌 부분을 살펴보겠습니다.
코아
라고 되있는 태그는 로고인데요. 이는 프로젝트의 핵심 요소입니다. 때문에 핵심적인 요소를 나타내는h1
태그로 감싸주는 것도 좋을 것 같다는 생각이 듭니다. <header className={"w-full h-[80px] md:h-[60px] sm:h-[60px] bg-headerBackground flex gap-10 items-center lg:px-container md:px-10 sm:px-10"}>
{/*로고,메뉴*/}
<h1>
<PrimaryLink
href={"/"}
color={"none"}
className={"text-primary-normal text-title1 font-bold"}
>
코아
</PrimaryLink>
</h1>
<nav>
<ul className={"flex gap-3 text-title3Normal text-primary-normal"}>
{NAVMENU.map((item, index) => (
<li key={index} className={`cursor-pointer px-[12px] flex justify-center items-center w-[100px] h-[32px] rounded-[8px] hover:bg-primary-dark hover:text-black ${getActiveClass(item.link)}`}>
<PrimaryLink
color={"none"}
href={`/${item.link}`}
>{item.title}</PrimaryLink>
</li>
))}
</ul>
</nav>
</header>
로고를 h1로 변경해주었고 중간에 굳이 필요없는 div태그를 없애주었습니다.
위 헤더 컴포넌트를 컴포넌트로 더 분리할 필요가 있어보이네요.
컴포넌트 분리까지 깔끔히 해보았습니다.
아래와 같이 변경하니 Navigation만 클라이언트 컴포넌트로 변경하여 번들 사이즈를 티끌만큼 줄이는 효과도 볼 수 있겠네요.
import HeaderContainer from "@/app/_layout/header/components/headerContainer";
import Logo from "@/app/_layout/header/components/logo";
import Navigation from "@/app/_layout/header/components/navigation";
import React from 'react';
/**
* 헤더
*/
const Header = () => {
return (
<HeaderContainer>
{/*로고*/}
<Logo/>
{/*네비게이션*/}
<Navigation/>
</HeaderContainer>
);
};
export default Header;
다음으로 메인 페이지입니다.
import HomeDescription from "@/app/_home_components/homeDescription";
import HomeInnerContainer from "@/app/_home_components/homeInnerContainer";
import HomeLink from "@/app/_home_components/homeLink";
import HomeOuterContainer from "@/app/_home_components/homeOuterContainer";
import HomeTitle from "@/app/_home_components/homeTitle";
/**
* 메인 페이지
* SSG
*/
export const dynamic = 'force-static'
export default function Home() {
return (
<HomeOuterContainer>
{/* 내부 카피 컨텐츠 */}
<HomeInnerContainer>
{/* 메인 타이틀 */}
<HomeTitle
title={"개발자들의 아지트, 코아"}
/>
{/* 메인 설명 */}
<HomeDescription
description={"퀴즈로 실력을 키우고, 함께 성장하세요."}
/>
{/* 메인 링크 */}
<HomeLink/>
</HomeInnerContainer>
</HomeOuterContainer>
);
}
컴포넌트들이 적절히 잘 나누어진 것으로 확인됩니다.
위에서부터 차례로 컴포넌트를 살펴보겠습니다.
import React from 'react';
// 메인 외부 컨테이너
function HomeOuterContainer({
children
}:{
children: React.ReactNode
}) {
return (
<div
className="w-full h-full
lg:pt-[250px] md:pt-[150px] sm:pt-[100px]"
>
{children}
</div>
);
}
export default HomeOuterContainer;
페이지 전체 부분의 레이아웃을 담당하는 컴포넌트입니다.
주 목적이 레이아웃이죠.
별다른 의미가 없고 레이아웃만의 목적일 때에는 div 태그를 사용해도 됩니다.
"순수한" 컨테이너로서, 이
<div>
요소는 본질적으로 아무것도 나타내지 않습니다.
그 다음 컴포넌트입니다.
이는 화면 사진과 함께 보면 어떠한 용도인지 파악이 더 빠를 것 같으니 화면도 첨부하겠습니다.
변경전
import React from 'react';
// 메인 내부 컨테이너
function HomeInnerContainer({
children
}:{
children: React.ReactNode
}) {
return (
<div
className={"flex justify-center items-center flex-col gap-[40px]"}
>
{children}
</div>
);
}
export default HomeInnerContainer;
화면 전체에서 위와 같이 타이틀과 설명 버튼을 감싸고 있는 컨테이너입니다.
div
태그로 감싸고 있습니다.
위 요소들은 메인 및 부제 카피 그리고 메인 서비스로 이동할 수 있는 버튼을 담고 있습니다.
서로 연관되어있는 요소들을 그룹핑할 때에는 section
태그를 사용하는 것이 좋습니다.
보통 section은 제목(<h1>~<h6>
)과 함께 사용되어야 하며, 제목으로 컨텐츠 집합을 명확히 구분합니다.
그래서 다음과 같이 section으로 변경해주었습니다.
변경후
import React from 'react';
// 메인 내부 컨테이너
function HomeInnerContainer({
children
}:{
children: React.ReactNode
}) {
return (
<section
className={"flex justify-center items-center flex-col gap-[40px]"}
>
{children}
</section>
);
}
export default HomeInnerContainer;
import React from 'react';
// 메인 타이틀
function HomeTitle({
title
}:{
title: string
}) {
return (
<h1 className={"lg:text-headline2 md:text-headline3 sm:text-headline3 text-center"}>
{title}
</h1>
);
}
export default HomeTitle;
h1태그로 적절히 핵심요소를 다루고 있습니다.
변경전
import React from 'react';
// 메인 설명
function HomeDescription({
description
}:{
description: string
}) {
return (
<p
className={"lg:text-headline3 md:text-title2Bold sm:text-title2Bold"}
>
{description}
</p>
);
}
export default HomeDescription;
위는 부제목을 나타내는 컴포넌트입니다.
p태그는 특정 컨텐츠의 설명을 나타냅니다.
위 컴포넌트의 목적은 설명보다는 제목의 뉘앙스가 더 강하기 때문에 h2로 변경해보겠습니다.
변경후
import React from 'react';
// 메인 설명
function HomeDescription({
description
}:{
description: string
}) {
return (
<h2
className={"lg:text-headline3 md:text-title2Bold sm:text-title2Bold"}
>
{description}
</h2>
);
}
export default HomeDescription;
퀴즈 시작하기 페이지에서는 다음과 같이 특정 옵션을 선택하여 퀴즈를 시작할 수 있습니다.
import QuizIntroSection from "@/app/(page)/quiz/_components/quizIntroSection";
import QuizOptionForm from "@/app/(page)/quiz/_components/quizOptionForm/quizOptionForm";
import QuizStartSubTitle from "@/app/(page)/quiz/_components/quizStartSubTitle";
import QuizStartTitle from "@/app/(page)/quiz/_components/quizStartTitle";
import {Metadata} from "next";
import React from 'react';
export const dynamic = 'force-static'
export const metadata: Metadata = {
title: '퀴즈 시작하기',
description: '퀴즈를 통해 개발 지식을 테스트해 보세요.' +
'프론트 엔드, 백엔드, 데이터베이스, 네트워크, 알고리즘 등 다양한 주제의 퀴즈를 풀어보세요.',
}
// 퀴즈 시작하기 페이지
async function Page (){
return (
<>
{/*퀴즈 시작하기 페이지의 설명을 나타내는 컴포넌트*/}
<QuizIntroSection>
{/*타이틀*/}
<QuizStartTitle
title={"퀴즈 시작하기"}
/>
{/*설명*/}
<QuizStartSubTitle
description={"퀴즈를 통해 개발 지식을 테스트해 보세요!"}
/>
</QuizIntroSection>
{/*퀴즈 폼*/}
<QuizOptionForm/>
</>
);
}
export default Page;
위와 같은 구조로 구성되어있습니다.
그 중에서 QuizForm을 살펴보겠습니다.
"use server"
import QuizOptionFormContainer from "@/app/(page)/quiz/_components/quizOptionForm/quizOptionFormContainer";
import QuizOptions from "@/app/(page)/quiz/_components/quizOptionForm/quizOptions";
import QuizStartButton from "@/app/(page)/quiz/_components/quizOptionForm/quizStartButton";
import React from 'react';
// 퀴즈 옵션 폼
function QuizOptionForm() {
return (
<QuizOptionFormContainer>
{/*옵션*/}
<QuizOptions/>
{/*시작하기 버튼*/}
<QuizStartButton>
퀴즈 시작하기
</QuizStartButton>
</QuizOptionFormContainer>
);
}
export default QuizOptionForm;
"use client"
import useQuizOptionFormAction from "@/app/(page)/quiz/_hook/useQuizOptionFormAction";
import React from 'react';
// 폼 컨테이너
function QuizOptionFormContainer({
children
}:{
children:React.ReactNode
}) {
const {formAction} =useQuizOptionFormAction()
return (
<form
className={"w-full"}
action={formAction}
>
{children}
</form>
);
}
export default QuizOptionFormContainer;
"use client"
import {FIELD_OPTIONS, LANGUAGE_OPTIONS} from "@/app/(page)/quiz/constant";
import Select from "@/app/_components/select/select";
import React from 'react';
// 퀴즈 옵션 컴포넌트(분야)
function QuizOptions() {
const [option,setOption] = React.useState<{field:string,lang:string}>({field:FIELD_OPTIONS[0].value,lang:LANGUAGE_OPTIONS[0].value});
function handleOptionChange(value:string,key:"field"|"lang"){
setOption({...option,[key]:value})
}
return (
<div className={"flex flex-col gap-10 w-full"}>
{/*분야*/}
<>
<Select
label={"분야"}
options={FIELD_OPTIONS}
handleOptionChange={(value) => handleOptionChange(value as string, "field")}
/>
<input
type={"hidden"}
name={"field"}
value={option.field}
/>
</>
</div>
);
}
export default QuizOptions;
위 중에서 QuizOptions
를 살펴볼게요.
퀴즈 옵션들 관련한 컴포넌트입니다.
이를 div 태그로 감싸고 있는데요.
다른 시맨틱 태그인 fieldset
로 변경해보도록 하겠습니다.
fieldset은 관련 있는 폼 내부 요소들을 그룹화하는 데 사용됩니다.
"use client"
import {FIELD_OPTIONS, LANGUAGE_OPTIONS} from "@/app/(page)/quiz/constant";
import Select from "@/app/_components/select/select";
import React from 'react';
// 퀴즈 옵션 컴포넌트(분야)
function QuizOptions() {
const [option,setOption] = React.useState<{field:string,lang:string}>({field:FIELD_OPTIONS[0].value,lang:LANGUAGE_OPTIONS[0].value});
function handleOptionChange(value:string,key:"field"|"lang"){
setOption({...option,[key]:value})
}
return (
<fieldset className={"flex flex-col gap-10 w-full"}>
<legend>퀴즈 옵션</legend>
{/*분야*/}
<>
<Select
label={"분야"}
options={FIELD_OPTIONS}
handleOptionChange={(value) => handleOptionChange(value as string, "field")}
/>
<input
type={"hidden"}
name={"field"}
value={option.field}
/>
</>
</fieldset>
);
}
export default QuizOptions;
위와 같인 옵션들을 fieldset으로 감싸주고 legend를 통해 퀴즈 옵션이라는 텍스트로 표현하여 어떠한 옵션인지를 나타내줬습니다.
다음과 같이 해설 페이지 이동하는 해설 버튼과 다음 문제로 이동하는 버튼이 있습니다.
import AfterCheckButtonContainer
from "@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/afterCheckButtons/afterCheckButtonContainer";
import ExplanationLink
from "@/app/(page)/quiz/(page)/[detailUrl]/_components/client/quizAnswerForm/afterCheckButtons/explanationLink";
import NextQuizLink from "@/app/(page)/quiz/_common_ui/client/nextQuizLink";
import React from 'react';
// 채점 후 버튼(해설, 다음문제)
function AfterCheckButtons() {
return (
<AfterCheckButtonContainer>
{/*해설 링크*/}
<ExplanationLink/>
{/*다음 문제 링크*/}
<NextQuizLink/>
</AfterCheckButtonContainer>
);
}
export default AfterCheckButtons;
AfterCheckButtonContainer
는 이 버튼들을 감싸주는 역할을 합니다.
import React from 'react';
function AfterCheckButtonContainer({
children
}:{
children: React.ReactNode
}) {
return (
<div
className={"flex justify-center items-center gap-2 w-full"}>
{children}
</div>
);
}
export default AfterCheckButtonContainer;
위와 같이 가운데 정렬 해주는 용도로 컨테이너를 만들어줬는데요.
아무래도 이동 관련한 버튼들이니 목적에 맞게 nav
태그로 변경해줘보겠습니다.
import React from 'react';
function AfterCheckButtonContainer({
children
}:{
children: React.ReactNode
}) {
return (
<nav
aria-label={"Quiz navigation"}
className={"flex justify-center items-center gap-2 w-full"}>
{children}
</nav>
);
}
export default AfterCheckButtonContainer;
다음은 퀴즈 해설 페이지입니다.
퀴즈에 대한 해설 페이지인데요.
이를 Fragment로 감싸고 있었습니다.
async function Page({
params
}:{
params:Params
}) {
const { detailUrl } = await params
const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)
return (
<>
{/*퀴즈 설명 타이틀*/}
<QuizExplanationTitle
title={data.metaTitle}
/>
{/*퀴즈 설명 내용*/}
<QuizExplanationContent
content={data.explanation}
/>
<ButtonContainer>
{/*돌아가기 버튼*/}
<ReturnButton returnUrl={detailUrl}/>
{/*다음 퀴즈 버튼*/}
<NextQuizLink/>
</ButtonContainer>
</>
);
}
export default Page;
위 컨텐츠들은 독립적으로 분류해서 사용할 수 있어보입니다.때문에 그 특성에 맞는 article 태그로 사용해보겠습니다.
<article>
요소는 문서, 페이지, 애플리케이션, 또는 사이트 안에서 독립적으로 구분해 배포하거나 재사용할 수 있는 구획을 나타냅니다. 사용 예제로 게시판과 블로그 글, 매거진이나 뉴스 기사 등이 있습니다.
async function Page({
params
}:{
params:Params
}) {
const { detailUrl } = await params
const {data} = await quizApiHandler.fetchQuizDetailByUrl(detailUrl)
return (
<article>
{/*퀴즈 설명 타이틀*/}
<QuizExplanationTitle
title={data.metaTitle}
/>
{/*퀴즈 설명 내용*/}
<QuizExplanationContent
content={data.explanation}
/>
<ButtonContainer>
{/*돌아가기 버튼*/}
<ReturnButton returnUrl={detailUrl}/>
{/*다음 퀴즈 버튼*/}
<NextQuizLink/>
</ButtonContainer>
</article>
);
}
export default Page;
위와 같이 기존에 있는 태그들을 목적에 맞게 시맨틱 태그로 변경해봤습니다.
현재 한번에 모든 태그들을 시맨틱 태그로 리팩토링해보았는데, 추후에 컴포넌트 설계할 때에는 컴포넌트의 목적을 한번 더 생각하며 태그를 구성해야겠습니다.