웹앱을 구성하는 강의가 있어서 쭈욱 따라해 보고 있다.
( 현재 주간에 교육받는 과정에서 첫번째 프로젝트가 있었던 탓에, 거의 클론 코딩 수준으로 따라해보는 중이다)
1. 모바일 기기에서 사이즈 표현
휴대용 모바일 기기의 사양한 사이즈 - 기기별로 비율늘리기(대각선 방향 비율늘리기)
태블릿 - 적정 폰트사이즈 이상인 경우 input등은 가로늘리기 ( 사진등은 여러 개를 표시 )
데스크톱 - 휴대용 모바일용 화면에서 가로 늘리기와 여백 늘리기를 통해 대응
1) 비율 늘리기 ( 상대 단위 활용 : em, rem, vw, vh, %, 절대 단위 : 1px(0.026cm))
- em : 해당 단위가 사용되고 있는 요소의 font-size에 연동 (폰트 사이즈에 설정에 따라 변경 )
- rem : 루트(html)요소의 font-size에 연동 (default 16px)
html의 font-size를 조정할 경우 동적 사이즈가 달라짐
예) 디자인에서 16px의 폰트로 표본 디자인이의 핸드폰 가로길이를 360px인 경우
특정 모바일 휴대기기의 vw(100%)를 360과 비교, 폰트사이즈 = 100vw / 360 * 16
//Next의 Global CSS에 font-size기준설정
html {
font-size: calc ((40 / 9) * 1vw)
}
2. 헤더 구성
- 화면 상단에 Top Layer로 구성하여 콘텐츠와 겹치게 구성 (fixed로 위치고정)
- 아래에 콘텐츠로 인해 스크롤이 있는 경우 해당층에 헤더와 같은 크기의 공간을 확보
( 헤더가 콘텐츠와 서로 의존하지 않도록 함 )
- 콘텐츠가 지도나 사진인 경우 투명하게 처리 ( underlayer만들 필요없음 )
(1) 글로벌 헤더
- 전체 페이지에 통용되는 헤더 구성, 설정 영역의 컴퍼넌트를 구성해주고 children(구조분해할당으로)을 props로 할당
** 헤더/푸터와 함께 구현하여 전체 레이아웃을 표현
(2) 로컬 헤더
- 헤더가 페이지의 톡특한 특성을 반영하도록 구성하여 유지/보수 편의성 확보
- unique한 값 ( 예: 통신을 통해 받아온 value나 동적페이지 구성 ) 혹은 헤더에 검색이나 북마트 등의 연동작업이 있는 경우
(3) 특수한 헤더구성
- 지도, 맵등의 경우 투명한 헤더 구성 ( 불투명한 헤더에 하단의 layer에 공간을 구성하여 콘텐츠 영역과 헤더가 간섭하는 것을 방지 )
3. 푸터 구성
- 통상 콘텐츠가 짧으면 바닥, 콘텐츠 길면 스크롤 하단배치
- 콘텐츠가 짧은 경우 푸터와의 사이에 길이 조정이 가능한 요소를 배치(푸터를 내려줌 )
화면 전체를 하나의 객체로 구성해서 flex-direction설정
➡ 화면의 전체 height설정 (기기의 vh를 minHeight로 하여 스크롤도 가능하도록 설정)
➡ div추가 (flex 설정, 텐츠와 푸터사이의 공간이 생기는 경우 푸터를 아래로 밀어냄)
** Layout에 Footer를 배치할 때 추가된 div와 footer가 특정 요소로 감싸지지 않고
전체 화면의 flex적용을 받을수 있도록 주의
프로젝트 전체의 레이아웃 (layout.tsx) 표시해 줄 페이지의 레이아웃 설정(commons/index.tsx)
//app폴더 내 기본레이아웃 구성, Layout.tsx
import type { Metadata } from "next";
... 기타 구성
import LayoutFooter from "@/commons/layout/02-05-layout-footer";
.. 폰트구성
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<LayoutFooter>{children}</LayoutFooter>
</body>
</html>
);
}
//LayoutFooter구성 (헤더와 푸터를 구성해서 전체 레이아웃 구성)
import { HeaderGlobal } from "./header-2-4";
export default function LayoutFooter({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<div
style={{
display: "flex",
flexDirection: "column",
minHeight: "100vh",
width: "100vw",
}}
>
<HeaderGlobal />
{children}
</div>
</>
);
}
4. 페이지별 헤더와 푸터의 옵션 (commons/constants)
헤더(commons/headers) - Global헤더와 Local헤더 구분(동적인 페이지)
푸터(commons/footer) - 콘텐츠의 길이와 관계없이 화면의 하단에 배치
//헤더와 푸터의 페이지별 표시 옵션 작성
import { IHeaderOptions } from "@/commons/types/types";
export const HEADER_OPTIONS = (params: {
id: string | null;
}): IHeaderOptions => ({
GLOBAL: {
"/section02/02-02-layout-header-global": {
hasLogo: true,
hasBack: false,
title: "게시글 등록",
},
"/section02/02-04-layout-header-transparent": {
hasLogo: true,
hasBack: true,
title: "사진이 보여요",
isTransparent: true,
},
"/section02/02-04-layout-header-opaque": {
hasLogo: true,
hasBack: false,
title: "게시글 등록",
isTransparent: false, //생략가능
},
"/section02/02-05-layout-footer": {
hasLogo: true,
hasBack: true,
title: "등록페이지",
isTransparent: false, //생략가능
},
},
LOCAL: {
[`/section02/02-03-layout-header-local/${params.id}`]: {
hasLogo: true,
hasBack: true,
title: "",
},
},
});
//헤더 표시 부분 작성
"use client";
import { usePathname } from "next/navigation";
import { HEADER_OPTIONS } from "./constants";
export default function HeaderGlobal() {
const pathname = usePathname();
const options = HEADER_OPTIONS.GLOBAL[pathname];
return (
<>
<header
style={{
width: "100vw",
height: "3.125",
backgroundColor: "yellow",
display: "flex",
gap: "0.3125rem",
}}
>
{options.hasLogo && <div>로고</div>}
{options.hasBack && <div>[뒤로가기버튼]</div>}
{options.title && <div>{options.title}</div>}
</header>
</>
);
}
//헤더 구성, Header Global내에 Local Header와 Glocal Header
"use client";
import { useParams, usePathname } from "next/navigation";
import { HEADER_OPTIONS } from "./constants2-5";
import { IHeaderOption } from "@/commons/types/types";
import { ReactNode } from "react";
const HeaderBase = ({
hasLogo,
hasBack,
title,
isTransparent,
children,
}: IHeaderOption) => {
console.log(hasLogo, hasBack, title, isTransparent);
return (
<>
<header
style={{
width: "100vw",
height: "3.125rem",
display: "flex",
flexDirection: "row",
gap: "0.3125rem",
backgroundColor: isTransparent ? "transparent" : "yellow",
position: "fixed",
}}
>
{hasLogo && <div>헤더로고</div>}
{hasBack && <div>[뒤로가기 버튼]</div>}
{title && <div>제목: {title}</div>}
{children && <div>{children}</div>}
</header>
{isTransparent ? (
<></>
) : (
<div
style={{
width: "100vw",
height: "3.125rem",
}}
></div>
)}
</>
);
};
export function HeaderGlobal() {
const pathname = usePathname();
const params = useParams<{ id: string }>();
const options = HEADER_OPTIONS(params).GLOBAL[pathname];
return (
<div style={{ display: options ? "block" : "none" }}>
<HeaderBase {...options} />
</div>
);
}
export function Header({ children, ...rest }: { children: ReactNode }) {
const pathname = usePathname();
const params = useParams<{ id: string }>();
const options = HEADER_OPTIONS(params).LOCAL[pathname];
return (
<div style={{ display: options ? "block" : "none" }}>
<HeaderBase {...options} {...rest}>
{children}
</HeaderBase>
</div>
);
}
//푸터 구성 (commons/footer)
import { ReactNode } from "react";
export default function Footer({ children }: { children: ReactNode }) {
return (
<>
<div style={{ flex: 1 }}></div>
<footer
style={{
display: "flex",
flexDirection: "column",
width: "100vw",
height: "3.125rem",
backgroundColor: "cyan",
}}
>
<div>footer</div>
{children}
</footer>
</>
);
}
4. 웹 페이지 내용 구성
- 콘텐츠가 짧은 경우 푸터가 아래에 표시 ( 버튼을 클릭하면 요소가 추가, 내용이 화면의 크기를 초과하면 하단 스크롤 표시 )
"use client";
import Footer from "@/commons/layout/02-05-layout-footer/footer";
import { useState } from "react";
export default function LayOutHeaderTransparent() {
const [islong, setIsLong] = useState(false);
const onClickToggle = () => {
setIsLong((prev) => !prev);
};
const arrayLong = new Array(30).fill(1);
return (
<>
<main>
<button
onClick={onClickToggle}
style={{ borderWidth: "1px", borderColor: "gray" }}
>
숏컨텐츠 / 롱컨텐츠 (토글)버튼
</button>
<hr />
{islong &&
arrayLong.map(() => (
<>
제목 : <input type="text" />
<br />
내용 : <input type="text" />
<br />
작성자 : <input type="text" />
<br />
</>
))}
제목 : <input type="text" />
<br />
내용 : <input type="text" />
<br />
작성자 : <input type="text" />
<br />
</main>
<Footer>
<button>등록하기 </button>
</Footer>
</>
);
}
** 페이지 렌더링 결과 (아이폰 13)
- 헤더는 윗부분에 fix하여 층을 달리하여 띄우고 하단에 div를 배치하여 스크롤에 밀리지 않게구성
- 푸터는 콘텐츠가 짧은 경우에도 UI하단에 배치되도록 구성
___________________________________________
https://nekocalc.com/px-to-rem-converter