회사에서 신사업을 진행 중에 있습니다. 신사업에서 주요한 전환 중 하나는 리드 확보입니다. 신사업 구조가 명확하게 만들어지지 못했기에, 개발 리소스를 투입하지 위해 리드 제출은 Tally를 사용하고 있습니다.
Tally 사용 이유는 간단한데, Pixel 연동과 GA4 연결, utm 추적을 위함입니다. 다른 폼에서도 진행이 가능하지만 Tally가 구성에 대한 UI가 편리했습니다.
리드 확보 문제 사항
리드 확보를 위해 폼을 구성하면서 몇 가지 문제가 발생했습니다.
- 이미지를 넣을 경우, 폼 제출율은 높아집니다.
- 이미지를 넣지 않는 경우, 폼 제출율은 떨어집니다.
- 제출 페이지를 분할할 경우 제출율은 떨어집니다.
- 이미지를 넣을 경우, 가독성이 낮아집니다. (세부적인 내용 파악 불가)
이미지는 잠재고객에게 혜택을 안내하기 좋은 요소이지만, 가독성을 낮추는 요소를 만듭니다. 결과적으로 서비스와 인지간 차이를 만듭니다.
이미지를 효과적으로 넣는 방법 - 롤링배너
Tally에서 이미지 기재가 문제되는 요소는 슬라이드 형태를 제공하지 않기 때문입니다. 슬라이드로 적은 영역에서 많은 내용을 제공하는 형태가 된다면 해결될 수 있습니다.
다행히도 저희는 Tally Pro모델을 구독 중이고, Custom CSS를 사용할 수 있습니다.
Tally 롤링 배너 구성하기
문제점
처음 마주하는 문제는 이미지의 위치값입니다. Tally는 Notion과 같이 입력 위치가 block 단위로 들어갑니다.
그리고 block id가 생성되죠. block id를 직접 조회하고 입력하는 것은 번거롭기에 img와 child를 활용해서 진행한 경우, 아래 문제가 발생했습니다.
이미지의 위치가 블럭을 벗어나 제목이나 다른 텍스트 등을 가린다.
해당 문제를 해결하는 간단한 방법은 block id를 직접 조회하고 입력하는 것입니다. (코드 내 요소 교체)
하지만 개발지식이 적은 사람이 해당 부분을 진행하는 것은 조금 어려울 수도 있고, 굉장히 번거롭습니다.
만약 폼을 매일 만들어야 한다면, 폼 제작 시간보다 block id를 교체하는 것이 더 오래걸리겠죠.
해결 사항
그래서 개발지식이 적어도 가능한 방법으로 DOM을 사용해서 function으로 처리하기로 했습니다.
코드 영역을 지정하고, 복사 붙여넣기도 귀찮기 떄문에 함수 내에 코드 복사 기능도 추가했습니다.
사용 방법
브라우저 개발자 도구 안에서 Console에 아래 코드를 입력하면 되죠. 그러면 알아서 block id를 조회하고 롤링 배너 코드를 생성합니다.
복사된 코드를 Tally 폼 내 Customize로 이동 후, 하단 Custom CSS에 붙여넣으면 됩니다.
⚠️ 주의 사항으로는 해당 코드는 현재 폼 내 이미지를 조회합니다. 즉, 이미지가 추가되거나 삭제되면 다시금 코드를 호출해서 붙여넣어야 합니다.
function copyToClipboard(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
const success = document.execCommand('copy');
document.body.removeChild(textarea);
if (success) {
console.log('📋 클립보드 복사 완료');
} else {
throw new Error('execCommand 실패');
}
} catch (e) {
document.body.removeChild(textarea);
console.warn('⚠️ 클립보드 자동 복사 불가 — 새 탭에서 Ctrl+A → Ctrl+C 하세요');
const win = window.open('', '_blank', 'width=800,height=600');
win.document.write(`<pre style="font-size:12px;white-space:pre-wrap;">${text}</pre>`);
win.document.close();
}
}
function generateTallyBannerCSS() {
const UUID_PATTERN = /tally-block-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
const allBlocks = document.querySelectorAll('[class*="tally-block-"]');
const imageBlocks = [...allBlocks].filter(el => {
return el.querySelector('img') && UUID_PATTERN.test(el.className);
});
if (imageBlocks.length === 0) {
console.warn('UUID 형식 이미지 블록을 찾지 못했습니다.');
console.log('감지된 블록 클래스 목록:');
[...allBlocks].forEach(el => console.log(el.className));
return;
}
if (imageBlocks.length > 5) {
console.warn(`이미지 블록 ${imageBlocks.length}개 감지 → 5개까지만 처리합니다.`);
}
const blocks = imageBlocks.slice(0, 5);
const count = blocks.length;
const ids = blocks.map(el => el.className.match(UUID_PATTERN)[0]);
console.log(`✅ 이미지 블록 ${count}개 감지:`, ids);
// 타이밍 계산
const duration = count * 4;
const holdPct = (3 / duration * 100).toFixed(1);
const fadePct = (4 / duration * 100).toFixed(1);
// 셀렉터 생성
const baseSelectors = ids.map(id => `.${id}`).join(',\n');
const imgSelectors = ids.map(id => `.${id} img`).join(',\n');
const containerSelectors = ids.map(id => `.${id} .block-container`).join(',\n');
const stackRules = ids.slice(1)
.map(id => `.${id} { margin-top: -400px !important; }`)
.join('\n');
const zDelayRules = ids.map((id, i) => {
const delay = i === 0 ? 0 : -(duration - i * 4);
return `.${id} { z-index: ${i + 1}; animation-delay: ${delay}s; }`;
}).join('\n');
const lines = [
`/* ── 공통 베이스 ── */`,
`${baseSelectors} {`,
` position: relative;`,
` height: 400px;`,
` overflow: hidden;`,
` animation: banner-cycle ${duration}s ease-in-out infinite;`,
`}`,
``,
`/* ── 이미지 크기 ── */`,
`${imgSelectors} {`,
` width: 100%;`,
` height: 400px;`,
` object-fit: cover;`,
` display: block;`,
`}`,
``,
`/* ── 컨테이너 ── */`,
`${containerSelectors} {`,
` height: 400px;`,
` overflow: hidden;`,
`}`,
``,
`/* ── 2번~ 블록: 1번 위치로 겹치기 ── */`,
stackRules,
``,
`/* ── z-index & animation-delay ── */`,
zDelayRules,
``,
`/* ── 키프레임 (${duration}s 기준) ── */`,
`@keyframes banner-cycle {`,
` 0% { opacity: 1; }`,
` ${holdPct}% { opacity: 1; }`,
` ${fadePct}% { opacity: 0; }`,
` 95% { opacity: 0; }`,
` 100% { opacity: 1; }`,
`}`,
];
const css = lines.join('\n');
copyToClipboard(css);
}
generateTallyBannerCSS();