그레이니 그라디언트와 노이즈 텍스처, 실제로 만드는 방법

노이즈는 결국 feTurbulence에서 나와요

웹에서 쓰는 그레인 효과는 거의 다 한 곳에서 출발해요. SVG 필터 프리미티브인 feTurbulence예요. 펄린 노이즈를 R·G·B·A 네 채널에 직접 만들어내는데, 원본 이미지가 필요 없는 생성기라서 빈 사각형 하나만 던져주면 알아서 채워줘요.

핵심은 속성 두 개예요. type은 turbulence와 fractalNoise 중에 고르는 값이에요. turbulence는 각 노이즈 층의 절댓값을 더해서 값이 0 근처에 몰리고 부호가 뒤집히는 자리마다 날카로운 능선이 생겨요. 연기나 대리석 무늬처럼 결이 살아 있는 모양이죠. fractalNoise는 부호를 그대로 두고 더해서 값이 중간값 주위로 모이고, 우리가 그레인이라고 부르는 부드럽고 고른 구름이 나와요. 질감 작업이라면 거의 항상 fractalNoise 쪽이 맞아요.

baseFrequency는 노이즈 한 칸의 크기를 정하는데, 단위당 주기 수라서 크기와 반대로 움직여요. 값이 클수록 무늬가 잘아진다는 뜻이에요. 0.60.9쯤이면 픽셀 단위에 가까운 촘촘한 점이 되고 필름 그레인 하면 떠오르는 그 느낌이 나와요. 0.02까지 낮추면 크고 느릿한 구름이 되고요. 숫자를 둘로 주면 가로·세로 주파수를 따로 잡아 노이즈가 줄무늬처럼 늘어나는데, 스캔 라인에는 쓸 만해도 그레인에는 거슬리기만 해요. numOctaves는 몇 겹을 더할지예요. 한 겹 올라갈 때마다 주파수는 두 배, 진폭은 절반이 돼요. 1이면 한 가지 크기의 디테일만 있고 34면 굵은 무늬 위에 잔 무늬가 얹혀 훨씬 자연스러워져요. 다만 옥타브 하나가 픽셀 전체를 한 번 더 훑는 계산이고, 4를 넘으면 새로 더해지는 층이 눈에 보이는 수준 아래인 경우가 많아요. 실무에서 챙길 속성이 둘 더 있어요. seed는 난수 순서를 고정해서 같은 값이면 같은 무늬가 다시 나오게 하고, stitchTiles="stitch"는 필터 영역 안에 주기가 정수로 떨어지도록 주파수를 조정해 이음선 없이 반복할 수 있게 해줘요.

한 번쯤 꼭 겪는 함정도 있어요. 알파 채널에도 노이즈가 들어가서, feTurbulence를 그냥 사각형에 걸면 회색 그레인이 아니라 알록달록 반투명한 색종이 조각처럼 보여요. 뒤에 <feColorMatrix type="saturate" values="0"/>를 이어 붙이면 색이 빠지면서 기대했던 흑백 노이즈가 나와요.

화면에 얹는 두 가지 방법

만든 필터를 적용하는 방법은 둘인데 동작이 꽤 달라요. 직접적인 쪽은 인라인 <svg> 필터를 두고 filter: url(#grain)으로 거는 방식이에요. 브라우저가 그 요소를 다시 그릴 때마다 필터를 새로 계산하고, 자식 요소까지 전부 영향을 받아요. 안에 있는 글자도 같이 뭉개진다는 뜻이라 배경용으로는 맞지 않아요.

실제로 많이 쓰는 방법은 SVG 전체를 데이터 URI로 만들어 의사요소 배경에 넣는 쪽이에요.

.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0.15;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

이러면 SVG가 그냥 이미지가 돼요. 브라우저가 한 번 래스터화해서 다른 이미지처럼 캐시하니까 다시 그릴 때 비용이 거의 없고, 아래 글자를 건드리지도 않고, 용량은 수백 바이트 정도예요.

인코딩에서 걸리는 지점이 몇 개 있어요. SVG 안쪽은 작은따옴표로 써야 바깥 CSS의 url("…")과 부딪히지 않아요. base64보다 퍼센트 인코딩이 용량도 작고 읽기도 편하고요. 그리고 #은 반드시 %23으로 바꿔야 해요. 손으로 고칠 때 제일 먼저 깨지는 게 색상 코드와 url(#n) 참조인 이유가 여기 있어요. <svg>에 width·height를 적지 않으면 배경 영역에 맞춰 늘어나서 반복 없이 한 덩어리 노이즈가 박스를 통째로 덮어요. 타일처럼 반복시키고 싶을 때만 크기를 적고 background-repeat을 함께 쓰면 돼요.

PNG 타일은 확실한 대신 용량을 내줘요

더 오래된 방식은 노이즈를 PNG로 만들어 까는 거예요. 확실한 장점이 하나 있어요. 결과가 정확하다는 점이에요. 같은 파일이면 어느 브라우저에서든 똑같이 보이고 피그마나 포토샵에 레이어로 그대로 가져갈 수 있는데, feTurbulence는 렌더링 엔진마다 결과가 미세하게 달라지거든요. 대신 용량을 내줘야 해요. PNG는 옆 픽셀로 다음 픽셀을 예측해서 압축하는데 무작위 노이즈에는 정의상 예측 가능한 이웃이 없어요. 그래서 512×512 흑백 노이즈 PNG가 수백 KB까지 가는 일이 흔한데, 같은 느낌을 내는 SVG는 수백 바이트예요.

반복도 걸림돌이에요. 무작위로 만든 타일은 마주 보는 변이 맞아떨어질 이유가 없어요. 픽셀 단위로 잔 그레인이면 아무도 눈치채지 못하지만, 굵은 그레인이면 눈에 띄는 덩어리가 일정한 격자마다 다시 나타나고 한 번 보이기 시작하면 계속 보여요. 굵은 그레인은 큰 텍스처를 한 장 만들어 한 번만 쓰는 게 맞고 잔 그레인은 반복시켜도 괜찮아요. 그리고 노이즈 PNG는 사실상 1:1로만 보여줄 수 있어요. 크기를 바꾸는 순간 점이 뭉개지거나 평균이 나서 사라지거든요.

밴딩을 노이즈로 가리는 원리

밴딩은 색을 정수로 끊어 쓰기 때문에 생기는 문제예요. 8비트면 채널당 256단계인데, 1200픽셀을 지나며 30단계밖에 안 변하는 그라디언트라면 40픽셀마다 한 번씩만 값이 바뀔 수 있어요. 게다가 눈은 경계를 과장해서 보는 성질이 있어서 그 한 단계 차이가 뚜렷한 줄무늬로 보여요.

노이즈는 이걸 디더링으로 풀어요. 한두 단계짜리 무작위 값을 얹으면 경계 근처 픽셀들이 무작위로 이쪽저쪽에 떨어져서, 반듯하던 경계선이 흩어지고 눈이 잡을 선 자체가 없어져요. 중요한 건 필요한 양이 아주 적다는 점이에요. 밴딩 잡는 용도의 그레인은 눈에 안 보여야 정상이고, 투명도로 치면 2~6% 정도면 충분해요. 그레인이 눈에 보인다면 밴딩 해결을 넘어 질감 표현으로 넘어간 거예요. 질감을 주는 것도 좋은 선택이지만, 밴딩을 잡다가 얼떨결에 넘어가는 게 아니라 따로 결정할 문제예요.

강도는 투명도가 먼저, 블렌드 모드가 다음이에요

가장 정직한 조절 손잡이는 오버레이 요소의 opacity예요. 그레이니 그라디언트라면 0.1~0.25 사이면 대체로 충분해요. mix-blend-mode는 양이 아니라 성격을 바꿔요. overlay는 중간 회색을 그대로 두고 밝은 쪽과 어두운 쪽만 벌리면서 색상은 지켜줘서 그레이니 그라디언트의 기본값처럼 쓰이고, soft-light는 같은 방향의 순한 버전이에요. multiply는 어둡게만 만들어서 그레인이 그림자 쪽에 내려앉아 인쇄물이나 종이 느낌이 나고, screen은 밝게만 만들어서 밝은 영역에 얹히고 필름이나 먼지 같은 인상을 줘요.

자주 걸리는 함정이 하나 있어요. mix-blend-mode는 가장 가까운 쌓임 맥락 안에서 뒷배경과 섞이는데, 부모에 opacity나 transform, filter, isolation: isolate가 걸려 있으면 그 부모가 쌓임 맥락이 돼요. 그러면 그레인이 섞일 대상이 사라져서 그냥 밋밋한 회색으로 보여요. 그라디언트와 노이즈가 같은 요소의 배경 두 겹이라면 background-blend-mode를 쓰는 게 이 문제를 통째로 피하는 방법이에요. 성능도 같은 맥락이에요. 노이즈는 살아 있는 filter가 아니라 배경 이미지로 넣고, 오버레이에는 pointer-events: none을 걸고, 애니메이션은 걸지 않는 게 좋아요. 화면 전체를 덮는 그레인 레이어가 스크롤할 때마다 다시 그려지면 멀쩡하던 페이지가 끊기기 시작해요.

그레인이 조용히 무너지는 곳

먼저 고해상도 화면이에요. 그레인은 특정 픽셀 크기에 붙어 있는 효과인데 CSS 픽셀과 기기 픽셀은 다르죠. 1배로 만든 노이즈 PNG를 2배 화면에 올리면 점 하나가 2×2 블록으로 그려져서 의도보다 굵고 흐릿해져요. 그렇다고 2배로 만들어 background-size로 절반으로 줄이면, 축소하면서 밝은 픽셀과 어두운 픽셀이 서로 평균 나버려요. 대비가 무너지면서 질감이 거의 사라지죠. “레티나에서 그레인이 안 보여요” 하는 이야기가 여기서 나오는데 버그가 아니라 산수예요. feTurbulence는 주파수를 사용자 단위로 잡아서 이 문제를 어느 정도 피해가요. 무늬 크기는 CSS 픽셀 기준으로 유지되면서 래스터화는 기기 해상도로 되거든요. 그래도 1배와 2배에서 같은 느낌으로 보이지는 않아요. 그레인이 가장 약하게 보이는 고밀도 화면에서 값을 잡고, 1배 모니터에서 너무 거칠어지지 않았는지 확인하는 순서가 안전해요.

다크 모드도 값을 따로 잡아야 해요. 그레인은 중간값을 기준으로 양쪽으로 값을 흔드는 건데 사람 눈은 양쪽을 똑같이 받아들이지 않아요. 밝은 배경에서는 어두운 점이, 어두운 배경에서는 밝은 점이 주로 보이는데 검정에 가까운 바탕 위의 밝은 점이 훨씬 도드라져요. 라이트 모드에서 은은하던 투명도가 다크 모드에서는 지직거리는 화면처럼 보이는 이유예요. 3분의 1에서 절반쯤 낮추는 건 흔한 보정이에요. 게다가 어두운 화면이 밴딩도 제일 심해요. 검정에 가까운 그라디언트는 쓸 수 있는 단계가 몇 개 안 되거든요. 디더링은 더 필요한데 질감은 덜 보여야 하는 상황인 거죠. 그레인 강도를 커스텀 속성으로 빼두고 다크 모드 블록에서 덮어쓰는 편이 한 값으로 양쪽을 버티는 것보다 나아요.

접근성도 한 줄 짚고 갈 만해요. 명도 대비는 단색 두 개 사이에서 계산하는 값이라 배경에 질감이 있으면 실제 대비가 픽셀마다 달라지고 그중 제일 나쁜 자리가 기준이 돼요. 글자가 올라가는 영역은 그레인을 약하게 가져가고, 가능하면 그레이니 배경 위에 글자를 바로 얹기보다 단색 패널을 하나 두고 그 위에 올리는 게 좋아요.

어떤 결과물을 언제 쓰면 좋을까요

노이즈 텍스처 생성기는 위에서 이야기한 두 갈래를 다 다뤄요. 노이즈 탭에서는 픽셀마다 값을 따로 뽑는 파인 그레인과, 값 노이즈를 3옥타브로 겹쳐 만드는 스무스 노이즈를 고를 수 있어요. 스무스 쪽은 그레인 크기를 4256px로 조절할 수 있고, 중간 회색을 기준으로 대비를 늘렸다 줄였다 하는 강도 슬라이더와 모노크롬 옵션, 틴트 색상도 있어요. 투명 모드를 켜면 RGB는 틴트 색으로 고정되고 노이즈가 알파를 맡는데, 피그마나 포토샵에서 별도 레이어로 얹어 블렌드 모드를 직접 정하고 싶을 때 받으면 좋은 형태예요. 그라디언트 탭은 선형·방사형 그라디언트에 시작·끝 색과 선택적인 중간 색을 두고, 그 위에 그레인을 오버레이로 합성해요. 양과 크기를 따로 조절할 수 있고요. 캔버스는 한 변 164096px까지 되고, 다시 만들기를 누르면 시드가 새로 뽑혀요.

내보내기는 두 가지인데 앞에서 이야기한 갈림길이 그대로 반영돼 있어요. PNG 다운로드는 화면에 보이는 픽셀을 원본 해상도 그대로 저장해서, 디자인 파일이나 목업처럼 어디서든 똑같이 보여야 하는 자리에 맞아요. CSS 코드 복사는 feTurbulence 데이터 URI를 주고 그라디언트 탭에서는 background-blend-mode: overlay까지 함께 넣어주는데, 이미지 파일을 아예 얹고 싶지 않은 실제 사이트 배경에 어울려요. 다만 이건 PNG를 그대로 옮긴 게 아니라 비슷하게 흉내 낸 결과예요. SVG 필터와 이 도구의 노이즈 엔진은 서로 다른 알고리즘이거든요.

남은 한계도 솔직하게 적어둘게요. PNG는 고른 크기로 만든 한 장짜리 텍스처지 이음선 없는 타일이 아니라서, 반복시키기보다 그 크기 그대로 쓰는 게 맞아요. 시드는 다시 뽑을 수만 있고 원하는 값을 직접 입력하지는 못해요. 애니메이션이나 가로·세로 주파수를 따로 주는 기능도 없고요. 모든 계산은 지금 쓰는 기기 안에서 끝나서 업로드 자체가 없는데, 새로 만들어내는 텍스처라 프라이버시 문제라기보다는 아무것도 오갈 필요 없이 바로 결과가 나온다는 쪽에 가까워요.