This post is only available in Korean.
웹사이트 애드센스 승인받기 (no blog)
서비스 웹사이트를 만들면서 애드센스 승인을 받은 과정 및 방법을 공유합니다.
최근 '바이브 코딩' 프로젝트로 수익형 웹사이트를 제작하는 과정에서 애드센스를 승인받은 과정을 공유합니다.
초보자도 따라 만드는 Fluid Gradient Background
멋진 유체형(Fluid) 그라디언트 배경은 어려운 수학이나 그래픽 지식이 있어야만 만들 수 있을 것 같지만, 실제로는 React + Three.js 조합만 알면 누구나 구현할 수 있습니다. 이 글에서는 apps/web/src/components/FluidBackground.tsx 파일을 예로 들어, 한 줄씩 따라 해 볼 수 있도록 정리했습니다.
1. 준비물
- Node 22 이상, yarn 1.x
- React 18 프로젝트 (Next.js라면 더 편합니다)
three패키지 설치:yarn add three- 컴포넌트를 전역 배경으로 깔아둘 레이아웃 (예:
<div className="fixed inset-0">) markdownmarkdow
2. 컴포넌트 뼈대 만들기
먼저 DOM을 붙일 컨테이너와 Three.js 리소스를 추적할 ref를 만듭니다.
"use client";
import { useEffect, useRef } from "react";
import * as THREE from "three";
const FluidBackground = () => {
const containerRef = useRef<HTMLDivElement>(null);
const sceneRef = useRef<{
scene: THREE.Scene;
camera: THREE.OrthographicCamera;
renderer: THREE.WebGLRenderer;
material: THREE.ShaderMaterial;
animationId: number | null;
} | null>(null);
// useEffect 안에서 나머지 로직을 작성
};
useRef를 두 개나 쓰는 이유는 DOM 컨테이너와 Three.js 리소스를 분리해 정리하기 위함입니다. 이렇게 해야 컴포넌트가 언마운트될 때 모든 GPU 리소스를 안전하게 해제할 수 있습니다.
3. 씬, 카메라, 렌더러 세팅
const width = container.clientWidth;
const height = container.clientHeight;
const aspect = width / height;
const camera =
aspect >= 1
? new THREE.OrthographicCamera(-aspect, aspect, 1, -1, 0, 1)
: new THREE.OrthographicCamera(-1, 1, 1 / aspect, -1 / aspect, 0, 1);
const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true });
const isMobile = window.innerWidth < 768;
const pixelRatio = isMobile
? Math.min(window.devicePixelRatio, 2)
: window.devicePixelRatio;
renderer.setPixelRatio(pixelRatio);
renderer.setSize(width, height);
container.appendChild(renderer.domElement);
- OrthographicCamera를 쓰면 화면 비율과 상관없이 평면이 가득 차도록 조정하기 쉽습니다.
- 모바일에서는
pixelRatio를 제한해 성능과 배터리를 지킵니다. - 랜더러의
alpha: true로 배경이 투명해져서 다른 요소와 자연스럽게 겹칩니다.
전체 화면을 덮는 평면은 헬퍼 함수로 만들면 재사용이 쉽습니다.
const createFullCoverPlane = (aspect: number) => {
const w = aspect >= 1 ? 2 * aspect : 2;
const h = aspect >= 1 ? 2 : 2 / aspect;
return new THREE.PlaneGeometry(w, h);
};
4. 셰이더의 3단 구성
(1) 버텍스 셰이더: 평면을 화면에 뿌리기
const vertexShader = `
void main() {
gl_Position = vec4(position, 1.0);
}
`;
정말 심플합니다. 정규화된 평면 좌표를 그대로 출력만 해도 전체 화면을 덮습니다.
(2) 프래그먼트 셰이더: 유체 무늬 만들기
핵심 uniform 두 개:
u_time: 애니메이션 진행도u_resolution: 픽셀 단위 해상도 (리사이즈 때마다 갱신)
프래그먼트 셰이더 안에서는 아래 순서로 색을 만듭니다.
- 난수/프랙탈 노이즈 (
noise,smoothNoise,fbm)로 유기적인 패턴 생성 - 여러 개의
drop좌표를 만들고, 각 좌표에서 흐르는flow를 계산해 UV를 왜곡 - 블루/핑크/화이트 세 계열로 색을 나누고, 각각에 다른 확산값과 tendril(실타래) 효과를 줘서 깊이를 만듦
mix로 색상을 합치고,alpha를 살짝 낮춰 다른 요소와 어우러지게 처리
Three.js에서는 문자열 셰이더를 그대로 ShaderMaterial에 넣습니다.
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
u_time: { value: 0 },
u_resolution: {
value: new THREE.Vector2(width * pixelRatio, height * pixelRatio),
},
},
transparent: true,
blending: THREE.AdditiveBlending,
});
AdditiveBlending을 켜면 색이 겹칠수록 밝아져 네온 느낌을 얻을 수 있습니다.
(3) 메시 추가
const geometry = createFullCoverPlane(aspect);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
5. 애니메이션 루프와 리사이즈 대응
const timeIncrement = isMobile ? 0.015 : 0.02;
const animate = () => {
material.uniforms.u_time.value += timeIncrement;
renderer.render(scene, camera);
animationId = requestAnimationFrame(animate);
};
animate();
- 모바일에서는 속도를 조금 줄여 부드럽게 보이도록 했습니다.
requestAnimationFrame아이디를 저장해 두었다가 언마운트할 때cancelAnimationFrame으로 정리합니다.
리사이즈 이벤트에서는 카메라/지오메트리/렌더러/해상도를 모두 갱신해야 픽셀이 깨지지 않습니다.
const handleResize = () => {
const newWidth = container.clientWidth;
const newHeight = container.clientHeight;
const newAspect = newWidth / newHeight;
// 1) 카메라 업데이트
if (newAspect >= 1) {
camera.left = -newAspect;
camera.right = newAspect;
camera.top = 1;
camera.bottom = -1;
} else {
camera.left = -1;
camera.right = 1;
camera.top = 1 / newAspect;
camera.bottom = -1 / newAspect;
}
camera.updateProjectionMatrix();
// 2) 지오메트리와 해상도 업데이트
mesh.geometry.dispose();
mesh.geometry = createFullCoverPlane(newAspect);
renderer.setSize(newWidth, newHeight);
const newPixelRatio =
window.innerWidth < 768
? Math.min(window.devicePixelRatio, 2)
: window.devicePixelRatio;
material.uniforms.u_resolution.value.set(
newWidth * newPixelRatio,
newHeight * newPixelRatio
);
};
6. 클린업 잊지 않기
return () => {
window.removeEventListener("resize", handleResize);
if (sceneRef.current?.animationId)
cancelAnimationFrame(sceneRef.current.animationId);
sceneRef.current?.renderer.dispose();
sceneRef.current?.material.dispose();
if (container.contains(renderer.domElement)) {
container.removeChild(renderer.domElement);
}
};
- GPU 리소스는 수동으로 해제해야 메모리 누수가 생기지 않습니다.
- DOM에 붙였던
<canvas>도 직접 제거합니다.
7. 내 취향으로 커스터마이징하기
- 색상 팔레트 바꾸기
lightBlue,deepPink같은 색 벡터를 브랜드 컬러에 맞게 조정하세요. - 물결 속도 조절
timeIncrement나 각drop의sin/cos주기를 바꾸면 더 느릿하거나 빠른 움직임을 만들 수 있습니다. - 패턴 밀도 조절
fbm반복 횟수,exp(-dist * k)계수 등을 조절하면 흐릿하거나 선명한 무늬를 얻습니다. - 성능 모드
저사양 기기에서는drop개수를 줄이거나timeIncrement를 더 낮춰 프레임을 확보하세요.
8. 화면에 띄우기
레이아웃에서 아래처럼 사용하면 끝입니다.
const LandingLayout = ({ children }: { children: React.ReactNode }) => (
<div className="relative min-h-screen bg-slate-950 text-white">
<FluidBackground />
<div className="relative z-10">{children}</div>
</div>
);
마무리
FluidBackground.tsx는 복잡해 보이지만, 컨테이너 준비 → Three.js 기본 세팅 → 셰이더 정의 → 루프/리사이즈/클린업 순서로 나누면 생각보다 단순합니다. 여기까지 따라 했다면 이제 원하는 색감과 움직임으로 자유롭게 변형해 보세요. 인터랙티브한 히어로나 랜딩 페이지 배경을 만드는 데 큰 도움이 될 것입니다. 즐거운 실험 되세요!