5주간 엄청난 일이 있었던 나만무의 끝! 후련하기도 하지만 막막하면서 마냥 속 시원하지만은 않다.
그래도 1, 2주차에 최악의 기획이라는 이야기를 들으면서 꼴등 할 것만 같았는데 그렇게 끝나지는 않아서 다행이라고 해야 하나? 수상자가 있거나 하는 발표가 아니었기 때문에 우리 팀의 위치가 정확히 어디인지는 확인할 수 없지만 나름 못하지는 않았다고 생각한다.
그리고 사실 이번 프로젝트의 가장 큰 성과는 정글 내의 등수나 발표보다도 배포! 지금까지는 진짜 토이프로젝트였고 홍보할만큼의 프로덕트가 아니었기때문에, 그리고 서버비 등의 이유로도 배포를 해봤다~ 정도의 가짜 배포에 가까웠지만 이번에는 크롬 웹스토어에 올라가있다. 물론 보안 문제로 링크 공개로 배포했기때문에 검색해서 나오지는 않지만, 내가 만든 서비스를 남이 쓰는 경험!은 정말로 신기한 경험이다:) 그래서 이건 조금 더 다듬어서 전체공개로 바꾸고, 계속해서 업데이트 해나갈 예정이다☺︎
1차 기획 발표 때는 2가지를 가져갔었다. '우리 아이 일기'와 '콜티쳐'. 우리 아이 일기는 내가 일기 쓸 때 귀찮았던 경험을 살려서 낸 아이디어였는데, 너무 뻔하고 확장 가능성이 없다고 까였다. 확장 가능성이 없는 건 맞지만 뻔한지는 아직도 잘 모르겠는데, 그냥 운영진의 공감을 사지 못할 아이디어였나 싶기도 하다. 콜티쳐는 그냥 흔하디 흔하고 뻔한 수학 과외앱이었는데, 할 수 있는 기능을 다 때려박아서 양으로 승부를 보자! 했던 거였다. 당연하게도 대차게 까였다.
말은 2차 기획이었지만 최소 기능은 구현이 되어있어야 하는 중간 발표였는데 이때도 우리는 대차게 까였다... 맛집 검색하면서 블로그 보는게 귀찮다는 팀원의 아이디어였는데 사실 내가 생각해도 공감이 안가긴 했다. 이미 네이버 지도가 모든걸 해주고 있어서리,,, 그래도 '미리보기'는 좋을 것 같았고 이 때는 ai 요약을 생각하고 있었기때문에 유의미한 요약을 위해 도메인을 맛집으로 한정했다.
그러고 대표님의 피드백을 받았는데, 아이디어는 좋으나 제발 개발 포폴용 프로젝트 만들 때 맛집 관련은 하지 말라고 하셨다. 이미 너무너무 흔해서 맛집 보는 순간 좋은 아이디어도 흥미가 팍 식는다고... 결국 최종 기획은 맛집이라는 도메인을 버리고 단순히 '검색'에 집중하기로 했다. 내가 생각하기에도 맛집은 너무 흔하디 흔한 주제이긴 했기 때문에.
미리보기+공동문서편집을 메인 기능으로 잡고 개발하다가 멘토링을 받았는데, 검색에서 갑자기 협업이 튀어나오는 부분이 너무 억지스럽다고 하셨다. 그리고 iframe만으로 충분히 기술적 챌린지가 될거라고. 이미 그룹 스크랩을 반정도 구현해놓은 상태였는데 결국 다시 엎었다😭 이쯤 되면 우리 팀이 너무 피드백 수용을 잘 하는 걸지도....
그 뒤로는 미리보기+부분스크랩을 메인으로 잡았는데, iframe도 위태위태하고 iframe이 안되면 부분스크랩은 절대 안되고(드래그 한 부분을 캐치해서 스크랩해야 하는데 그러면 iframe 내에서 우리의 스크립트가 동작하도록 주입해야 한다) 해서 또 뒤집힐 뻔 하다가 다행히 나의 기지(ㅎ)로 스크린샷과 유사한 형태의 스크랩으로 해결했다. 지금 생각하면 그 때 뒤집었으면 진짜 완성 못할 뻔 했다.
항상 트러블슈팅 로그를 남기던 나였는데, 이번에는 이상하게도 진전이 없어서 그런지 초반에 남기다 말았다... 왜 그랬는지 너무너무 아쉽지만 깃헙과 기억을 더듬어서라도 복기해봐야겠다.
일단 제일 큰 문제는 iframe 우회였다. 우리의 핵심 기능이 '미리보기'인데 iframe이 막히면...(이하생략)
사실 기획하면서 처음으로 iframe이라는걸 알게 됐는데, src에 링크 주소만 넣으면 미니 브라우저가 생기는 신기한 기능과 달리 열리지 않는 페이지가 너무 많았다. 거의 반 정도는 막혀있다고 볼 정도..
MDN에 따르면 보안 상의 이유로 iframe에 본인 페이지가 뜨지 않도록 막는 x-frame-options 헤더가 문제였다. 처음에 생각한 해결책은 두 가지.
- chrome extension의 api중 http 요청을 수정할 수 있는 api를 사용한다 (
chrome.webRequest)- 프록시 서버로 iframe에 오는 응답을 가로채서 http response 헤더에 있는
x-frame-options의 값을allow from ~~으로 바꾼다
아무래도 서버를 하나 더 파는 것보다는 익스텐션 내에서 해결하는 게 더 간단하고 적합한 방식이 아닐까 싶어서 시도했으나 바로 문제에 봉착해버렸다. chrome.webReqeust는 manifest v2까지만 있던 api이고 이제는 declarativeNetReqeust로 바뀌었는데 webRequest 때 있던 block 기능을 제공하지 않았다. block 없이는 응답을 바꿔서 잘 보낸 것 같았는데 개발자도구 네트워크 탭에는 반영되어서 보이지 않았고, 이 원인이 원래의 응답을 막고 내가 수정한 응답을 줘야 하는데 막지 못해서 이미 수정 전의 응답을 iframe에 띄우기 때문이라고 생각했다.(나중에 알게 된 사실이지만 원래 바뀐 통신 결과는 네트워크 탭에서 확인할 수 없는 거였다.)
그래서 다음으로 2번을 시도했으나 결론적으로는 이것도 실패했다. 프록시 서버로 요청을 가로채 응답을 보냈는데, http의 응답을 변경하는건 됐는데 https는 불가능했다. 우리의 프록시 서버가 http로 배포되어있어 안된다고 생각했기 때문에 https 배포를 시도했는데 이것만 꼬박 이틀이 걸렸다. 포트포워딩과 로드밸런싱의 개념 없이 무작정 튜토리얼만 보고 따라한 것이 화근이었다.
어쨌든 https 배포를 성공한 뒤 다시 시도했는데 이번에는 프록시 미들웨어가 요청을 이상하게 보내는 것 같았다. 우리는 express 서버였기 때문에 http-proxy-middleware를 사용했고, iframe에 https://proxy_addr/api/원래 요청 주소를 넣으면 프록시 서버에서 뒷부분만 파싱해서 요청을 보내야 하는데, 분명 미들웨어 내에서 찍히는 콘솔에는 파싱이 잘 되는데 요청에는 api/를 포함해서 보내서 404페이지가 뜨는 것이었다. 그래도 해당 404 페이지가 요청 주소의 404였기 때문에 좀 희망이 보이는 듯 했다. (간헐적으로 페이지가 잠깐 보였다가 502가 뜨는 경우도 있었다)
여기까지 오는데만 며칠이 걸렸기때문에 위기의식을 느끼고 이 뒤로는 백엔드 팀원과 병렬적으로 진행하게 되었다. 나는 manifest v3로 iframe을 뚫은 것으로 보이는 타 익스텐션을 파보고, 팀원은 프록시 서버를 그대로 이어나가기로 했다. 아무래도 나보다는 백엔드 전문이 서버는 더 잘 할것이라고 생각했기 때문...
백엔드 팀원의 프록시 서버에서도 위에서와 같은 문제가 있었다. 헤더를 수정해서 보내려는 시점에 이미 응답이 클라이언트에게 전송된 상태였던 것. 이건 http-proxy-middleware의 onProxyRes 말고 expressd의 기본 미들웨어에서 Response Header를 수정하니까 해결되었다. 그래도 여전히 iframe은 502를 띄워줬는데, 이것저것 다 실험해보다 결론은 안된다!였다. http 명세서에 안된다고 써있었기 때문...임과 동시에 내가 성공했기 때문이었는데, 더 찾아보니 프록시 서버로 우회에 성공하신 분들의 글도 꽤 있어서 그냥 우리가 못한거였다고 생각하는 중이다.
나는 그동안 타 익스텐션을 핀토스[명사, 산더미 같은 코드를 샅샅이 파헤치다]하고 있었다. 사실 뜯어보고 나니 그냥 declarativeNetRequest를 올바르게 사용한 것밖에 없었지만, 이 코드 핀토스하면서 많은 걸 배웠다. 코드가 정말.... 아름다웠다. 객체에 함수를 넣어서 키값을 함수명으로 사용하다니! 그렇게 모든 기능을 기능별 동작 위치별로 세세하게 나눠서 마치 자바스크립트에서 기본 제공하는 메소드인 양 사용하고 있는걸 보고 코드를 이렇게 짤 수도 있구나 싶었다. 역시 잘 만든 코드를 보는 것도 공부가 되는구나.
나름 익스텐션의 핵심 기능 중 하나인 키보드. 처음에는 키보드가 주요 기능이라고 생각하지 않아서 마우스 이벤트 따로, 키보드 이벤트 따로 붙였는데 뒤에 가서 중복 로직이 늘어나면서 후회가 막심해졌다. 거기다가 어떤 글을 봐버려서 현재의 css 선택자 방식 코드가 너무너무 부끄러운 관계로 조만간 이 방식으로 리팩토링 할 예정. 코치님이 DOM 구조 바뀌면 소용 없는거 아니예요? DOM 구조가 바뀌어도 적용되는 코드정도는 되어야 자랑할 만 하지. 라고 했을 때 뭐야 그걸 어떻게 해; 했었는데 가능하다는걸 알고 나니까 너무 신나고 궁금하다!! 사실 지금 당장 하고싶어서 손이 근질근질한데 지금은 더 급한 일이 있으니 잠시 뒤로 미뤄놓는다.
처음에는 화면에 있는 모든 제목 클래스명을 가져와서 리스트에 넣어두고 아래 키보드를 누를 때마다 인덱스를 하나씩 증가시켜 다음 목록으로 이동하는 방식이었다. 그러나 당연하게도 버그가 상당히 많았는데, 제일 심한 것은 한번이라도 마우스로 포커스를 다른 곳으로 이동시키면 단축키를 눌렀을 때 다시 목록의 제일 처음으로 이동하는 것이었다. 이건 포커스가 이동하고 화면이 바뀌면 리액트 컴포넌트가 리렌더링되면서 인덱스가 초기화되는 문제였기 때문에 useRef로 나름 간단하게 잡을 수 있었다.
그러나 또 하나의 더 요상한 문제는 키보드와 마우스 포커스가 따로인 것이었다. 포커스가 따로라는게 무슨 말이냐면 키보드로 세번째까지 내려갔다가 마우스로 첫번째 링크를 보고 나서 다시 키보드 아래 버튼을 누르면 두번째로 포커스가 이동해야 하는데, 이전 키보드 포커스의 다음 항목인 네번째로 이동한다는 소리이다. 참으로 이상하고 불편한 단축키가 아닌가?!
마침 이 때 closest라는 DOM api를 처음 알게되었고 그럼 현재 위치를 안다면 다음 포커스 대상도 찾을 수 있을 것이라고 생각했다. 단축키가 눌리면 iframe의 src에서 가져온 주소를 href값으로 가지고 있는 a 태그를 찾고, 해당 a 태그의 최상위 div 박스를 closest로 찾아 nextElementSibling으로 다음 목록으로 이동했다.
그런데 여기서 보스몹이 나타났다. 최상위 div 박스 찾는걸 그냥 개발자도구 뜯어보고 css 선택자 긁어와서 한 것이 화근이었다. 다시 한번 말하지만 구글의 DOM트리는 정말이지 너무나도.... 너무나 복잡해서 몇개의 클래스명으로는 예외케이스가 너무 많았다. 반쯤 포기할 뻔 했다가 팀원의 도움으로 클래스명 규칙을 찾아내어 해결할 수 있었다. 최종 로직 자체는 위에서 말한 것과 똑같고, 다시 생각해보니 rb트리 할 때 배웠던 중위순회를 DOM트리에서 한 셈이 되었다! 이런거 배워서 어따 쓰나 했는데, 정말로 써먹긴 하는군 이라고 생각한 포인트였다.
처음 익스텐션 시작 구조를 어떻게 잡아야 하나 하고 리액트로 크롬 익스텐션 만들기를 검색했을 때는 나오는 코드가 죄다 타입스크립트 기반이었다. 심지어 코드 상으로는 자바스크립트만 사용하더라도 형식자를 .ts로 되어있었다. 팀 내의 회의 결과 타입스크립트는 사용하지 않기로 했는데 타입스크립트 파일에 작성하기는 싫었다. 누가 봐도 설정할 줄 몰라서 그냥 넣은거잖아!
근데 도저히 content scripts에 자바스크립트가 적용되지 않았다. 이건 뭐 로그가 찍히는 것도 아니고 왜 안되는지는 모르겠고 미칠 것 같았다. 그렇다고 내가 뇌피셜로 멘땅에 헤딩!하면서 config를 작성한 것도 아니고 CRA-ts 글을 보고 따라했는데도 내가 한 건 안되고 클론받은건 됐다. 왜지 싶어서 파일 하나하나 한글자씩 비교해봤는데도 똑같음!!!! 진짜 미춰버리겠네..
일단 중요하게는 package.json의 build script를 바꾸지 않고 계속 빌드한 문제가 있었다. ㅋㅋㅋㅋ 기본 build 스크립트로 계속 하고있던 거였다. 빌드 스크립트를 webpack으로 수정하고 webpack.config.js에 .jsx 확장자를 추가했는데도 동작하지 않았다. 뭐가 되었던간에 내 코드에 한글자라도 틀린게 있어서 그런가? 싶어 클론 받은걸 jsx로 바꾸니까 이제 그것조차도 동작하지 않았다....
그 뒤로 뭐 이것저것 건드리면서 될 법할걸 시도했더니 이제 컴파일은 되는데 리액트 모듈을 import하지 못하는 문제가 있었다. import라면 바벨 문제인가 싶어서 babel-react를 추가했더니 갑자기 build조차 안됨..
[BABEL] /Users/cofla/Desktop/develop/react-chrome-extension/react-chrome-app/src/index.jsx: Using `babel-preset-react-app` requires that you specify `NODE_ENV` or `BABEL_ENV` environment variables.
환경 변수가 없대서 스크립트에 export NODE_ENV=development 를 추가하니 빌드는 됐는데 여전히 리액트 임포트는 안됨.
여기까지가 3일동안 고민한거였기 때문에 더 이상은 시작을 지체할수가 없었다. 익스텐션이 메인 기능인데 익스텐션은 준비도 안된 상태로 중간 발표라니! 결국 어쩔 수 없이 클론받은 ts코드로 일단 시작했고 나중에 뜯어 고치기로 다짐했다. 지금 생각하면 잘 한 선택이었고, 사실 더 빨리 포기하고 ts로라도 시작할 껄 그랬다. 결국 나중에 여유있을 때 웹팩을 공부해서 js로 잘 바꿨기 때문에. 역시 처음엔 전혀 이해도 안갔던 게 나중에 다시 보면 쉬워보이는 법이다.
결론: 뭔가 도저히 모르겠을 때는 일단 시작하고, 나중에 큰 그림이 그려질 때 다시 보자.
마우스 미리보기도 간단하지는 않았다. 처음에는 클릭 가능한 모든 요소의 className을 가져와서 다 이벤트를 달려다가 이벤트 위임을 사용해서 이벤트 자체는 document에 달고 원하는 className이 아니면 리턴하게 했다. (전에 배운 이벤트 위임 써먹어서 뿌듯...✌️) 그러나 구글의 DOM 구조는 참으로... 참으로 복잡해서 자꾸 a태그까지 이벤트가 발생해 링크를 클릭해버리는 문제가 있었다.

이걸 해결하려고 이벤트를 버블링이 아닌 캡쳐링 방식으로 달았는데 예외케이스가 너무 많았다. 연관검색어 부분이나 푸터의 정책부분이나... 그래서 버블링&캡쳐링 다 달고 예외적인 className이나 id를 가지면 예외 처리하도록 했다. 나름 고급지게 처리한다고 예외 처리할 className&id 리스트 만들어서 타겟의 이름이 리스트에 포함되는지 확인하고 중복이벤트는 함수로 빼서 이벤트리스너에 달아줬는데, 생각해보니 이벤트리스너가 붙은건 document라서 버블링이 의미가 없는 짓이었다.
그리고 나는 저 미세하게 다른 h3 태그와 a 태그의 영역 차이때문에 이벤트를 a 태그에서 실행시키고 싶었다. "React" 부분에 마우스를 올렸을 때 처음 이벤트 전파 시작점은 h3 태그지만 버블링되면서 바깥쪽의 a 태그에도 이벤트가 전파되니까 event.target이나 event.currentTarget으로 a태그를 구분할 수 있을 줄 알았는데 아무리 해도 a 태그는 타겟으로 나오지 않았다. GPT 말로는 된다는데 나는 왜 안되는 것인가.. 결국 이벤트 타겟은 항상 h3로 고정되어 나오길래 타겟의 parentElement가 a 태그일 때 이벤트를 실행하는 것으로 할 수 밖에 없었다.
이후에 키보드가 추가되고 이 로직을 공용 함수로 빼면서 타겟의 closest 중 a 태그를 찾는 방식으로 바뀌었지만 그래도 썩 마음에 드는 코드는 아니다. 나중에 이벤트리스너에 대해 더 빠삭해지면 다시 고민해봐야겠다.