Sign In

프론트엔드

프론트엔드 개발에 관련된 내용을 포스팅합니다.
시맨틱 태그 파헤쳐보기 (2)
어떤 경우에 어떤 시맨틱 태그를 써야할까? <article> vs <section>, 둘 중 어떤 것으로 래핑을 해야할까? 엘리먼트 구조를 구성할 때, 두 태그 모두 콘텐츠를 묶는 역할로 사용을 하고 있다. 그런데 두 태그를 "언제 어떻게" 사용을 해야하는지에 대해서는 명확하게 사용법을 알고있지 못하는 것 같다. 핵심 기준은 하나이다, 페이지에서 떼어내도 독립적으로 의미가 있으면 <article>, 아니면 <section>. <article> 안에 <section>이 들어갈 수 있고, <section> 안에 <article>도 들어갈 수 있다. <article>은 중첩도 가능하다. <button> vs <a>, 둘 중 어떤 것을 사용해야할까? <a>는 링크(URL 이동)용이고, <button>은 동작(스크립트, 폼 제출)용이다. <a href="#">나 <a onclick="...">로 버튼 역할을 구현하는 경우를 종종 봤는데, 스크린리더는 이걸 "링크"로 읽는다. 키보드로 Enter를 눌렀을 때 동작이 예상과 다를 수 있다. <time> 태그는 어떤 경우에 사용할까? 화면에 표시되는 텍스트는 "2주 전"처럼 사람이 읽기 편한 형식이어도, datetime 속성에 ISO 형식을 넣으면 검색엔진과 보조 기술이 정확한 시각을 파악할 수 있다. heading 계층은 순서를 지키는 것이 좋다 스크린리더 사용자는 heading 목록으로 페이지를 탐색한다. h1 → h3처럼 계층이 건너뛰면 탐색 구조가 깨진다. 만약, 텍스트 크기가 이유라면 CSS로 조정하는 것이 올바르다. 시맨틱 태그의 주의사항 <section>은 기본적으로 랜드마크가 아니다 앞서 설명했지만, 접근 가능한 이름(accessible name)이 없는 <section>은 스크린리더의 랜드마크 목록에 등록되지 않는다. <section>을 쓸 때는 aria-label이나 aria-labelledby로 이름을 주거나, 제목을 <h2>~<h6>로 명확히 제공하는 것이 좋다. ARIA role을 중복으로 선언하지 않는 것이 좋다
  • 현우
시맨틱 태그 파헤쳐보기 (1)
처음 웹 퍼블리싱을 진행할 때 거의 모든 레이아웃을 div 태그에 의존하여 구성을 진행했었다. 클래스 이름이나 아이디 값을 붙여 다른 사람들이 잘 이해하면 그렇게 퍼블리싱이 끝난 줄 알았다. 처음 프론트엔드 개발에 본격적으로 진입을 했을 때, HTML이 사실 "의미"를 담는 언어라는 건, 한참 뒤에야 알게 됐다. 시맨틱 태그는 태그 하나하나가 "이 콘텐츠가 어떤 것을 의미하는지" 브라우저와 검색엔진, 스크린리더에게 알려주는 도구이다. <div class="header"> 와 <header> 는 눈으로 보기에 같은 의미를 담고 있을 수 있지만, 기계가 읽을 때는 전혀 다르게 수행한다. 당시에는 "잘 동작하니까" 라는 이유로 이 차이점을 체감하지 못했으나, 접근성 이슈에 대해 조금 고찰을 하고나서 의문이 생겼다. 스크린 리더가 내 페이지를 어떻게 읽는지, 그리고 어떤 태그가 실제로 영향을 주는 것인지 다시 한번 시맨틱 태그에 대해 들여다보고자 한다. div 수프가 표준이던 시절 HTML이 처음 만들어진 건 1991년이다. 팀 버너스리(Tim Berners-Lee)가 문서를 서로 연결하기 위해 설계한 언어였다. 초기에는 구조보다 내용이 중요했다. 폰트 크기를 키우고 싶으면 <font size="5">, 글씨를 굵게 하고 싶으면 <b> 태그를 썼다. 그 시절 웹 개발자들은 테이블로 레이아웃을 만들었다. <table>, <tr>, <td>를 격자처럼 배치해 컬럼 구조를 구현했다. 이후 CSS가 등장하면서 테이블 레이아웃은 줄었지만, 그 자리를 <div>가 채웠다. <div>를 쌓아 레이아웃을 잡는 방식이 표준처럼 자리잡았고, 이를 "div 수프(div soup)"라고 부르게 됐다. 이 구조는 시각적으로는 잘 동작한다. 스타일도 입힐 수 있고, 레이아웃도 자유롭게 잡힌다. 근데 기계 입장에서는 모든 게 그냥 "빈 상자"다. 어디가 헤더인지, 어디가 탐색 영역인지, 어디가 주요 콘텐츠인지 알 수 없다. 2008년 HTML5 초안이 나오면서 상황이 달라졌다. W3C와 WHATWG는 <div> 대신 의미를 가진 구조 태그들을 도입했다. <header>, <nav>, <main>, <article>, <section>, <aside>, <footer>. 이 태그들이 현재 우리가 쓰는 시맨틱 HTML의 기반이다.
  • 현우
1
Nuxt4의 useFetch · useAsyncData · $fetch
Nuxt를 처음 쓸 때 데이터를 가져오는 방법이 여러 개라는 게 헷갈렸다. useFetch도 있고, useAsyncData도 있고, $fetch도 있다. 셋 다 API를 호출하는 것 같은데 왜 따로 존재하는걸까? $fetch가 네이티브 fetch나 axios 처럼 이벤트처럼 사용하는 것이고, 또 useFetch는 데이터 뿐만 아니라, 로딩 및 에러 상태를 받아볼 수 있고.. 이 세가지의 패칭 방식에 대해서 온전히 잘 모르다보니 어느 상황에서 사용해야하고 적용해야하는지 헷갈렸다. Nuxt는 서버와 클라이언트 양쪽에서 코드가 실행되는 환경을 제공한다. 같은 컴포넌트 코드가 서버에서 한 번 실행되고, 브라우저에서 또 실행된다. 이 구조에서 데이터를 어떻게 가져오느냐에 따라 API 호출 횟수, 사용자 경험, 성능이 모두 달라진다. 세 가지 방법은 이 구조 안에서 각자 다른 역할을 위해 설계됐다. 먼저 Nuxt4의 SSR 방식 가볍게 알아보기 브라우저에서만 돌아가는 React 앱이라면 데이터 페칭은 단순하다. 컴포넌트가 마운트되면 API를 호출하고, 응답이 오면 화면에 표시한다. 그게 전부다. Nuxt의 SSR(서버 사이드 렌더링)은 다르다. 같은 페이지를 두 번 렌더링한다. SSR(Server-Side Rendering) 브라우저가 아닌 서버에서 HTML을 미리 만들어서 보내주는 방식. 첫 화면이 빠르게 보이고, 검색엔진 최적화(SEO)에 유리하다. 하이드레이션(Hydration) 서버에서 만든 정적 HTML에 Vue가 달라붙어서 상호작용 가능한 앱으로 만드는 과정. 이 시점에 <script setup> 코드가 브라우저에서 다시 실행된다. 이 이중 호출 문제를 해결하기 위해 Nuxt는 페이로드(payload) 메커니즘을 사용한다. 서버에서 가져온 데이터를 HTML 안에 직렬화해서 담아두고, 브라우저에서 하이드레이션할 때 그 데이터를 재사용한다. useFetch와 useAsyncData는 이 메커니즘을 활용한다. 서버에서 실행하고, 데이터를 페이로드에 담고, 클라이언트에서는 API를 다시 호출하지 않는다. $fetch는 이 메커니즘을 사용하지 않는다.
  • 현우
1
깃 서브모듈과 NPM 라이브러리 비교하기
사내에서 각 프로젝트 별로 유틸 함수들이 따로 관리되다보니 동기화 과정 자체가 너무 힘들었다. 그래서 모노레포로 프로젝트를 만들기에는 빌드 설정과 리소스가 부족하다보니 유틸 함수 라이브러리를 만들어서 배포하자는 생각을 했다. 그 당시 당연하게만 생각헀던 NPM 라이브러리였다. 그러다 우연히 면접 과정에서 "서브 모듈로도 고려해보시지 않은 이유가 무엇인가요?" 라는 질문이 나왔고, "왜 서브모듈이어야할까?" 라는 생각이 문득 들었다. 사실 NPM이 아니라 서브모듈이라는게 많이 낯설었다. "그게 뭐가 다른 거지?" 라는 의문이 생겼고, 막상 찾아보니 둘의 동작 방식이 생각보다 근본적으로 달랐다. 그냥 나한테 익숙하다고 빠른 선택을 하는 것이 아닌, 어떤 것들이 존재하고 무엇이 최선의 선택인지를 항상 생각해야한다는 것을 다시 한번 일깨워주었다. 그래서 GIt 서브 모듈로 했을 때의 경험들은 어떠할까에 대한 내용을 배워보고자 포스팅을 진행한다. 두 방식이 각각 왜 생겼을까? 코드를 공유하는 방법의 역사 소프트웨어에서 코드를 공유하는 방법은 크게 두 방향으로 발전해왔다. 하나는 소스 코드를 직접 참조하는 방식이고, 다른 하나는 배포된 패키지를 의존성으로 선언하는 방식이다. Git 서브모듈은 전자의 계보에 있다. 2008년 Git 1.5.3에 처음 도입됐다. 당시에는 모노레포 도구도 없었고, 하나의 저장소에 여러 프로젝트를 담는 표준적인 방법이 없었다. "다른 저장소를 내 저장소 안에 포함시키는" 서브모듈은 그 공백을 채우는 자연스러운 해결책이었다. 내부 공유 라이브러리, 서드파티 코드, 플러그인 시스템 등에 쓰였다. NPM은 후자의 계보다. 2010년 Node.js 생태계와 함께 등장했다. 소스 코드가 아니라 빌드되고 버전이 붙은 패키지를 중앙 레지스트리*에서 내려받는 방식이다. 버전 범위를 선언하면 npm이 알아서 호환되는 버전을 찾아주고, node_modules에 설치해준다. 레지스트리 패키지를 저장하고 배포하는 중앙 서버. 기본값은 npmjs.com이며, 사설 레지스트리(GitHub Packages, Verdaccio 등)를 직접 운영할 수도 있다.
  • 현우
Nuxt 2와 Nuxt 3, 무엇이 달라진걸까?
Vue2 → Vue3 마이그레이션을 진행하면서 Options API와 Composition API를 다양하게 사용해왔다. 그러면서 자연스레 Vue의 서버 사이드 렌더링에도 관심을 가지게 되었고, Nuxt에 대해 공부를 하고 싶어졌다. Nuxt의 신규 프로젝트를 스캐폴딩하여 만들어보니, 신규 프로젝트는 Nuxt3로 시작을 하게 되었고 Nuxt2와 Nuxt3의 차이는 무엇이 있는지 문득 궁금해졌다. (like Next의 Page 라우터와 App 라우터의 변천사처럼 ^,^..) Nuxt가 어떻게 여기까지 왔을까? Nuxt는 2016년에 등장했다. Next.js가 React 생태계에서 SSR을 풀어낸 방식에서 영감을 받아, Vue 커뮤니티에서 같은 개념을 구현한 것이다. Nuxt 1, Nuxt 2를 거치면서 Vue 생태계의 표준 SSR 프레임워크 위치를 굳혔다. Nuxt 2는 2018년에 출시됐다. Vue 2 기반, Options API, Vuex, webpack, Connect 미들웨어 스택. 안정적이었고 생태계가 풍부했다. 지금도 레거시 코드베이스에서는 주류다. Nuxt 3는 2022년 말에 정식 출시됐다. 단순한 메이저 버전 업이 아니다. Vue 3와 Composition API를 중심으로 프레임워크 전체를 다시 작성했다. 빌드 도구는 Vite로 교체됐고, 서버 엔진은 Nitro라는 완전히 새로운 것으로 바뀌었다. asyncData와 Vuex는 사라졌다. 그 과정에서 Nuxt 2 유지보수는 2024년 6월에 공식 종료됐다. 지금 Nuxt 2를 프로덕션에서 쓰고 있다면 보안 패치도 없는 상태다. 내부에서 실제로 무슨 일이 일어날까? 표면적인 API 차이보다 내부 동작의 차이가 더 중요하다고 한다. Nuxt 2와 Nuxt 3가 어떻게 다르게 동작하는지를 이해하면, "왜 이런 식으로 써야 하는가"가 설명된다. 빌드 파이프라인: webpack vs Vite Nuxt 2는 webpack을 사용한다. webpack은 모든 모듈을 번들링한 뒤 개발 서버를 시작한다. 프로젝트 규모가 커질수록 이 초기 번들링 시간이 길어진다. (사내의 백오피스 프로젝트도 Webpack 기반의 프로젝트였다가, 초기 번들링 시간이 너무 오래 걸려 각 모듈을 그대로 서빙해주는 Vite로 변경을 하게 되었다)
  • 현우
"Ref" 가 화면에 전달되기까지의 과정들
문득 ref.value = x 가 화면으로 전달되기 까지의 내부 과정들이 궁금해졌다. 상태 값이 바뀌면 컴포넌트가 리렌더링되는 라이프사이클과 빗대어 생각을 하게 되지만, 내부적으로는 어떤 값들이 트리거되어 레이아웃 상에 반영되는지 그 코어의 개념을 모르는 듯한 느낌이 들었다 ^,^ .. Vue의 반응형이 왜 이런 구조를 갖게 됐을까? 초기 프론트엔드 개발에서 상태 변화를 화면에 반영하려면 직접 DOM을 조작해야 했다. document.getElementById로 노드를 찾고 innerHTML을 바꾸는 방식이었다. 상태가 늘어날수록 "이 값이 바뀌면 저 DOM을 바꿔야 한다"는 연결고리를 직접 관리해야 했고, 그 관계가 복잡해지면 추적이 불가능해졌다. Vue 2에서 이 문제를 Object.defineProperty 기반의 반응형 시스템으로 해결하려 했다. 데이터가 바뀌면 연결된 DOM이 자동으로 업데이트되는 구조였다. 근데 배열 변이나 런타임에 추가된 프로퍼티는 감지하지 못하는 한계가 있었다. Vue 3에서 Proxy 기반으로 전면 재설계했다. ref는 그 반응형 시스템의 가장 기본적인 단위다. 숫자, 문자열 같은 primitive 값은 Proxy로 직접 감쌀 수 없기 때문에, ref는 .value 프로퍼티 안에 값을 담고 거기에 getter/setter를 붙이는 방식으로 반응형을 구현한다. 왜 Object.defineProperty 는 배열 변이나 추가된 프로퍼티를 감지하지 못할까? Object.defineProperty 는 이미 존재하는 속성의 값을 변경할 때 가로채기 위해 사용이 되는데, 동적으로 추가되는 구조 전체를 감시하는 것이 아니라 정의하는 싲머에 존재하는 특정 키들에 대한 게터와 세터를 설정하기 때문에 동적으로 추가되는 프로퍼티에 대해서는 감시가 설정되어있지 않아 감지를 할 수 없다. ref는 내부적으로 어떻게 생겼을까? ref(0)을 호출하면 RefImpl이라는 클래스의 인스턴스가 만들어진다. dep이라는 필드가 지금 현재 바라보고 있는 ref 를 바라보고 있는 이펙트들의 집합이기 때문에 핵심이며, "지금 나를 읽고 있는 코드"를 여기에 담아둔다. 값이 바뀌면 이 목록에 있는 코드들에게 알린다. 이 구독/알림 메커니즘이 반응형의 본질이다.
  • 현우
내가 읽으려고 만든 Nuxt 입문하기
Vue3 + Next.js App Router 경험자 기준으로 작성. 1. 핵심 구조 차이 2. 파일 기반 라우팅 개념은 같지만 파일명과 폴더 구조 방식이 다르다. Next.js App Router 폴더가 라우트 세그먼트이고, 폴더 안의 page.tsx가 실제 페이지. Nuxt3 .vue 파일 자체가 라우트. 폴더 구조도 동일하게 지원. 동적 파라미터 접근 라우터 이동 3. 컴포넌트 모델 — 가장 큰 차이점 이 부분이 Next.js App Router와 Nuxt3의 가장 근본적인 차이다. Next.js App Router: Server Component vs Client Component Nuxt3: Universal Component (단일 모델) Nuxt3에는 Server Component / Client Component 구분이 없다. 모든 컴포넌트는 SSR 시 서버에서 실행되고, hydration 후 클라이언트에서도 실행된다. <script setup> 하나로 모든 것을 처리한다. 브라우저 전용 코드 처리 4. Auto-imports Nuxt3의 핵심 기능. import 없이 사용 가능한 것들: components/ 폴더의 모든 컴포넌트 composables/ 폴더의 모든 composable
  • 현우
선언적 프로그래밍과 절차적 프로그래밍
코드 리뷰를 하다 보면 "이 코드 좀 더 선언적으로 작성하면 좋겠는데요"라는 말을 종종 듣는다. 근데 막상 "선언적이 뭔지 설명해봐"라고 하면 정확히 말하기가 쉽지 않다. 처음 컴퓨터 공학과에서 선언적 프로그래밍과 절차적 프로그래밍의 개념을 숙지하고, 프론트엔드에 적용을 할 때 조차 이 구분이 명확하게 와닿지 않았다. 예전만 해도 목록에서 특정 조건을 만족하는 항목만 골라 새 배열을 만드는 코드를 for 루프로 짰고, 그게 잘못됐다는 생각을 해본 적이 없었다. 코드 리뷰를 스스로 돌이켜보면서 .filter() 로 쓰면 의도가 더 잘 드러난다는 것을 생각해보면서, 무엇(what)을 원하는 선언적 프로그래밍과 어떻게(how)를 원하는 절차적 프로그래밍이 단순히 스타일 차이가 아니라는 것을 깨닫게 되었다. 본래 프로그래밍의 시작은 절차적 프로그래밍이였다. 컴퓨터는 본래 절차적으로 작동한다. CPU는 명령어를 위에서 아래로, 순서대로 실행한다. 초기 프로그래밍 언어(FORTRAN, C)도 그 구조를 그대로 따랐으며, "데이터를 읽어라 → 조건을 확인해라 → 이 변수에 결과를 써라" 같은 방식으로 진행이 된다. JS도 초기에는 이 패러다임이 주류였다. for 루프로 배열을 순회하고, 변수에 값을 직접 쌓고, 콜백을 중첩했다. 지금도 JS 엔진 자체는 절차적으로 코드를 실행한다. 선언적 코드도 결국 런타임에서는 절차적 명령어의 연속이다. 선언적 프로그래밍은 그 절차적 세부 사항을 추상화 뒤로 숨긴다. 개발자는 무엇을(What) 원하는지를 표현하고, 어떻게(How) 할지는 추상화가 담당하게 한다. 두 방식의 내부 동작은 어떻게 다를까? 같은 결과이지만 신기하게도 다르게 표현이 될 수 있다. 배열에서 짝수만 골라 제곱한 결과를 만드는 코드를 두 방식으로 써보자. 두 코드는 실행 결과가 동일하다. 근데 읽히는 방식이 다르다. 절차적 코드는 루프 변수 i의 흐름을 따라가야 의도가 보인다. 선언적 코드는 .filter().map()이라는 서술만 봐도 의도가 드러난다. 런타임에서 실제로 무슨 일이 일어날까?
  • 현우
SCSS를 사용하다가 알게된 BEM (Block Element Modifier)
회사 사내에서 Vue 기술스택을 많이 사용하다보니, 자연스레 SCSS 를 많이 사용하게 된다. React 의 Emotion 등의 스타일, 그리고 최근 많이 사용하고 있는 Tailwind 의 경우에는 자연스럽게 내부 난수와 같은 값으로 클래스 명을 유니크하게 지정해주면서 스타일을 지정해준다. SCSS 는 스타일은 서비스 도메인의 바닥부터 함께 스타일 구조들을 천천히 쌓아나가며, 직접 이름을 짓고 붙여주는 과정을 진행하게 되는데 이를 사용해보았다면 CSS 클래스 이름을 어떻게 지어야 할지 고민을 했던 적이 있을 수 있다. 처음 CSS를 배웠을 때, 클래스 이름은 그냥 의미 있어 보이면 된다고 생각했다. .header, .menu, .button처럼. 회사 업무를 진행하면서 막상 생각치도 못한 곳에서 문제가 생겼다. 누군가 .button을 이미 쓰고 있다면, 거기에 내가 .button을 또 정의하면서 스타일이 충돌하게 된다는 것인데, 만약 스타일시트에 모듈과 같은 스코프가 정의되어있지 않다면 CSS의 특성상, 이건 단순한 실수가 아니라 구조적인 문제였다. 그때부터 "이름을 어떻게 지어야 하는가"가 진짜 문제처럼 느껴졌다. 그러다 — , __ 클래스 네임에 프리픽스가 붙는 클래스 네임들을 발견하게 되었고, 이게 뭔가 찾아보다가 포스팅을 시작하게 되었다 ^,^ .. 바로 BEM(Block Element Modifier)이라는 개념인데, CSS 클래스 네이밍 방법론 중 가장 오래되고, 가장 널리 쓰이는 방식이다. BEM은 왜 생겼을까? BEM은 러시아 인터넷 기업 Yandex에서 2005년경 내부적으로 사용하기 시작한 방법론이다. Yandex는 대규모 웹 서비스를 수십 명의 개발자가 함께 유지보수해야 했고, 스타일 충돌과 유지보수 어려움이 반복됐다. 2010년대 초, CSS 생태계에는 마땅한 기준이 없었다. 클래스 이름은 개발자마다 달랐고, 프로젝트가 커질수록 스타일 파일은 누구도 건드리기 싫은 영역이 됐다. OOCSS(Object-Oriented CSS)나 SMACSS 같은 개념들이 있었지만, 구체적인 네이밍 규칙까지 제시한 건 BEM이 처음이었다.
  • 현우
1
실행 컨텍스트 · 호이스팅 · 클로저의 상관관계
자바스크립트가 동적언어라서 런타임 시점에 타입을 지정해주고, 이것저것 마법같은 동작들이 많아 마법같은 언어라고 칭하기도 하지만, 자바스크립트 내부 엔진이 코드를 실행하는 방식은 생각보다 구조적이다. "왜 선언 전에 함수를 호출할 수 있지?", "왜 클로저는 외부 변수를 기억하고 있지?" 와 같은 질문들이 전부 하나의 개념으로 연결된다. 바로 실행 컨텍스트(Execution Context)로 부터 시작된다. 실행 컨텍스트는 무엇일까? 자바스크립트 엔진은 코드를 실행하기 전에 실행에 필요한 정보들을 하나의 구조체로 묶는다. 이게 실행 컨텍스트다. 변수가 어디에 있는지, this가 무엇을 가리키는지, 외부 스코프는 어디인지에 대한 모든 정보가 실행 컨텍스트 안에 들어있다. 실행 컨텍스트는 주로 세 가지 상황에서 생성된다. 실행 컨텍스트가 생성되는 시점 전역 코드 실행 시 → 전역 실행 컨텍스트 (Global Execution Context) 함수 호출 시 → 함수 실행 컨텍스트 (Function Execution Context) eval() 실행 시 → eval 실행 컨텍스트 (거의 사용 안 함) 실행 컨텍스트들은 콜 스택(Call Stack)이라는 구조에 쌓인다. 실행 컨텍스트의 내부 구조 실행 컨텍스트는 크게 세 가지로 구성된다. 핵심은 LexicalEnvironment다. 이것 역시 두 부분으로 나뉜다. outer 참조가 스코프 체인(Scope Chain)을 만든다. 변수를 찾을 때 현재 EnvironmentRecord에 없으면 outer를 타고 올라가게된다. inner에서 x를 참조하면: inner EC → outer EC → 전역 EC 순으로 탐색한다. 전역 EC에서 x: 1을 발견하고 반환한다. 실행 컨텍스트는 두 단계로 동작한다 실행 컨텍스트가 생성될 때 바로 코드를 실행하지 않는다. 먼저 생성 단계(Creation Phase)가 있고, 그 다음 실행 단계(Execution Phase)가 진행된다. 이 두 단계가 호이스팅(hoisting)의 실제 이유다.
  • 현우
React와 Vue에서 추구할 수 있는 좋은 코드는 무엇일까?
React와 Vue를 함께 쓰다 보면 두 프레임워크 모두 지정된 포맷팅이 너무 다르기도 하고, 추상화 방식도 생각보다 달라서 Vue 를 쓰다가 React 를 사용하게 되면 혼동되는 경우가 많이 생긴다. 예전에 풀던 과제들도 다시 풀면 롤백되어 탈락하기도 하고.. React 를 Vue 의 template 문법처럼 사용하는 경우도 많아서, 천천히 돌아볼겸 두 프레임워크를 동시에 사용하는 사람들을 위해 다시 한번 정리를 하고자한다. 사실 두 프레임워크의 차이를 명확하게 알고있으면 되긴 하지만, React 에서는 한 파일에서 여러 모듈들을 임포트할 수 있고, Vue 의 경우에는 SFC (Single File Component) 이기 때문에 단 하나의 모듈만 임포트할 수 있다. 사실 자바스크립트라는 교집합이 존재하지만 이 과정에서 두 프레임워크의 차이를 명확하게 알고 있고, 어떤 방식에서 차이점을 가지고 있는지 알아야 프레임워크에서 추구하는 추상화가 가능하다는 생각이 들었다. 인터넷에서 여러 문서들을 참고하고, 또 여러 과제들을 진행하면서 느꼈던 부분들을 적은 내용들이라 아마 개발자들마다 코드에 대해 생각하는 부분들이 다를 수 있겠지만 여러 문서들을 읽으며 동의했던 부분들과 내가 생각한 좋은 코드에 대한 기준을 적은 문서이니 지나가시는 분들은 가볍게 보시면 좋을 것 같다 ^,^ .. React — 좋은 코드를 작성하는 방법 코드는 예측 가능해야 한다 좋은 코드의 기준은 하나라고 생각한다. 다음 줄이 어떻게 생겼을지 읽기 전에 예측할 수 있어야 한다. 이름, 구조, 인터페이스가 일관된 패턴을 따를수록 읽는 사람의 인지 부하가 줄어든다. 예측이 맞으면 빠르게 읽히고, 빗나가면 흐름이 끊긴다. 컴포넌트는 하나의 관심사만 가지는 것이 좋다 컴포넌트가 커지는 가장 흔한 이유는 "여기에 추가하면 빠르니까"다. 하지만 한 컴포넌트가 데이터 페칭, 레이아웃, 인터랙션을 모두 담당하면 어느 하나를 바꿀 때 다른 것이 영향을 받는다. ProductPage는 데이터를 가져오는 것에만 집중하고, ProductDetail은 레이아웃에만 집중하고, ProductActions는 인터랙션에만 집중한다. 각 컴포넌트를 독립적으로 테스트하고 교체할 수 있다.
  • 현우
1
👍
1
Vue가 채택한 Proxy, React는 왜 안 사용할까?
Proxy는 JavaScript 객체에 대한 접근을 가로채서 중간에서 원하는 동작을 끼워 넣을 수 있는 기능이다. Vue 3의 반응형 시스템이 이 위에서 동작하고, MobX도 같은 방식을 쓴다. Vue 3를 공부하다가 reactive()로 감싼 객체의 프로퍼티를 직접 수정했는데 화면이 자동으로 업데이트되는 걸 보고 신기했다. React에서는 반드시 setState를 호출해야 하는데, Vue는 그냥 obj.count++만 해도 자동으로 반응성을 유지하는데, 내부에서 어떤 일이 일어나고 있을까? Proxy 이전에 라이브러리는 어떻게 만들어졌을까? 전에는 SPA 라는 개념 자체가 생소했고, MPA 개념으로 웹을 주로 사용해왔다. Proxy 라는 강력한 기능이 나오기 전에는 SPA 라이브러리는 어떻게 구성이 되었는지 너무 궁금했다. 현재는 React와 Vue를 섞어쓰다보니 메이저 버젼이 바뀌면서 아예 내부 패러다임이 변경된 Vue를 기준으로 알아보자면 Vue 3 이전, Vue 2는 Object.defineProperty()로 반응형을 구현했다. Object.defineProperty는 이미 존재하는 프로퍼티의 getter/setter만 가로챌 수 있다. 그래서 Vue 2에는 구조적인 한계가 있었다. 이 문제를 해결하려고 Vue 2는 배열 메서드를 직접 패치하고, Vue.set()이라는 특별한 API를 만들었다. 근본적인 해결이 아니라 우회였다. 2015년 ES6에 Proxy가 추가됐다. Vue 3는 2020년에 이 Proxy를 기반으로 반응형 시스템을 완전히 재설계했다. Proxy의 내부 동작 new Proxy(target, handler)는 target 객체를 감싸는 래퍼를 만든다. 이 래퍼에 대한 모든 작업(읽기, 쓰기, 삭제 등)이 handler의 트랩(trap) 을 통해 가로채진다. Reflect는 Proxy 트랩과 1:1로 대응하는 API다. 트랩 안에서 원래 동작을 수행할 때 사용한다. target[key]로 직접 접근하는 것과 결과는 같지만, receiver(프록시 자신)를 올바르게 처리하기 위해 Reflect를 쓰는 것이 안전하다.
  • 현우
2
👍
1
프론트엔드는 CJS에서 ESM으로 넘어가려고 할까?
JavaScript에서 모듈을 불러오는 방법은 두 가지다. Node.js가 오랫동안 사용해온 CJS(CommonJS) 와, ES2015 표준에 추가된 ESM(ES Modules) 이다. 라이브러리 문서에서 "This package is ESM only"라는 문구를 처음 봤을 때, 단순히 문법이 다른 것 정도로 생각했다. 그런데 파고들수록 두 시스템은 모듈을 불러오는 방식 자체가 근본적으로 달랐다. 단순히 require 대신 import를 쓰는 게 아니라, 브라우저와 런타임이 코드를 이해하는 방식이 바뀌는 것이었다. JavaScript에는 원래 모듈이 없었다? JavaScript가 처음 설계됐을 때 모듈 시스템은 없었다. 브라우저에서 스크립트 파일을 순서대로 나열하는 게 전부였다. 모든 변수가 전역 스코프에 쌓였다. 파일이 많아질수록 어떤 파일이 어떤 변수를 만드는지 추적하기 어려워졌고, 이름이 겹치면 나중에 로드된 쪽이 덮어썼다. 의존 순서를 틀리면 런타임에서야 에러가 났다. CJS — Node.js가 직접 만든 모듈 시스템 2009년 Node.js가 등장했다. 서버에서 수백 개의 파일을 다루려면 모듈 시스템이 필수였는데, 당시 JavaScript 표준에는 없었다. 그래서 Node.js가 직접 만든 게 CommonJS(CJS) 다. CJS의 내부 동작 require와 module.exports는 JavaScript 문법이 아니다. Node.js가 파일을 실행하기 직전에 모듈 래퍼 함수로 감싸서 인자로 주입하는 것들이다. require()는 그냥 함수다. 그래서 코드 어디서든 호출할 수 있다. require()가 호출되면 Node.js는 아래 순서로 동작한다. 핵심은 동기적이라는 것이다. require()가 호출되면 파일을 다 읽고 실행할 때까지 다음 줄로 넘어가지 않는다. CJS의 값 복사 방식 CJS에서 module.exports로 내보낸 값은 복사본이다. 원본이 바뀌어도 가져간 쪽에서는 모른다. count를 구조분해할 때 그 시점의 값(0)이 복사됐다. 이후 increment()로 원본 count가 바뀌어도, 복사본은 그대로다. 객체를 내보내면 참조가 공유되지만, 원시값은 항상 이 문제가 생긴다.
  • 현우
브라우저 스태킹 컨텍스트란?
CSS로 레이아웃을 만들다 보면 요소들이 겹치는 상황이 생긴다. 모달, 툴팁, 드롭다운 같은 UI가 대표적이다. 이때 어떤 요소가 위에 올라올지를 제어하는 속성이 z-index다. 그런데 z-index는 단순히 "숫자가 크면 위에 온다"는 규칙으로 동작하지 않는다. 스태킹 컨텍스트(Stacking Context) 라는 경계 안에서만 비교되는 값이기 때문이다. z-index: 9999를 줬는데 다른 요소 뒤에 가려진 적이 있다. 분명히 숫자는 더 큰데, 원하는 대로 쌓이지 않았다. 한참을 디버깅하다가 결국 찾아낸 원인은 스태킹 컨텍스트를 이해하지 못한 채로 z-index를 쓰고 있었던 것이었다. 스태킹 컨텍스트는 무엇일까? 스태킹 컨텍스트(Stacking Context) 는 z축(화면 깊이) 방향으로 요소를 쌓는 독립적인 단위다. 각 스태킹 컨텍스트는 내부적으로 고유한 레이어 순서를 가지며, 외부 스태킹 컨텍스트와 완전히 독립된다. 쉽게 말하면, z-index 비교는 같은 스태킹 컨텍스트 안에서만 유효하다. 부모가 다른 스태킹 컨텍스트에 속해 있다면, 자식의 z-index 값이 아무리 커도 그 경계를 넘어갈 수 없다. 위 구조에서 A-1의 z-index가 9999여도, B보다 아래에 쌓인다. A 자체가 z-index: 1로 B(z-index: 2)보다 낮기 때문이다. A-1은 A의 스태킹 컨텍스트 안에서만 의미가 있다. 스태킹 컨텍스트는 언제 만들어질까? 모든 요소가 스태킹 컨텍스트를 생성하는 건 아니다. 특정 CSS 속성을 가진 요소가 새로운 스태킹 컨텍스트를 만든다. 이 중에서 실수가 가장 많이 발생하는 경우는 transform과 opacity다. 애니메이션 성능 최적화를 위해 transform: translateZ(0) 이나 will-change: transform을 추가했다가, 의도치 않게 스태킹 컨텍스트가 생성되어 z-index가 꼬이는 경우가 많다. 같은 스태킹 컨텍스트 안에서의 쌓임 순서 스태킹 컨텍스트 안에서도 요소들은 일정한 순서로 쌓인다. z-index를 명시하지 않아도, position: relative를 추가하는 것만으로도 일반 블록 요소보다 위에 올라온다. z-index가 auto인 경우 새 스태킹 컨텍스트를 만들지 않으면서도 쌓임 순서에 영향을 준다.
  • 현우
👍
1
레이아웃 스레싱이란?
DOM을 조작하다 보면 분명히 코드는 간단한데, 스크롤 할 때마다 버벅이고, 애니메이션은 뚝뚝 끊긴다. 레이아웃 작업이 수백 밀리초를 잡아먹고 있는데, 이 원인은 바로 레이아웃 스레싱(Layout Thrashing)이다. 변경이 생길 때 브라우저가 거치는 단계 (픽셀 파이프라인) 초기 로드 이후, JS로 DOM이 변경될 때마다 브라우저는 아래 흐름을 다시 탄다. Style: 변경된 요소에 어떤 CSS 규칙이 적용되는지 재계산 (DevTools에서는 "Recalculate Style"로 표시) Layout: 각 요소의 위치와 크기를 계산 (Reflow라고도 부른다) Paint: 픽셀을 채우는 단계 Composite: 레이어를 합성 이 중에서 Layout 단계가 제일 비싸다. 요소 하나의 크기가 바뀌면, 연관된 모든 요소의 위치를 다시 계산해야 하기 때문이다. 브라우저는 JavaScript 실행 중에 발생하는 DOM 변경들을 즉시 반영하지 않는다. 일괄 처리(batch)해서 한 번에 처리한다. 레이아웃 스레싱은 바로 이 배치 처리를 무력화할 때 발생한다. 픽셀 파이프라인이란? 브라우저 렌더링 파이프라인은 "페이지를 처음 만드는 과정" 이라면, 픽셀 파이프라인은 "만들어진 페이지를 또 다시 갱신하는 과정"을 의미한다. 픽셀 파이프라인은 무조건 변경될 때마다 전부 다 실행되는 것이 아닌 어떤 것을 변경했냐에 따라 순서가 달라진다. width 변경 → Style → Layout → Paint → Composite color 변경 → Style → Paint → Composite transform → Style → Composite (단, opacity, transform은 Style/Layout/Paint를 건너뛰고 Composite만 실행) 레이아웃 스레싱이 무엇일까? 레이아웃 스레싱(Layout Thrashing)은 JavaScript가 DOM의 기하학적 속성을 읽고 쓰는 작업을 번갈아 가면서 반복할 때 발생한다. 왜 이게 문제냐면 ^,^ .. boxes[i].style.width를 변경하면 브라우저는 레이아웃을 무효(dirty)로 표시한다
  • 현우
2
👍
1
프엔 패키지매니저 비교하기 (npm에서 pnpm? yarn berry?)
npm install을 치고 멍하니 기다린 적이 있을 것이다. node_modules가 1GB를 넘어서 당황했던 경험도. 팀원이 yarn을 쓰는데 나는 npm을 써서 package-lock.json과 yarn.lock이 동시에 생겨버린 아찔한 기억도. 패키지 매니저는 워낙 당연한 도구라 깊이 생각하지 않고 그냥 쓰게 되는데, 사실 선택에 따라 설치 속도, 디스크 사용량, 팀 협업 경험이 꽤 크게 달라진다. 요즘 npm에서 pnpm이나 Yarn Berry로 갈아타는 팀이 늘고 있다. 어떤 패키지 매니저가 무엇이 다른지, 내부적으로 어떻게 동작하는지 제대로 정리해보려 한다. 패키지 매니저의 역사를 짧게 짚고 넘어가면 2010년 — npm이 Node.js와 함께 등장. 사실상 표준이 됨 2016년 — Facebook이 Yarn(v1) 출시. npm의 느린 속도와 비결정적 설치 문제를 해결하기 위해 2017년 — npm v5에서 package-lock.json 도입. Yarn의 장점을 상당 부분 흡수 2018년 — pnpm이 주목받기 시작. 디스크 효율성과 속도에서 차별화 2020년 — Yarn Berry(v2) 출시. node_modules를 아예 없애는 PnP 방식 도입 2022년 — Bun 등장. JS 런타임과 패키지 매니저를 통합한 새로운 접근 2023년 — Deno 2.0에서 npm 호환성 강화 패키지를 어떻게 설치하는가? — 핵심은 node_modules 구조 패키지 매니저들의 가장 근본적인 차이는 패키지를 디스크에 어떻게 배치하는가에 있다. npm — 플랫 호이스팅(Flat Hoisting) 초창기 npm(v1~v2)은 의존성을 중첩(nested) 구조로 설치했다. package-a가 package-b를 필요로 하고, package-b가 package-c를 필요로 하면 이렇게 깊이 중첩됐다. Windows에서는 경로 길이 제한(260자)에 걸려 설치가 실패하는 일도 있었고, 같은 패키지가 여러 곳에 중복 설치되어 디스크를 낭비했다. npm v3부터는 플랫 호이스팅을 도입했다.
  • 현우
1
👍
1
로컬에서 포트 번호와 작별하기(?)
내가 노드 서버를 언제 켜놓았더라? 하면서 새로운 포트번호의 localhost 서버가 실행되어 포트 번호를 직접 입력해주었던 경험.. 그래서 포트 번호를 3001로 바꾸고, API 서버는 8080인지 3001인지 헷갈리고, 어제 열어둔 브라우저 탭은 이미 죽은 서버를 가리키는 문제점들이 모든 프론트엔드 개발자라면 한번 쯔음 모두 경험해본다. 거기에 요즘 AI 코딩 에이전트까지 쓰게 되면 문제가 더 심해지는데, 에이전트가 서비스 포트를 추측하다가 틀려서 엉뚱한 곳에 요청을 보내는 일이 생긴다. 이 문제들은 사실 전부 포트 번호라는 개념에서 비롯된다. 포트 번호는 원래 개발자가 매번 의식해야 할 대상이 아니다. 네트워크 계층의 구현 세부사항인데, 어쩌다 보니 우리가 매일 외우고 관리해야 하는 것이 되어버렸다. Portless는 바로 이 문제점들을 지적하기 위해 나온 개념이다. Portless가 무엇일까? 포트 번호를 이름 기반의 안정적인 로컬 URL로 바꿔주는 도구 localhost:3000 대신 my-app.localhost 같은 URL로 앱에 접근할 수 있게 된다. 그리고 이 URL은 세션이 바뀌어도, 컴퓨터를 껐다 켜도 항상 동일하게 유지된다. 내부적으로는 경량 로컬 프록시가 동작하는 방식인데, 실제 앱은 여전히 포트에 바인딩되지만 그 포트를 Portless가 알아서 관리해준다. 개발자는 포트를 몰라도 되는 것이다. 4000~4999 범위에서 빈 포트를 자동으로 찾아 PORT 환경변수로 주입하고, HOST는 127.0.0.1로 설정해 IPv4 루프백에 바인딩한다. 기존 방식 http://localhost:3000 Portless 사용 시 http://my-app.localhost 어떤 문제를 해결하려고 했을까? 포트 충돌과 관리 피로 멀티 서비스 프로젝트를 하다 보면 포트 관리가 은근히 번거롭다. 프론트엔드는 3000, 백엔드 API는 8080, 어드민은 3001... 팀마다 포트 번호 컨벤션도 다 다르고, 로컬 환경에 다른 뭔가가 실행 중이면 충돌이 생긴다. EADDRINUSE라는 에러를 처음 봤을 때의 당혹감을 기억하는 사람이 많을 것이다.
  • 현우
1
👍
2
확장성을 결정짓는 컴포넌트 API
React를 사용하면서 여러 기능들이 추가되며 화면에서 사용되는 컴포넌트들이 존재하는 경우가 많아진다. 이때, 확장성을 위해 고려할 수 있는 2가지 정도의 컴포넌트 설계 패턴에 대해서 알아보자. Flat 패턴 Flat 컴포넌트는 내부 구조를 감추고, 대부분의 변형을 props 로 제공하는 방식을 의미한다. 위의 방식은 사용은 직관적이지만, Flat API 설계의 단점은 분명하다. 시스템이 상정하지 않는 요구사항이 등장하면, props 는 끝없이 늘어나게 된다는 한계점을 가지고 있다. actionLabel 을 hover 했을 때, callback 을 전달하고 싶을 때, button 이 아니라 anchor 로 그려 href 를 전달하고 싶을 때는 이런 flat 패턴이 오히려 확장과 유지보수를 어렵게 할 수 있다. Compound 패턴 하위 컴포넌트를 제공하고, 제품 팀이 직접 조합하여 사용하도록 제공하는 방식이다. 사용하는 쪽에서는 아래와 같이 활용할 수 있다. Flat 패턴과 비교했을 때, Compound 패턴의 장점은 유연함이다. 시스템이 미리 예측하지 못한 레이아웃도 조합으로 해결이 가능하며, 확장 지점이 코드 레벨에서 자연스럽게 제공되는 것을 확인할 수 있다. 다만, Compound 패턴도 만능은 아니며, 사용하는 측에서 구현 난이도와 코드량이 늘어나게 되는 특징을 가지고 있다. 변형 여지가 거의 없는 컴포넌트까지 Compound 패턴으로 컴포넌트화 하게 되면 사용하기 오히려 불편해지는 상황이 발생한다. 어떤 것을 사용해야 좋은 패턴 구조일까? 두 패턴을 비교해보자, 공통 컴포넌트 틀에서 title 만이 다른 상태라고 가정했을 때, Flat 패턴과 Compound 패턴은 아래와 같이 도출될 수 있다. 둘의 차이를 확인해보면 조립하는 쪽과, 조립된 상태에서 사용법을 통해 컨트롤하는 것과 같이 둘의 차이는 명확한 것을 확인할 수 있다. Flat API 는 사용 방법 또한 간결하고, Card 컴포넌트 외에는 알아야할 것들이 그다지 많지 않다. 반면 Compound API 는 개발자가 Card 구조를 이해하고 있어야하며, Header 안에 Title 들어가야하는지, Body 는 필수인지 등 학습해야할 것들이 많다. dot operator 로 하위 컴포넌트들을 탐색할 수 있지만, 올바른 조합 방법을 찾는 것은 또 다른 러닝 커브가 된다. 결과적으로 친절한 도구보다 번거로운 도구로 경험될 수 있다.
  • 현우
navigator.sendBeacon vs fetch + keepalive
사용자가 페이지를 떠날 때마다 백엔드에 몇가지 요청을 보내려고 할 때, 두가지 정도의 접근 방식이 존재한다. 바로 navigator.sendBeacon 또는 fetch + keepalive 방식이다. 두 방식의 트레이드 오프 Navigator.sendBeacon Navigator.sendBeacon의 장점 페이지가 언로드될 때 데이터를 전송하도록 특별히 설계되었다. 우선순위가 낮은 비동기식으로 페이지 언로드 프로세스를 차단하지 않는다. (네트워크 탭의 우선순위 참고) 브라우저는 페이지가 닫힌 후에도 데이터 전송을 완료한다. 간편한 API 인터페이스로 사용할 수 있다. POST 메서드를 주로 지원하기 때문에, 자동으로 POST 메서드를 사용한다. 응답을 처리할 필요없으며, 단방향 데이터에 이상적이다. Navigator.sendBeacon의 제약사항 POST 요청만을 지원한다. 응답 내용을 받을 수 없다. 크기 제한이 있다. (크롬의 경우 약 64KB) 사용자 커스텀 헤더 지원을 하지 않는다. 사용 예시 Fetch + keepalive Fetch + keepalive의 장점 모든 HTTP 메서드 (GET, POST, PUT 등)을 지원한다. 사용자 커스텀 헤더를 허용한다.
  • 현우
IndexedDB로 컨텐츠 자동 저장 구현해보기
IndexedDB는 기술 면접을 준비할 때 브라우저의 로컬 저장소에 관련된 내용들을 배우면서, 한번쯔음 들어보았을 용어이다. 사실 쿠키를 많이 사용해보았지, IndexedDB의 경우에는 사용할 일이 많이 없다고 생각해 실제로 써보지는 못했는데 이번에 게시글 자동저장 기능을 구현해보면서 여러 로컬 저장소들과의 특징들을 비교하며 도입을 하고자했다. 객체 형태의 저장 형태 · 대용량 저장소 · 비동기 처리 등의 특징과 메인 스레드를 블로킹하지 않는 특성들이 IndexedDB가 자동 저장에 적합하다고 생각이 들었다. 오늘은 IndexedDB 를 활용해보며 배웠던 내용들을 복기차 포스팅을 해보려고 한다. IndexedDB란? 브라우저에 내장된 비동기 NoSQL 데이터베이스로, 대용량 및 구조화된 데이터를 영구적으로 저장할 수 있는 클라이언트 DB이다. IndexedDB는 웹이 단순 문서에서 오프라인 및 대용량 데이터를 다루는 어플리케이션으로 진화하면서, 기존 저장소(Local Stoarge, Cookie)가 이러한 요구 조건들을 감당하지 못해 등장하게 되었다. 초창기 웹 저장소의 한계점 우리가 흔히 알고 있는 Cookie 와 LocalStorage 와 같은 로컬 스토리지의 경우에는 아래와 같은 한계점을 가지고 있었다. Cookie 4KB의 용량을 가진다. 모든 요청마다 서버로 전송된다. LocalStorage / SessionStorage ~5MB의 용량을 가진다. 동기 API로 메인 스레드의 블로킹을 한다. 문자열만 저장이 가능하며, 인덱싱 및 검색이 불가하다. (별도 로직 필요) 브라우저에서 제공하는 기존 로컬 저장소들은 설정 값을 저장하는 저장소 수준이지, DB 개념으로 사용하기에는 무리가 있었다. 2008 ~ 2012년 Gmail, Google Docs, Facebook 등의 웹 서비스들이 급부상되면서 수천수만 건의 데이터가 저장되어야하고 오프라인에서도 동작을 해야했다. 또 빠른 검색이 필요하며 UI 멈춤 없이 비동기 처리가 가능해야했기에 브라우저 자체에서 DB가 필요하다고 느끼게 되었고, 기존 브라우저의 로컬 저장소로는 해결이 되지 않던 문제를 IndexedDB를 정의하며 해결하고자 했던 것이다.
  • 현우
悠悠自適
삼평동 연구원 이야기
운영중인 프로덕트
© 2026 悠悠自適, Inc. All rights reserved.
Made with Slashpage