요즘IT
위시켓
최근 검색어
전체 삭제
최근 검색어가 없습니다.

*FEConf2023에서 발표한 <몇 천 페이지의 유저 가이드를 새로 만들며>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 유저 가이드가 무엇인지, 왜 새롭게 만들게 되었는지와 그 개발 과정에서 만난 두 가지 문제 중 한 가지 문제를 다룹니다. 2회에서는 개발 과정에서 만난 두 번째 문제를 다루고 마무리합니다. 본문에 삽입된 이미지의 출처는 모두 동명의 발표자료로, 따로 출처를 표기하지 않았습니다. 발표자료는 FEConf2023 홈페이지에서 다운받을 수 있습니다.

회원가입을 하면 원하는 문장을
저장할 수 있어요!

다음

회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!

확인

개발

몇 천 페이지의 유저 가이드를 새로 만들며(1)

년차,
어떤 스킬
,
어떤 직무
독자들이 봤을까요?
어떤 독자들이 봤는지 궁금하다면?
로그인

*FEConf2023에서 발표한 <몇 천 페이지의 유저 가이드를 새로 만들며>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 유저 가이드가 무엇인지, 왜 새롭게 만들게 되었는지와 그 개발 과정에서 만난 두 가지 문제 중 한 가지 문제를 다룹니다. 2회에서는 개발 과정에서 만난 두 번째 문제를 다루고 마무리합니다. 본문에 삽입된 이미지의 출처는 모두 동명의 발표자료로, 따로 출처를 표기하지 않았습니다. 발표자료는 FEConf2023 홈페이지에서 다운받을 수 있습니다.

 

FEConf2023에서 발표된 ‘몇 쳔 페이지의 유저 가이드를 새로 만들며’ / AB180 이찬희 프론트엔드 팀 엔지니어링 매니저

 

블로그를 직접 만들고 글을 꾸준히 쓰고 계시는 분들 있으시죠? 저도 사실 제 블로그가 있는데 글이 20개가 안 넘습니다. 

 

하나의 정적인 사이트를 처음에는 쉽고 재미있게 만들 수 있습니다. 하지만 이제 블로그 글이 1,000개를 넘어간다면 혹은 접근성, 인쇄 환경같이 상대적으로 깊게 고려해보지 않았던 것들을 신경 쓰기 시작하게 됩니다. 그때 우리는 어떤 문제를 만나게 될까요? 그리고 그 문제들은 얼마나 어려울까요?

 

안녕하세요. 저는 사용자에게 데이터를 정보로 전달하는 일에 관심이 많으며, AB180이라는 데이터 스타트업에서 프론트엔드 팀 엔지니어링 매니저를 맡고 있는 이찬희입니다. 오늘은 여러분들께 다음과 같은 이야기를 들려드리려고 합니다.

 

유저 가이드에 관하여

  • 유저 가이드가 무엇인가요?
  • 왜 어떻게 갈아엎었나요?
  • 리액트 서버 컴포넌트에 배팅하기

 

개발 과정에서 만난 문제들

  • 접혀진 아코디언을 검색할 수 없나요?
  • 정적 사이트 생성은 적절한 방법인가요?
 

유저 가이드란?

본격적인 시작에 앞서, 오늘 이야기에 주로 나올 유저 가이드가 정확히 무엇이고 왜 새로 만들게 되었는지 짧게 이야기를 드리려고 합니다.

 

저희는 에어브릿지(Airbridge)라는 마케팅 성과 분석 도구를 만들고 있습니다. 아래 화면은 그 대시보드인데요. 마케팅 성과 분석을 위해서 사용자분이 알아야 하는 것도 해야 하는 것도 굉장히 많습니다.

 

그림1 에어브릿지 대시보드

 

SDK 설치, 각종 광고 매체와 연동, 애플이나 구글의 개인정보 보호 정책에 관한 이해 같은 것도 있을 거고요. 이러한 내용은 제품 안에서 안내하기에는 다소 한계가 있습니다. 그래서 저희 회사에서는 이러한 고객들의 궁금증을 해소하고 사용자들의 허들을 낮추기 위해 유저 가이드라는 페이지를 운영하고 있습니다.

 

그림2 에어브릿지 유저 가이드(출처: 에어브릿지)

 

이 유저 가이드는 회사 내의 PW(Product Writing, 프로덕트 라이팅) 팀에서 운영합니다.  처음에는 젠데스크(Zendesk)라는 CMS(Content Management System)로 유저 가이드를 운영했습니다. 보통 문의 창구나 가이드를 만들 때 일반적으로 젠데스크를 많이 사용하시는데요. 특징이 있다면 레이아웃과 테마는 ‘Handlebars’라는 템플릿 엔진을 사용하고, WYSIWYG 에디터에 글을 작성하면 HTML로 코드를 생성한다는 것입니다. 만약 블로그를 직접 만들어보신 분들이라면 Ghost, Jekyll, 그리고 커스텀 로직이 섞인 무언가 정도로 생각을 하시면 될 것 같습니다.

 

 

왜 어떻게 갈아엎었나?

언제나 그렇듯 처음에는 별문제가 없었습니다. 그런데 지난 몇 년간 젠데스크를 사용해보면서 불편한 점이 보이기 시작했는데요. 가이드를 작성하다 보니 생각보다 중복되는 화면, 내용이 많았습니다. 하지만 젠데스크에는 콘텐츠 모델 내지는 컴포넌트의 개념이 없었기 때문에 PW분들께서는 비슷한 모양과 내용을 보여주기 위해 매번 HTML 코드를 복사, 붙여넣기 하실 수밖에 없었습니다.

 

이게 점점 많아지다 보니 반복되는 내용을 수정하기 위해 하나씩 뒤져가며 찾기에도 어려움이 있었고요. 프론트엔드 개발자들의 입장에서는 콘텐츠, 스타일, 스크립트가 강결합되어 유지보수에서도 굉장히 큰 어려움이 있었습니다.

 

그래서 조금 속된 말로, 닮았지만 엄연히 다른 것들이 유저 가이드를 꽉꽉 채우기 시작한 것입니다. 저는 진짜 미쳐버릴 것 같았죠.

 

그림3. 닮았지만 엄연히 다른 존재들

 

저만 그러지는 않았을 거고요. 당연히 PW팀에서도 이런 문제에 대해서 인식을 하기 시작했습니다. 작성된 가이드가 천 개를 넘어가던 시점이 올해 초쯤이었는데요. 그때 저와 PW팀은 불편함을 해소하고 서로가 서로의 일을 잘하는 데 집중하기 위해 헤드리스 CMS(Headless CMS)를 도입하기로 결정합니다.

 

헤드리스 CMS가 무엇인지 쉽게 설명해보겠습니다. PW팀은 ‘콘텐트풀(Contentful)’이라는 CMS에서 구조화 가능한 형태로 콘텐츠를 작성하고, 프론트엔드 팀은 작성된 콘텐츠를 API로 불러와 보여주는 사이트를 만들기로 합니다.

 

아래는 대략적인 업무 흐름 내지는 정리된 자료들이 어떻게 보이는지를 간단히 그려본 것입니다. 

 

그림4 헤드리스 CMS

 

그렇게 해서 유저 가이드를 바닥부터 직접 만들게 되었습니다. 개발자들에게 항상 즐거운 시간인, 기술 선택의 시간이 다가온 건데요. 간단한 프로토타이핑을 위해서 뭘 쓰면 좋을까 고민하다가, 제가 가장 익숙하면서도 가장 궁금했던 ‘리액트 서버 컴포넌트’를 다뤄보고 싶어서 Next.js의 앱 라우터를 선택했습니다.

 

실제로 개발에 들어가기에 앞서, 유저 가이드에는 다음과 같은 특성 내지는 문제가 있습니다.

 

1. 먼저, 코드 블록 수용도 등 다양한 콘텐츠를 보여주어야 합니다. 

이로 인해 많은 라이브러리를 사용하게 되어 번들이 무거워질 수가 있을 거고요.

 

2. 다음으로는 GraphQL을 통해 JSON형태로 콘텐츠를 가져온다는 점입니다.

일반적으로 블로그를 만드시는 분들은 mdx나 이런 포맷들을 사용하셨겠지만, 어떤 CMS는 별도의 형태로 API를 호출해 JSON형태로 데이터를 가져와야 하고, 그에 대해 렌더링을 해줘야 하는 형태를 가지고 있는데요. 이 GraphQL은 생각해보면 쿼리 복잡도의 한계가 있기 때문에 무겁거나 복잡한 데이터는 여러 번 쿼리를 한 뒤에 재조합해야 합니다. 이러한 조합에 흔히 BFF(Backend for Frontend)라고 부르는 것을 만들어 내려준다면 렌더링 로직 자체는 쉽게 짤 수 있겠지만, 사실 BFF를 만든다는 것 자체가 조금은 귀찮은 일이긴 합니다.

 

그러면 위 두 가지를 조금 더 쉽게 해결해 볼 수는 없을까요? 만약 유저 가이드에서 클릭, 애니메이션 등 클라이언트에서 상호작용이 이루어지는 컴포넌트와 로직을 분리하고 나머지는 서버에서 렌더링을 수행해 클라이언트로 결과만 전송한다면 어떨까요? 그렇다면 번들 크기도, 그리고 제가 해야 할 일도 이제 줄어들 것입니다. 특히 제가 할 일이 줄어든다는 게 가장 중요한 부분입니다. 이것을 리액트 서버 컴포넌트의 기본 개념으로 이해해 주시면 될 것 같습니다.

 

실제 저희 사례로 이야기를 해보자면, 기존에 VSCode처럼 코드를 하이라이팅 해주는 shiki.js라는 라이브러리가 있습니다. shiki.js를 기존에 사용하려면 언어 파서, 웹어셈블리 구동 로직 등 약 1~2메가 정도의 번들이 포함되어 번들 사이즈가 늘어나게 되는데요.

 

그림5. 언어 파서, 스타일, 웹어셈블리 코드 등, 1MB 정도 번들 사이즈가 늘어남

 

이를 다음과 같이 이제 서버에 위임하고 클라이언트에서는 결과만 받도록 변경하여 번들 사이즈를 크게 줄일 수 있었습니다.

 

그림6. 서버에서 실행하고 결과만 클라이언트로 = 번들사이즈 감소

 

또한 DB 쿼리나 API 호출을 컴포넌트 안에서 직접 할 수도 있습니다.

 

그림7. DB 쿼리나 API 호출도 컴포넌트 안에서, 결과만 클라이언트에 전송

 

서버는 클라이언트로 렌더링 결과만 전송하기 때문에 데이터를 가져오기 위한 BFF를 만들지 않아도 되고 만약에 오래 걸리는 렌더링 작업이 있다면 해당 렌더링 작업을 서스펜스로 묶어 점진적 렌더링, 즉 스트리밍을 손쉽게 구현할 수 있었습니다.

 

그림8. 작업이 더 오래 걸린다면 컴포넌트를 Suspense로 묶어 점진적으로 렌더링 = Streaming

 

제가 이것의 프로토타이핑을 한 3일 만에 끝냈는데요. 생각보다 좀 쉽게 됐습니다. 그래서 ‘이거 원래 하던 대시 보드 만드는 일보다 너무 쉬운데 이거 금방 끝나겠다’ 이런 생각을 좀 했는데요, 실제로 그랬다면 이 글을 쓰지 않았을 겁니다. 결국 그 업보를 호되게 당했죠. 
 

 

개발 과정에서 만난 문제들

1. 접힌 아코디언은 검색할 수 없나요?

이제 제가 경험했던 문제들 중에서 상대적으로 간단하게 풀린, 하지만 좀 고민해 볼 요소가 있는 문제 두 가지를 꼽아서 이야기하려고 합니다.

 

첫 번째 문제는 의외로 아코디언을 만들면서 발생했습니다. 내용을 접었다 펼 수 있는 UI를 ‘아코디언’이라고 하는데요.

 

유저가이드 gif #1

 

이 아코디언에 QA 항목이 한 줄 추가됩니다.

 

그림9

 

“Command+F로 특정 내용을 검색을 했을 때, 해당 내용이 아코디언 안에 들어있다면 자동으로 아코디언이 펼쳐졌으면 좋겠습니다.” 이런 내용이죠. 이 QA 항목이 올라오자마자 바로 한 줄을 또 추가를 하시더라고요.

 

그림10

 

경쟁사에선 된다는 거예요. 받은 내용이 제 예상을 초월을 했습니다. 심지어 이제 경쟁사에서 된다는 내용까지 있어서 저는 도망갈 수가 없었어요.

 

그림11. Cmd+F 이후의 모든 키보드 이벤트를 기록이라도 하나…? 한글 같은 조합형 문자면 추측도 어려운데…? (출처: 몇 천 페이지의 유저 가이드를 새로 만들며 발표자료)

 

정말 충격적인 상황이었는데요.

 

그래도 뭐 이렇게 된 거 도망칠 수 없으니 즐겨야겠죠. 그래서 열심히 자료를 찾아보고 또 경쟁사 사이트도 좀 이것저것 뜯어보다가 HTML 스펙에 추가된 속성과 이벤트를 사용하면 이를 쉽게 구현할 수 있다는 것을 알게 되었습니다. 바로 hidden=”until-found”속성과 beforematch 이벤트입니다.

그림12

 

hidden=”until-found”속성은 정보 접근성 향상을 위해 추가되었으며, 검색에서 일치하는 항목이 해당 영역 내에 있을 때 beforematch 이벤트를 발생시킵니다. 그렇다면 이제 해당 이벤트를 저희가 이벤트 리스너로 받아서 커스텀 동작을 수행할 수도 있을 겁니다. 이 스펙 자체는 상대적으로 최근에 추가되었고, 기본 스타일이 적용되어 이 hidden=”until-found”를 적용하면 랜더링 상태는 유지하며 요소는 숨기는 content-visibility: hidden이 적용됩니다. 쉽게 생각하시면 display: none과  visibility: hidden이 합쳐진 스타일이 기본적으로 적용된다고 이해해 주시면 될 것 같습니다.

 

그러면 이 속성 하나만 적용하면 다 될까요? 한번 바로 적용해보겠습니다. 바로 코드로 좀 넘어가 보면요. hidden=”until-found”를 그 해당 영역에 적용하고, 아코디언을 펼치는 핸들러를 선언한 뒤에 beforematch 이벤트에 대한 이벤트 리스너를 등록합니다.

그림13

 

그러면 한번 해볼까요? 열심히 엔터를 눌러보았습니다.

 

그림14. 동작하지 않는 화면

 

그런데 동작하지가 않습니다. 내가 뭘 잘못했나, 뭔가 잘못 적용한 부분이 있나 해서 열심히 찾아보았는데, 크롬 인스펙터를 켜보니까 JSX에 넣은 히든 속성이 렌더링된 결과에서는 다르게 나타납니다.

 

그림15

 

분명 제가 잘못 작성한 것은 없는데 왜 이런 문제가 발생했을까요? 한번 이 문제를 렌더링 단계를 나눠서 생각해보면 좋을 것 같습니다.

 

그림16

 

렌더링 단계를 보면, JSX로 작성된 객체를 리액트에서 읽어서, 리액트 컴포넌트로 변환합니다. 그리고 상태의 업데이트, 이펙트같이 렌더링 대상을 연산한 뒤에 ReactDOM이 실제로 DOM에 반영합니다. 동시에 이제 DOM API를 연결해서 클릭 같은 인터랙션이 활성화되겠죠.

 

이게 정확한 설명은 아니지만 대략적인 렌더링 흐름을 보자면 이렇게 말씀드릴 수 있을 것 같습니다. 그러면 우리는 여기서 바로 ReactDOM에 주목할 필요가 있습니다. hidden 속성을 다른 값으로 바꾼 곳이 바로 이곳이기 때문입니다.

 

그래서 다음 코드는 ReactDOM의 코드 중 한 부분을 가져왔는데요. 여기서 하이라이팅한 부분을 보시면 케이스 절에 있는 속성들은 function이나 symbol이 아니면 모두 빈 문자로 변환됩니다.

 

그림17

 

이는 프로덕션에서 발생 가능한 보안 관련 문제나 충돌을 최소화하기 위해서 ReactDOM이 갖고 있는 속성 값 검증 로직인데요. 이 속성값 검증 로직이 우리가 주었던 히든 속성을 바꿨다고 예상을 해볼 수 있습니다.

 

리액트 팀도 이걸 이미 알고 있습니다. 이미 코어 컨트리뷰터에 의해서 1년 전에 이슈가 올라왔는데요. 1년째 업데이트도 없고 그동안 리액트가 서버 컴포넌트 준비한다고 내부적으로 코드가 엄청 많이 바뀌어서 이 PR이 사실상 무용지물이 됐습니다.

 

그리고 저희가 이런 비슷한 것들을 또 유저 가이드 만들면서 몇 개 작업을 해야 됐기 때문에, 업스트림을 뜯어서 패치 패키지 같은 걸로 코드를 수정하는 방향은 생각하지 않기로 했습니다.

 

정리를 하자면, 제가 주었던 [hidden=”until-found”] 속성은 올바른 HTML의 스펙입니다. 하지만 크롬 12부터 적용 가능한, 상대적으로 최신 스펙이기 때문에 리액트에서는 반영되지 않았고, 리액트는 이를 잘못된 값으로 인식해서 빈 문자로 변환을 한 것입니다.

 

그래서 의도 자체는 알겠지만 결론만 놓고 보자면 코어 라이브러리가 접근성을 위한 기능을 제한했다고 볼 수가 있습니다. 그러면 이러한 상황에서는 어떻게 해야 될까요? 관점을 한번 바꿔봅시다.

 

그래서 아까의 렌더링 단계를 다시 보겠습니다.

 

그림18

 

만약에 저희가 ReactDom을 건드릴 수 없다면 렌더링에 관여하는 다른 요소를 속이면 됩니다. 저는 그중에서 브라우저, 정확히는 HTML 쪽을 사용해서 ReactDom을 속여보기로 합니다.

 

여러분들께 한 가지 질문을 좀 드려보겠습니다. 다음 코드는 동작할까요?

 

그림19

 

여러분들이 이제 보통 JSX에서 스타일을 줄 때에는 오브젝트로 넣어주셨을 겁니다. 그런데 지금 코드는 문자열로 스타일을 주고 있죠 이 코드는 과연 동작을 할까요? 정답은 ‘동작한다’입니다.

 

앞에 STYLE이 대문자인 것을 확인할 수 있을 텐데요. 사실 모든 HTML 속성은 기본적으로 대소문자를 구분하지 않습니다. 기본적으로 HTML이 만들어졌을 때부터 그랬고요. 그리고 ReactDom은 정의된 소문자 속성들, 그리고 대소문자 구분 없이 on으로 시작하는 속성들을 검증합니다.

 

이를 활용해서 JSX에 대문자로 속성을 입력하면 JSX는 그대로 표시할 것이고, ReactDom은 별도의 검증 로직을 거치지 않을 것이며, 그렇게 해서 이제 대문자 속성이 적용된 DOM은 올바르게 입력한 것과 동일하게 동작할 것입니다.

 

그럼 이제 우리는 아까의 beforematch 이벤트 리스너만 추가하면 정상적으로 동작이 되겠죠. 바로 한번 코드에 적용을 해보겠습니다. 아까의 코드로 돌아와서, 소문자 hidden을 대문자로 바꾸고, 실행해보겠습니다.

 

보시는 것처럼 잘 동작합니다.

 

유저가이드 gif#2 동작하는 화면

 

그러면 아까 전에 저희가 크롬인스펙터(chrome inspector)를 켰을 때 렌더링된 게 조금 이상하게 나왔는데 제대로 이번에는 표시가 될까요? 제대로 표시됩니다.

 

그림20

 

이렇게 우회를 통해서 라이브러리가 지원하지 않는 최신 HTML 스펙을 사용할 수 있게 된 것입니다.

 

그렇다면 여기서 한 발짝 더 나아가, 만약에 애니메이션을 적용하고 싶다, 뭔가 접혔다 펼쳐졌다 하면서 높이가 자유롭게 변하는 애니메이션을 적용하고 싶다면, 어떻게 하면 좋을까요? 애니메이션은 기본적으로 DOM의 영역이고 애니메이션 실행 관련 제어를 위해서는 리액트 도움이 좀 필요할 것 같긴 합니다.

 

그러면 이 부분도 바로 코드로 생각해보면, 우선 [hidden=”until-found”]에는 아까 말씀드렸던 기본 스타일인 content-visibility: hidden이 있습니다. 이 스타일이 적용되면 기본적으로 애니메이션 자체가 제대로 동작하지 않을 가능성이 있기 때문에 이걸 애니메이션 실행 중에는 제거해야 하고요. 그렇기 때문에 애니메이션 실행을 위한 상태를 추가하고, 아코디언이 펼쳐져 있거나 애니메이션이 실행 중일 때 hidden 속성을 비움으로써 애니메이션 실행에 문제가 없도록 만들면 됩니다.

 

그림21

 

참고로 저 애니메이트는 프레이머 모션에서 제공하는 함수이고요. 프레임워크를 딱히 타지 않기 때문에 여러분들이 만약에 vue나 다른 것들을 사용하신다면, 기본적인 메커니즘 자체는 비슷하기 때문에 거기서도 사용하실 수 있을 겁니다. 그럼 한번 또 보겠습니다.

 

유저가이드gif#3 동작하는 화면 두 번째

 

애니메이션도 잘 동작을 하고요. 검색했을 때 내용도 잘 펼쳐집니다. 이제 좀 뿌듯합니다. 이제 경쟁사에서 되는 기능 우리도 된다고 말할 수 있게 됐네요.

 

지금까지 유저 가이드가 무엇이고 왜, 어떻게 갈아엎었는지, 그리고 개발하면서 부딪쳤던 첫 번째 문제를 이야기했습니다. 분량이 길어져서, 두 번째 문제는 다음 글에서 소개하도록 하겠습니다.

 

요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

좋아요

댓글

공유

공유

댓글 0
FEConf
508
명 알림 받는 중

작가 홈

FEConf
508
명 알림 받는 중
프론트엔드 컨퍼런스 FEConf 공식 계정입니다.

좋아요

댓글

스크랩

공유

공유

요즘IT가 PICK한 뉴스레터를 매주 목요일에 만나보세요

요즘IT가 PICK한 뉴스레터를
매주 목요일에 만나보세요

뉴스레터를 구독하려면 동의가 필요합니다.
https://auth.wishket.com/login