nhcodingstudio
@nhcodingstudio
수강생
702
수강평
44
강의 평점
4.8
안녕하세요, 우리동네코딩 스튜디오에 오신 것을 환영합니다!
우리동네코딩 스튜디오는 카네기 멜론, 워싱턴, 토론토, 워터루 등 북미의 주요 대학에서 컴퓨터공학을 전공하고, Google, Microsoft, Meta 등 글로벌 IT 기업에서 실무 경험을 쌓은 개발자들이 함께 만든 교육 그룹입니다.
처음에는 미국과 캐나다의 컴퓨터공학 전공자들끼리 함께 공부하며 성장하고자 만든 스터디 모임에서 시작되었습니다. 각기 다른 대학, 다른 시간대에 있었지만 함께 문제를 해결하고 서로에게 배운 그 시간은 매우 특별했고, 자연스럽게 이런 생각이 들었습니다.
“우리가 공부하던 이 방식, 그대로 다른 사람에게도 전하면 어떨까?”
그 물음이 바로 우리동네코딩 스튜디오의 출발점이었습니다.
현재는 약 30명의 현직 개발자와 컴퓨터공학 전공 대학생들이 각자의 전문 분야를 맡아, 입문부터 실전까지 아우르는 커리큘럼을 직접 설계하고 강의합니다. 단순한 지식 전달을 넘어, 진짜 개발자의 시선으로 배우고 함께 성장할 수 있는 환경을 제공합니다.
“진짜 개발자는, 진짜 개발자에게 배워야 합니다.”
저희는 웹 개발의 전 과정을 처음부터 끝까지 체계적으로 다루되, 이론에 머무르지 않고 실습과 실전 중심의 피드백을 통해 실력을 키워드립니다.
수강생 한 사람, 한 사람의 성장을 함께 고민하고 이끌어가는 것이 우리의 철학입니다.
🎯 우리의 철학은 분명합니다.
"진정한 배움은 실천에서 오고, 성장은 함께할 때 완성된다."
개발을 처음 시작하는 입문자부터, 실무 능력을 키우고 싶은 취업 준비생, 진로를 탐색 중인 청소년까지.
우리동네코딩 스튜디오는 모두의 출발점이자, 함께 걷는 든든한 동반자가 되고자 합니다.
이제, 혼자 고민하지 마세요.
우리동네코딩 스튜디오가 여러분의 성장을 함께하겠습니다.
Welcome to Neighborhood Coding Studio!
Neighborhood Coding Studio was founded by a team of developers who studied computer science at top North American universities such as Carnegie Mellon, the University of Washington, the University of Toronto, and the University of Waterloo, and went on to gain hands-on experience at global tech companies like Google, Microsoft, and Meta.
It all began as a study group formed by computer science students across the U.S. and Canada, created to grow together by sharing knowledge, solving problems, and learning from one another.
Though we were attending different schools in different time zones, the experience was so meaningful that it led us to one simple thought:
“What if we shared this way of learning with others?”
That thought became the foundation of Neighborhood Coding Studio.
Today, we are a team of around 30 active developers and computer science students, each taking responsibility for their area of expertise—designing and delivering a curriculum that spans from foundational knowledge to real-world development.
We’re not just here to teach—we’re here to help you see through the lens of real developers and grow together.
“To become a real developer, you must learn from real developers.”
Our courses take you through the entire web development journey—from start to finish—focused on hands-on practice, real-world projects, and practical feedback.
We care deeply about each learner’s growth and are committed to supporting your path every step of the way.
🎯 Our philosophy is simple but powerful:
"True learning comes from doing, and true growth happens together."
Whether you're just getting started, preparing for your first job, or exploring your future in tech,
Neighborhood Coding Studio is here to be your launchpad—and your trusted companion on the journey.
You don’t have to do it alone.
Let Neighborhood Coding Studio walk with you toward your future in development.
강의
수강평
- 제대로 배우는 HTML + CSS: 입문부터 실전까지 완벽 정복 Part2 - [중급편]
- 제대로 배우는 Express.js: Part2 엔진 내부 동작 원리와 클론 프로젝트
- React 마스터 클래스: Part 1 - 미션으로 깨우치는 렌더링 본질과 설계
- 제대로 배우는 Express.js: Part1 기초부터 심화까지 [기초편]
- React 마스터 클래스: Part 1 - 미션으로 깨우치는 렌더링 본질과 설계
게시글
질문&답변
정적 파일 직접 구현하기 강의 수강 후 궁금한 점 질문드립니다!
안녕하세요 TAESUN님, 강의를 통해 serveStatic 함수를 직접 코드로 구현해 보신 것은 웹 서버가 하드디스크에 있는 데이터를 읽어 네트워크라는 통로로 쏴주는 그 근본적인 원리를 체득하신 아주 값진 과정입니다. 사실 실무적인 관점에서 보면 각 도구의 장점을 극대화하기 위해 역할 분담을 철저히 나누는 구조를 지향하기 때문에, TAESUN님께서 생각하신 것처럼 Node.js는 동적인 처리에 집중하고 정적 파일은 전문 서버에 맡기는 방향이 현업의 정석인 것은 분명합니다.그럼에도 불구하고 이렇게 정적 파일 처리를 직접 구현해 보는 과정이 중요한 이유는 우리가 사용하는 수많은 라이브러리나 프레임워크의 내부 동작을 명확히 이해할 수 있는 기초 체력이 되기 때문입니다. 단순히 남이 만든 기능을 가져다 쓰는 것과 파일 시스템 모듈을 통해 스트림을 열고 데이터 조각을 네트워크 패킷으로 실어 보내는 과정을 직접 경험해 보는 것은 하늘과 땅 차이이며, 이러한 깊이 있는 이해가 뒷받침되어야 나중에 예상치 못한 네트워크 병목 현상이 발생하거나 특수한 보안 헤더를 직접 제어해야 할 때 근본적인 해결책을 찾아낼 수 있는 진짜 실력이 생깁니다.물론 실제 운영 환경에서 Node.js에게 직접 정적 파일을 서빙하라고 시키지 않는 데에는 그만한 이유가 있습니다. 무엇보다 Node.js라는 귀한 인력을 가장 효율적인 곳에 배치해야 하기 때문인데, 이를 비유하자면 고도로 숙련된 수석 셰프에게 요리는 뒷전으로 미뤄두고 식당 입구에서 물컵만 나눠주라고 시키는 것과 비슷합니다. Node.js는 기본적으로 싱글 스레드라는 하나의 몸으로 수많은 손님을 응대하기 때문에 만약 덩치 큰 고화질 사진을 읽어서 보내느라 시간을 쓰고 있다면 그사이에 들어온 로그인이나 결제 같은 중요한 주문들은 아무런 처리도 받지 못한 채 하염없이 대기해야만 합니다.반면 Nginx나 Apache 같은 전문 웹 서버는 이런 단순 배달 업무에만 수십 년간 특화된 베테랑들입니다. 이들은 파일을 보낼 때 CPU의 간섭을 최소화하고 운영체제 레벨에서 데이터를 네트워크로 즉시 쏴버리는 Zero-copy라는 기술을 사용해 Node.js보다 훨씬 빠르고 영리하게 일을 처리하며, 파일을 압축해서 보내거나 브라우저 캐싱을 설정하고 보안 벽을 세우는 일도 설정 몇 줄이면 아주 견고하게 해결할 수 있어 시스템 전체의 안정성을 높여줍니다.이러한 역할 분담은 프로젝트의 성장에 따라 단계적으로 변화해 가는데, 먼저 공부하는 단계나 아주 작은 규모의 프로젝트라면 서버를 여러 대 관리하는 것이 오히려 번거로울 수 있어 Node.js가 요리와 서빙을 모두 도맡아 하기도 하며 트래픽이 적은 상황에서는 일단 돌아가게 만드는 것이 목표이기에 이 정도로도 충분한 역할을 해낼 수 있습니다. 하지만 서비스가 조금씩 커지는 일반적인 운영 환경으로 넘어가면 앞단에 Nginx라는 노련한 지배인을 세우고 뒷단에 Node.js라는 셰프를 배치하는 리버스 프록시 구조가 표준이 됩니다. Nginx가 모든 요청을 먼저 받아서 이미지나 CSS 같은 파일 요청은 직접 창고에서 꺼내 빛의 속도로 응답하고 요리가 필요한 데이터 요청만 Node.js에게 넘겨주는 방식으로 확실히 분업화해야 Node.js가 오직 복잡한 비즈니스 로직 연산에만 모든 에너지를 쏟아부을 수 있습니다.더 나아가 전 세계 사용자가 몰리는 대규모 서비스가 되면 웹 서버 한 대조차도 버거워지는 순간이 오는데, 이때는 파일들을 전 세계 곳곳의 거점 창고에 미리 뿌려두고 사용자와 가장 가까운 곳에서 파일을 바로 전달해 주는 CDN(Content Delivery Network) 인프라를 활용하게 됩니다. 이 단계에 이르면 우리의 Node.js 서버는 정적 파일이 어디에 사는지조차 신경 쓸 필요가 없는 완벽한 분업 시스템이 완성되는 것입니다.이런 맥락에서 지금처럼 원리를 하나씩 파헤쳐 가는 학습 방식은 앞으로 TAESUN님의 성장에 아주 좋은 밑거름이 될 것 같습니다. 이번 Express Part 1에서는 기초적인 serveStatic 함수를 직접 구현하며 동작 방식을 이해하는 데 집중했다면, 이어지는 Express Part 2 등에서는 단순히 기능을 사용하는 수준을 넘어 파일의 용량 관리나 성능 제약 사항 등 조금 더 보완적이고 고도화된 형태를 배우게 됩니다. 또한 라우터와 미들웨어 같은 핵심 엔진들을 직접 구현해 보며 프레임워크의 심장부를 깊게 들여다보는 더 흥미로운 경험을 하시게 될 텐데, 그 이후에 Nginx 같은 외부 서버를 연동해 보거나 AWS S3와 CloudFront를 활용해 인프라를 분리해 보는 단계로 차근차근 나아가신다면 실무에서 훨씬 더 탄탄한 설계 능력을 발휘하실 수 있을 것입니다.정리하자면 이번 실습은 웹 서버라는 기계가 내부적으로 어떻게 기름칠 되어 돌아가는지 그 핵심 엔진의 구조를 파악하신 것과 같습니다. 이 원리를 아는 개발자는 나중에 서비스 성능을 최적화할 때 단순히 컴퓨터 사양을 높이는 대신 정적 파일 서빙을 전문 서버로 분리해 부하를 줄이는 날카로운 판단을 내릴 수 있게 됩니다. 실제 서비스를 구축하실 때는 질문하신 생각 그대로 전문 서버에게 파일 서빙을 맡기되 그 아래에서 돌아가는 원리는 이번에 구현해 본 로직과 같다는 점을 기억하시면 좋겠습니다.참고해주세요!
- 0
- 2
- 27
질문&답변
res.writeHead 질문
안녕하세요, Taejin Kim님! 질문 주셔서 감사합니다.사실 네이티브 Node.js로 하나하나 코드를 짜다 보면 "이런 반복적인 것까지 내가 다 적어야 하나?" 싶은 순간들이 생기기 마련인데, 바로 그 가려운 부분을 시원하게 긁어주는 게 Express의 핵심입니다. 질문하신 헤더 설정 부분부터 비교해 보면 그 차이가 정말 명확하게 체감되실 거예요.먼저 Express를 사용하기 전인 Before 단계, 즉 순수 Node.js 환경에서는 응답을 보낼 때마다 아래와 같이 상태 코드와 콘텐츠 타입을 개발자가 직접 손으로 하나하나 작성해야만 했습니다.// 순수 Node.js (Before) res.writeHead(200, { "Content-Type": "text/html" }); res.end("Hello World"); 하지만 Express를 사용하는 After 단계로 넘어가면 res.send()나 res.json() 같은 메서드가 이 과정을 내부적으로 싹 다 대신 처리해 줍니다. 덕분에 우리는 아래처럼 핵심 데이터에만 집중할 수 있게 되죠.// Express.js (After) res.send("Hello World"); 이 코드가 실행되는 순간 Express는 우리가 보내는 데이터가 HTML 문자열인지, 아니면 객체 형태의 JSON인지 똑똑하게 판단해서 상태 코드 200과 그에 딱 맞는 Content-Type을 알아서 붙여 응답을 보냅니다. 개발자가 일일이 "이건 HTML이야"라고 설명할 필요가 없어지는 셈입니다.이러한 간결함의 진가는 에러 처리를 할 때 훨씬 더 크게 느껴집니다. Express를 쓰지 않는다면 요청 경로를 찾지 못하거나 서버에 문제가 생겼을 때, 매번 아래와 같이 if-else나 try-catch를 동원해 res.writeHead를 직접 명시해야 합니다.// 순수 Node.js 에러 처리 (Before) if (req.url === '/') { res.writeHead(200, { "Content-Type": "text/html" }); res.end("메인 페이지"); } else { // 404 에러를 직접 작성하지 않으면 클라이언트는 무한 대기에 빠집니다. res.writeHead(404, { "Content-Type": "text/plain" }); res.end("Not Found"); } 만약 이 작업을 깜빡한다면 클라이언트는 서버가 죽은 건지 응답이 오는 중인 건지도 모른 채 무한정 대기에 빠지게 됩니다. 반면 Express는 기본적인 에러 핸들러를 품고 있어서, 아래와 같이 라우팅에 없는 곳은 404, 코드 실행 중 터진 에러는 500으로 알아서 응답을 보내주는 안전장치 역할을 합니다.// Express.js 에러 처리 (After) app.get('/', (req, res) => { res.send("메인 페이지"); }); // 정의되지 않은 경로나 내부 에러는 Express가 알아서 writeHead를 처리해 응답합니다. 이 상황을 식당 주문에 비유하면 더욱 직관적인데, 순수 Node.js가 주방장이 재료 손질부터 서빙, 메뉴 설명까지 혼자 다 하는 1인 식당 시스템이라면 Express는 든든한 매니저가 있는 프랜차이즈 매장과 같습니다. 1인 식당에서는 접시를 꺼내고 음식을 담아 전달하는 모든 과정을 주방장이 직접 챙겨야 하고 손님이 없는 메뉴를 찾아도 직접 설명해야 하지만, 프랜차이즈에서는 주방에서 "음식 나갑니다"라고 신호만 주면 매니저가 알아서 적절한 접시를 골라 세팅해 서빙하고 없는 메뉴 주문이 들어와도 정해진 매뉴얼에 따라 직원이 자동으로 응대하는 것과 같습니다.결론적으로 Express는 res.send()를 쓸 때 헤더를 자동으로 설정해 주어 코드를 훨씬 간결하게 만들어 주며, 개발자가 일일이 챙기기 힘든 에러 상황까지 안전하게 방어해 줍니다. 물론 특수한 상황에서 내가 원하는 상태 코드를 보내고 싶을 때도 아래와 같이 훨씬 읽기 쉬운 문법을 제공하니 효율성 면에서 비교가 안 될 정도로 강력합니다.// Express에서 원하는 에러 코드를 직접 보낼 때 res.status(404).send('페이지를 찾을 수 없습니다!');참고해주세요!
- 0
- 1
- 26
질문&답변
미션18
안녕하세요 영훈님!우선 학습에 불편을 드려 정말 죄송합니다.말씀해 주신 미션 18의 누락된 내용과 43강 첫 페이지의 오타를 모두 확인하여 현재 수정을 완료했습니다. 꼼꼼하게 확인해 주시고 소중한 피드백을 주신 덕분에 강의를 더 완벽하게 보완할 수 있었습니다. 제보해 주셔서 진심으로 감사합니다.수정된 내용으로 학습을 이어가시는 데 어려움이 없으시길 바라며, 앞으로도 학습 중 궁금한 점이나 불편한 사항이 생기면 언제든 편하게 말씀해 주세요.더 좋은 강의를 제공하기 위해 늘 노력하겠습니다. 감사합니다.
- 0
- 2
- 26
질문&답변
미션18
안녕하세요 영훈 님, 불편을 드려 죄송합니다.말씀하신 부분은 금일 12시 내로 조속히 수정해 두겠습니다.꼼꼼히 체크하지 못한 점 사과드리며, 앞으로 더욱 유의하겠습니다.
- 0
- 2
- 26
질문&답변
readFileSync
안녕하세요 코딩님!학습에 대한 열정이 느껴지는 질문을 주셔서 정말 감사합니다. 질문하신 내용은 Node.js의 핵심 철학인 비동기 처리를 아주 정확하게 관통하고 있으며, 강의의 구성 의도에 대한 분석 또한 매우 타당합니다.질문하신 내용을 면밀히 살펴보면 readFileSync 메서드에 대한 우려는 기술적으로 매우 정확한 지적입니다. 이 메서드는 이름 그대로 파일을 읽어오는 작업을 수행하는 동안 다음 코드로 넘어가지 않고 전체 실행 흐름을 멈춰 세우는 동기 방식으로 동작합니다. Node.js는 기본적으로 한 번에 하나의 작업만 처리할 수 있는 싱글 스레드 기반의 이벤트 루프 모델을 사용하기 때문에, 이 루프가 멈춘다는 것은 다른 사용자의 접속이나 데이터 요청을 전혀 처리할 수 없는 상태가 됨을 의미합니다. 이는 마치 좁은 단선 도로에서 앞차가 통과하기 전까지 뒤에 있는 수많은 차가 하염없이 기다려야 하는 상황과 같아서, 실제 서비스 환경의 비즈니스 로직에서는 매우 치명적인 코드가 될 수 있습니다.이러한 맥락에서 파트 1 과정은 말씀하신 대로 실무 전체를 대변하기보다는 Express 프레임워크와 서버의 기본적인 구조를 잡는 단계로 이해하시면 좋습니다. 이 단계에서 readFileSync를 활용하는 이유는 비동기 처리라는 다소 복잡한 개념을 배우기 전에 서버가 어떻게 요청을 받고 응답을 보내는지 그 직관적인 흐름을 먼저 익히기 위해서입니다. 이것은 마치 운전을 처음 배울 때 엔진의 내부 복잡한 작동 원리보다는 핸들을 조작하고 브레이크를 밟는 법부터 익히는 것과 비슷합니다. 따라서 학습자가 서버의 전체적인 골격을 파악하는 데 방해가 되지 않도록 의도적으로 단순화된 코드를 사용하는 과정이라 볼 수 있습니다.본격적인 실무 관점의 최적화와 깊이 있는 내용은 파트 2에서 중점적으로 다뤄지게 됩니다. 파트 2에서는 단순히 기능을 구현하는 것을 넘어 Express의 내부 엔진이 실제로 어떻게 미들웨어를 실행하고 요청을 처리하는지 그 내부를 깊숙이 들여다봅니다. 특히 대규모 트래픽을 견디기 위한 성능 최적화 기법을 배우면서, 왜 실무에서는 비동기 방식인 readFile이나 async/await를 활용해 이벤트 루프를 방해하지 않아야 하는지를 실무적인 코드로 확인하게 됩니다. 결과적으로 파트 2까지 모두 경험하셔야 비로소 실제 서비스에 적용 가능한 수준의 깊이 있는 개발 역량을 갖추실 수 있습니다.그렇다면 실무에서는 이 동기 메서드를 아예 배제하는 것일까요? 사실 실무에서도 이 방식이 정답이 되는 순간이 있는데, 바로 서버가 가동되기 직전의 초기 설정 단계입니다. 예를 들어 서버 운영에 반드시 필요한 보안 인증서나 환경 설정 파일을 불러오는 작업은 서버가 손님의 요청을 받기 시작하기 전에 딱 한 번만 수행됩니다. 이는 식당이 문을 열기 전 주방을 세팅하는 시간에는 잠시 작업이 멈춰도 손님에게 지장을 주지 않는 것과 같습니다. 오히려 이런 초기화 단계에서는 실행 순서를 확실히 보장하면서도 코드가 간결한 동기 방식이 더 안전하고 효율적일 수 있습니다.결론적으로 파트 1은 서버의 기초 체력을 기르는 과정이며, 파트 2는 그 체력을 바탕으로 실제 서비스에서 최적의 성능을 끌어내는 법을 배우는 과정입니다. 지금처럼 코드 한 줄이 실무에서 어떤 영향을 미칠지 고민하며 학습하신다면 파트 2에서 다룰 심화 개념들도 아주 빠르게 본인의 것으로 만드실 수 있을 것입니다.참고해주세요!
- 0
- 1
- 54
질문&답변
11강 내용과 12강 내용이 충돌하는 것 같아요.
안녕하세요 hans님!먼저 중요한 부분을 깊이 있게 확인해주셔서 정말 감사드립니다. 11강과 12강을 연달아 보셨다면 자연스럽게 생길 수 있는 궁금함이고, 얼핏 보면 두 설명이 서로 다른 방향을 이야기하는 것처럼 보일 수 있습니다. 하지만 실제로 두 강의는 서로 충돌하지 않으며, 각각 전혀 다른 상황을 설명한 것이고, 이 차이를 이해하면 전체 흐름이 한 방향으로 자연스럽게 이어집니다.12강의 툴팁 예제를 보면 구조가 매우 단순합니다. 마우스를 올리면 툴팁 요소가 잠시 생성되고, 마우스를 떼는 순간 바로 DOM에서 제거되며, 코드 어디에서도 이 요소나 콜백을 변수로 저장하거나 다시 참조하지 않습니다. 이런 경우 브라우저의 가비지 컬렉터는 “이 DOM 요소는 다시 사용할 일이 없겠구나”라고 판단하고, 해당 요소와 그에 연결된 이벤트 리스너까지 함께 메모리에서 정리할 수 있습니다. 그래서 12강에서는 “요소를 제거하는 흐름만으로도 인터랙션이 정리된다”라고 설명드릴 수 있었습니다.하지만 이 설명이 “모든 상황에서 요소만 지우면 리스너도 자동으로 제거된다”는 뜻은 절대 아닙니다. 실제 개발 환경에서는 이와 다른, 더 복잡한 상황들이 훨씬 자주 발생합니다. 콜백 함수가 외부 변수를 참조하고 있어서 브라우저가 해당 함수를 쉽게 정리하지 못하거나, DOM 요소가 눈앞에서는 사라졌지만 자바스크립트 변수나 배열 속에 여전히 참조가 남아 있거나, SPA처럼 같은 화면 구성 요소가 반복해서 생성되고 제거되면서 동일한 이벤트가 중첩으로 등록되는 일이 자연스럽게 일어납니다. 이런 경우에는 DOM 요소가 사라져도 브라우저는 “이 함수나 요소가 아직 필요할 가능성이 있다”고 판단하여 리스너를 메모리에서 지우지 않습니다. 이런 누적은 시간이 지날수록 메모리 누수, 성능 저하, 이벤트 중복 실행 같은 문제로 이어질 수 있습니다.이 차이를 조금 더 직관적으로 비유해서 설명하면 다음과 비슷합니다. 툴팁처럼 잠깐 쓰고 바로 버리는 일회용 종이컵은 버리면 그대로 사라지지만, 만약 어떤 가방 안에 물건이 들어 있다면 책상 위에서는 보이지 않더라도 그 물건을 쉽게 버릴 수 없습니다. 코드에서도 요소가 화면에서 사라졌다고 해서, 어딘가에서 그 요소나 리스너를 잡고 있다면 브라우저는 마음대로 버리지 못합니다. 이 비유가 바로 자동 정리와 미정리의 차이를 보여주는 핵심 원리입니다.또 한 가지, 자동 정리가 일어난다 하더라도 그 시점이나 판단 기준은 브라우저 엔진마다 조금씩 다를 수 있습니다. 자바스크립트의 가비지 컬렉션 동작은 개발자가 직접 통제할 수 있는 것이 아니라, 브라우저가 내부적으로 안전하다고 판단할 때 이루어지는 작업이기 때문에, “언제 자동으로 정리가 될지”를 가정해서 코드를 설계하는 것은 예측하기 어렵고 장기적으로 안정성이 떨어지는 방식입니다.이 때문에 11강에서는 removeEventListener를 명시적으로 사용하는 것이 필요하다고 강조드렸습니다. 실무에서는 UI 구조가 조금만 복잡해져도 자동 정리가 이루어지지 않는 상황이 자연스럽게 등장하고, 특히 SPA처럼 화면이 자주 전환되거나 여러 상태가 얽히는 환경에서는 이벤트 중복 등록과 메모리 누수 문제를 방지하기 위해 “정리 루틴”을 반드시 명시적으로 가져가는 것이 업계 표준입니다. 이는 React, Vue 같은 프레임워크에서도 동일하게 적용되는 개념으로, useEffect의 cleanup 함수나 beforeUnmount 같은 정리 메커니즘이 존재하는 이유도 바로 이 때문입니다. 프레임워크가 자동으로 정리해줄 것처럼 보이지만, 실제로는 개발자가 정리 코드를 직접 작성해야 올바르게 동작하는 구조입니다.정리하자면, 12강은 단발성으로 생성되었다 바로 사라지는 단순한 UI에서는 브라우저가 요소와 리스너를 함께 정리할 수 있다는 흐름을 보여준 것이고, 11강은 실제 프로젝트 규모에서는 자동 정리를 기대할 수 없는 상황이 훨씬 더 많기 때문에 명시적인 이벤트 해제가 필요하다는 실무 관점을 설명한 것입니다. 두 강의가 서로 다른 원칙을 말한 것이 아니라, 각기 다른 복잡도의 상황에서 발생하는 동작을 설명한 것이라고 이해하시면 전체 그림이 매끄럽게 연결됩니다.그리고 마지막으로 가장 중요한 결론을 말씀드리면, 실제 개발에서는 자동 정리에 의존하기보다는 가능하면 removeEventListener를 명시적으로 호출하는 방식을 기본 원칙으로 가져가는 것이 가장 안전하고 일관된 방법입니다. 자동 정리는 말 그대로 “상황이 아주 단순할 때만 우연히 작동하는 보조 장치”에 가깝고, 실무에서는 예상 외의 참조나 상호작용이 매우 자연스럽게 발생하기 때문에, 리스너 정리를 직접 제어하는 방식이 장기적으로 안정적이고 성능 면에서도 신뢰할 수 있는 구조를 만들어줍니다.이번 질문 덕분에 두 강의 사이에서 혼동될 수 있는 부분을 더 명확하게 정리할 수 있었고, 앞으로 강의에서도 더욱 매끄럽게 연결해 설명드릴 수 있을 것 같습니다. 좋은 질문 정말 감사드리고, 언제든지 더 궁금한 점 있으시면 편하게 말씀해주세요.감사합니다🙏
- 0
- 2
- 52
질문&답변
클래스 vs 인라인 스타일 성능 질문
안녕하세요 hans님! 먼저 정확한 지점을 짚어주셔서 정말 감사합니다. 이 주제는 겉보기에는 “인라인 스타일 vs classList” 정도로 단순해 보이지만, 실제로는 브라우저 내부 구조, 렌더링 타이밍, 그리고 대규모 UI에서의 코드 조직 방식까지 모두 연결된 이야기라 조금 더 설명이 길어질 수밖에 없습니다.우선 hans님이 질문에서 이해하신 내용은 정확합니다. 단순히 인라인 스타일을 루프 안에서 두 번 정도 바꾸는 정도라면, 그 순간마다 리플로우나 리페인트가 여러 번 발생하지 않습니다. 브라우저는 자바스크립트가 실행되는 동안 스타일이 바뀌어도 곧바로 레이아웃을 다시 잡지 않고, 보통은 자바스크립트 실행이 끝나는 타이밍이나 다음 프레임을 그릴 때 스타일 계산 → 레이아웃 → 페인트를 한꺼번에 처리합니다. 그래서 이런 간단한 예시에서는 인라인 스타일을 쓰든 classList를 쓰든 렌더링 비용 자체는 거의 비슷하다는 점이 맞아요.다만 제가 강의에서 이 부분을 조금 더 강조했던 이유는, 코드가 실제 서비스 수준으로 커졌을 때 인라인 스타일이 성능과 유지보수 양쪽에서 문제가 발생하기 쉬운 구조를 만들어버리기 때문입니다. 이 위험성은 예시 코드 같은 단순한 상황에서는 절대 드러나지 않고, 인터랙션이 많아지고 화면 요소가 많아질수록 서서히 누적되다가 어느 순간 체감될 정도로 큰 문제로 터지는 편입니다.조금 더 현실적인 예를 하나 들어볼게요. 쇼핑몰 메인 페이지처럼 수백 개의 상품 카드가 흐르는 UI를 떠올려 보면, 화면 아래쪽에 있는 이미지는 아직 보이지 않지만 스크롤이 내려오면 보여야 하죠. 이런 상황에서 흔히 사용하는 것이 “lazy loading”입니다. lazy loading은 화면에 보이지 않는 이미지는 미리 로딩하지 않았다가, 사용자가 스크롤해서 해당 이미지가 화면 근처에 왔을 때 로딩을 시작하는 기법입니다. 이걸 효율적으로 구현하려고 최근에는 Intersection Observer라는 기능을 자주 씁니다. 이 기능은 “어떤 요소가 화면 안에 들어왔는지, 혹은 화면에 어느 정도 겹쳐졌는지”를 브라우저가 알려주는 API인데, 이 API를 이용해서 스크롤 이벤트를 직접 처리하지 않고도 필요한 타이밍에만 이미지 로딩을 시작할 수 있습니다.문제는 이런 환경에서 인라인 스타일이 과도하게 들어가기 시작하면, 눈에 보이지 않던 성능 문제가 매우 자연스럽게 드러나기 시작한다는 점입니다. 예를 들어 카드 요소마다 style.backgroundColor, style.boxShadow, style.opacity 같은 스타일을 직접 넣기 시작하면, 화면에 등장하는 카드마다 조금씩 다른 스타일이 인라인으로 흩어지게 됩니다. 시간이 지나 디자이너의 요구가 더 늘어나면, 할인 상품에는 테두리 굵기 변화, 특정 카테고리에는 hover 애니메이션, 장바구니 담긴 상품에는 배지가 붙는 등 조건이 더 다양해지죠. 이런 상태 조합이 모두 인라인 스타일로 섞여 있으면, 코드를 따라가며 “이 카드가 어떤 상태일 때 어떤 스타일이 적용돼야 하는지”를 추적하는 일이 점점 더 어려워집니다.이 상황에서 스크롤 위치 계산을 위해 getBoundingClientRect() 같은 레이아웃 값을 읽는 코드가 들어오면 문제가 한층 더 복잡해집니다. 이 API는 화면에 그려진 최신 위치 정보를 반환해야 하므로, 그 직전까지 쌓여 있던 인라인 스타일 변경을 모두 반영한 뒤에야 값을 반환할 수 있습니다. 즉 브라우저는 그 시점에서 강제로 레이아웃을 다시 계산해야 하고, 스크롤 중에는 이 작업이 거의 매 프레임 반복됩니다. 이렇게 “스타일 바꿈 → 최신 위치 읽음 → 또 스타일 바꿈 → 다시 위치 읽음”이 반복되면 화면이 뚝뚝 끊기기 시작하는데, 이런 상황을 복잡한 용어로는 layout thrashing이라고 부릅니다. 그냥 쉽게 말하면 “브라우저가 숨 돌릴 틈도 없이 계속 새로 레이아웃을 잡느라 바빠서 버벅이는 상황”이라고 할 수 있습니다.여기에 인라인 스타일이 흩어져 있으면 문제가 더 커집니다. 어떤 조건에서 어떤 스타일이 적용되는지 한 곳에서 관리하기 어려워지고, 어떤 변화가 레이아웃을 다시 계산하게 만드는지 추적하기가 매우 힘들어집니다. 크롬 DevTools로 성능 프로파일링을 해도, 스타일이 여기저기 흩어져 있으면 원인이 분명하게 드러나지 않기도 합니다.반대로 클래스 기반 구조를 사용하면 .card--discount, .card--loading, .card--hovered 같은 상태 클래스를 명확하게 만들고, 스타일은 CSS에서 한 번만 정의합니다. 자바스크립트에서는 단지 “지금 이 카드가 어떤 상태인지”만 클래스로 표시해주면 되기 때문에, UI 상태와 스타일이 깔끔하게 분리됩니다. 이런 구조는 기능이 많아질수록 유지보수가 쉬워지고, 나중에 레이아웃 관련 성능 문제가 생겨도 어디를 먼저 살펴봐야 하는지 훨씬 명확해집니다.따라서 질문에서 주신 코드 수준에서는 hans님 말씀이 정확하고, 인라인 스타일 때문에 리플로우, 리페인트가 갑자기 여러 번 발생한다고 볼 수 없습니다. 다만 실서비스 규모에서는 인라인 스타일이 구조적인 혼란을 만들기 쉽고, 그 결과로 스크롤, 애니메이션, 반응형 컴포넌트 등이 섞인 상황에서 성능 병목으로 이어지기 쉬운 구조를 만들기 때문에, 학습 초기부터 클래스를 중심으로 스타일을 관리하는 습관을 들이셨으면 하는 의도로 설명을 드렸던 것입니다.이런 질문 덕분에 강의에서도 앞으로 더 직관적으로 이해할 수 있는 사례와 실제 현업에서 마주치는 상황을 더 많이 포함해야겠다는 생각을 하게 되었습니다. 강의를 더 풍부하고 실용적으로 만드는 데 큰 도움이 되었습니다.궁금한 점이 있으시면 언제든 편하게 말씀 부탁드립니다.그리고 추가로, 현재 업로드된 다음 강의들에서도 이번 질문과 이어지는 흐름을 더 깊게 다루고 있으니 참고해주시면 좋을 것 같습니다. 이벤트부터 SPA까지, 상호작용 웹의 필수 엔진을 다룬 [DOM 완전 정복 Part 2], 그리고 DOM에서 실제 픽셀로 이어지는 렌더링 여정을 깊게 파헤친 [DOM 완전 정복 Part 3]까지 많은 관심 부탁드립니다!감사합니다!
- 0
- 2
- 42
질문&답변
지금 이 화면에서 뭘로 fps를 알 수 있나요?
안녕하세요 ah young kim님😊지금 보신 Performance 패널 화면에서 FPS, 즉 초당 프레임 수를 확인하는 방법은 상단의 “Frames” 구간을 보면 됩니다. 이 영역의 녹색 바 하나하나가 브라우저가 한 프레임을 그린 시간을 의미하며, 마우스를 올리면 “33.3 ms Frame”과 같은 표시가 나타납니다. 이 값이 바로 한 프레임을 그리는 데 걸린 시간, 즉 frame time이며, FPS는 이 값을 이용해 계산할 수 있습니다. 계산식은 FPS = 1000 ÷ frame time(ms) 입니다. 예를 들어 한 프레임이 33.3ms 걸린다면 1000 ÷ 33.3 = 약 30fps가 되고, 16.6ms일 경우 1000 ÷ 16.6 = 약 60fps로 계산됩니다. 지금 화면에서는 각 프레임의 시간이 약 33ms 정도로 측정되어 대략 30fps 수준으로 볼 수 있습니다. 이 말은 hover와 같은 사용자 인터랙션이 발생했을 때 브라우저가 한 프레임을 그리는 데 더 오랜 시간이 걸리고 있음을 의미합니다. FPS가 낮아질수록 초당 그릴 수 있는 프레임 수가 줄어들어 화면이 끊기거나 버벅거리는 현상이 나타납니다. 반대로 frame time이 짧아지고 FPS가 높아질수록 브라우저는 더 많은 프레임을 매끄럽게 처리하게 되어 애니메이션과 전환이 부드럽게 느껴집니다.보통 어느 정도 FPS가 좋은가를 판단할 때는 다음과 같은 기준을 참고합니다. 60fps 이상이면 이상적인 상태로, 대부분의 고급 애니메이션이나 게임이 이 수준을 목표로 합니다. 50fps 근처에서는 체감상 충분히 부드럽고 안정적인 움직임을 보이며, 대부분의 웹 인터페이스에서 이 정도면 만족스러운 사용자 경험을 제공합니다. 그러나 30fps 이하로 떨어지면 hover나 scroll 시 눈에 띄는 버벅임이 생기며, UI 전환이 즉각적으로 반응하지 않는 느낌을 주기 때문에 성능 개선이 필요하다고 판단할 수 있습니다.FPS 수치만 보는 것보다 중요한 것은 각 프레임의 시간과 함께 어떤 렌더링 단계가 반복적으로 일어나고 있는지를 함께 살펴보는 것입니다. Performance 패널의 아래쪽에는 Rendering, Painting, Composite 단계가 색깔별로 표시되는데, 이를 통해 브라우저가 어떤 자원을 더 많이 사용하고 있는지 파악할 수 있습니다. Paint 단계가 자주 발생한다면 CPU가 매번 픽셀을 다시 계산하고 그려야 하기 때문에 처리 시간이 길어지고 FPS가 떨어질 수 있습니다. 반대로 Composite 단계에서 대부분의 작업이 처리된다면 GPU가 레이어 합성을 담당하기 때문에 훨씬 가볍고 효율적인 렌더링이 이루어집니다. 따라서 Paint 블록의 빈도를 줄이고 Composite 위주로 최적화하는 것이 FPS 안정성과 렌더링 효율을 동시에 높이는 핵심입니다.실무에서는 단순히 코드 최적화뿐 아니라, CSS, HTML, JS 모두가 렌더링 성능에 영향을 미칩니다. CSS 측면에서는 transform, opacity 등 GPU 가속이 가능한 속성을 활용하고, top, left처럼 layout이나 paint를 유발하는 속성은 애니메이션에 사용하지 않는 것이 좋습니다. box-shadow, filter, border-radius와 같은 시각 효과는 과도하게 사용하면 Paint 비용이 커지므로 주의가 필요합니다. HTML 구조에서는 불필요하게 중첩된 엘리먼트를 줄이고, display 변경이 잦은 요소를 별도의 레이어로 분리하는 것이 도움이 됩니다. JavaScript에서는 연산이 무거운 루프나 동기 처리 로직을 최소화하고, requestAnimationFrame을 이용해 브라우저의 렌더링 타이밍에 맞춰 애니메이션을 수행하면 훨씬 자연스럽고 효율적인 프레임 타이밍을 유지할 수 있습니다.결국 FPS를 제대로 분석하려면 Performance 패널의 Frames 영역에서 frame time을 확인하고, FPS = 1000 ÷ frame time(ms) 공식을 통해 초당 프레임 수를 계산하며, Paint와 Composite 단계의 분포를 함께 보는 것이 중요합니다. 16.6ms 근처의 frame time, 즉 60fps에 가까운 상태가 가장 이상적이고, 50fps까지는 충분히 부드럽다고 평가할 수 있으며, 30fps 이하에서는 사용자 경험이 급격히 떨어집니다. 이때 렌더링 단계별 부담을 줄이고 CSS·HTML·JS 코드를 GPU 친화적으로 구성하면, 브라우저는 더 가볍고 효율적으로 동작하며 화면 전환이 눈에 띄게 매끄러워집니다. 이런 점을 함께 고려하면 단순히 FPS 수치를 높이는 것을 넘어, 실무 환경에서도 안정적이고 부드러운 사용자 경험을 설계할 수 있습니다.
- 0
- 2
- 68
질문&답변
만약 문서 수가 매우 많아진다면 성능 이슈는 없을까요?
안녕하세요 쌀밥님 😊질문 주셔서 감사합니다.이 내용에서 다루는 최적화 방법을 완전히 이해하려면 약간의 컴퓨터 과학적 배경 지식이 필요합니다.특히 다음 세 가지 개념을 알고 있으면 훨씬 쉽게 이해할 수 있습니다.첫째는 시간 복잡도(Time Complexity) 개념으로, 어떤 코드가 데이터의 양이 늘어날수록 얼마나 느려지는지를 나타내는 지표입니다. 예를 들어 배열을 처음부터 끝까지 모두 훑는 코드는 O(n), 중첩 반복문이 있는 코드는 O(n²)처럼 표현합니다.둘째는 자료구조(Data Structure) 중 배열(Array)과 맵(Map, 해시맵 HashMap)입니다. 배열은 순서대로 저장하는 구조이지만 검색할 때는 처음부터 끝까지 찾아야 하고, 맵은 키(key)를 이용해 값을 거의 즉시 찾을 수 있는 구조입니다.셋째는 인덱스(Index) 개념으로, 데이터베이스나 검색 시스템에서 “필요한 데이터를 빠르게 찾기 위해 미리 만들어두는 참조용 지도”를 말합니다.이 세 가지 개념이 이 글의 모든 최적화 논리의 토대가 됩니다.지금의 구조는 state.docs 배열 전체를 매번 find()나 filter()로 훑어보는 방식이라, 문서가 수백 개나 수천 개로 늘어나면 자연스럽게 성능 저하가 생깁니다. 초반에는 아무 문제가 없어 보여도, 트리 렌더링이 일어날 때 각 노드마다 childrenOf()가 반복적으로 호출되고, 거기에 문서 이동, 제목 수정, 즐겨찾기 토글 같은 이벤트가 동시에 일어나면 한 프레임 안에서 선형 탐색이 여러 번 겹치게 됩니다. 이렇게 되면 체감상 O(n²) 형태로 느려져서 브라우저가 잠시 멈추는 현상이 발생할 수 있습니다.이 문제를 근본적으로 해결하려면 세 가지가 함께 고려되어야 합니다. 첫째, 조회를 거의 즉시 끝낼 수 있게 만드는 인덱스 구조 설계. 둘째, DOM 갱신 자체를 최소화하는 렌더링 방식. 셋째, 저장과 렌더 호출 빈도를 조절해 메인 스레드가 갑자기 과부하되지 않도록 하는 것입니다.이 원리를 조금 더 쉽게 설명하면 “자주 일어나는 일은 최대한 빠르게, 드물게 일어나는 일은 조금 느려도 괜찮게 만든다”는 개념입니다. 문서 탐색은 매우 자주 일어나는 작업이므로 반드시 빠르게 만들어야 하고, 반면 문서를 새로 만들거나 이동하거나 삭제하는 일은 상대적으로 덜 자주 일어나기 때문에 그때만 약간의 추가 비용을 내도 전체 앱의 체감 속도는 훨씬 빨라집니다.이 구조를 쉽게 구현할 수 있는 도구가 바로 자바스크립트의 Map입니다.Map은 일반 객체(Object)와 비슷하게 키(key)와 값(value)을 짝지어 저장하는 자료 구조이지만, 몇 가지 중요한 차이가 있습니다.첫째, Map은 어떤 타입이든 키로 사용할 수 있습니다. 숫자, 문자열뿐 아니라 객체나 심볼도 가능합니다.둘째, get, set, has, delete, size 같은 전용 메서드를 통해 데이터를 매우 빠르게 읽고 쓸 수 있습니다.셋째, Map은 내부적으로 해시 구조를 사용하기 때문에 평균적으로 데이터를 찾거나 넣는 데 걸리는 시간이 거의 일정합니다. 즉, 데이터가 아무리 많아져도 조회 속도가 거의 변하지 않습니다.이걸 전화번호부로 비유해 보면 훨씬 직관적입니다.일반 배열을 탐색하는 것은 마치 “전화번호부를 처음부터 끝까지 한 줄씩 넘기며 이름을 찾는 방식”이라면, Map은 “이름을 입력하자마자 바로 번호가 뜨는 스마트폰 연락처 검색”에 가깝습니다.이처럼 Map을 이용하면 id로 문서를 즉시 찾거나 특정 부모 ID에 속한 자식 문서들을 한 번에 가져올 수 있습니다.이 원리를 현재 코드에 적용하려면 두 가지 관점에서 인덱스를 설계하면 됩니다.하나는 id → 문서 객체 형태의 인덱스이고,다른 하나는 parentId → [자식 문서들] 형태의 인덱스입니다.앱이 처음 실행될 때 한 번 전체 데이터를 훑어서 이 두 인덱스를 미리 만들어 두고, 이후에는 문서가 새로 만들어지거나 수정되거나 이동되거나 삭제될 때마다 인덱스를 즉시 갱신하도록 유지하면 됩니다.이렇게 하면 문서를 찾을 때 전체 배열을 훑을 필요가 없고, 필요한 정보만 즉시 꺼내올 수 있습니다. 또한 변경 시에도 그 부모에 해당하는 작은 그룹만 다시 정렬하면 되므로 전체를 다시 계산할 필요가 없습니다.정렬 기준은 기존과 동일하게 order를 우선으로 하고, 값이 같을 때만 title을 비교하는 것이 좋습니다. localeCompare는 CPU 비용이 상대적으로 크기 때문에 보조 비교로만 사용하는 것이 효율적입니다.아래 코드는 이런 인덱스를 실제로 구현하는 최소한의 예시입니다. 호출부는 그대로 두고, 내부 동작만 교체하는 방식이라 전체 코드 구조를 바꾸지 않아도 안전하게 적용할 수 있습니다.// 인덱스 const index = { byId: new Map(), byParent: new Map() }; const byOrder = (a,b)=> (a.order - b.order) || a.title.localeCompare(b.title); function ensureBucket(pid){ const key = pid ?? null; if (!index.byParent.has(key)) index.byParent.set(key, []); return index.byParent.get(key); } function buildIndexes(){ index.byId.clear(); index.byParent.clear(); for (const d of state.docs){ index.byId.set(d.id, d); ensureBucket(d.parentId).push(d); } for (const arr of index.byParent.values()) arr.sort(byOrder); } // 조회 헬퍼 내부 구현만 교체 function findDoc(id){ return index.byId.get(id) || null; } function childrenOf(pid){ const bucket = index.byParent.get(pid ?? null) || []; return bucket.slice(); // 외부 변형 방지 } 여기서 나오는 “버킷(bucket)”이라는 단어는, 같은 부모를 가진 문서들을 하나의 묶음 단위로 저장해 둔 배열을 뜻합니다.즉, index.byParent라는 Map 안에 부모 ID → [해당 부모의 자식 문서 목록]이 저장되는데, 이 자식 문서 배열 하나하나가 바로 “버킷”입니다.쉽게 말해, “한 부모 밑에 있는 아이들 폴더를 한 바구니(bucket)에 담아두고, 부모별로 바구니를 따로 관리하는 구조”라고 생각하면 됩니다.이 덕분에 특정 부모 밑의 자식 문서들만 빠르게 찾거나 정렬할 수 있습니다.여기서 중요한 것은 “항상 최신 상태를 유지하는 것”입니다.Map 구조는 빠르지만, 한 곳이라도 갱신을 놓치면 실제 데이터(state.docs)와 인덱스(Map) 사이에 불일치가 생깁니다.따라서 createDoc, updateDoc, moveDoc, archiveDoc, restoreDoc, removeDoc 등 모든 변경 함수가 반드시 인덱스를 함께 갱신해야 합니다.이걸 보장하기 위해 기존 함수를 얇게 감싸서 “상태 변경이 일어날 때마다 인덱스도 함께 갱신되도록” 만드는 것이 안전한 방식입니다.그 원리는 간단합니다.문서를 새로 만들면 byId에 등록하고, 해당 부모의 버킷(즉, 자식 문서 배열)에 새 문서를 정렬된 위치에 끼워 넣습니다.제목이나 순서를 바꾸면 같은 부모 버킷 안에서 한 번 빼고 정렬 기준에 맞게 다시 넣습니다.문서를 다른 부모로 옮길 때는 이전 부모의 버킷에서 제거하고 새 부모의 버킷에 추가합니다.아카이브는 자신과 자식들을 인덱스에서 제거하고, 복원은 반대로 다시 삽입합니다.특히 이동이나 수정 같은 경우는 “이전 부모”와 “새 부모”를 구분해서 둘 다 업데이트해야 버킷이 꼬이지 않습니다.// createDoc 래핑 원리: 상태 반영 → byId 등록 → 부모 버킷에 정렬 삽입 const _createDoc = createDoc; createDoc = function(payload){ const id = _createDoc(payload); const d = state.docs.find(x => x.id === id); index.byId.set(id, d); // 정렬 비용 최소화를 위해 '이진 삽입' 같은 방식으로 들어갈 위치만 찾아 넣는 것이 유리 const arr = ensureBucket(d.parentId); let lo=0, hi=arr.length; while(lo>1; (byOrder(arr[mid], d)렌더링과 관련된 병목은 조회와는 별도의 문제입니다.예를 들어 childrenOf()를 한 함수 안에서 여러 번 부르면 매번 같은 데이터를 다시 가져오게 되므로 불필요한 연산이 쌓이게 됩니다.이럴 때는 한 번만 호출해 변수에 저장해 두고 재사용하는 것이 훨씬 효율적입니다.또한 변경이 일어난 부모 섹션만 부분적으로 다시 그리도록 분기하면 체감 속도가 눈에 띄게 좋아집니다.만약 한 화면에 수천 개의 문서를 펼쳐서 보여줘야 하는 상황이라면, 화면에 실제로 보이는 부분만 렌더링하는 “가상 스크롤(virtual scroll)” 방식을 사용하는 것도 좋은 선택입니다.그리고 normalizeOrders(pid) 함수는 이미 byParent 버킷이 정렬되어 있으니, 굳이 다시 정렬을 반복하지 말고 그 순서대로 0부터 순서 번호를 다시 매기기만 하면 됩니다.제목 검색이나 정렬이 자주 발생한다면, titleLower 속성을 문서에 추가해두어 미리 소문자로 저장해두는 것도 좋습니다.이렇게 하면 매번 toLowerCase()를 호출하지 않아 CPU 사용량을 줄일 수 있습니다.// renderNode 안에서 중복 호출을 없애는 간단한 형태 const kids = childrenOf(doc.id); const hasChildren = kids.length > 0; // hasChildren 사용 및 kids 재사용 … 저장과 렌더 호출은 사용자의 조작이 빠르게 이어질 때 짧은 시간 안에 여러 번 발생할 수 있습니다.이럴 때 localStorage 저장이 너무 자주 일어나면 메인 스레드를 잠시 멈추게 만들어 브라우저가 끊기는 느낌을 줄 수 있습니다.따라서 저장 함수를 바로 실행하지 않고, 약간의 지연을 둬서 일정 시간 동안 요청이 여러 번 들어오면 한 번만 실행되도록 하는 것이 좋습니다. 이를 “쓰로틀(throttle)”이라고 부릅니다.쓰로틀을 추가하면 기능은 그대로 유지하면서도 화면이 훨씬 부드러워집니다.let saveQueued = false; function saveThrottled(){ if (saveQueued) return; saveQueued = true; setTimeout(()=>{ save(); saveQueued = false; }, 250); } 이 구조를 적용할 때 반드시 지켜야 할 점이 있습니다.첫째, load()가 실행된 직후 단 한 번 buildIndexes()를 호출해 초기 인덱스를 반드시 만들어야 합니다.둘째, 문서를 생성하거나 수정하거나 이동하는 등 모든 변경 함수가 반드시 인덱스와 동기화되어야 합니다.셋째, findDoc()과 childrenOf()의 이름과 인자 구조는 그대로 유지해야 기존 코드들이 모두 정상적으로 동작합니다.이렇게 하면 조회는 항상 빠르게 유지되고, 렌더링은 필요한 부분만 다시 그리게 되며, 저장은 쓰로틀 덕분에 부드럽게 이루어집니다.이 방법은 문서가 수백 개 정도밖에 되지 않아도 트리 전체를 자주 렌더링하거나 문서 이동이 잦은 상황에서 큰 효과를 볼 수 있습니다.Map 인덱스를 적용했는데도 여전히 프레임 드랍이 느껴진다면, 먼저 childrenOf()의 중복 호출을 없애고, 전체를 새로 그리는 대신 필요한 부분만 다시 그리도록 바꾸는 것이 비용 대비 효과가 가장 큽니다.데이터가 수천 개로 늘어나거나 한 화면에서 수많은 노드를 동시에 펼쳐야 하는 경우에는 가상 스크롤을 함께 도입하는 것이 좋습니다.저장 쓰로틀은 어떤 규모에서도 유용하므로 기본적으로 활성화해 두는 것을 권장합니다.결국 핵심은 단순합니다.Map 인덱스 두 개(byId, byParent)를 만들어 문서를 빠르게 찾고, 중복 호출을 제거해 DOM 연산량을 줄이며, 저장과 렌더링을 쓰로틀로 제어해 메인 스레드가 버벅이지 않게 만드는 것입니다.이 세 단계를 함께 적용하면, 문서 수가 수천 개로 늘어나더라도 현재 코드 기반에서 부드럽고 안정적인 성능을 유지할 수 있습니다.또 한 가지 덧붙이자면, 실제로 데이터를 장기간 보관하거나 탭 간 동기화가 필요한 상황이라면, 메모리 기반 Map 인덱스만으로는 부족할 수 있습니다. 이런 경우에는 IndexedDB 같은 브라우저 내장 데이터베이스를 함께 활용해 인덱스를 디스크에 캐시 형태로 저장해 두면, 새로고침 후에도 빠른 초기 조회가 가능합니다. 이렇게 하면 메모리와 디스크 양쪽에서 균형 잡힌 구조로 더 안정적인 성능을 얻을 수 있습니다.감사합니다!
- 0
- 2
- 65
질문&답변
CSS까지만 지연에 영향을 주는건가요?
안녕하세요, 정수지님 🙂DOM과 CSSOM이 결합되어 렌더 트리가 만들어진 다음에야 실제 화면이 그려진다는 원리를 정확히 이해하고 계시네요. 이 개념 위에 script 태그의 동작 방식을 더 깊이 이해하면, 브라우저가 어떻게 렌더링을 멈추거나 이어가며, async와 defer 속성이 실제로 CRP(Critical Rendering Path, 중요 렌더링 경로) 에 어떤 영향을 주는지를 완전히 체감하실 수 있습니다.브라우저는 HTML 문서를 위에서 아래로 읽어가며 DOM(Document Object Model) 을 생성하고, 동시에 외부 CSS 파일을 요청해 CSSOM(CSS Object Model) 을 만듭니다.이 두 구조가 모두 준비되어야 브라우저는 Render Tree를 만들 수 있습니다.Render Tree가 완성되면 그때서야 Layout(요소의 위치 계산)과 Paint(픽셀 그리기) 단계를 거쳐 화면을 표시하게 됩니다.이 일련의 과정 전체를 Critical Rendering Path라고 부릅니다.여기서 “HTML을 읽는다”는 것은 사람의 눈으로 읽는 것이 아니라,브라우저가 , 같은 태그를 하나씩 해석해서 트리 구조로 쌓는 과정을 의미합니다.그리고 CSS 파일은 이 트리 위에 “색, 크기, 위치” 정보를 입히는 설계도라고 생각하시면 됩니다.여기서 한 가지 중요한 사실이 있습니다.CSS 파일 또한 렌더링 경로를 차단(blocking) 할 수 있습니다.브라우저는 를 만나면 CSS 파일을 다운로드하면서, JS 실행을 잠시 멈춥니다. 왜냐하면 자바스크립트가 getComputedStyle()이나 DOM 변경을 통해 스타일에 영향을 줄 수 있기 때문입니다. 즉, CSS가 완전히 로드되고 CSSOM이 만들어질 때까지는 JS 실행이 연기될 수 있습니다.여기서 “차단(blocking)”이란, 브라우저가 다른 일을 잠시 멈추고“이거 먼저 끝내야지!” 하고 우선순위를 주는 상태를 말합니다.예를 들어, 요리를 하다 말고 갑자기 물을 끓여야 해서 모든 일을 멈추는 것과 비슷합니다.CSS 파일을 다 불러와야 브라우저가 다시 HTML을 계속 읽을 수 있기 때문입니다.(정확히는, CSS는 렌더링을 차단하고, 동기 실행을 지연시킵니다.HTML 파싱 자체를 즉시 멈추는 것은 아니지만,곧바로 이어지는 스크립트 때문에 실제로는 파싱이 잠시 멈춘 것처럼 보입니다.)이때 HTML 안에 태그가 등장하면,브라우저는 “여기서 JavaScript를 실행해야 한다”고 인식합니다.문제는 이 스크립트가 파서-블로킹(parser-blocking) 이라는 점입니다.즉, 브라우저는 스크립트를 다운로드하고 실행하는 동안 HTML 파싱을 멈춥니다.여기서 ‘파싱을 멈춘다’는 건, 브라우저가 HTML 문서를 더 이상 아래로 내려가지 않고“이 스크립트를 먼저 실행할게요” 하고 일시 정지하는 상태입니다.그래서 JS가 오래 걸리면, HTML의 나머지 부분(예: )이 늦게 그려지는 겁니다.예를 들어 다음과 같은 코드가 있다고 해보겠습니다. Hello, world! This text might not appear immediately. 이 경우 브라우저는 main.js를 다운로드하고 실행할 때까지 HTML 파싱을 중단합니다.main.js가 네트워크에서 내려받는 데 시간이 오래 걸린다면, 과 는 그동안 화면에 그려지지 않습니다.즉, 스크립트의 실행 순서뿐 아니라 CRP 자체가 지연되는 것이죠.“네트워크에서 내려받는다”는 것은, 브라우저가 인터넷을 통해 main.js 파일을 서버에서 요청해서 받아오는 과정입니다.만약 인터넷 속도가 느리면, 화면이 하얗게 멈춰 있는 시간이 길어질 수 있습니다.이 문제를 해결하기 위해 등장한 속성이 async와 defer입니다.async를 붙이면 브라우저는 HTML을 파싱하면서 스크립트를 병렬로 다운로드합니다.여기서 “병렬”이란 동시에 여러 일을 하는 것입니다.즉, HTML을 계속 읽으면서 백그라운드에서는 JS 파일도 내려받습니다.하지만 다운로드가 완료되는 순간, 파싱을 잠시 멈추고 스크립트를 즉시 실행합니다.즉, 다운로드는 비동기(=따로따로 동시에)지만 실행은 즉시 일어나므로,실행 시점마다 파서가 잠깐씩 중단될 수 있습니다.또한 여러 개의 async 스크립트가 있을 경우,도착한 순서대로 실행되므로 실행 순서가 보장되지 않습니다.이 말은, 먼저 내려받은 파일이 먼저 실행된다는 뜻으로,코드 사이에 의존 관계가 있으면 문제가 생길 수 있습니다.반면 defer는 다운로드는 비동기이지만 실행은 HTML 파싱이 끝난 직후에 이루어집니다.즉, 모든 DOM 파싱이 완료된 다음, 문서의 순서대로 스크립트가 실행됩니다.이렇게 되면 HTML 파싱을 막지 않기 때문에, 초기 렌더링 속도에 거의 영향을 주지 않습니다.쉽게 말해 async는 “먼저 도착하면 바로 실행”,defer는 “HTML 다 읽고 나서 한꺼번에 실행”이라고 기억하시면 됩니다.둘 다 HTML과 JS를 같이 다운로드하지만, 실행 시점이 다릅니다.현대 자바스크립트에서는 type="module" 속성을 사용할 수도 있습니다.모듈은 기본적으로 defer와 같은 동작 방식을 취하므로,파싱을 방해하지 않고 HTML이 모두 읽힌 뒤에 실행됩니다.단, 모듈 간의 의존 관계에 따라 여러 파일이 병렬로 다운로드되며,실행은 import 순서를 따릅니다.이 경우 defer 속성을 함께 붙이더라도 무시됩니다.(예외적으로, 모듈 스크립트에 async 속성을 함께 붙이면,다운로드가 끝나는 즉시 실행되어 defer와 달리 파싱 완료 전에 실행될 수도 있습니다.)추가로 import() 문법을 사용하면 완전히 비동기적 실행이 가능하며,이는 파서나 렌더링을 전혀 블로킹하지 않습니다.즉, HTML과 상관없이 나중에 “필요할 때만” JS를 불러오는 방식입니다.즉, async와 defer는 단순히 스크립트의 “실행 순서”만 바꾸는 속성이 아니라,HTML 파싱과 렌더링의 타이밍 자체를 바꾸는 성능 조절 도구입니다.실무에서 자주 쓰이는 시나리오를 하나 만들어 보겠습니다.예제 상황:전자상거래 페이지를 예로 들어보죠.사용자가 index.html을 처음 방문할 때, 메인 이미지가 빠르게 보여야 전환율이 높습니다.하지만 개발자가 다음과 같이 스크립트를 작성했다고 가정해봅시다. My Shop (사진) Buy Now 이 코드는 analytics.js와 main.js를 순차적으로 블로킹 방식으로 실행하므로,사용자가 페이지를 열면 메인 이미지가 한참 동안 보이지 않습니다.“순차적 실행”이란, 첫 번째 스크립트를 완전히 끝내야 두 번째 스크립트가 실행된다는 뜻입니다.즉, 한 번에 한 일만 하는 거죠.그래서 JS가 길거나 네트워크가 느리면, 이미지조차 늦게 보이게 됩니다.이를 수정하면 이렇게 됩니다. My Shop (사진) Buy Now 이제 두 스크립트가 CRP에 미치는 영향은 완전히 달라집니다.브라우저가 HTML을 읽으면서 동시에 CSS와 두 스크립트를 병렬로 다운로드합니다.(즉, 세 가지 일이 동시에 일어납니다: HTML 읽기, CSS 받기, JS 받기)analytics.js는 async이므로 도착 즉시 실행되지만,DOM에 의존하지 않는 분석 코드라 페이지 구조에는 영향을 주지 않습니다.main.js는 defer이므로, HTML 파싱이 모두 끝난 후에 실행됩니다.DOM이 완전히 준비된 뒤 실행되므로 안전하며, 첫 페인트에도 영향을 주지 않습니다.그 결과, 사용자는 메인 이미지와 버튼을 즉시 볼 수 있고,동시에 백그라운드에서는 스크립트 로딩이 병렬로 진행됩니다.이런 변화 하나로 첫 페인트(First Paint) 와 DOMContentLoaded 이벤트 시점이 훨씬 빨라집니다.참고로, defer 스크립트는 DOM 파싱이 끝난 직후 실행되고 나서DOMContentLoaded 이벤트가 발생합니다.반면 async 스크립트는 다운로드 완료 시점에 즉시 실행되므로,어떤 것은 DOMContentLoaded보다 먼저, 어떤 것은 나중에 실행될 수도 있습니다.따라서 DOMContentLoaded 전후 실행 타이밍이 중요한 로직이라면 defer가 훨씬 안정적입니다.다음은 이해를 돕기 위한 간단한 비교 요약입니다.기본 동기적 실행 방식으로, 파일을 바로 실행하며 순서를 보장하지만 HTML 파싱을 완전히 멈춥니다. 가장 느린 방식입니다.async비동기 다운로드 방식으로, HTML을 읽는 도중 JS를 동시에 내려받고, 다운로드가 끝나는 즉시 실행합니다. 실행 순서는 보장되지 않으며, HTML 파싱이 잠깐씩 멈출 수 있습니다. 광고나 통계 코드처럼 독립적인 코드에 적합합니다.defer비동기 다운로드 방식이지만, HTML이 전부 파싱된 이후에 문서 순서대로 실행됩니다. HTML 파싱을 멈추지 않기 때문에 렌더링 속도에 거의 영향을 주지 않습니다. 메인 코드에 적합합니다.type="module"모듈 시스템을 사용하는 최신 방식으로, 자동으로 defer처럼 동작하며 여러 파일을 동시에 다운로드합니다. import한 순서대로 실행됩니다. 단, async 속성을 함께 주면 도착 즉시 실행될 수도 있습니다.요약하자면, 기본 스크립트는 차단이 심하고 느리고,async는 빠르지만 순서가 불안정하며,defer와 type="module"은 안정적이면서 렌더링을 방해하지 않습니다.표에서 나왔던 “동기”는 한 번에 한 가지 일만 하는 것이고,“비동기”는 여러 가지 일을 동시에 처리하는 것을 의미합니다.“차단 없음”은 브라우저가 멈추지 않고 계속 HTML을 읽을 수 있다는 뜻입니다.정리하자면, async와 defer는 단순한 순서 옵션이 아니라,브라우저의 렌더링 경로를 최적화하는 속성입니다.async는 다른 코드와 의존성이 없는 외부 스크립트(예: 광고, 분석, 채팅 위젯 등)에 적합하고,defer나 type="module"은 메인 애플리케이션 코드에 적합합니다.이 원리를 이해하면,“어떤 스크립트를 언제 실행해야 가장 빠르게 사용자에게 첫 화면을 보여줄 수 있을까?”라는 질문에 직접 답을 낼 수 있게 됩니다.마지막으로, Chrome DevTools의 Network 탭에서실제 async/defer 스크립트의 다운로드 타이밍과 실행 시점을 관찰할 수 있습니다.async 스크립트는 ‘Received’ 시점에 즉시 실행되며,defer 스크립트는 DOMContentLoaded 직전에 실행되는 것을 확인할 수 있습니다.만약 정수지님께서 실제로 작성 중인 페이지의 구조를 보여주신다면,어디에 어떤 속성을 적용하면 최적의 성능이 나올지구체적인 리팩터링 예시까지 함께 도와드릴 수 있습니다.감사합니다😊😊
- 0
- 2
- 43




