구름이 배경 넘어로 튀어나오는 현상은 배경도 positon을 relative로 변경했더니 들어갔다. overflow-hidden는 부모 자식 같이 static이거나 아니면 둘 다 static 아니여야 적용이 되는 것 같다.
처음에 연습 삼아 index.css에 작성해서 tailwind로 수정했는데 이미지 파일을 계속 인식 못하는 에러 발생
./scr/index.css에서 에러나 난 이유는 css에서 절대 경로가 src폴더 이기 때문이다. 현재 경로에서 /image/cloud.png
를 찾아야 하므로 ./image/cloud.png
으로 고쳐주면 에러가 사라진다.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
backgroundImage: {
'hero-pattern': "url('./image/cloud.png')"
}
},
},
plugins: [],
}
section에는 h1이 있어야 웹표준에 좋지만 h1이 화면에 필요 없다면 숨김처리 해두면 좋다고 해서 h1을 넣어주고 css로 숨기 처리 해주었다.
.visuallyhidden {
position: absolute;
clip: rect(1px, 1px, 1px, 1px);
-webkit-clip-path: inset(0px 0px 99.9% 99.9%);
clip-path: inset(0px 0px 99.9% 99.9%);
overflow: hidden;
height: 1px;
width: 1px;
padding: 0;
border: 0;
}
프로젝트 시 대부분 리액트 아이콘을 사용해 왔었는데 이번에는 구글의 material-icons 사용해 보았다.
npm i @fontsource/material-icons
으로 설치 후 src/index.tsx 파일에서 패키지를 임포트 해준다. import '@fontsource/material-icons'
마지막으로 src/index.css에서 아이콘을 설정해 준다.
.material-icons {
font-family: 'Material Icons';
display: inline-block;
}
사용법은 무척 간단한데 아래처럼 span태크에 class를 material-icons으로 주고 value에 사용하고 싶은 아이콘 이름을 적어주면된다. 특정 상태를 기준으로 아이콘을 변경하고 싶다면 삼항연산자를 사용해 값을 작성해도 적용된다.
<span className="material-icons">{isNavActive ? 'close':'menu'}</span>
react-scroll를 사용해 nav기능을 구현하였다. activeClass을 사용하면 해당 스크롤 위치에 있을 때만 사용할 class를 정해 줄 수 있다. 잘 작성했는데 적용이 잘 안된다면 새로고침을 해보자.
<Link
onClick={() => setIsNavActive(!isNavActive)}
activeClass="underline decoration-solid"
spy={true}
to={to}
smooth={true}
duration={500}
>
처음에는 Nav컴포넌트 하나로 구성했다가 반복되는 list부분을 분리했다. 모바일 버전와 데스크탑 버전에 중복해서 코드가 들어가니깐 컴포넌트화해서 빼는게 좋다고 생각했지만 사실 확신은 들지 않는다.. 🥴 이런 컴포넌트 구성은 반복되면 컴포넌트화 할려고 하지만 별로 사용 안 하는 거면 그냥 두는게 더 좋은가 싶기도해서 항상 고민이다. 그래도 빼니깐 보기는 깔끔해 보인다.
import React, { FC, useState } from 'react';
import NavList from './NavList';
export const Nav: FC = () => {
const [isNavActive, setIsNavActive] = useState(false);
return (
<>
{/* 데스크탑 버전 */}
<nav className="hidden md:block md:fixed md:z-50 md:w-full md:p-5">
<ul className="flex justify-end blackItalic gap-7">
<NavList isNavActive={isNavActive} setIsNavActive={setIsNavActive} />
</ul>
</nav>
{/* 모바일 버전 */}
<nav className="fixed z-50 w-full p-5 md:hidden">
<div
className="relative z-40 flex items-center justify-center w-10 h-10 rounded-full highlighted"
onClick={() => setIsNavActive(!isNavActive)}
>
<span className="text-2xl font-semibold material-icons">{isNavActive ? 'close':'menu'}</span>
</div>
{isNavActive && (
<div className="absolute inset-0 w-full h-screen bg-white/90">
<ul className="flex flex-col items-center justify-center h-full gap-10 blackItalic">
<NavList isNavActive={isNavActive} setIsNavActive={setIsNavActive} />
</ul>
</div>
)}
</nav>
</>
);
};
export default Nav;
Title이 스크롤에 맞춰서 밑줄은 옆으로 글자는 위로오는 애니메이션을 넣고 싶었다. 그래서 화면에 나오면 true로 사라지면 false로 알려주는 커스텀훅을 만들어 보기로 했다.
useRef로 여러개 요소를 사용할 수 있다고 해서 useRef 타입을 Element[]
으로 하였다. 그리고 그 요소들의 화면 보임 여부를 저장할 배열인 상태를 추가 했다.
import { useEffect, useRef, useState } from 'react';
const useScrollObserver = (initalValue:boolean[]): [React.MutableRefObject<Element[]>, boolean[]] => {
const targetRef = useRef<Element[]>([]);
const [isVisible, setIsVisible] = useState<boolean[]>(initalValue);
useEffect(() => {
const observer = new IntersectionObserver ( entries => {
const newIsVisible = isVisible
entries.forEach( (entry, idx) => {
newIsVisible[idx] =(entry.isIntersecting)
})
setIsVisible(newIsVisible)
})
if (targetRef.current) {
targetRef.current.forEach( el => observer.observe(el));
}
return () => {
if (targetRef.current) {
targetRef.current.forEach( el => observer.unobserve(el));
}
};
}, []);
return [targetRef, isVisible];
};
export default useScrollObserver;
나는 ref가 [A, B, C, D]이라면 [ture, false, false, false]인 상태를 기대했는데..
배열 길이가 계속 다르고 첫번째 요소만 애니메이션이 적용이 되었다. 확인해 보니 observer가 요소마다 각각 적용되었으므로 요소 한 개의 값이 바뀌었다면 한 개의 isIntersecting만 확인해 idx로 구분했던게 소용이 없었던 것 이다.
그래서 idx가 아닌 키 값으로 구분해 주어야 할 것같은데 key로 어떤 걸 사용할지 엄청 고민하다가 target.id를 가져올 수 있길래 title을 id로 추가하고 그 값을 기준으로 상태를 변경하였다.
import { useEffect, useRef, useState } from 'react';
const useScrollObserver = (): [React.MutableRefObject<Element[]>, Record<string, boolean>] => {
const targetRef = useRef<Element[]>([]);
const [isVisible, setIsVisible] = useState<Record<string, boolean>>({});
const observer = new IntersectionObserver((entries) => {
const newIsVisible: Record<string, boolean> = {};
entries.forEach((entry) => {
const targetId = entry.target.id;
if (targetId) {
newIsVisible[targetId] = entry.isIntersecting;
}
});
setIsVisible((prevIsVisible) => ({
...prevIsVisible,
...newIsVisible,
}));
});
useEffect(() => {
if (targetRef.current) {
targetRef.current.forEach((el) => {
observer.observe(el);
});
}
return () => {
if (targetRef.current) {
targetRef.current.forEach((el) => {
observer.unobserve(el);
});
}
};
}, [targetRef]);
return [targetRef, isVisible];
};
export default useScrollObserver;
tailwind 처음에는 사용 어색한데 쓰다보니 편하다. class 뭐로 해야하나 고민 안 해도 되서 너무 좋다. useScrollObserver 만드는데 생각보다 시간이 많이 걸렸다😵 역시 라이브러리가 짱인 것이다. 다음에는 framer-motion 써야지ㅋㅋ