프론트엔드

프론트엔드 개발에 관련된 내용을 포스팅합니다.
Tanstack Query ㅡ staleTime, gcTime 한 눈에 복기하기
한 눈에 확인해보자면 .. staleTime : 이 데이터가 신선하다고 보는 시간 gcTime : 아무 컴포넌트가도 안쓰는 캐시를 메모리에 남겨놓는 시간 staleTime은 refetch를 할지 말지 영향을 주는 요소이다. Q. 5분이라고 가정했을 때, 흐름은 아래와 같다. 0분 : 데이터 fetch 0 ~ 5분 : 데이터가 fresh한 상태 5분 이후 : stale 상태 fresh 상태에서는 같은 querykey로 다시 useQuery가 실행되어도 보통 네트워크 요청을 다시하지 않고, 캐시 데이터를 사용한다. Q. 만약 5분이 지난 경우라면? 0분 : "/organizations" 진입 → API 요청 7분 : 다른 페이지 갔다가 다시 "/organizations" 진입 → stale 상태 → 기존 캐시 데이터 먼저 보여줄 수 있음 → 백그라운드 refetch 가능 2. gcTime은 캐시를 언제 알게할지에 대한 영향을 주는 요소이다. 여기서 중요한 점은 gcTime은 쿼리가 inactive 된 순간부터 돌아간다. active와 inactive의 차이점 active : 어떤 컴포넌트가 useQuery로 이 데이터를 사용 중 inactive : 아무 컴포넌트도 이 queryKey를 사용하지 않음
  • 현우
Tanstack Query ㅡ placeholderData : 이전 데이터를 미리 보여주고싶을 때
서버 상태를 데이터 테이블 컴포넌트에서 그대로 사용하면서, 페이지가 넘어갈 때 새로운 데이터를 패칭하면서 데이터 테이블 컴포넌트의 페이지가 undefined으로 떨어지며, 이전 상태 값을 잃어버리는 상황이 발생했다. 이렇게 되면 로컬 상태로 현재 페이지를 다뤄야하는 상황인데.. 서버 상태를 그대로 사용하면서 패칭할 수 있는 방법이 없을까 고민을 하다가 placeholderData 옵션으로 해결이 된다는 것을 알게되었다. placeholderData란? 이전의 keepPreviousData: true 플래그 형식으로 이전 데이터를 보여줄 것인지에 대한 옵션 값으로 사용되었으나, 현재는 placeholderData: (previousData) => previousData 형태로 사용을 할 수 있다. Tanstack Query에 placeholderData는 새 쿼리 결과가 오기 전까지 임시로 보여줄 데이터를 저장하는 옵션이다. 기본적으로 이전에 성공했던 결과 값이 전달되며, 미리 보여줄 데이터를 사전에 설정할 수 있다. queryKey가 바뀌어서 새 요청을 보낼 때 → 응답이 오기 전까지 화면을 비우지 않고 → 직전 데이터(위에 코드에서는 previousData)를 그대로 보여준다. 어떻게 사용할 수 있을까? 나 같은 경우에는 데이터 테이블 컴포넌트에 클라이언트 상태를 넣지 않고, 모두 서버 상태로 대치를 하기 때문에 리스트에서 placeholderData가 존재하지 않으면 data가 undefined가 되어 전체 아이템 개수가 0으로 잠깐 내려가는 현상이 발생할 수 있다. 그래서 placeholderData에 이전 데이터를 사용할 것이라 명시를 해준다면 이전 데이터 잠시 유지되며 데이터 테이블 컴포넌트가 급격하게 1페이지로 보정되는 현상을 줄일 수 있다. 즉, 로딩 중에도 이전 값을 일시적으로 유지해주는 기능이라고 생각을 하면 된다. placeholderData는 staleTime, gcTime과 수행하는 역할이 다르기 때문에 독립적으로 운용되며 전혀 관여하지 않으니 주의하자.
  • 현우
SCSS에서 Flex 스타일을 반복적으로 사용하고 있을 때, 어떻게 추상화할 수 있을까?
SCSS를 사용하는 환경에서 Flex 박스로 레이아웃을 정의하며 계속 하위 트리로 내려간다고 가정을 할 때, 아래와 같은 구조가 발생하게 된다. 부모에서 Flex 스타일 속성을 정의하고, 또 자식 요소에서 레이아웃 구조 구성을 위해 Flex 스타일을 정의하게 되면 무한으로 Flex 스타일 속성을 매번 정의를 해줘야하는 상황이 발생한다. 이러한 반복 구조를 매번 직접 작성을 해주면 개발 피로도가 생각보다 엄청나다, 그래서 SCSS에서는 @mixin 키워드를 사용하여 이러한 문제들을 해결할 수 있다. 1. @mixin으로 스타일 반복 구조 줄여보기 flex 라는 mixin을 정의하고, 이 구조를 프로그래밍 언어의 함수처럼 사용을 할 수 있다. 먼저 .scss 파일에 아래와 같이 정의를 하면 된다. 그런데 여기서, Vue의 SFC scope SCSS를 사용하는 사람이라면, 해당 블록은 독립적으로 컴파일 되기 때문에 아무리 @mixin을 글로벌로 선언하더라도 모듈을 불러오지 못하기 때문에 사용을 하지 못한다. 그럴 경우에는 파일에서 @use 키워드를 통해 직접 임포트를 하거나, 아래와 같이 vite.config.js에 모든 SCSS에 자동 주입을 하는 방법이 있다. 2. 컴포넌트로 추상화해보기 위의 @mixin 스타일을 그대로 사용해 컴포넌트로 만들기는 했지만, 위의 @mixin 스타일 적용을 해보았다면 아래 컴포넌트 구조화는 쉽게 이해가 될 수 있다. 컴포넌트를 래핑하여 하위 컴포넌트들의 엘리먼트 배치를 컴포넌트 선언단에서 제어하는 방식이다. 래핑된 컴포넌트들은 <slot/> 에 렌더링되면서 사전에 정의한 배치 구조로 나오게 된다.
  • 현우
Axios 배열 파라미터에 [] 형태가 붙는 경우 어떻게 해결할까?
Axios를 사용하면서, 배열 파라미터로 서버에 데이터를 전송할 때 클라이언트 상태에서는 정상적으로 ['A', 'C', 'D'] 형태로 삽입되는 것을 확인했다. 그런데 Axios로 서버에 전송을 할 때마다 아래와 같이 전송이 되는 현상이 발생한다. Axios에서 제공하는 paramSerializer 를 사용하게 된다면 원하는 형태의 배열을 브라켓 없이 손쉽게 전송할 수 있다. axios@1.x 기준으로 아래 코드를 참고한다. 위의 전송 동작으로 네트워크 탭의 페이로드를 확인하게 되면 URL 인코딩이 되면서 아래와 같이 노출된다. 서버 측에서는 아래와 같이 배열을 브라켓 없이 반복 key로 보내는 방식으로 원하고 있는데, 이 경우에는 클라이언트에서 어떻게 처리를 해줄 수 있을까? Axios paramsSerializer 사용해보기 indexs = null 을 주면 배열이 아래와 같이 서버로 전송된다. 반대로 기본 값 또는 ìndexes: false 면 직렬화가 되지 않으니 주의해야한다.
  • 현우
EventSource vs Fetch + ReadableStream (SSE)
전사 AX(Artificial Intelligence Transformation) 전환을 하면서 자연스레 서비스에도 Agentic UI를 적용하면서, SSE를 적용하는 과정에서 브라우저에서 제공하는 new EventSource 를 POST 메서드에서는 사용하지 못한다는 것을 알게되었고 EventSource 방식과 Fetch + ReadableStream(SSE) 방식의 차이를 공부해보고자 해요. AX(Artificial Intelligence Transformation)이란? '인공지능 전환'을 뜻하는 말로, 기업이나 조직이 인공지능(AI)을 핵심 동력으로 삼아 업무 방식, 의사결정 체계, 비즈니스 모델 전반을 새롭게 재설계하는 혁신 과정을 의미합니다. SSE(Server-Sent Events)란? 서버 → 클라이언트 단방향 실시간 스트리밍 프로토콜입니다. Content-Type: text/event-stream 으로 연결을 유지하면서 서버가 data: ...\n\n 형식으로 계속 데이터를 밀어냅니다. 실제로 호출을 하게 되면 아래와 같은 결과 값을 네트워크 탭에서 확인할 수 있습니다. EventSource (브라우저 내장 API) 브라우저가 자동으로 재연결 처리를 지원합니다. (retry : 필드 지원) 연결이 끊기면 자동으로 재시도를 합니다. GET 전용입니다, URL에 파라미터를 쿼리스트링으로만 전달이 가능합니다. Authorization 헤더 추가가 불가합니다. 즉, JWT 인증이 불가한 상황이라는 겁니다. 쿠기 기반의 인증만 가능합니다. (withcredentials : true) 제약 사항으로는 GET 메서드 전용이며, 헤더를 추가하지 못합니다. 보통 주식 시세, 알림, 공개 피드 같은 곳에서 사용을 합니다. Fetch + ReadableStream (SSE) POST + Request Body → 긴 질문, 복잡한 옵션 전달이 가능합니다. Authorization: Bearer ... 헤더가 가능합니다. 즉, JWT 인증이 가능합니다. 자동 재연결이 없으며, 직접 구현해야합니다.
  • 현우
Vercel 서버리스 함수란?
Vercel 서버리스 함수는 서버를 직접 관리하지 않고, 백엔드 로직을 실행할 수 있는 기능이에요. 기본적으로 상태가 없는 Stateless 하며, 각 요청은 독립적으로 처리되어요. 함수는 HTTP 요청이 들어올 때만 실행이 되고, 유후 상태에서는 비용이 발생되지 않아요. 또, 트래픽에 따라 자동으로 인스턴스가 늘어나는 구조를 가지게 됩니다. /api 폴더 아키텍쳐로 서버리스 함수 구현해보기 비교적 간단하게 사용을 할 수 있는데, 아래와 같은 예제로 구성을 할 수 있어요. 서버리스 함수는 서버가 없어도 호출할 수 있는 구조이기에, 단순한 정적 사이트에서도 API 호출을 테스트하거나 간단한 서버 로직을 구현하는데 활용 할 수 있어요. 프론트엔드 중심의 프로젝트에서 별도의 백엔드 환경 없이도 빠르게 데이터를 처리할 수 있다는 것이 강점인 것이죠. vercel.json으로 서버리스 함수 구현해보기 /api 폴더 아키텍쳐가 아닌 vercel.json 으로 서버리스 함수를 명시적으로 구현을 하는 방법도 존재해요. 만약 서버 자원이 아닌, 서버에서 html 을 받아 html 의 일부를 파싱하고, 수정하는 서버리스 함수를 만든다고 가정을 해볼게요. Vercel 서버리스 함수, 편하고 마냥 좋은거 아닌가요? 마냥 좋지만은 않아요. 콜드스타트 방식이면서 Vercel 서버리스 함수는 실행에 제약을 가지고 있는데, 기본은 10초 · Pro 플랜은 60초의 최대 요청 시간을 제공해요. 메모리는 최대 1GB이기 때문에 주의해서 사용을 해야합니다. Vercel 서버리스 함수의 동작 원리 일반 서버와 서버리스 함수를 비교해보면 아래와 같은 동작 원리를 가지게 되는 것을 볼 수 있어요. 요청이 들어올 때만 함수를 생성해서 요청을 처리한다는 것은 "콜드 스타트" 라는 방식이에요. 콜드 스타트와 웜 스타트? 위에서 확인한 콜드스타트의 개념을 차용해서 보면 Vercel 서버리스 함수는 아래와 같은 내부적인 세부 동작을 가지게 돼요. 또 각각의 Vercel 서버리스 함수는 각 요청 시 마다 개별적이고 독립적으로 움직이게 됩니다.
  • 현우
1
시맨틱 태그 파헤쳐보기 (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
悠悠自適
삼평동 연구원 이야기
운영중인 프로덕트
© 2026 悠悠自適, Inc. All rights reserved.
Made with Slashpage