Awaik
/ Blog
List

This post is only available in Korean.

웹사이트 애드센스 승인받기 (no blog)

서비스 웹사이트를 만들면서 애드센스 승인을 받은 과정 및 방법을 공유합니다.

Awaik
####
웹사이트 애드센스 승인받기 (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: 픽셀 단위 해상도 (리사이즈 때마다 갱신)

프래그먼트 셰이더 안에서는 아래 순서로 색을 만듭니다.

  1. 난수/프랙탈 노이즈 (noise, smoothNoise, fbm)로 유기적인 패턴 생성
  2. 여러 개의 drop 좌표를 만들고, 각 좌표에서 흐르는 flow를 계산해 UV를 왜곡
  3. 블루/핑크/화이트 세 계열로 색을 나누고, 각각에 다른 확산값과 tendril(실타래) 효과를 줘서 깊이를 만듦
  4. 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. 내 취향으로 커스터마이징하기

  1. 색상 팔레트 바꾸기
    lightBlue, deepPink 같은 색 벡터를 브랜드 컬러에 맞게 조정하세요.
  2. 물결 속도 조절
    timeIncrement나 각 dropsin/cos 주기를 바꾸면 더 느릿하거나 빠른 움직임을 만들 수 있습니다.
  3. 패턴 밀도 조절
    fbm 반복 횟수, exp(-dist * k) 계수 등을 조절하면 흐릿하거나 선명한 무늬를 얻습니다.
  4. 성능 모드
    저사양 기기에서는 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 기본 세팅 → 셰이더 정의 → 루프/리사이즈/클린업 순서로 나누면 생각보다 단순합니다. 여기까지 따라 했다면 이제 원하는 색감과 움직임으로 자유롭게 변형해 보세요. 인터랙티브한 히어로나 랜딩 페이지 배경을 만드는 데 큰 도움이 될 것입니다. 즐거운 실험 되세요!