안녕하세요, 피처링 프론트엔드 파트 루카입니다.
올해 저희는 일본 서비스를 성공적으로 런칭하며 새로운 도전을 시작했습니다!🎉
일본 서비스 개발 과정에서 겪었던 경험과 문제 해결 과정을 여러분과 공유하고자 합니다.
1) 인플루언서 찾기 / 관리의 개편
일본 서비스를 준비하며 대량의 인플루언서 데이터를 효율적으로 관리할 수 있도록 인플루언서 찾기 / 관리 화면을 전면 개편했습니다.
인플루언서 찾기 개편
고객이 인플루언서를 쉽게 찾기 위하여 찾기 필터의 종류를 추가하고, 테이블을 전면 개편하였습니다.
변경 전 인플루언서 찾기 필터
변경 후 인플루언서 찾기 필터
그 중 테이블 컴포넌트는 오픈소스 라이브러리 TanStack Table을 도입해 기능성과 유저 편의성을 대폭 향상시켰습니다.
변경 전 인플루언서 찾기 테이블

변경 후 인플루언서 찾기 테이블
인플루언서 관리 개편
그룹목록과 인플루언서 목록을 2 페이지로 나눠 (그룹목록에 속해있는 그룹단위 분석정보 제공 업데이트 예정) 사용자 편의성과 사용성을 증가시켰고, 테이블을 전면 개편하였습니다.

변경 전 인플루언서 관리

변경 후 인플루언서 관리 1

변경 후 인플루언서 관리 2
왜 Tanstack Table로 사용하였나?
-
헤드리스 테이블
TanStack Table은 기본 로직만 제공하는 헤드리스 형태로, 스타일링과 기능 커스터마이징이 자유롭습니다. 이를 통해 피처링만의 디자인과 요구사항을 쉽게 반영할 수 있었습니다. -
활발하게 유지 관리 되는 라이브러리
TanStack Table은 프론트엔드 개발자들 사이에서 널리 사용되는 TanStack Query와 같은 TanStack 생태계의 주요 라이브러리입니다. 꾸준한 관리와 안전성은 라이브러리를 선택하는 하나의 요소였습니다. -
자세하고 다양한 예제를 제공하는 문서
오픈소스 라이브러리들이 제공하는 문서들의 품질은 TanStack Table은 기능에 대한 자세한 문서를 제공하고, 바로 따라할 수 있는 다양한 예제를 제공함으로써, 개발 속도를 크게 높일 수 있었습니다. -
기존 성공 사례
우리 회사의 다른 상용 서비스인 데이터이펙트에서도 TanStack Table을 성공적으로 도입해 사용 중입니다. 긍정적인 피드백과 안정적인 운영이 라이브러리를 선택하는데 많은 도움이 되었습니다.
테이블 개발 과정 및 회고
새로운 테이블 컴포넌트는 많은 데이터를 표시하는 기능외에, 사용자 경험(UX)을 높이는데 초점을 맞췄습니다. 특히, 스크롤에 따라 테이블의 헤더와 필터가 고정되는 스티키(Sticky) 속성 적용이 주요 기능였습니다.
StickyManager 컴포넌트: 스티키 요소들이 겹치지 않게 자동 계산
피처링은 기능 추가와 수정 주기가 빠르기 때문에, 유연하면서도 일관적인 UI를 유지하는 것이 중요합니다.
StickyManager 컴포넌트는 스티키 속성이 적용된 요소들의 높이를 자동으로 계산해 top 값을 설정해줍니다.
이를 통해 스크롤 시 스티키 요소들이 겹치지 않고 자연스럽게 동작하도록 만들어주는 유틸리티 컴포넌트 입니다.
컴포넌트의 주요 기능은
-
Sticky 속성을 가진 요소들의 높이 자동 계산
StickyManager는 children 컴포넌트의 height를 동적으로 계산해 top 속성을 자동으로 설정해 줍니다.
-
쉽고 간단한 사용성
별도로 높이를 지정할 필요 없이, StickyManager를 감싸는 것만으로 간단하게 적용할 수 있습니다.
import React, { PropsWithChildren, useEffect, useRef } from 'react'; const StickyManager = ({ children }: PropsWithChildren) => { const containerRef = useRef<HTMLDivElement>(null); const arrangeStickyElements = (parentNode: HTMLElement) => { const stickyElements = Array.from(parentNode.children).filter((child: any) => { const style = window.getComputedStyle(child); return style.position === 'sticky'; }) as HTMLElement[]; let cumulativeHeight = 0; stickyElements.forEach((el) => { el.style.top = `${cumulativeHeight}px`; cumulativeHeight += el.offsetHeight; }); }; useEffect(() => { if (containerRef.current) { arrangeStickyElements(containerRef.current); } }, [children]); return <div ref={containerRef}>{children}</div>; }; export default StickyManager;
아래와 같이 사용
... //자식들중 sticky 속성들의 top속성을 자동계산 //선언적으로 사용할 수 있다. <StickyManager> <PlatformSection/> <FilterSection /> <SelectedTableContainer /> </StickyManger ...
StickyManager 컴포넌트를 사용하여 동적인 환경에서도 스크롤 UX를 깔끔하게 유지할 수 있습니다.
또한, 선언적으로 사용할 수 있어 유지보수와 확장성 측면에서도 큰 이점을 제공했습니다.
테이블 컬럼과 본문 분리
테이블의 컬럼(header)과 본문(body)을 분리하여 개발했습니다.
컬럼은 sticky 속성을 적용하고, 본문과 컬럼의 스크롤 이벤트를 연동해 자연스럽게 동작하도록 구성했습니다.
//테이블 헤드와 바디의 스크롤 싱크를 맞춘다. const handleHeadSyncScroll = (e: React.UIEvent<HTMLDivElement>) => { if (isScrollingRef.current) return; const target = e.target as HTMLDivElement; if (bodyScrollRef.current) { bodyScrollRef.current.scrollLeft = target.scrollLeft; } }; const handleBodySyncScroll = (e: React.UIEvent<HTMLDivElement>) => { if (isScrollingRef.current) return; const target = e.target as HTMLDivElement; if (headerScrollRef.current) { headerScrollRef.current.scrollLeft = target.scrollLeft; } }; ... //컬럼에 스티키 적용을 위해 헤드부분과 바디부분을 나눠서 구성 <div ref={headerScrollRef} onScroll={handleHeadSyncScroll} style={sticky...}> <table> <thead> <tr>...</tr> <tr>...</tr> </thead> </table> </div> <div ref={bodyScrollRef} onScroll={handleBodySyncScroll} > <table> <tbody> </tody> </table> </div> ... <Table> <Column stlye={display: sticky} /> </Table> <Table> <Body> </Table>
이렇게 분리된 구조는 컬럼의 스티키함을 유지하면서 바디와 원활히 연동되도록 처리할 수 있었습니다.
2) 일본 서비스를 위한 다국어화 자동화
일본어 다국어 작업은 서비스 출시의 핵심 과제 중 하나였습니다. 효율적인 작업을 위해 Next.js의 i18n 기능과 자동화 스크립트를 활용했습니다.
다국어화 프로세스
-
번역 데이터 추출: 한국어 페이지에서 필요한 문장/단어를 추출하고 정리.
-
스프레드시트 관리: Google 스프레드시트에 번역 내용을 정리.
-
JSON 변환: 자동화 스크립트를 통해 스프레드시트 데이터를 JSON 파일로 변환.
-
Next.js에 적용: 변환된 데이터를 Next.js의 i18n 시스템에 반영.
이와 같은 과정을 통해 번역 작업의 정확성과 효율성을 크게 향상시킬 수 있었습니다. 다국어 관리부터 코드 반영까지 자동화하여 작업 속도를 크게 단축하고, 서비스 품질을 높였습니다.
이 작업의 더 자세한 내용은 다른 글에서 다뤘으니 참고해 보세요!
3) 일본 서비스 개발 중 겪은 이슈와 해결 방법
일본 서비스를 준비하면서 다양한 문제를 마주하고 해결하였습니다. 몇 가지 내용을 소개하고 해결한 방법 간단히 공유드립니다.
폰트 적용 문제
문제
일본 서비스를 준비하며, 일본어 페이지에서 폰트가 올바르게 적용되지 않는 문제가 발생했습니다.
이따금 한국어 폰트가 적용되거나, 한자가 중국 한자로 표시되는 사례도 있었습니다. 이 문제는 팀 내에서도 큰 이슈가 되어 관련 논의만 56개의 댓글을 기록하며 화제가 되었습니다.
해결
이외로 간단하게 해결되었는데요, 문제의 원인은 브라우저가 해당 페이지를 일본어 서비스로 인식하지 못한 데 있었습니다.
이를 해결하기 위해 HTML 태그에 lang=”ja” 속성을 추가하여 브라우저가 페이지를 일본어로 인식하도록 설정했습니다.
이 간단한 조치로 일본어 폰트가 정상적으로 적용되었으며, 한자 표시 문제도 해결되었습니다.
참고한 문서 : https://heistak.github.io/your-code-displays-japanese-wrong/
<html lang="ja">
전각/반각 입력 시 발생하는 이슈
전각(Full-width)과 반각(Half-width)
일본어 키보드에서는 전각(Full-width)과 반각(Half-width) 문자를 입력할 수 있습니다.
이 두 문자 유형은 외형적으로는 비슷해 보이지만, 데이터 처리에서 다른 문자로 인식되기 때문에 변환 과정이 필요합니다.
전각과 반각의 차이
-
전각(Full-width): 글자 하나가 정사각형 공간을 차지하며, 일본어 및 한자에서 주로 사용. 예: abc123
-
반각(Half-width): 글자 하나가 전각의 절반 너비를 차지하며, 라틴 문자와 숫자에 주로 사용.예: abc123
문제
두 가지 문제가 있었습니다:
– 전각 → 반각 변환
전각 문자가 데이터 처리나 검색에서 다른 값으로 인식되는 문제를 해결하기 위해 전각 → 반각 변환이 필요했습니다.
– Windows 입력 이벤트 문제
Windows 환경에서 전각 문자를 입력할 때 onChange 이벤트가 두 번 호출되어 중복으로 입력되는 문제가 발생하였습니다.
해결
전각 → 반각 변환 함수 구현
전각 문자를 반각 문자로 변환 하는 간단한 함수 작성으로 해결하였습니다.
//전각 반각을 변경하는 함수 const convertFullWidthToHalfWidth = <T>(str: T) => { if (typeof str !== 'string') return str; return str .replace(/[\uFF01-\uFF5E]/g, (char) => { return String.fromCharCode(char.charCodeAt(0) - 0xfee0); }) //전각문자를 반각 문자로 변환 .replace(/\u3000/g, ' '); //전각 공백을 반각 공백으로 변환 };
console.log(convertFullWidthToHalfWidth("abc123 テスト!")); // 출력: "abc123 テスト!"
입력 이벤트 문제 해결
이 문제를 해결하기 위해 일본 개발자 커뮤니티를 포함한 다양한 자료를 조사한 결과, 일본어 전각 입력 시 발생하는 컴포지션 이벤트(Composition Event) 처리 방식이 문제의 원인임을 확인했습니다.
또한, 세 자리 숫자마다 쉼표를 적용해야 하는 요구사항이 있었기 때문에, onChange 이벤트에서는 입력값에 쉼표(comma)를 적용하는 로직을 처리하고, onBlur 이벤트를 활용해 입력이 완료된 시점에서만 데이터를 입력되도록 내부 로직을 변경했습니다. 이러한 내용을 통해 컴포지션 이벤트로 인한 중복 호출 문제를 효과적으로 해결할 수 있었습니다.
일본서비스 메타태그 설정 문제
문제
일본어 페이지의 메타 태그 설정이 올바르지 않아 SEO 최적화가 제대로 이루어지지 않았습니다.
특히, 브라우저 탭에 표시되는 제목이 한국어로 출력되는 문제가 있었습니다.
해결
router의 locale 값을 활용하여 각 언어에 맞는 메타 정보를 동적으로 설정하도록 수정했습니다.
다국어 번역 시트에서 번역된 내용을 가져와 SEO 관련 메타 태그에 적용했습니다.
//다국어 번역 시트에서 번역하여 seo관련 내용을 입력해주는 내용 const CommonHead = ({ pageName, title, desc }: { pageName?: string; title?: string; desc?: string }) => { const { t } = useTranslation(); const { locale } = useRouter(); const defaultTitle = t('components.common-head.featuring', '피처링'); const defaultDesc = t('components.common-head.description', 'All-in-One 인플루언서 마케팅 서비스 피처링'); return ( <Head> <title>{pageName ? `${title ?? defaultTitle} | ${pageName}` : title ?? defaultTitle}</title> <meta name="description" content={desc ?? defaultDesc} /> <meta property="og:title" content={title ?? defaultTitle} /> <meta property="og:description" content={desc ?? defaultDesc} /> <meta property="og:locale" content={locale === 'ja' ? 'ja_JP' : 'ko_KR'} /> <meta property="og:image" content={locale === 'ja' ? '/og-image-jp.png' : '/og-image.png'} /> <meta property="og:site_name" content={`featuring ${defaultTitle}`} /> <meta property="og:type" content="website" /> <link rel="icon" href="/favicon.ico" /> </Head> ); }; export default CommonHead;
맺음말
이번 일본 서비스 런칭은 새로운 시장에 도전하며 팀원들과 함께 성장할 수 있었던 의미 있는 과정이었습니다.
크고 작은 문제들을 해결하며 쌓은 경험을 이렇게 기록으로 남기며, 비슷한 상황에 있는 분들에게 도움이 되길 바랍니다.
일본 서비스를 준비하는 개발자들에게 조금이나마 참고가 되길 바라며 이 글을 마무리하겠습니다. 😊