웹 서비스를 개발하다 보면 사용자의 입력이나 스크롤과 같이 매우 빈번하게 발생하는 이벤트를 처리해야 하는 상황이 자주 생겨요. 예를 들어 검색창에 글자를 한 자씩 입력할 때마다 서버에 요청을 보내거나, 마우스 휠을 돌려 화면을 스크롤할 때 실시간으로 요소의 위치를 계산하여 애니메이션 효과를 주는 경우를 생각해보면 되는데요. 이러한 이벤트들은 아주 짧은 시간 동안 수십 번에서 수백 번까지 발생할 수 있고, 이때마다 무거운 연산이나 API 호출을 수행하게 되면 브라우저의 메인 스레드가 점유되어 화면이 버벅거리거나 전체적인 성능이 저하될 위험이 있더라고요. 따라서 빈번한 이벤트의 실행 횟수를 적절히 제어하여 사용자 경험을 부드럽게 유지하는 과정이 꼭 필요해요.

이러한 문제를 해결하기 위해 실무에서 기본적으로 활용되는 기법이 Debounce와 Throttle이에요. 먼저 Debounce는 이벤트가 발생할 때마다 타이머를 초기화하고, 일정 시간이 경과한 뒤에 마지막으로 실행된 이벤트에 대해서만 함수를 호출하는 방식이라서요. 사용자가 입력을 마칠 때까지 기다렸다가 한 번만 동작해야 하는 검색어 자동 완성이나, 윈도우 크기가 조절되는 과정에서 최종 크기에 맞춰 레이아웃을 재계산할 때 매우 유용하게 쓰여요.

function debounce(func, wait) {
  let timeout; 
  return function(...args) {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), wait);
  };
}

반대로 Throttle은 이벤트가 아무리 빈번하게 발생하더라도 설정한 시간 간격 내에서만 함수를 실행하도록 제한하는 방식이에요. 스크롤을 따라 움직이는 메뉴바나 무한 스크롤 구현처럼, 사용자의 동작에 즉각적으로 반응하면서도 성능을 위해 처리 횟수를 일정 수준으로 묶어두어야 하는 상황에서 아주 효과적이라서요. 이 기법은 이벤트가 발생할 때마다 실행 여부를 판단하여 정해진 주기 내에서만 실행되도록 보장하는 특징이 있어요.

function throttle(func, limit) {
  let inThrottle = false;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

결국 어떤 방식을 선택할지는 해당 기능이 사용자에게 어떤 피드백을 제공해야 하는지에 따라 결정하면 돼요. 사용자의 동작이 완료되는 시점의 최종 결과값이 중요하고 그 과정 중에 발생하는 중간 단계들을 생략해도 무방한 경우라면 Debounce를 사용하는 것이 훨씬 효율적이더라고요. 반면 실시간으로 변화하는 상태에 맞춰 지속적인 반응을 보여주되 브라우저 부하를 줄이기 위해 실행 횟수만 제한해야 하는 상황이라면 Throttle이 더 적합한 선택지가 되는 것 같아요. 이 두 가지 개념의 차이를 정확히 이해하고 상황에 맞게 적용하면 불필요한 리소스 낭비를 막으면서도 훨씬 매끄러운 웹 인터페이스를 구현할 수 있어요.

Svelte 5는 지난 18개월간의 개발 끝에 안정화되었으며, 기존 Svelte 4 앱과 거의 완전히 호환되거든요. 다만 내부가 처음부터 다시 설계된 만큼 몇 가지 핵심 변화가 있어요. 가장 눈에 띄는 것은 반응성 시스템의 변경이에요. 이전 버전에서는 변수 재할당에 기반한 자동 추론이 주를 이뤘지만, 규모가 커질수록 어떤 코드가 언제 다시 실행되는지 파악하기 어려웠어요. Svelte 5에서는 Runes라는 새로운 시스템을 도입해 이를 명확히 구분하고 있어요. 덕분에 개발자가 코드 구조를 더 직관적으로 이해할 수 있게 되었네요.

성능 측면에서도 큰 변화가 있어요. derived 값이 의존성 단위에서만 갱신되므로, 복잡한 계산이나 큰 배열을 다룰 때 렌더링 비용이 확실히 줄어든다고 해요. 컴파일러가 브라우저 대신 npm run build 시점에 최적화를 처리하는 구조라 앱 크기도 작아지고 안정성도 올라가요. 또한 TypeScript 환경을 염두에 두고 설계되었어요. 컴포넌트 간 데이터 계층을 연결하는 Props 전달 규칙도 이전보다 훨씬 명확해졌어요.

{
  "devDependencies": {
    "@sveltejs/vite-plugin-svelte": "^4.0.0",
    "svelte": "^5"
  }
}

기존 프로젝트에 적용할 때는 devDependencies의 svelte 버전을 5로, @sveltejs/vite-plugin-svelte를 4로 업데이트하면 대부분 즉시 동작해요. 만약 현재 Svelte 3을 사용 중이라면 먼저 Svelte 4로 업그레이드한 뒤 진행하는 것이 안전하거든요. 레거시 컴포넌트와 새로운 Runes 기반 컴포넌트를 같은 앱에서 섞어 쓸 수도 있어요. 다만 장기적으로는 Svelte 6이나 7에서 일부 구형 기능이 지원 중단될 예정이니, 점진적으로 마이그레이션하는 것이 좋네요. 공식 제공되는 마이그레이션 도구를 활용하면 작업 부담을 크게 줄일 수 있습니다.

출처

React 를 본격적으로 다루다 보면 컴포넌트가 갑자기 재렌더링되는 이유나 memo 가 예상대로 동작하지 않는 경우가 종종 생기거든요. 이런 상황을 마주할 때 정확한 mental model 을 가지고 있으면 원인을 빠르게 파악할 수 있네요. Advanced React 는 이미 기본기를 갖춘 개발자를 대상으로 깊이 있는 이해를 도와주는 자료입니다. 컴포넌트가 재렌더링되는 이유나 memo 가 예상대로 동작하지 않는 상황, 그리고 stale closure 나 race condition 이 발생하는 원인을 차근차근 설명하고 있어요. 16 개의 심화 챕터와 105 개의 인터랙티브 예제를 통해 useMemo 나 useCallback 을 무조건 붙이는 습관에서 벗어나, 실제로 어떤 상황에서 최적화가 필요한지 구분하는 기준을 잡을 수 있죠. 코드 리뷰를 진행하거나 면접 질문을 준비할 때도 이 mental model 이 큰 도움이 될 거라고 생각해요. 대규모 애플리케이션으로 넘어갈 때는 여러 개의 JavaScript 툴체인을 고려하게 되며, 이를 통해 풍부한 React 에코시스템을 최대한 활용할 수 있네요. 개발 환경에서는 hot-reload 기능을 적극 활용하면 좋죠. src 폴더 안에 있는 React 컴포넌트를 수정할 때마다 브라우저에 바로 반영되니까 빠른 피드백 루프를 돌릴 수 있네요. content 디렉터리의 마크다운 파일도 동일하게 적용됩니다. 다만 플러그인을 추가했을 때는 .cache 디렉토리를 삭제한 뒤 서버를 재시작해야 최신 변경사항이 제대로 로드되는 점을 꼭 기억해 주세요. React 는 처음부터 점진적으로 적용할 수 있도록 설계되었어요. 온라인 플레이그라운드로 CodePen 이나 CodeSandbox 를 활용하거나 로컬 HTML 파일로 빠르게 테스트해 보는 것도 좋은 시작점이죠. 새로운 React 앱을 시작한다면 스크립트 태그를 사용한 간단한 HTML 페이지를 만드는 것이 좋지만, 애플리케이션이 커진다면 통합된 설정을 고려하는 것이 좋아요. hot-reload 로 빠른 피드백을 받으며 mental model 을 다지다 보면, 컴포넌트 렌더링 흐름이 자연스럽게 머릿속에 그려질 거예요.

출처

브라우저 환경에서 인증 토큰이나 사용자 설정을 보관할 때 로컬 스토리지는 편리하지만, 새 창으로 열거나 다른 탭에서 값을 수정하면 기존 탭은 변경 사실을 모른다는 점이 자주 막히는 지점인데요. 기본적으로 로컬 스토리지는 각 브라우저 탭이 독립적으로 동작하기 때문에 한쪽에서 setItem을 호출해도 다른 쪽의 메모리상 변수는 자동으로 업데이트되지 않더라고요. 이 문제를 해결하려면 브라우저가 제공하는 StorageEvent를 구독하면 되네요.

window.addEventListener('storage', (event) => {
  console.log(event.key, event.newValue);
});

이벤트 리스너를 등록한 후 다른 탭에서 동일한 키로 값을 쓰면 현재 탭의 콜백이 실행됩니다. 이때 event 객체에는 변경된 키 이름과 이전 값 oldValue, 새로운 값 newValue가 들어오는데, 같은 탭에서 발생시킨 이벤트는 전달되지 않도록 설계되어 있어 무한 루프에 빠질 걱정은 없네요. 실제 구현 시에는 newValue가 null인 경우를 삭제 이벤트로 처리하는 로직을 추가하면 더 안전하더라고요. 만약 프레임워크 기반 프로젝트라면 전역 이벤트 매니저나 훅 형태로 래핑해 두면 코드 중복을 줄일 수 있어요.

다만 이 이벤트는 스토리지 값이 실제로 바뀐 뒤 비동기로 디스패치되므로, 핸들러 안에서 바로 localStorage.getItem()을 호출해도 이미 반영된 최신 값을 얻을 수 있습니다. 따라서 구값을 걱정하며 다시 조회할 필요는 없네요. 프레임워크를 사용할 때는 컴포넌트가 마운트될 때 리스너를 등록하고 언마운트 시 제거하는 규칙을 지키는 게 좋네요. 이벤트가 빈번하게 발생할 경우 불필요한 렌더링을 막기 위해 debouncing 기법을 적용하거나, 상태 업데이트를 batching 방식으로 묶으면 성능 상의 이득을 볼 수 있어요. 브라우저 스토리지 API는 표준화되어 있어 크로스 브라우징 이슈가 거의 없으니, 실제 프로젝트에 적용해 보면 탭 간 데이터 흐름이 훨씬 깔끔해진다는 걸 체감할 수 있을 거예요. 다만 일부 오래된 모바일 브라우저에서는 이벤트 전파 속도가 느릴 수 있으니 테스트 환경에서 직접 확인하는 과정을 생략하지 않는 게 좋네요.

React 19가 안정화되면서 개발 환경이 한결 가벼워졌더라고요. 특히 기존에 useEffect와 useState로 비동기 데이터를 Fetch할 때 작성하던 보일러플레이트 코드가 정말 많이 줄었네요. 저도 처음엔 상태 관리가 복잡해서 매번 헷갈렸는데, use 훅을 만나면서 그 고민이 사라졌거든요. 2024년 4월 Release Candidate 버전 발표 이후 약 8개월간의 테스트와 피드백을 거쳐 안정화된 React 19는 비동기 작업을 단순화하는 데 큰 중점을 뒀어요. React 팀은 복잡한 보일러플레이트 코드를 제거하는 데도 집중했어요.

import { use, Suspense } from 'react';

React에서 use 훅을 불러오는 방법은 간단합니다. 위처럼 한 줄로 끝낼 수 있어요. 기존에 useEffect와 useState로 처리하던 비동기 데이터 페칭을 훨씬 간결하게 쓸 수 있게 됐다는 점이 가장 인상적이었죠. 이제 Promise를 그대로 use에 넘기기만 하면 React가 알아서 렌더링 상태를 관리해 줍니다. 그래서 Suspense와 함께 쓰면 로딩 UI 처리도 훨씬 깔끔해지더라고요.
다만 한 가지 꼭 기억해야 할 점이 있어요. 다른 훅들과 달리 조건문이나 반복문 안에서도 호출할 수 있지만, 반드시 Promise 객체 자체를 인자로 전달해야 합니다. promise.status나 promise.value를 직접 읽어서 우회하려고 하면 에러가 나거나 예상치 못한 동작을 일으킬 수 있으니 절대 피하셔야 해요. React가 Promise를 받아서 처리하게 맡기는 게 가장 안전한 방법입니다.

비동기 작업을 다루는 패턴이 정말 직관적으로 바뀌었네요. React 19는 Actions 패턴을 도입해 폼 제출이나 데이터 가져오기를 더욱 직관적으로 처리할 수 있도록 했어요. use 훅 외에도 낙관적 업데이트나 자동 상태 관리 같은 기능들이 함께 마련되면서 전반적인 개발자 경험도 한층 개선되었죠. Server Components 통합 강화와 Context Provider 간소화 같은 변화들도 함께 고려해 보세요. Actions를 통한 비동기 작업 자동 관리와 Document Metadata 컴포넌트 내부 관리 같은 새로운 기능들도 추가되었어요. ref를 props로 직접 전달 가능해지고 forwardRef가 불필요해진 점도 개발자 경험 개선의 일환이에요. 아직 익숙하지 않으시다면 작은 컴포넌트부터 하나씩 적용해 보시는 걸 추천드려요. 코드가 간결해질수록 유지보수 부담도 확실히 줄어드니까요.

출처

페이지 헤더나 사이드바를 스크롤해도 계속 화면에 붙어 있게 하고 싶어서 position: sticky를 줬는데, 아무 반응이 없어서 당황한 적 있으실 거예요. 저도 처음엔 top 값도 줬고 오타도 없는데 왜 안 붙나 한참 헤맸거든요. 알고 보니 sticky가 안 먹는 데는 거의 정해진 몇 가지 이유가 있더라고요.

가장 흔한 건 inset 값을 안 준 경우예요. sticky는 top, bottom, left, right 중 하나를 반드시 지정해야 그 지점에서 멈춰요. position: sticky만 쓰고 top: 0을 빼먹으면 그냥 일반 요소처럼 흘러가 버리거든요.

두 번째가 진짜 많이들 놓치는 부분인데, 부모나 그 위 조상 요소 중에 overflow가 hidden이나 auto, scroll로 걸려 있으면 sticky가 뷰포트가 아니라 그 요소를 기준으로 잡혀서 사실상 안 붙는 것처럼 보여요. 특히 body나 큰 wrapper에 무심코 overflow: hidden을 준 경우가 범인일 때가 많더라고요. 이럴 땐 그 overflow를 빼거나, 가로 잘림만 필요하다면 overflow: hidden 대신 overflow-x: clip을 쓰면 돼요. clip은 스크롤 컨테이너를 새로 만들지 않아서 sticky를 방해하지 않거든요.

세 번째는 부모의 높이 문제예요. sticky 요소는 자기 부모(컨테이닝 블록) 영역 안에서만 붙어 있을 수 있어요. 부모 높이가 요소랑 거의 같으면 움직일 공간 자체가 없어서 안 붙는 것처럼 보이죠. 부모가 콘텐츠만큼 충분히 길어야 그 안에서 스크롤되며 멈춰 있는 거예요.

코드로 보면 이런 식이에요.

/* 안 되는 예: inset 값이 없어 그냥 흘러간다 */
.header {
  position: sticky;
}

/* 되는 예: top 같은 inset 값을 준다 */
.header {
  position: sticky;
  top: 0;
}

/* 조상에 이게 있으면 sticky가 깨진다 */
.wrapper {
  overflow: hidden;   /* → overflow-x: clip 으로 대체 */
}

정리하면 inset 값 확인, 조상들의 overflow 확인, 부모 높이 확인 이 셋만 순서대로 짚어보면 대부분 해결돼요. 저는 이 중 overflow 때문에 제일 오래 헤맸는데, 크롬 개발자도구에서 부모를 하나씩 타고 올라가며 overflow 속성을 확인하는 게 제일 빨랐어요.

참고: MDN - CSS position

프론트 따로, 백엔드 따로 띄워놓고 개발하다 보면 콘솔에 빨간 글씨로 has been blocked by CORS policy 가 뜨는 순간이 꼭 오거든요. 저도 처음엔 코드가 잘못된 줄 알고 fetch 옵션만 계속 만졌는데, 알고 보니 이건 프론트에서 손볼 수 있는 문제가 아니더라고요.

- 왜 막히나

브라우저는 스크립트가 다른 출처(origin)로 보낸 요청의 응답을 그냥 넘겨주지 않아요. 출처는 프로토콜·호스트·포트 세 가지가 전부 같아야 같은 출처인데, localhost:3000에서 localhost:8080을 부르는 순간 이미 다른 출처거든요. 이때 서버가 응답에 Access-Control-Allow-Origin 헤더로 이 출처는 허용한다고 알려주지 않으면 브라우저가 응답을 버립니다. 재미있는 건 요청 자체는 서버에 도착해서 처리까지 다 됐다는 점이에요. 서버 로그엔 200이 찍혀 있는데 프론트만 에러가 나는 황당한 상황이 여기서 나옵니다. 그래서 해결은 결국 서버에서 헤더를 내려주는 쪽으로 해야 해요.

- 프리플라이트

메서드가 GET·HEAD·POST가 아니거나, Authorization 같은 커스텀 헤더를 붙이거나, Content-Type이 application/json이면 브라우저가 본 요청을 보내기 전에 OPTIONS 메서드로 이거 보내도 되냐고 미리 물어봐요. 이게 프리플라이트입니다. 서버가 OPTIONS를 처리하지 않고 404나 405를 뱉으면 본 요청은 아예 나가지도 않아요. 네트워크 탭에 OPTIONS만 덩그러니 실패해 있으면 십중팔구 이 경우더라고요.

- 쿠키를 같이 보낼 때 (제일 많이 걸리는 함정)

credentials를 쓰는 순간 Access-Control-Allow-Origin: * 는 안 먹혀요. 와일드카드는 자격 증명이 없는 요청에서만 유효해서, 쿠키가 실리면 브라우저가 응답을 차단해 버리거든요. 그래서 출처를 정확히 하나 적어주고 Access-Control-Allow-Credentials: true 도 같이 내려줘야 합니다. Access-Control-Allow-Headers의 * 도 마찬가지라서, 자격 증명이 붙은 요청에선 와일드카드가 아니라 그냥 * 라는 이름의 헤더로 취급돼요. 저도 여기서 한참 헤맸어요.

클라이언트에서 쿠키를 같이 보내려면 이렇게 씁니다.

// 다른 출처 API에 쿠키를 같이 보내는 요청
fetch('https://api.example.com/me', {
  method: 'GET',
  credentials: 'include', // 쿠키 포함
  headers: { 'Content-Type': 'application/json' } // 프리플라이트 발생
})
  .then(res => res.json())
  .then(console.log);

- 해결 방법

서버에서 허용할 출처와 헤더를 명시해 주면 됩니다. Express라면 cors 미들웨어로 이렇게 끝나고, 스프링이나 nginx도 이름만 다를 뿐 내려주는 헤더는 똑같아요.

// Express + cors 설정
const cors = require('cors');

app.use(cors({
  origin: 'https://front.example.com', // 와일드카드(*) 대신 정확한 출처
  credentials: true,                   // Access-Control-Allow-Credentials: true
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 600                          // 프리플라이트 응답 캐시(초)
}));

- 주의할 점

개발하다 귀찮다고 모든 출처를 열어두는 설정을 그대로 운영에 올리는 건 피하세요. 허용 출처는 환경변수로 받아 화이트리스트로 관리하는 게 안전합니다. 그리고 출처별로 응답 헤더가 달라지니, 중간 캐시가 엉뚱한 출처의 응답을 돌려주지 않도록 Vary: Origin 도 같이 내려주는 걸 잊지 마시고요. 프리플라이트가 잦은 API라면 Access-Control-Max-Age로 캐시 시간을 줘서 OPTIONS 왕복을 줄일 수 있습니다. 마지막으로, 프론트 개발 서버의 프록시 기능을 쓰면 브라우저 입장에선 같은 출처가 돼서 CORS가 안 생기는데, 이건 로컬 한정 우회책이지 근본 해결은 아니라는 걸 기억해두면 좋아요.

참고: MDN - Cross-Origin Resource Sharing (CORS) https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS

참고: MDN - Access-Control-Allow-Credentials https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Credentials

table 태그의 tr에 클릭시 상세보기를 보여주는 링크를 거는 경우가 있는데,
해당 tr 내부에 button 태그가 있어도 button의 클릭 이벤트를 무시하고 tr의 onclick 메서드가 실행된다

button이 있는 td에다가 아래의 속성을 추가하면 정상적으로 동작한다

<td onclick="event.cancelBubble = true">

 

{아파치 프로세스명} -V

apache2 -V
혹은
httpd -V

실행 결과

 

Config variable ${APACHE_LOG_DIR} is not defined 오류 발생하는 경우: 아파치 실행파일을 직접 실행 시 발생. 아래와 같은 명령어를 순차적으로 실행해준다.

source {아파치 설정파일 경로}/envvars
{아파치 프로세스명} -V

source /etc/apache2/envvars
apache2 -V

- 웹 ROOT 폴더에 저장
- 서브도메인은 제외하고 싶은경우 주석해제

<IfModule mod_rewrite.c>
        RewriteEngine On
        RewriteBase /

        RewriteCond %{HTTPS} off
#       RewriteCond %{HTTP_HOST} !^(sub1\.mydomain\.com|sub2\.mydomain\.com)
        RewriteRule ^.*$ https://%{HTTP_HOST}%{REQUEST_URI} [R,L]
</IfModule>

+ Recent posts