코드 한 줄로 할머니들의 손맛을? CSS로 그린 세 가지 부엌 이야기
2026. 8. 2.
코드 한 줄로 할머니들의 손맛을? CSS로 그린 세 가지 부엌 이야기
안녕하세요, 10년 차 IT 실무자이자 테크 블로거, 피위(Simphiwe Twala)입니다. 최근 Frontend Challenge: Comfort Food Edition에 '완벽한 랜딩(Perfect Landing)'이라는 주제로 참가했습니다. 이 프로젝트는 단순한 과제 제출을 넘어, 제 웹 개발 여정에 의미 있는 이정표가 된 작업이었기에 여러분께 소개하려 합니다.
무엇을 만들었을까요?
저는 세상에 존재하지 않는, 가상의 가족 식당을 위한 랜딩 페이지를 만들었습니다. 이 식당에는 세 분의 할머니가 요리사로 계세요. '오우마(Ouma)'는 더반과 케이프 지방의 음식을, '테타(Teta)'는 레반트와 섬 지역의 요리를, 그리고 '논나(Nonna)'는 나폴리 음식을 만듭니다. 이들은 하나의 긴 식탁을 공유하지만, 누가 만든 음식이 가장 맛있는지에 대해서는 한 번도 합의한 적이 없습니다.
프로젝트를 기획하면서 가장 중요하게 생각했던 지점은 바로 '위로 음식은 대대로 전해 내려오는 맛'이라는 철학이었어요. 이런 전제를 통해 세 가지 다른 요리를 한 메뉴에 올려도 그저 기능 시연에 그치지 않고 의미 있는 스토리를 담을 수 있었습니다. 덕분에 버니 차우, 샥슈카, 티라미수가 한 페이지에 어우러져도 어색함 없이 각자의 이야기를 품을 수 있었죠.
사용자는 메뉴를 둘러보며 자신만의 '공유 식탁'을 만들게 됩니다. 특정 요리를 선택하면 단순히 사이드바에 추가되는 것을 넘어, 페이지 상단의 식탁 일러스트 위에 해당 요리가 직접 놓입니다. 이때 각 할머니의 부엌을 상징하는 색깔의 연기가 피어오르고, 총액이 실시간으로 업데이트되며, 스크린 리더를 통한 안내도 함께 제공되죠. 선택 자체가 하나의 예술 작품이 되는 것. 이것이 제가 이 프로젝트를 통해 시험하고 싶었던 핵심 아이디어였습니다.
특히 이 페이지의 모든 요리는 CSS로만 그려졌습니다. 총 12가지 요리 모두 스프라이트 이미지, 아이콘 폰트, SVG 없이 오직 CSS 코드로만 탄생했죠. 페이지 상단의 커버 일러스트만이 유일한 이미지 파일입니다.
마법 같은 요리 시스템: CSS로 그리는 맛
이 프로젝트에서 가장 자랑스럽게 생각하는 부분입니다. 모든 요리는 놀랍도록 단순하게 단 여섯 개의 공통 요소로 구성되어 있습니다.
<span class="dish dish--shakshuka">
<span class="dish__steam"><i></i><i></i><i></i></span>
<span class="dish__vessel"></span>
<span class="dish__base"></span>
<span class="dish__sauce"></span>
<span class="dish__accent"></span>
<span class="dish__accent2"></span>
<span class="dish__garnish"></span>
</span>
흥미로운 점은 각 요리의 CSS 수정자(modifier)가 새로운 기하학적 형태를 추가하지 않는다는 것입니다. 대신, 미리 정의된 CSS 커스텀 속성(custom properties)의 값만 재정의할 뿐입니다. 이 여섯 가지 공유 요소들이 완전히 다른 요리로 변신하는 비결이죠.
.dish--shakshuka {
--d-vessel: #3b3936; /* 무쇠 팬 */
--d-base: #cf3f24; /* 토마토와 고추 */
--d-sauce-bg: /* 두 개의 노른자, 한 레이어에 */
radial-gradient(circle .085em at 39% 45%, #ffd84a 0 68%, #e39b16 69% 86%, transparent 87%), radial-gradient(circle .075em at 58% 61%, #ffd84a 0 68%, #e39b16 69% 86%, transparent 87%);
--d-accent-shape: inset(46% 0% 48% 88% round .03em); /* 팬 손잡이 */
--d-specks: -.14em -.05em 0 var(--d-speck), … /* 고수 */
}
이 시스템을 통해 두 가지 중요한 이점을 얻을 수 있었습니다.
첫째, 요리 내의 모든 요소가 em 단위로 측정됩니다. 덕분에 하나의 font-size 속성만으로 전체 일러스트레이션의 크기를 조절할 수 있습니다. 스토리 카드에서는 font-size: 6rem으로, 메뉴 카드에서는 clamp(5.5rem, 17vw, 8.5rem)로, 그리고 식탁 위에 놓일 때는 4.4rem으로 말이죠. 같은 마크업으로 세 가지 다른 크기를 표현하며, 아무것도 다시 그릴 필요가 없었습니다.
둘째, 가니시(고명)는 단 하나의 요소로 처리됩니다. 후추, 고수, 피스타치오, 코코아 가루 등 모든 점들은 box-shadow 목록으로 여러 번 복제되어 표현됩니다. em 단위의 오프셋을 사용했기에, 뿌려진 가니시의 산포도도 함께 스케일링됩니다. 특히 각 그림자의 네 번째 값인 spread는 하나의 점이 여러 크기의 가루로 보이게 하는 핵심 역할을 합니다.
--d-specks:
-.15em -.07em 0 .003em var(--d-speck), -.03em -.13em 0 0 var(--d-speck),
.09em -.10em 0 .004em var(--d-speck), .16em .00em 0 -.002em var(--d-speck);
이 코드는 카치오 에 페페(cacio e pepe) 위의 검은 후추를 표현한 것입니다.
접근성은 기본! 모두를 위한 따뜻한 식탁
이 챌린지는 접근성 평가가 최우선이었기에, 나중에 감사(Audit)하는 방식이 아니라 처음부터 설계 단계에 접근성을 포함했습니다.
- 요리 선택은 실제
<form>태그를 사용했습니다. 각 할머니의 부엌마다 하나의<fieldset>과<legend>가 있고, 각 요리에는 실제<input type="checkbox">를 사용했습니다. 페이지 어디에도 클릭 가능한<div>를 사용하지 않았습니다. - 카드 설명은
<label>태그 외부에 배치하고aria-describedby로 연결했습니다. 덕분에 접근 가능한 이름은 "버니 차우 R128"처럼 간결하게 유지되면서도, 필요하면 전체 설명을 들을 수 있습니다. aria-live="polite"영역을 통해 "보보티 추가. 식탁에 3개, 총 379랜드."와 같이 모든 변경 사항을 음성으로 안내합니다.- 식탁에서 요리를 제거할 경우, 포커스가 문서 아무 곳에나 떨어지는 대신 해당 요리의 체크박스로 다시 이동하도록 구현했습니다.
- 모든 일러스트레이션은
aria-hidden처리하여, 텍스트로 이미 제공되는 정보 외에 어떤 추가 정보도 전달하지 않도록 했습니다. prefers-reduced-motion: reduce설정은 모든 애니메이션을 멈추고 캔버스를 시작하지 않도록 합니다.forced-colors: active설정은 모든 아트워크를 제거하고 각 카드에 시스템 기본 테두리를 적용합니다.- 라이트 모드와 다크 모드 모두 지원합니다. 브라우저의 기본 색상은 테마에 맞춰 반응하지만, 음식 색상은 그렇지 않습니다. 어두운 밤에도 토마토는 여전히 붉은색이니까요.
라이트하우스(Lighthouse) 검사 결과, 접근성 100점, 모범 사례 100점, SEO 100점, 성능 99점을 기록했고, axe-core 검사에서는 단 하나의 위반 사항도 발견되지 않았습니다. 이는 라이트 모드, 다크 모드, 그리고 요리가 식탁 위에 놓인 모든 상황에서 동일하게 나타난 결과입니다.
직접 경험해보세요!
프레임워크나 외부 라이브러리, 별도의 빌드 과정 없이 순수 HTML, CSS, JavaScript로만 만들었습니다. GitHub 저장소를 클론(Clone)한 후 index.html 파일을 바로 열어보세요. 이 간결함이 제 작업의 핵심 철학이기도 합니다.
개발 여정에서 얻은 교훈들
세 가지 다른 요리를 하나의 메뉴에 담으려면 그럴싸한 이유가 필요했습니다. 처음에는 단순히 '요리 전환기' 형태로 구현해볼까 했지만, 어딘가 생명력이 부족하고 따뜻한 위로의 느낌이 살지 않더군요. 그러다 할머니 세 분이 한 부엌을 공유한다는 아이디어를 떠올리자마자, 메뉴는 단순한 필터 데모를 넘어 각자의 사연과 정겨운 논쟁이 담긴 가족 이야기가 되었습니다. 이때부터는 글을 쓰고 코드를 짜는 모든 과정이 훨씬 자연스러워졌죠.
요리 시스템도 두 번째 시도 끝에 정착했습니다. 처음에는 버니 차우를 자유롭게 그려봤는데, 대략 40줄짜리 CSS 코드가 필요했어요. 샥슈카도 같은 방식으로 그리고 나니 문득 깨달았죠. '이걸 11번 더 반복하면 아무도 손대기 싫어할 800줄짜리 코드가 되겠군!' 순간 식은땀이 흘렀습니다. 그래서 모든 요리가 공통적으로 무엇을 갖는지 다시 고민했습니다. 그 결과, 음식을 담는 용기, 바닥에 깔리는 것, 위에 부어지는 소스, 그리고 각 요리를 특별하게 만드는 한두 가지 악센트와 뿌려지는 점들, 이렇게 여섯 가지 요소로 추릴 수 있었습니다.
그 후로는 새로운 요리를 추가하는 것이 단 15줄의 커스텀 속성만으로 가능해졌습니다. 이 시스템은 오히려 새로운 아이디어를 제안해주기도 했습니다. 예를 들어 --d-accent-bg가 전체 배경 값을 받을 수 있게 되자, 남아공 소시지 '워스(wors)'는 radial-gradient 링이 되고, 할루미 치즈는 repeating-linear-gradient로 그릴 자국을 표현할 수 있게 되었죠.
em 단위를 활용하여 크기를 조절한 것은 가장 큰 효과를 거둔 결정이었습니다. 사실 처음부터 메뉴에 선택된 요리들이 미리보기처럼 작은 크기로 식탁 위에 놓일 것을 염두에 둔 건 아니었어요. 이 패널을 구현할 때 .dish--sm { --d-size: 4.4rem } 단 한 줄만으로 모든 요리의 크기가 완벽하게 조절되는 걸 보고 저조차도 깜짝 놀랐습니다. 픽셀 단위를 고집했다면 상상할 수 없는 일이었죠.
제가 겪었던 버그 중 가장 많은 것을 가르쳐준 사건입니다. 요즘 트렌드에 맞춰 ES 모듈 방식으로 JavaScript를 작성했는데, 웹 서버를 통해 실행할 때는 완벽하게 작동했어요. 그런데 index.html 파일을 더블클릭해서 로컬에서 열어보니, 페이지는 멀쩡하게 보이는데 요리를 선택해도 아무 일도 일어나지 않는 겁니다. file:// 프로토콜에서는 CORS 정책 때문에 모듈 로딩이 차단된다는 것을 그제야 깨달았습니다. JS가 비활성화된 상태에서도 페이지가 너무 '잘 작동'해서 초기에는 오류를 인지조차 못했어요. 결국 범용성을 위해 모듈 문법 대신 defer 속성이 붙은 클래식 스크립트로 전환했습니다. 조금 덜 '모던'해 보일지 몰라도, 누가 파일을 열어도 제대로 작동하는 것이 더 중요하다고 판단했죠.
또 다른 고마운 버그는 transition: all 사용에 대한 교훈을 주었습니다. 필터 칩에 이 속성을 적용했더니 all에 outline-width까지 포함되어 포커스 링이 0에서부터 애니메이션되어 들어오는 문제가 발생했습니다. 키보드 사용자는 포커스 링이 엉뚱한 타이밍에, 그것도 이상한 색으로 나타나는 불편함을 겪게 되었죠. axe-core나 대비 검사기는 잡아내지 못했지만, 탭으로 페이지를 이동하며 직접 확인했을 때 비로소 문제점을 발견했습니다. 이제 저는 transition: all이 단순히 깔끔하지 못한 코드를 넘어, 잠재적인 접근성 위험 요소라고 생각하게 되었습니다.
12가지 요리 중 가장 어려웠던 것은 카치오 에 페페(cacio e pepe)였습니다. 다른 요리들은 샥슈카의 노른자나 바클라바의 격자무늬처럼 시각적으로 '잡을 거리'가 있었는데, 카치오 에 페페는 옅은 색 그릇에 담긴 옅은 색 파스타였거든요. 처음에는 동심원의 반복적인 방사형 그라디언트로 표현했는데, 영락없이 과녁처럼 보였습니다. 이 문제를 해결한 두 가지 변화가 있었어요. 첫째, 면발의 꼬임을 중심에서 벗어난 타원형으로 그리고, 다른 중심에서 또 다른 꼬임을 겹쳐서 '엉킨' 느낌을 주었습니다. 과녁이 아니라 진짜 면발처럼 보이게요. 둘째, 색상 중단점을 부드럽게 처리했습니다. 딱딱한 띠는 밧줄 같았지만, 각 코일에 그라디언트를 적용하여 입체감을 주니 비로소 한 가닥의 면처럼 살아났습니다.
다음 단계에서 고려해볼 만한 부분은 메뉴 콘텐츠를 JavaScript 데이터가 아닌 HTML에 직접 넣어둔 방식입니다. JavaScript 없이도 페이지가 작동하게 하려는 의도였지만, 열세 번째 요리를 추가하려면 단순히 복사 붙여넣기를 해야 한다는 점이 단점이죠. 그럼에도 불구하고 이 트레이드오프는 다시 선택할 것 같습니다. 완벽한 것은 없으니까요.
이 프로젝트는 순수 HTML, CSS, JavaScript로 구현했습니다. MIT 라이선스가 적용됩니다. 레스토랑은 가상의 공간이지만, 메뉴 속 요리들은 모두 실존하는 맛있는 음식들입니다!
원문: https://dev.to/piwe/ouma-nonna-teta-one-table-three-kitchens-1ibo 수집일: 2026-08-02 01:25:03