nhcodingstudio
@nhcodingstudio
受講生
2,186
受講レビュー
130
講義評価
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.
講義
受講レビュー
- JavaScriptオブジェクト指向、理論ではなく「作りながら」学びます(戦略AI編)
- DOMからピクセルまで、ブラウザレンダリングとCRP完全攻略 - [DOM完全攻略 Part 3]
- イベントからSPAまで、インタラクティブWebの必須エンジン - [DOM完全攻略 Part 2]
- Next.jsマスタークラス:Part 2 - フルスタックアーキテクチャとフレームワーク深化 (Server Actions、キャッシュ革命、高度なルーティング)
- Next.jsマスタークラス:Part 3 - 実践アーキテクチャの完成(サーバー・クライアント同期、究極の最適化、フルスタックセキュリティ)
投稿
Q&A
ai가 만든 강의인가요?
안녕하세요 고건호 님, 먼저 강의에 관심을 가져주시고 문의를 남겨주셔서 진심으로 감사드립니다. 우려하시는 부분에 대해 오해를 풀어드리고자 솔직하고 상세하게 답변을 드리고자 합니다.먼저, 본 강의는 TTS가 아닌 제 목소리로 직접 녹음한 것이 맞습니다. 다만, 강의를 한 번에 연속해서 녹음하는 것이 아니라 틈틈이 시간을 내어 여러 차례로 나누어 녹음을 진행했습니다. 예를 들어 10분짜리 영상이라면 2분은 먼저 찍고, 또 2분은 시간이 날 때 찍는 방식이었습니다. 그러다 보니 녹음할 때마다 주변 환경이나 제 컨디션에 따라 목소리의 톤과 질감이 많이 달라지는 경우가 많았습니다. 이를 보완하기 위해 최종적으로 녹음을 완료하고 전체적인 음성의 높낮이와 톤을 균일하게 맞추는 과정을 거쳤는데, 이 과정에서 음성이 다소 불편하고 기계음처럼 들리게 된 것 같습니다. 학습하시면서 불편함을 느끼셨다면 너른 양해를 부탁드립니다.또한, 강의 자료를 대충 AI로 만들었다는 말씀에 대해서는 절대 그렇지 않다는 점을 분명히 말씀드리고 싶습니다. 이 강의는 'AI 딸깍의 시대 원리로 돌파하는 Node.js와 CS'라는 이름에 걸맞게, 얕은 지식이 아닌 깊은 원리를 전달하기 위해 오랜 기간 철저하게 준비했습니다. 수많은 공식 문서와 신뢰할 수 있는 기술 블로그들을 꼼꼼히 살펴보고 최대한 논란의 여지가 없도록 심혈을 기울였습니다. 수강생분들의 시각적 이해를 돕기 위해 일부 이미지 요소에 AI 도구를 활용하는 경우는 있으나, 전체 슬라이드쇼는 제가 직접 하나하나 꼼꼼하게 기획하고 제작했습니다. 더불어 강의 영상뿐만 아니라 내용 전반을 정리한 노트까지 하나부터 열까지 제 손을 거쳐 다듬었습니다. 고건호 님께서 이 강의를 위해 지불해 주신 비용, 그 이상의 값어치를 반드시 얻어 가실 수 있도록 진심을 다해 준비한 강의입니다.확인해 보니 아직 1강밖에 수강하지 않으신 것으로 보이는데, 만약 이 강의 수강을 계속 희망하신다면 꼭 여러 강의를 이어서 수강해 보시는 것을 권장해 드립니다. 그럼에도 불구하고 강의가 만족스럽지 않고 도저히 납득이 되지 않으신다면, 제가 제 개인 사비로 전액 환불해 드리겠습니다. 환불 및 기타 문의를 위한 제 이메일 주소는 jeony0535@naver.com입니다.끝으로 강의를 들으시면서 내용에 대해 궁금한 점이 생기신다면 언제든 편하게 질문 남겨주세요. 상세히 답변해 드리겠습니다. 감사합니다.
- いいね数
- 0
- コメント数
- 1
- 閲覧数
- 46
Q&A
BFF의 필요성
안녕하세요 져니님, 수업을 너무 잘 듣고 계신다니 정말 감사드립니다. 단순한 기능 구현을 넘어서 인증과 인가의 아키텍처, 그리고 보안의 본질적인 부분까지 깊이 고민하시며 프로젝트에 적용해 보려는 모습이 시니어 개발자로 성장해 나가는 아주 훌륭한 과정이라고 생각합니다. 질문하신 내용에 대해 실무적인 관점과 실제 데이터 흐름을 바탕으로 최대한 상세하게 답변해 드리겠습니다.결론부터 말씀드리자면, 소셜 로그인을 도입하여 인가 처리를 구글이나 카카오 같은 외부 프로바이더에게 위임하더라도 프론트엔드와 백엔드 서버 사이에 위치하는 BFF 패턴은 여전히 매우 필요하며, 오히려 보안과 전체적인 시스템 아키텍처 측면에서 훨씬 더 중요하고 강력한 역할을 수행하게 됩니다.OAuth가 인증과 인가를 위임하는 것은 맞지만, 그것은 어디까지나 우리의 서비스와 외부 프로바이더 사이의 신뢰 관계를 구축하는 문제일 뿐, 최종적으로 브라우저와 우리의 서비스 간에 이루어지는 통신을 어떻게 안전하게 보호할 것인가는 여전히 우리가 직접 설계하고 해결해야 할 핵심 과제이기 때문입니다.실무 케이스의 전체적인 흐름을 따라가며 BFF가 구체적으로 어떤 역할을 하는지 살펴보겠습니다. 사용자가 브라우저에서 구글 로그인 버튼을 클릭하면, 브라우저가 직접 구글 서버와 통신하는 것이 아니라 먼저 우리의 BFF 서버로 요청을 보냅니다. 그러면 BFF가 사용자를 구글의 로그인 페이지로 리다이렉트 시키고, 사용자가 로그인을 완료하면 구글은 다시 우리의 BFF 서버로 인가 코드를 전달하게 됩니다.여기서 OAuth 2.0 스펙이 정의하는 클라이언트의 두 가지 종류인 퍼블릭 클라이언트(Public Client)와 기밀 클라이언트(Confidential Client)의 개념이 중요해집니다. 브라우저처럼 모든 코드가 외부로 노출되는 환경을 퍼블릭 클라이언트라고 하고, 서버처럼 비밀키를 안전하게 숨길 수 있는 환경을 기밀 클라이언트라고 부릅니다. 만약 프론트엔드에서 직접 소셜 로그인을 처리한다면, 외부 프로바이더에게 인증 토큰을 받아오기 위해 반드시 필요한 클라이언트 시크릿 값 등이 브라우저의 네트워크 탭이나 자바스크립트 소스 코드에 그대로 노출되는 치명적인 보안 취약점이 발생하게 됩니다.물론 최근에는 프론트엔드 같은 퍼블릭 클라이언트에서도 시크릿 없이 인가 코드를 안전하게 교환할 수 있는 PKCE(Proof Key for Code Exchange) 방식이 표준으로 자리 잡고 있기는 합니다. 하지만 PKCE를 사용하더라도 결국 발급받은 '액세스 토큰' 자체를 브라우저 어딘가에 저장해야 한다는 근본적인 보안 취약점은 고스란히 남게 됩니다. 따라서 이 과정을 BFF를 통해 처리하면, 브라우저는 단순히 화면 이동만 담당하고 실제 인가 코드를 사용해 토큰을 발급받는 민감한 네트워크 통신은 클라이언트 시크릿이 안전하게 숨겨진 서버 사이드, 즉 BFF 내부에서만 진행되므로 보안 수준이 비약적으로 상승합니다.이렇게 BFF가 구글이나 카카오로부터 액세스 토큰과 리프레시 토큰을 성공적으로 발급받은 이후의 흐름도 매우 중요합니다. 만약 이 토큰들을 그대로 브라우저로 내려보내서 로컬 스토리지 등에 저장하게 만든다면, 이는 악의적인 스크립트가 브라우저 내의 데이터를 탈취하는 크로스 사이트 스크립팅(XSS) 공격의 아주 좋은 먹잇감이 되어버립니다.바로 이 지점에서 BFF가 아주 우아하고 견고한 해결책을 제공합니다. BFF는 프로바이더로부터 받은 토큰이나 혹은 이를 바탕으로 우리 내부 백엔드에서 새로 발급한 자체 토큰을 브라우저에 절대 넘기지 않고, 자신의 서버 메모리나 레디스 같은 외부 캐시 시스템, 혹은 안전한 세션 스토리지에 보관합니다. 그 대신 브라우저에는 자바스크립트 코드가 절대 접근할 수 없도록 HttpOnly 속성과 Secure 속성이 엄격하게 부여된 세션 쿠키만을 발급하여 내려줍니다.이때 쿠키를 사용하게 되면 XSS 공격은 완벽하게 방어할 수 있지만, 대신 크로스 사이트 요청 위조(CSRF) 공격에 노출될 수 있다는 점을 함께 고려해야 합니다. 이를 방어하기 위해 쿠키에 SameSite=Strict (또는 환경에 따라 Lax) 속성을 부여하거나, CSRF 토큰을 활용하는 아키텍처를 추가로 구성하게 됩니다.이후 사용자가 브라우저를 통해 마이페이지 정보를 요청하거나 결제를 시도하는 등 API를 호출할 때마다 이 안전한 쿠키가 브라우저에 의해 자동으로 서버로 전송됩니다. 요청을 받은 BFF는 쿠키를 확인하여 자신이 안전하게 보관하고 있던 진짜 만능 키인 액세스 토큰을 꺼낸 뒤, 실제 비즈니스 로직을 처리하는 다운스트림 백엔드 API 서버로 요청을 대리하여 전달하는 프록시 역할을 완벽하게 수행하게 됩니다.사용자가 서비스를 이용하다 보면 필연적으로 액세스 토큰의 유효 기간이 만료되는 순간이 찾아옵니다. 이때 리프레시 토큰을 이용해 새로운 토큰을 재발급받는 로직은 생각보다 매우 까다롭습니다. 프론트엔드에서 여러 개의 컴포넌트가 동시다발적으로 API 요청을 보내는 상황에서 토큰이 만료되었다면, 수많은 요청이 동시에 갱신을 시도하게 되어 충돌이 발생하는 이른바 경쟁 상태(Race Condition)에 빠질 수 있습니다. 이를 막기 위해 프론트엔드의 Axios 인터셉터 등에서 뮤텍스 락을 걸거나 요청들을 큐(Queue)에 담아두고 순차적으로 처리해야 하는 등 복잡한 제어가 필요해집니다.이러한 무겁고 복잡한 비즈니스 로직을 리액트 컴포넌트 같은 프론트엔드 영역에 두는 것은 관심사 분리 원칙에 크게 어긋납니다. 하지만 BFF 아키텍처에서는 프론트엔드가 토큰의 만료 여부를 전혀 신경 쓸 필요가 없습니다. 백엔드 API가 토큰 만료 에러를 반환하면, 중간에 있는 BFF가 이를 가로채서 백그라운드 환경에서 즉각적으로 토큰 갱신 로직을 전담하여 처리합니다. 새로운 토큰을 성공적으로 받아 자신의 스토리지를 업데이트한 후, 실패했던 원래의 요청을 다시 백엔드로 보내어 정상적인 응답을 받아낸 뒤 프론트엔드에게 매끄럽게 전달해 줍니다. 프론트엔드 코드는 토큰이라는 존재 자체를 모른 채 오로지 사용자에게 보여줄 사용자 인터페이스 렌더링에만 온전히 집중할 수 있게 되는 것입니다.마지막으로 데이터 정규화(Normalization) 과정에서의 이점도 빼놓을 수 없습니다. 구글, 카카오, 네이버 등 여러 소셜 로그인을 연동하다 보면 각 프로바이더가 반환해 주는 사용자 정보의 데이터 구조와 필드 이름이 모두 제각각이라는 것을 알게 됩니다. 이 정제되지 않고 파편화된 원본 데이터를 프론트엔드가 직접 받아서 조건문을 남발하며 파싱하고 처리하는 것은 유지보수 측면에서 매우 비효율적입니다. BFF는 이렇게 다양한 형태로 들어오는 응답 데이터를 프론트엔드가 화면에 그리기 딱 좋은 하나의 통일된 규격으로 예쁘게 정제하고 조립하여 넘겨주는 역할을 수행하여 프론트엔드의 부담을 크게 덜어줍니다.최근 실무에서 많이 활용되는 넥스트제이에스(Next.js)의 라우트 핸들러(Route Handlers)나 서버 액션(Server Actions), 혹은 노드제이에스(Node.js) 기반의 서버 환경을 활용하여 BFF를 구성하고 계신다면, 현재 져니님이 설계하고 나아가는 방향이 아키텍처적으로 아주 정확하다고 말씀드리고 싶습니다. 외부 프로바이더에게 인증을 위임하더라도, 발급받은 토큰의 전체적인 수명 주기 관리와 브라우저 환경의 보안이라는 가장 중요하고 무거운 책임을 BFF가 가장 안전한 위치에서 통제하게 되는 것입니다.탄탄한 컴퓨터 공학적 기반 지식과 네트워크 흐름에 대한 이해를 바탕으로 아키텍처를 설계하고 계시니, 분명 구조적으로 아주 훌륭하고 견고한 프로젝트가 완성될 것이라 확신합니다. 프로젝트를 진행하시면서 더 깊이 있는 고민이 생기거나 궁금한 점이 있다면 언제든지 편하게 질문 남겨주시길 바랍니다. 져니님의 멋진 프로젝트 완성을 진심으로 응원합니다.참고해주세요!
- いいね数
- 0
- コメント数
- 2
- 閲覧数
- 28
Q&A
state 객체로 묶기
안녕하세요 코딩님. 처음 React를 접하거나 기본기를 다지는 과정에서 누구나 한 번쯤은 '관련된 데이터인데 왜 굳이 따로 관리할까?'라는 아키텍처적인 의문을 가지게 됩니다. React의 렌더링 및 상태 관리 메커니즘이라는 근본적인 측면에서 설명해 드리겠습니다.가장 큰 이유는 학습자의 인지 부하를 줄이고, React의 상태 불변성(Immutability) 원칙을 직관적으로 보여주기 위함입니다.과거 클래스형 컴포넌트의 상태 업데이트 메서드(this.setState)는 객체의 일부분만 업데이트해도 React가 얕은 병합(Shallow Merge)을 통해 자동으로 나머지 속성을 유지해 주었습니다. 하지만 함수형 컴포넌트의 훅(useState)은 기존 상태를 완전히 새로운 값으로 대체(Replace)하게 됩니다. 만약 이름, 이메일, 동의 여부라는 세 가지 상태를 하나의 폼 데이터 객체로 묶는다면 다음과 같이 코드를 작성해야 합니다.const [formData, setFormData] = useState({ username: "", email: "", agree: false }); // 상태 업데이트 시 전개 연산자(...)를 사용해 기존 상태를 복사해야 함 onChange={(e) => setFormData(prev => ({ ...prev, username: e.target.value }))} 이렇게 입력 필드 하나를 변경할 때마다 상태 업데이트 함수 내부에서 이전 상태값을 가져와 전개 연산자를 사용해 기존 객체를 복사하고, 변경할 속성만 덮어씌워야 합니다. 학습 단계에서는 JavaScript의 전개 연산자와 React의 메모리 주소 비교 방식을 동시에 이해해야 하므로, 각 상태의 독립적인 변화를 명확히 보여주기 위해 분리하는 것이 정석입니다.여기서 자바스크립트 엔진의 메모리 구조 관점으로 팩트 체크를 해보겠습니다. 객체는 힙(Heap) 메모리에 동적으로 할당되고, 변수는 그 메모리 주소를 참조합니다. React의 재조정(Reconciliation) 알고리즘은 상태 객체의 내부 프로퍼티가 변경된 것을 일일이 깊은 비교(Deep Compare)로 감지하지 않습니다. 대신 Object.is 비교를 통해 객체의 메모리 주소, 즉 포인터가 완전히 바뀌었는지를 O(1)의 시간 복잡도로 빠르게 참조 비교하여 렌더링을 트리거합니다. 따라서 전개 연산자를 통해 새로운 메모리 참조를 만들어내는 불변성 유지는 React에서 매우 중요한 근본 개념입니다.또한, 상태를 개별 변수로 분리하면 향후 로직을 재사용하기 위해 Custom Hook으로 분리하기가 훨씬 수월해집니다. 현재 코드는 폼이 단순하지만 실무에서는 각 입력 필드마다 복잡한 유효성 검사가 필요합니다. 상태가 개별적으로 존재하면 입력 상태와 핸들러를 캡슐화하여 다음과 같이 별도의 훅으로 추상화하기 좋습니다.// 입력 상태와 핸들러를 캡슐화한 Custom Hook 적용 예시 const usernameInput = useInput(""); const emailInput = useInput(""); // 컴포넌트 내부가 훨씬 깔끔해짐 이렇게 훅으로 분리하면 컴포넌트 내부에서는 단순히 훅이 반환한 객체를 전개하여 인풋 태그에 속성으로 넘겨주기만 하면 되므로 구조가 아주 깔끔해집니다. 반대로 만약 상태가 하나의 거대한 객체로 묶여 있다면, 특정 필드만의 로직을 분리해 재사용하거나 useEffect의 의존성 배열(Dependency Array)에 특정 필드만 넣고 불필요한 사이드 이펙트 없이 로직을 트리거하기가 매우 까다로워집니다.성능 측면에서도 의문이 들 수 있습니다. '상태를 따로 관리하면 하나를 입력할 때마다 각각 렌더링이 발생해서 성능에 안 좋은 것이 아닌가?' 생각할 수 있죠.과거 React 17 이전 버전에서는 합성 이벤트(Synthetic Event)가 아닌 Promise(비동기 통신 이후), setTimeout, 네이티브 이벤트 등 특정 상황에서 여러 상태를 업데이트할 때 리렌더링이 여러 번 발생할 위험이 있었습니다. 하지만 React 18부터 도입된 자동 배칭(Automatic Batching) 덕분에, 어떠한 환경이든 이벤트 핸들러 내에서 아무리 많은 상태 업데이트 함수를 개별적으로 호출하더라도 React가 이를 모아서 단 한 번의 리렌더링으로 일괄 처리합니다. 따라서 렌더링 최적화 관점에서도 상태를 객체로 묶어야 할 강제성이 완전히 사라졌습니다.학습 단계를 넘어 실제 프로덕션 레벨이나 최신 React 패러다임에서는 이 접근법도 변화하고 있는데, 여기서 정말 유용한 실무 아키텍처 팁을 더해드리겠습니다.최근의 프론트엔드 트렌드는 질문해주신 코드처럼 모든 입력값을 상태로 추적하는 제어 컴포넌트(Controlled Component) 방식 자체를 점진적으로 지양하는 추세입니다. 대신 웹 표준인 FormData와 비제어 컴포넌트, 그리고 React 19의 Actions를 활용하여 다음과 같이 클라이언트 측의 상태 관리 자체를 최소화하는 방향으로 아키텍처를 설계합니다.// 최신 React/Next.js 환경에서의 폼 처리 패러다임 (상태 생략) function SignupForm() { function handleSubmit(formData) { const data = Object.fromEntries(formData.entries()); console.log("제출 데이터:", data); } return ( 가입하기 ); } 특히 최신 Next.js 환경에서 Server Actions와 결합하게 되면, 폼 태그의 action 속성에 핸들러 함수를 바로 연결하고 인자로 넘어온 폼 데이터를 서버에서 직접 처리할 수 있습니다. 이는 클라이언트 측으로 전송되는 자바스크립트 번들 사이즈를 대폭 줄이고 하이드레이션(Hydration) 비용을 최소화합니다. 더 나아가 자바스크립트가 완전히 로드되기 전에도 폼 제출이 가능해지는 점진적 향상(Progressive Enhancement)을 달성할 수 있으며, 스트리밍(Streaming) 아키텍처와 맞물려 브라우저 네이티브에 가장 가까운 최적화를 이끌어냅니다.결론적으로 해당 학습 코드는 React의 개별 상태 업데이트 로직을 명확히 이해시키고 객체 불변성 유지라는 추가적인 허들을 제거하기 위해 상태를 분리해 둔 아주 잘 설계된 입문용 패턴입니다. 실무에서 필드가 5개에서 10개 이상으로 늘어나는 복잡한 폼을 구축해야 한다면, 제어 컴포넌트 환경에서는 react-hook-form 같은 최적화 라이브러리를 적극적으로 사용하거나, 최신 환경에서는 방금 말씀드린 Action 기반의 웹 표준 폼 데이터 처리를 도입하는 것이 탄탄한 아키텍처 선택이 될 것입니다.참고해주세요!
- いいね数
- 0
- コメント数
- 1
- 閲覧数
- 27
Q&A
3강 질문
안녕하세요 박은송님, 질문 주셔서 감사합니다.이 부분을 잘 이해하고 넘어가는 것은 시스템 전체를 조망하며 원리를 기반으로 제어할 수 있는 시니어 레벨로 나아가기 위해 꼭 짚고 넘어가야 할 좋은 질문입니다. 결론부터 명확하게 말씀드리면, 많은 분들이 모든 비동기 작업이 스레드 풀(Thread Pool)로 간다고 생각하지만 실제로는 그렇지 않습니다. Node.js(정확히는 libuv 라이브러리)는 작업의 성격이나 하드웨어적 특성에 따라 운영체제(OS) 커널에 직접 위임할지, 아니면 자신이 관리하는 워커 스레드 풀(Worker Thread Pool)에 던질지를 철저하게 분리해서 처리합니다. 이 두 가지가 어떻게 나뉘고, 또 어떻게 유기적으로 맞물려 돌아가는지 컴퓨터 공학(CS)의 하드웨어 레벨부터 상세히 설명해 보겠습니다.본격적인 설명에 앞서, 쉽게 이해하실 수 있도록 핵심 용어 네 가지를 먼저 가볍게 짚고 넘어가겠습니다. 첫째로 스레드(Thread)란 컴퓨터가 작업을 처리하는 가장 작은 작업자 단위로, 식당의 직원 한 명이라고 생각하시면 됩니다. 둘째로 블로킹(Blocking)은 이 직원이 고기를 굽느라 불판 앞을 떠나지 못하고 다른 손님의 주문을 전혀 받지 못하는 멈춤 상태를 말합니다. 셋째로 오프로딩(Offloading)은 매니저가 직접 고기를 굽지 않고 주방 담당자나 외부 업체에 그 일을 떠넘겨 위임하는 효율적인 행동입니다. 마지막으로 스레드 풀(Thread Pool)은 일이 생길 때마다 직원을 새로 뽑는 대신, 미리 고용해 둔 4명 정도의 전담 대기조를 의미합니다.이제 하드웨어와 CS의 관점에서 누가 진짜 일을 하는지 살펴보겠습니다. 컴퓨터의 핵심 부품들은 속도 차이가 큽니다. CPU는 초당 수많은 연산을 하는 매우 빠른 장치이며, RAM은 CPU가 작업할 데이터를 올려두는 넓고 빠른 작업대입니다. 반면 Network Card(NIC)는 외부 인터넷망과 통신하는 출입구로서 상대적으로 느리고 언제 데이터가 올지 모르는 특성이 있으며, Storage(SSD/HDD)는 데이터를 영구 저장하는 거대한 창고로 물리적이고 구조적인 한계 때문에 가장 느립니다. Node.js의 메인 스레드인 이벤트 루프는 오직 CPU와 RAM 위에서만 아주 빠르게 돌아갑니다. 외부 데이터를 기다려야 하는 네트워크 통신이나, 느린 하드디스크를 뒤져야 하는 작업에 메인 스레드가 묶여버리는 블로킹 상태가 되면 전체 서버가 멈추게 됩니다. 그래서 Node.js는 이 무거운 짐들을 외부로 오프로딩하여 넘기는데, 여기서 두 가지 다른 경로가 발생합니다.이 두 가지 위임 경로는 OS 커널과 스레드 풀로 나뉩니다. libuv는 메인 스레드에게 비동기 작업을 받았을 때, 이 작업이 네트워크 통신인지 아니면 파일 입출력이나 CPU 연산인지 판단하여 작업을 다른 곳으로 보냅니다. 첫 번째 경로는 주로 네트워크 통신에서 발생하는 OS 커널 위임(epoll / kqueue)입니다. 운영체제(OS)는 이미 네트워크 카드(NIC)를 다루는 데 있어 전문가입니다. 네트워크 소켓 통신이나 HTTP 요청 등이 들어오면, libuv는 스레드 풀의 대기조를 쓰지 않고, 대신 OS 커널에게 이 소켓에서 데이터가 들어오면 나한테 알려달라고 알람만 맞추고 뒤돌아섭니다. 실제로 데이터를 받고 RAM에 적재하는 것은 NIC와 OS가 알아서 처리하며, 작업이 끝나면 OS가 알람을 울려 메인 스레드에게 가져가라고 통지합니다. 다만 여기서 한 가지 주의해야 할 예외가 있습니다. 도메인 주소를 IP로 바꾸는 dns.lookup() 메서드의 경우, OS의 동기적인 시스템 함수를 사용하기 때문에 네트워크 작업임에도 예외적으로 OS 커널이 아닌 libuv 스레드 풀을 사용하게 됩니다. 이는 실무에서 병목을 추적할 때 아주 중요한 포인트입니다. 두 번째 경로는 주로 파일 I/O, 암호화, 압축에서 사용되는 libuv 스레드 풀 위임입니다. 파일 시스템 접근은 운영체제마다 비동기를 지원하는 방식이 파편화되어 있고, 암호화나 압축 같이 순수하게 CPU를 많이 써야 하는 연산은 OS 커널의 비동기 API로 해결하기 어렵습니다. 이때 libuv가 꺼내 드는 것이 바로 기본값 4개를 가진 스레드 풀입니다. 메인 스레드가 이 파일을 읽어오라거나 비밀번호를 암호화해 달라고 하면, libuv는 스레드 풀의 워커 스레드 중 하나를 깨워서 그 일을 전담시킵니다. 이 워커 스레드는 백그라운드에서 열심히 파일을 읽거나 연산을 한 뒤, 끝나면 메인 스레드에게 결과를 돌려줍니다. 워커 스레드 자체가 블로킹되더라도 메인 스레드와는 분리되어 있으니 메인 스레드는 안전하게 다른 일을 계속할 수 있습니다.이 모든 과정을 종합하여 대형 레스토랑의 운영 시스템에 비유해 보겠습니다. 메인 스레드는 홀을 총괄하는 단 1명의 매니저로서, 손님의 주문을 받고 완성된 요리를 서빙하는 교통정리만 하며 주방에 들어가서 직접 요리하지 않습니다. RAM은 매니저가 주문서와 완성된 요리를 임시로 올려두는 카운터 테이블 역할을 합니다. OS 커널과 NIC는 외부 배달 대행사가 음식을 수령해 가는 자동화 드라이브스루 시스템과 같으며, libuv 스레드 풀은 창고에서 무거운 식자재를 꺼내오거나 장시간 고기를 구워야 하는 4명의 전담 특수 작업반과 같습니다. 이를 바탕으로 사용자가 사진첩 앱에 접속해 프로필 사진을 요청하는 API를 호출했다고 가정해 보겠습니다. 먼저 인터넷망을 타고 들어온 손님의 요청이 서버의 통신 장비에 도착합니다. OS는 이 요청을 조립해 카운터인 RAM에 올려두고 매니저인 메인 스레드에게 알람을 울립니다. 알람을 들은 매니저는 달려가 주문서를 확인하고, 사진 파일을 스토리지에서 읽어와야 한다는 것을 파악합니다. 매니저는 이 작업을 자신이 직접 하거나 드라이브스루 장비가 할 수 없다는 것을 알기 때문에, 즉시 4명의 특수 작업반인 스레드 풀 중 대기하고 있는 1명에게 작업 지시서를 던지고 다른 손님의 주문을 받으러 홀연히 떠납니다. 그동안 워커 스레드는 지하 창고로 내려가 물리적인 하드디스크를 뒤져서 사진을 가져옵니다. 작업이 끝나면 특수 작업반은 카운터에 사진을 올려두고 매니저에게 작업이 끝났다는 표시를 남깁니다. 매니저는 홀을 돌다가 이를 보고 카운터로 와서 사진을 챙긴 뒤, 사진을 손님에게 보내기 위해 다시 드라이브스루의 배출구 소켓에 데이터를 던져 넣고 OS에게 전송을 지시하며 끝을 맺습니다. 실제 네트워크 바깥으로 데이터를 전송하는 것은 통신 장비와 OS가 알아서 처리합니다.요약하자면, 메인 스레드는 쉬지 않고 OS 커널과 스레드 풀 양쪽으로 작업을 오프로딩하여 던지고 완료된 결과를 받아오는 컨트롤 타워 역할을 수행합니다. 데이터베이스 연결이나 HTTP 통신 같은 네트워크 통신은 OS 커널이 전담하므로 스레드 풀을 전혀 소모하지 않으며, 이 덕분에 수만 개의 동시 접속 처리가 가능해집니다. 반면 파일 입출력이나 암호화는 libuv 스레드 풀이 백그라운드 스레드를 소모하여 처리하게 됩니다. 이 작업이 너무 몰리면 스레드 풀 4개가 꽉 차서 병목이 발생할 수 있으므로 상황에 따라 스레드 풀의 크기를 조절하는 튜닝이 필요하기도 합니다. 이때 Node.js의 환경 변수인 UV_THREADPOOL_SIZE를 설정하여 스레드 풀의 크기를 제어할 수 있습니다. 기본값은 4이지만, 서버의 물리적 코어 수나 작업 특성에 맞춰 최대 1024까지 늘릴 수 있어 암호화나 압축 같은 무거운 작업이 많을 때 유용합니다. 이처럼 운영체제 시스템 콜과 스레드 풀은 서로 다루는 영역이 명확히 다르며, 이벤트 루프를 중심으로 완벽하게 분업하여 유기적으로 서버를 운영하게 만들어 줍니다.마지막으로, 실제 실무 상황 예제를 통해 이 메커니즘이 어떻게 적용되는지 살펴보겠습니다. 우리가 흔히 만드는 '회원가입 기능'을 개발한다고 가정해 보겠습니다. 사용자가 이메일과 비밀번호를 입력하고 가입 버튼을 누르면, 먼저 사용자의 가입 데이터가 서버에 도착하는 과정은 네트워크 통신이므로 OS 커널이 전적으로 담당합니다. 데이터가 모두 도착하면 OS 커널이 메인 스레드에게 알림을 주고, 메인 스레드는 요청 데이터를 확인합니다. 그다음, 보안을 위해 사용자의 비밀번호를 해싱(Bcrypt 등)하는 과정을 거쳐야 하는데, 이 작업은 CPU 연산을 매우 많이 요구하는 무거운 작업입니다. 만약 메인 스레드가 이를 직접 처리하면 그동안 다른 사용자의 접속을 받지 못하는 블로킹 상태가 되므로, 메인 스레드는 스레드 풀에 있는 워커 스레드에게 비밀번호 암호화를 오프로딩하여 위임합니다. 워커 스레드가 백그라운드에서 암호화를 마치고 결과를 돌려주면, 이제 메인 스레드는 이 완성된 데이터를 데이터베이스(DB)에 저장해야 합니다. 흥미롭게도 애플리케이션 서버가 DB 서버와 통신하는 과정은 내부망이든 외부망이든 결국 네트워크 통신입니다. 따라서 메인 스레드는 이번에는 스레드 풀의 직원을 부르지 않고, 다시 OS 커널에게 데이터를 DB로 보내고 답변이 오면 알려달라고 오프로딩합니다. 최종적으로 DB에서 저장 완료 응답이 오면 OS 커널이 메인 스레드에게 알림을 주고, 메인 스레드는 비로소 사용자에게 가입 완료 메시지를 전송하게 됩니다. 이처럼 단 하나의 회원가입 API를 처리할 때도, 작업의 성격이 CPU 연산인지 네트워크 통신인지에 따라 스레드 풀과 OS 커널을 오가며 유기적으로 협력하는 것이 Node.js 실무 서버의 핵심 동작 방식입니다.참고해주세요!
- いいね数
- 0
- コメント数
- 2
- 閲覧数
- 48
Q&A
2강 nodejs 3단계 설명 질문
안녕하세요 박은송님, 강의를 깊이 있게 수강해 주시고 훌륭한 질문을 남겨주셔서 감사합니다. 질문해 주신 내용은 Node.js의 아키텍처를 이해하는 데 있어 가장 중요한 핵심이자, 내부 동작 원리를 깊이 고민하지 않으면 도달할 수 없는 훌륭한 질문입니다.결론부터 말씀드리면 남겨주신 내용이 맞습니다. 수업에서 직관적으로 설명해 드리려다 보니 비유적인 표현이 들어가 혼동이 있으셨던 것 같아요. 이번 기회에 기술적인 디테일을 조금 더 보태어, 내부에서 실제로 어떤 과정을 거쳐 맞물려 돌아가는지 짚어드리겠습니다.먼저 첫 번째로 V8 엔진의 결과물이 기계어가 되지 않느냐는 질문에 대해 답변을 드리자면, 은송님 말씀이 정확합니다. V8 엔진의 본질적인 역할은 자바스크립트 코드를 C++ 코드로 번역하는 것이 아니라 자바스크립트를 컴퓨터의 두뇌인 CPU가 직접 실행할 수 있는 0과 1의 기계어로 컴파일하는 것입니다.이 과정을 조금 더 세밀하게 들여다보면, V8 엔진은 우리가 작성한 텍스트 형태의 자바스크립트 코드를 가장 먼저 파싱이라는 문법 분석 과정을 거쳐 추상 구문 트리, 즉 AST라는 나무 잔가지 모양의 데이터 구조로 변환합니다. 이것은 마치 우리가 영어 문장을 읽을 때 주어, 동사, 목적어로 구조를 나누어 의미를 파악하는 것과 같이, 컴퓨터가 코드의 논리적 구조를 이해할 수 있도록 형태를 잡아주는 작업입니다.이렇게 구조화된 AST가 준비되면, V8 엔진 내부에 있는 이그니션이라는 이름의 인터프리터가 투입됩니다. 인터프리터는 코드를 한 줄씩 빠르게 읽어 내려가며 바이트코드라는 중간 단계의 언어로 일차 변환하여 곧바로 실행을 시작합니다. 그런데 프로그램이 실행되는 동안 똑같은 코드가 여러 번 반복해서 사용되는 경우가 생깁니다. 이때 V8 엔진은 이러한 반복 코드를 뜨거운 코드라고 인식하고, 터보팬이라는 이름의 JIT 컴파일러를 가동합니다.여기서 JIT 컴파일러란 프로그램이 실행되고 있는 그 찰나의 순간에 바이트코드를 해당 컴퓨터의 CPU 아키텍처에 완벽하게 들어맞는 최적화된 기계어로 즉각 변환해 버리는 기술입니다. 덕분에 코드가 반복될수록 실행 속도는 기하급수적으로 빨라지게 되죠. 그렇다면 강의 텍스트에서 언급된 C++가 이해할 수 있는 형태로 통역한다는 표현은 코드를 통째로 C++ 문법으로 바꾼다는 뜻이 아니라, 자바스크립트의 데이터를 C++가 알아들을 수 있는 데이터 규격으로 변환해 준다는 데이터 매핑 과정의 비유적 표현이었습니다.이해를 돕기 위해 메모리 구조라는 컴퓨터 공학적 밑단으로 논리적인 흐름을 이어가 보겠습니다. 자바스크립트와 C++는 메모리에 데이터를 저장하고 관리하는 방식이 완전히 다릅니다. 예를 들어 자바스크립트에서 만든 글자 데이터는 V8 엔진이 관리하는 힙 메모리 영역에 가비지 컬렉터의 꼼꼼한 관리를 받는 독립적인 객체 형태로 존재하지만, C++에서 사용하는 글자 데이터는 메모리상에 연속된 바이트 배열 형태로 날것 그대로 기록되는 등 물리적인 형태 자체가 다릅니다.만약 자바스크립트로 특정 파일을 읽어오라는 명령을 내린다고 가정해 보면, 이 명령을 수행하려면 결국 Node.js 내부의 C++ 프로그램이 나서서 시스템 콜을 통해 운영체제에 파일 읽기 요청을 해야 합니다. 그런데 C++ 코어는 자바스크립트의 V8 힙 메모리에 있는 데이터를 있는 그대로 넘겨받으면 메모리 주소 체계와 타입이 달라 이게 도대체 무슨 형태의 데이터인지 알아듣지 못합니다.바로 이때 V8 엔진이 두 언어 사이의 다리 역할을 하며 다시 한번 개입하게 됩니다. V8 엔진은 단순히 코드를 기계어로 바꾸는 일만 하는 것이 아니라, C++로 작성된 Node.js가 자바스크립트의 메모리에 안전하게 접근할 수 있도록 해주는 V8 API라는 정교한 도구를 제공합니다. 이를 통해 자바스크립트의 데이터를 C++가 안전하게 다룰 수 있는 원시 데이터 형태로 예쁘게 포장해서 넘겨주는 작업을 수행합니다.반대로 C++가 운영체제를 통해 파일을 다 읽고 자바스크립트에게 결과를 돌려줄 때도, C++의 데이터를 V8 API를 이용해 자바스크립트가 이해할 수 있는 객체로 변환하여 힙 메모리에 새롭게 할당해 줍니다. 결과적으로 여기서 말하는 통역은 자바스크립트라는 언어를 C++ 언어로 번역한다는 뜻이 아니라, 두 언어가 서로 데이터를 주고받으며 협업할 수 있도록 메모리 공간과 데이터의 형식, 그리고 규격을 서로 완벽하게 맞춰주는 과정을 의미합니다.두 번째로 자바스크립트로 불가능한 운영체제 작업은 미리 C++로 만들어 놓은 모듈을 호출하는 방식이 아닌가 하는 부분 역시 은송님의 이해가 완벽하게 맞으며 아주 정확히 짚어주셨습니다. Node.js는 단순히 V8 엔진을 켜두기만 하는 껍데기가 아니라, 그 주변에 파일 시스템 제어나 비동기 입출력을 담당하는 강력한 libuv 라이브러리, 그리고 네트워크 통신 등을 할 수 있는 수많은 C++ 기능들을 미리 꽉꽉 채워 넣고 V8 엔진과 단단히 연결해 둔 거대한 C++ 애플리케이션 덩어리입니다. 이렇게 자바스크립트 세상의 함수 호출을 C++ 세상의 함수 실행으로 매핑하여 연결해 둔 고리를 전문 용어로 바인딩이라고 부릅니다.실제 Node.js 내부에서 자바스크립트 코드가 운영체제의 기능을 사용하는 과정은 은송님이 말씀하신 대로 메모리와 프로세스 수준에서 아주 정교한 톱니바퀴처럼 맞물려 흘러가게 됩니다. 가장 먼저 Node.js가 실행되는 환경 구성을 위한 사전 준비 단계가 있습니다. 우리가 터미널에서 실행 명령어를 치는 순간, 운영체제로부터 프로세스에 메모리가 할당되고 Node.js는 내부적으로 파일 읽기나 네트워크 통신 등을 처리할 수 있는 수많은 C++ 함수들을 V8 엔진의 템플릿 기능을 활용해 자바스크립트의 전역 객체에 미리 싹 등록해 둡니다. 누군가 자바스크립트에서 특정 내장 함수를 부르면, 단순히 자바스크립트 로직을 도는 것이 아니라 미리 빌드해둔 C++ 내부의 진짜 함수를 실행하도록 메모리 포인터를 연결해 두는 것입니다.그 다음으로 개발자가 작성한 자바스크립트 코드에서 해당 함수가 호출되는 실행 단계가 이어집니다. 이 과정에서 V8 엔진이 자바스크립트 코드를 기계어로 맹렬히 실행하다가 이 외부 의존성 명령을 마주치면 단번에 상황을 파악하게 됩니다. V8은 이 함수가 자바스크립트 혼자서 샌드박스 내부에서 끙끙대며 할 수 없는 일이고, 사전에 V8 API를 통해 약속된 C++ 콜백 함수를 호출하라는 의미로 인지합니다.상황 파악이 끝나면 자바스크립트의 실행 흐름은 잠시 숨을 고르며 호출 스택에서 새로운 C++ 스택 프레임을 쌓아 올리고, 제어권이 미리 대기 중이던 C++ 코어인 현장 작업 반장으로 완전히 넘어갑니다. 이때 매개변수로 넘긴 데이터들은 앞서 설명한 V8 API 전용 객체에 안전하게 담겨 C++ 쪽으로 전달됩니다.제어권을 넘겨받은 이 C++ 코어가 비로소 윈도우나 맥 같은 운영체제의 가장 깊숙한 커널 영역에 진입하여 시스템 콜을 직접 두드려서 실제 하드디스크의 파일을 읽어오는 등 운영체제와의 진짜 소통을 진행합니다. 이 과정에서 파일 읽기처럼 시간이 오래 걸리는 작업 때문에 전체 시스템이 멈춰 서는 병목 현상을 막기 위해, 든든한 조력자인 libuv가 나서서 자신이 거느린 스레드 풀의 일꾼들에게 작업을 위임하여 백그라운드에서 조용히 처리하도록 돕습니다.마지막으로 C++ 코어와 libuv의 일꾼들이 파일을 성공적으로 다 읽어내면, 이벤트 루프라는 순환 시스템을 통해 완료된 작업의 결과를 대기열에 올려두고, 앞서 설명해 드린 V8 엔진의 정교한 데이터 통역 과정을 역으로 거쳐 그 결과물을 다시 자바스크립트 세상의 콜백 함수로 무사히 돌려보내 주는 것으로 이 장대한 모든 단계가 완벽한 분업 속에 마무리됩니다.글로 설명해 드린 이 전체적인 아키텍처의 흐름과 톱니바퀴 같은 분업 구조를 한눈에 그려보실 수 있도록, 자바스크립트 코드가 실행되어 운영체제에 닿기까지의 과정을 아래에 시각화해 보았습니다.[ 1. JavaScript 세상 ] 개발자의 코드 작성 (예: fs.readFile 호출) │ ▼ [ 2. V8 엔진 (해석, 컴파일, 통역) ] ┌─────────────────────────────────────────────────────────────────┐ │ 1) 파싱(Parsing) : 텍스트 코드를 분석하여 AST(추상 구문 트리) 생성 │ │ 2) 이그니션(Ignition) : AST를 읽어 '바이트코드'로 변환 및 1차 실행 │ │ 3) 터보팬(TurboFan) : 자주 쓰이는 코드를 초고속 '기계어'로 JIT 컴파일 │ │ │ │ * 역할 인지 : "이건 OS 작업이군! C++에 연결된 함수를 부르자!" │ │ * 데이터 맵핑 : 자바스크립트의 데이터를 C++ 호환 데이터로 변환 (V8 API)│ └─────────────────────────────────────────────────────────────────┘ │ (제어권 및 데이터 전달) ▼ [ 3. Node.js C++ Bindings (바인딩) ] 미리 등록된 C++ 함수 실행 자바스크립트 메모리와 C++ 시스템 사이의 단단한 연결 고리 │ ▼ [ 4. Node.js C++ 코어 & libuv (작업 반장 및 일꾼들) ] ┌─────────────────────────────────────────────────────────────────┐ │ * C++ 코어 : 운영체제에 명령을 내리기 위한 시스템 제어 로직 수행 │ │ * libuv : 무거운 파일 읽기 작업을 스레드 풀(Thread Pool)의 │ │ 백그라운드 일꾼에게 위임하여 비동기 처리 │ └─────────────────────────────────────────────────────────────────┘ │ (System Call) ▼ [ 5. 운영체제 (OS / 커널 영역) ] 실제 하드디스크의 물리적 섹터에 접근하여 파일 읽기 완료 │ ▼ (작업 완료 후 역순으로 데이터 반환: 이벤트 루프 -> C++ 코어 -> V8 엔진 맵핑 -> JS 콜백 실행) 요약하자면 은송님의 생각대로 V8 엔진은 자바스크립트를 기계어로 번역하여 실행하는 것이 맞습니다. 그리고 자바스크립트가 브라우저 밖의 세상인 커널, 파일, 네트워크 등과 소통해야 할 때는 Node.js 개발자들이 미리 정교하게 만들어둔 내부 C++ 모듈들을 V8 엔진의 바인딩 연결 통로를 통해 호출하여 시스템 콜을 대신 발생시키도록 일을 시키는 방식으로 동작합니다.강의의 흐름을 쫓아오시면서 프로그래밍 언어의 런타임과 엔진의 경계를 이토록 정확하게 분리해서 유추해 내신 점에 다시 한번 큰 박수를 보냅니다. 컴퓨터 시스템 밑단의 메모리 구조와 운영체제 원리를 파악하는 감각이 매우 훌륭하셔서 이어지는 3강의 동기 비동기 아키텍처나 더 깊은 서버 지식도 아주 수월하게 흡수하실 수 있을 거라 확신합니다.학습하시다가 이처럼 조금이라도 모호하거나 더 깊이 알고 싶은 기술적 원리가 생기면 언제든지 편하게 질문 남겨주세요. 은송님의 깊이 있는 성장을 항상 응원하겠습니다.참고해주세요!
- いいね数
- 0
- コメント数
- 1
- 閲覧数
- 55
Q&A
[실습] 80강 없음
안녕하세요, icoon2님! 우리동네코딩 스튜디오입니다.해당 섹션의 수업 자료와 관련하여 혼선을 드려 죄송합니다.본래 76. 전문성 더하기: 놓치면 아쉬운 관련 기술들 섹션에서는 Better Auth를 비롯하여 Next.js 생태계에서 주목받는 최신 관련 기술들을 소개해 드리려고 준비했었습니다.하지만 현재 Next.js와 관련 라이브러리들의 트렌드가 워낙 빠르게 변화하고 있다 보니, 자칫 고정된 자료를 제공해 드리는 것이 수강생분들께 오히려 버전 불일치나 기술적 혼동을 드릴 여지가 있다고 판단하였습니다.이에 따라 수강생분들께 더욱 정확하고 최신의 정보를 전달하기 위해, 고민 끝에 해당 섹션을 커리큘럼에서 제외하게 되었습니다. 기대를 가지고 기다려 주셨을 텐데 양해를 부탁드립니다.대신 커뮤니티나 다른 섹션을 통해 더 안정적이고 검증된 기술들을 지속적으로 업데이트해 드릴 수 있도록 노력하겠습니다.강의를 수강해 주셔서 진심으로 감사드리며, 학습 중 다른 궁금한 점이 생기면 언제든 질문 남겨주세요!감사합니다.우리동네코딩 스튜디오 드림
- いいね数
- 0
- コメント数
- 2
- 閲覧数
- 74
Q&A
76. 전문성 더하기: 놓치면 아쉬운 관련 기술들 수업 자료가 없어요.
안녕하세요, icoon2님! 우리동네코딩 스튜디오입니다.해당 섹션의 수업 자료와 관련하여 혼선을 드려 죄송합니다.본래 '76. 전문성 더하기: 놓치면 아쉬운 관련 기술들' 섹션에서는 Better Auth를 비롯하여 Next.js 생태계에서 주목받는 최신 관련 기술들을 소개해 드리려고 준비했었습니다.하지만 현재 Next.js와 관련 라이브러리들의 트렌드가 워낙 빠르게 변화하고 있다 보니, 자칫 고정된 자료를 제공해 드리는 것이 수강생분들께 오히려 버전 불일치나 기술적 혼동을 드릴 여지가 있다고 판단하였습니다.이에 따라 수강생분들께 더욱 정확하고 최신의 정보를 전달하기 위해, 고민 끝에 해당 섹션을 커리큘럼에서 제외하게 되었습니다. 기대를 가지고 기다려 주셨을 텐데 양해를 부탁드립니다.대신 커뮤니티나 다른 섹션을 통해 더 안정적이고 검증된 기술들을 지속적으로 업데이트해 드릴 수 있도록 노력하겠습니다.강의를 수강해 주셔서 진심으로 감사드리며, 학습 중 다른 궁금한 점이 생기면 언제든 질문 남겨주세요!감사합니다.우리동네코딩 스튜디오 드림
- いいね数
- 0
- コメント数
- 2
- 閲覧数
- 54
Q&A
[화면 이슈]useReduce 첫걸음 - 행동 중심 상태 변화를 처음 경험하기 화면
안녕하세요, eddie님! 우리동네코딩 스튜디오의 지식공유자입니다.우선 강의 시청 중에 불편을 드려 정말 죄송하다는 말씀부터 올립니다. 제보해 주신 [useReducer 첫걸음] 강의의 특정 구간(00:50 - 01:08) 블랙아웃 현상을 확인하였고, 방금 수정된 영상으로 즉시 교체하여 정상적으로 반영해 두었습니다.꼼꼼하게 확인하고 업로드했어야 하는데, 소중한 시간을 내어 수강하시던 중에 흐름을 끊어 드린 것 같아 마음이 무겁습니다. 이렇게 직접 스크린샷까지 첨부해 오류를 짚어주셔서 정말 큰 도움이 되었고, 진심으로 감사드립니다.🎁 감사의 마음을 담은 작은 선물죄송하고 감사한 마음을 담아, eddie님께 작은 보답을 해드리고 싶습니다.현재 우리동네코딩 스튜디오에서 운영 중인 강의들 중, 평소 수강을 희망하셨던 강의나 관심 있는 주제가 있으시다면 아래 메일로 편하게 연락해 주세요. 확인하는 대로 바로 수강하실 수 있도록 전달해 드리겠습니다.이메일:jeony0535@naver.com앞으로는 더 꼼꼼한 검수를 통해 학습에만 집중하실 수 있는 최고의 환경을 만들도록 노력하겠습니다. 다시 한번 제보해 주셔서 감사합니다!우리동네코딩 스튜디오 드림
- いいね数
- 0
- コメント数
- 2
- 閲覧数
- 27
Q&A
next.js 를 2대이상 실행하는 경우 제공하는 cache 기능들은 어떻게 되나요?
안녕하세요 icoon22님! 질문의 깊이를 보니 단순히 프레임워크의 기능적인 사용법을 넘어 운영체제의 메모리와 네트워크가 얽히는 분산 시스템의 본질까지 아주 깊게 고민하고 계신 것이 느껴지며 클론 코딩 수준을 벗어나 실제 프로덕션 레벨의 아키텍처를 설계하려는 모습이 정말 훌륭합니다. Next.js를 마이크로서비스 아키텍처 환경에서 프론트엔드를 위한 백엔드 즉 BFF로 활용하며 트래픽을 감당하기 위해 클라우드 환경인 AWS ECS나 Kubernetes 등에서 서버를 두세 대 이상으로 스케일 아웃하여 늘리게 되면 가장 먼저 캐시의 파편화라는 문제에 직면하게 됩니다. 기본적으로 Next.js의 내장 데이터 캐시는 해당 서버가 떠 있는 파드나 인스턴스의 로컬 파일 시스템 및 메모리에 저장되는 방식으로 동작하기 때문입니다. 예를 들어 대규모 이커머스 서비스의 특가 이벤트 상황을 실무 시나리오로 가정해 보겠습니다. 사용자가 특정 상품의 상세 페이지를 요청하면 로드밸런서가 이 요청을 1번 서버로 보내고 1번 서버는 백엔드에서 데이터를 가져와 자신의 로컬에 캐싱합니다. 그런데 1분 뒤 다른 사용자가 똑같은 상품을 요청했을 때 라운드로빈 방식 등에 의해 2번 서버로 요청이 가면 2번 서버의 로컬에는 해당 캐시가 없으므로 또다시 백엔드 API를 찔러 데이터를 가져와야 합니다. 결과적으로 BFF를 도입하여 백엔드 부하를 줄이려 했음에도 인스턴스 개수만큼 백엔드 호출이 중복 발생하고 심지어 서버마다 갱신 주기가 어긋나 데이터 불일치까지 발생합니다.현재 프로덕션 환경에서는 이 문제를 해결하기 위해 커스텀 캐시 핸들러를 설정하여 로컬 캐시가 아닌 Redis 같은 중앙 집중형 메모리 데이터베이스를 바라보도록 아키텍처를 변경해야 합니다. 이를 구현하기 위해 아래와 같이 캐시 핸들러 파일을 작성하게 됩니다.// cache-handler.mjs (2026년 Next.js 분산 Redis 캐시 핸들러 아키텍처 예시) import { CacheHandler } from '@neshca/cache-handler'; import createLruCache from '@neshca/cache-handler/local-lru'; import createRedisCache from '@neshca/cache-handler/redis-strings'; import { createClient } from 'redis'; CacheHandler.onCreation(async () => { let client; try { // 다중 인스턴스가 공통으로 바라보는 Redis 클러스터에 연결합니다. client = createClient({ url: process.env.REDIS_URL }); await client.connect(); } catch (error) { console.warn('Redis 연결 실패, 로컬 캐시로 안전하게 폴백합니다.', error); } return { handlers: [ client ? createRedisCache({ client }) : null, createLruCache(), // 단일 진실 공급원인 Redis에 장애가 생겼을 때를 대비한 방어 로직입니다. ], }; }); export default CacheHandler; 이렇게 구성하고 설정 파일에 핸들러를 등록하면 1번 서버가 Redis에 캐시를 구워두었을 때 2번 서버와 3번 서버도 Redis를 조회하여 단일 진실 공급원으로서의 캐시를 온전히 공유할 수 있게 됩니다. 이러한 캐시 아키텍처의 변화는 과거 단순한 데이터 전달 통로였던 BFF를 초고속 캐시 레이어로 진화시킨 캐싱 혁명과 맞닿아 있습니다. Next.js 16 환경에서는 React 19의 서버 컴포넌트와 결합된 데이터 캐시가 변경이 잦지 않은 백엔드의 응답을 BFF 단에서 완전히 정적 상태로 얼려버리며 revalidateTag 함수를 통해 수십 개의 마이크로서비스 중 데이터가 변경된 특정 서비스의 캐시만 무효화하는 세밀한 제어가 가능합니다.그런데 서버에서 이렇게 적극적으로 캐싱을 하는 상황에서 클라이언트에도 TanStack Query라는 강력한 캐시 도구가 존재한다면 어느 캐시가 최신 데이터인가를 두고 충돌이 발생할 수밖에 없으므로 이 둘의 역할을 명확히 분리하고 동기화하는 것이 핵심입니다. 사용자가 좋아요를 누르거나 리뷰를 수정하는 등의 상태 변경 액션을 취할 때 서버의 캐시와 클라이언트의 캐시를 순차적으로 날려주는 파이프라인을 구축해야 이전 데이터가 다시 나타나는 고스트 데이터 버그를 막을 수 있습니다. 2026년 실무에서는 Next.js의 서버 액션과 TanStack Query의 v5 이상을 결합하여 이 동기화를 아래와 같이 완벽하게 처리합니다.// 1. Server Action (actions/review.ts) 'use server'; import { revalidateTag } from 'next/cache'; export async function updateReviewAction(reviewId: string, content: string) { // 마이크로서비스 백엔드의 리뷰 수정 API를 호출하여 실제 데이터를 변경합니다. const res = await fetch(`http://api.internal/reviews/${reviewId}`, { method: 'PUT', body: JSON.stringify({ content }), }); if (!res.ok) throw new Error('리뷰 수정 실패'); // Redis에 저장된 해당 분산 캐시를 즉시 무효화하여 모든 BFF 인스턴스가 최신 상태를 알게 합니다. revalidateTag(`review-${reviewId}`); return { success: true }; } // 2. Client Component (components/ReviewEditor.tsx) 'use client'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { updateReviewAction } from '@/actions/review'; export default function ReviewEditor({ reviewId }: { reviewId: string }) { const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (content: string) => updateReviewAction(reviewId, content), onSuccess: () => { // 서버 측 Redis 캐시가 날아간 성공 응답을 받은 직후, 클라이언트의 캐시도 날려줍니다. // 이를 통해 즉시 가장 최신의 서버 상태를 다시 Fetching 하여 완벽한 동기화를 이룹니다. queryClient.invalidateQueries({ queryKey: ['review', reviewId] }); } }); return ( // 사용자 UI 및 이벤트 핸들러 바인딩... mutation.mutate('업데이트된 리뷰 내용')}>저장 ); } 이 코드를 통해 서버 액션에서 서버 캐시를 먼저 무효화하고 성공 응답 후 클라이언트에서 TanStack Query의 캐시를 무효화하여 순차적으로 가장 최신화된 데이터를 다시 받아오게 만드는 것이 완벽한 동기화 파이프라인입니다. 추가적으로 다중 인스턴스 BFF 환경을 실제 클라우드에 구축하실 때 발생할 수 있는 장애 상황들을 고려하여 캐시 스탬피드 혹은 썬더링 허드라고 불리는 현상을 방지하는 것을 반드시 짚고 넘어가셔야 합니다. 무거운 통계 데이터 캐시가 만료된 찰나의 순간에 대규모 트래픽이 몰리면 3대의 BFF 인스턴스가 동시에 캐시가 없음을 인지하고 백엔드로 쿼리를 무수히 날려 데이터베이스가 뻗어버릴 수 있으므로 백그라운드에서 캐시를 갱신하는 패턴을 활용하고 Redis 분산 락 기술을 도입하여 여러 인스턴스 중 단 하나의 인스턴스만이 갱신 작업을 수행하도록 제어해야 합니다. 또한 세션과 인증 상태의 무상태성을 철저히 유지하여 유저가 1번 서버에서 로그인한 후 로드밸런서에 의해 2번 서버로 접속했을 때도 로그인 상태가 유지되도록 JWT 기반의 무상태 인증을 구현하거나 세션 스토어 자체를 별도의 Redis 클러스터로 분리하여 관리해야 세션 단절 이슈를 막을 수 있습니다. 마지막으로 수많은 마이크로서비스가 얽힌 환경에서는 유저가 에러를 겪었을 때 어느 구간에서 터진 것인지 단순 로그로는 추적이 불가능하므로 클라이언트 최초 요청 시 고유한 트레이스 ID를 헤더에 붙여서 보내고 BFF가 이를 로그에 남김과 동시에 백엔드 API로 전달하는 분산 트레이싱 릴레이 시스템을 구축해야만 모니터링 도구에서 전체 요청의 흐름과 병목 구간을 시각적으로 한눈에 파악할 수 있게 됩니다.이러한 아키텍처적인 고민을 더욱 깊이 있게 발전시키기 위해 앞으로 몇 가지 컴퓨터 공학의 근본적인 주제들을 추가로 공부해보시는 것도 큰 도움이 될 것 같습니다. 웹 프레임워크의 화려한 기능 이면에 있는 운영체제 수준의 스레드 관리나 네트워크 프로토콜의 본질을 파고들면 시야가 한층 넓어집니다. 예를 들어 마이크로서비스 간의 통신에서 REST를 넘어선 gRPC의 원리나, 분산 환경에서의 데이터베이스 트랜잭션 동기화 모델을 학습하시면 지금 직면한 문제들을 훨씬 더 구조적으로 해결하실 수 있습니다. 나아가 최근 급부상하는 인공지능과 대규모 언어 모델을 이러한 인프라에 어떻게 접목할지 고민해보시는 것도 추천해 드립니다. 가령 파인튜닝된 경량 모델을 활용해 BFF 레이어에서 들어오는 트래픽의 의도를 지능적으로 라우팅하거나, 모델 예측을 통해 캐시 만료 시점을 동적으로 조절하는 등 차세대 엔지니어링의 아주 흥미로운 가능성을 탐구해보실 수 있을 것입니다.icoon22님 이처럼 최신 프레임워크의 편리한 마법 뒤에는 탄탄한 컴퓨터 공학적 기초가 자리 잡고 있습니다. 지금처럼 근본적인 동작 원리와 아키텍처의 한계를 묻고 탐구하는 학습 태도를 유지하신다면 어떤 복잡한 대규모 환경에서도 흔들리지 않는 견고한 프로덕션 시스템을 설계하실 수 있을 것입니다. 파이팅입니다!
- いいね数
- 0
- コメント数
- 2
- 閲覧数
- 70
Q&A
17강 zustand store 서버에서 생성
안녕하세요 byeong님! 많은 분들이 "어차피 서버에서 가져온 데이터라면 TanStack Query만 사용하면 되지, 왜 굳이 Zustand에 초기값으로 주입하는 번거로운 과정을 거쳐야 할까?"라는 의문을 가지십니다. 핵심부터 말씀드리자면, 서버 데이터를 '단순히 보여주기만' 할 때는 TanStack Query가 정답입니다. 하지만 서버 데이터를 초기 '씨앗(Seed)'으로 삼아 클라이언트에서 복잡한 상호작용(예: 폼 입력, 임시 상태 저장 등)을 처리해야 할 때는, 이 데이터를 Zustand로 넘겨받아 관리하는 것이 훨씬 유리하고 안전하기 때문입니다. 상태의 성격이 '서버의 스냅샷'에서 '사용자의 상호작용'으로 넘어가는 경계선인 셈이죠.이를 실제 코드로 구현할 때 가장 먼저 주의해야 할 점은 Next.js App Router 환경에서의 Zustand 스토어 스코프(Scope)입니다. React 렌더링이 일어나는 서버 환경(Node.js)은 여러 요청이 동일한 서버 인스턴스를 공유합니다. 따라서 일반적인 싱글 페이지 애플리케이션(SPA)처럼 컴포넌트 외부에 전역 스토어 변수를 선언하게 되면, 'A 유저의 초기값이 B 유저의 화면에 노출되는' 치명적인 보안 버그가 발생합니다.이를 방지하려면 반드시 Context API를 활용하여 렌더링 사이클(혹은 사용자 요청)마다 독립적인 스토어 인스턴스를 새로 생성해야 합니다. 아래와 같이 바닐라 Zustand 스토어를 생성하는 팩토리 함수와, 이를 하위 컴포넌트에 공급하는 Provider 패턴을 작성하는 것이 그 표준적인 해결책입니다.import { createStore } from 'zustand'; import { createContext, useRef, useContext, ReactNode } from 'react'; import { useStore } from 'zustand'; // 1. 상태 타입 및 스토어 생성 팩토리 함수 정의 (전역 스토어가 아님에 주의합니다) interface ProfileState { name: string; setName: (name: string) => void; } export const createProfileStore = (initialName: string) => { return createStore()((set) => ({ name: initialName, setName: (name) => set({ name }), })); }; export type ProfileStore = ReturnType; // 2. React Context 및 Provider 생성 export const ProfileContext = createContext(null); export function ProfileProvider({ children, initialName }: { children: ReactNode, initialName: string }) { const storeRef = useRef(null); if (!storeRef.current) { // 최초 렌더링 시에만 서버로부터 받은 초기값을 주입하여 스토어 인스턴스를 생성합니다. storeRef.current = createProfileStore(initialName); } return ( {children} ); } // 3. 컴포넌트에서 스토어를 쉽게 사용하기 위한 커스텀 훅 export function useProfileContext(selector: (state: ProfileState) => T): T { const store = useContext(ProfileContext); if (!store) throw new Error('ProfileProvider가 필요합니다.'); return useStore(store, selector); } 이렇게 안전한 스토어 공급망을 구축했다면, 이제 복잡한 다중 스텝 폼 시나리오를 서버 컴포넌트에서 안전하게 시작할 수 있습니다. 사용자가 프로필 수정 페이지에 진입하면, 서버 컴포넌트는 데이터베이스나 외부 API를 통해 유저의 기존 프로필 정보를 페치(Fetch)합니다. 그리고 이 데이터를 앞서 만든 Provider의 initialName으로 밀어 넣어, 클라이언트 상태의 초기값으로 설정하는 역할을 수행합니다.import { ProfileProvider } from '@/components/ProfileProvider'; import ProfileForm from '@/components/ProfileForm'; export default async function ProfilePage() { // 서버에서 초기 데이터를 Fetching 합니다. // 이 단계에서는 TanStack Query의 prefetchQuery와 Hydration Boundary를 조합하여 캐시를 채울 수도 있습니다. const response = await fetch('https://api.example.com/user/profile'); const initialData = await response.json(); return ( // 서버에서 가져온 초기값을 Provider에 주입하여 클라이언트 상태의 '씨앗'으로 삼습니다. ); } 마지막으로 클라이언트, 즉 Zustand의 역할은 사용자가 입력칸을 채우고 지우는 모든 과정을 API 통신 없이 철저히 브라우저 메모리 내에서만 가볍고 빠르게 관리하는 것입니다.사용자가 타이핑을 할 때마다 TanStack Query의 캐시를 억지로 업데이트하는 것은 '서버 상태 동기화'라는 본래 목적과 맞지 않으며, 불필요한 성능 저하를 유발합니다. 따라서 아래 코드처럼 Zustand를 통해 즉각적인 UI 반응성을 확보하고, 모든 수정이 끝난 후 사용자가 '최종 저장' 버튼을 누를 때 TanStack Query의 Mutation을 통해 한 번에 서버로 전송하는 것이 바람직합니다.'use client'; import { useProfileContext } from '@/components/ProfileProvider'; import { useMutation } from '@tanstack/react-query'; export default function ProfileForm() { // 서버와의 동기화는 끊어지고, 오직 클라이언트 메모리에서만 상태를 조작합니다. const name = useProfileContext(state => state.name); const setName = useProfileContext(state => state.setName); // 사용자의 상호작용이 모두 끝난 후 최종 결과물만 서버에 동기화하기 위한 Mutation입니다. const updateProfileMutation = useMutation({ mutationFn: (newName: string) => fetch('/api/user/profile', { method: 'POST', body: JSON.stringify({ name: newName }) }) }); return ( { e.preventDefault(); updateProfileMutation.mutate(name); // 폼이 제출될 때 비로소 서버와 다시 소통합니다. }}> setName(e.target.value)} placeholder="이름을 입력하세요" /> 최종 저장 ); } 이처럼 상품 필터링의 가격 슬라이더나 이미지 크롭 툴의 캔버스 상태처럼, 초당 수십 번씩 바뀌는 순수 UI 상태는 Zustand가 가볍고 빠르게 처리하도록 맡겨야 합니다. 현업에서 상태 관리를 설계할 때 "이 상태의 주인이 누구인가?"를 스스로에게 질문해 보세요. 내 화면에서 내가 조작하기 위한 임시 상태라면, 초기 데이터만 서버에서 받고 클라이언트로 그 통제권을 완전히 분리하는 것이 맞습니다.또한, 단일 진실 공급원(Single Source of Truth)을 훼손하지 않기 위해 Zustand에 서버 상태를 계속 복사하여 병렬로 유지하려는 안티 패턴(Anti-pattern)을 피해야 합니다. 초기 렌더링 시점에 딱 한 번만 주입한 후, 해당 데이터의 생사결정권을 완전히 Zustand가 가지도록 독립시키는 것이 핵심입니다.byeong님, 이 답변에 포함된 아키텍처 패턴이 실무의 그림을 명확히 그리시는 데 도움이 되셨기를 바랍니다. 상태 관리의 경계를 명확히 구분하는 이 감각은 앞으로 복잡한 웹 애플리케이션을 견고하게 설계하실 때 아주 강력한 무기가 될 것입니다!참고해주세요!
- いいね数
- 1
- コメント数
- 1
- 閲覧数
- 51




