안녕하세요, 우리동네코딩 스튜디오에 오신 것을 환영합니다!
우리동네코딩 스튜디오는 카네기 멜론, 워싱턴, 토론토, 워터루 등 북미의 주요 대학에서 컴퓨터공학을 전공하고, 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.
강의
수강평
- "AI 딸깍의 시대" 원리로 돌파하는 Node.js와 CS Part 2 - 스트림 아키텍처와 하드웨어 통제기
- React 마스터 클래스: Part 2 - 미션으로 완성하는 고성능 훅과 실전 아키텍처
- Next.js 마스터 클래스: Part 1 미션으로 배우는 App Router의 본질과 렌더링 설계
- 제대로 배우는 HTML + CSS: 입문부터 실전까지 완벽 정복 Part1 - [기초편]
- "AI 딸깍의 시대" 원리로 돌파하는 Node.js와 CS Part1 - V8과 코어 해체기
게시글
질문&답변
강의가 누락된것 같습니다.
안녕하세요 bridge130님, 문의해 주셔서 감사합니다!먼저 강의 시청에 불편을 드려 진심으로 죄송합니다. 말씀해 주신 섹션 3의 "Node.js 설치와 첫 코드 실행" 강의가 누락된 부분을 확인하였으며, 수강하시는 데 지장이 없도록 빠르게 보완하여 업로드해 두겠습니다.죄송한 마음을 담아, 혹시 저희 우리동네코딩스튜디오에서 추가로 수강하고 싶으신 강의가 있으시다면 사용하실 수 있는 할인 쿠폰을 챙겨드리고자 합니다. 필요한 강의가 있으실 경우 언제든 jeony0535@naver.com 이메일로 연락해 주시면 바로 확인하여 쿠폰을 전달해 드리겠습니다.다시 한번 이용에 불편을 드려 죄송하며, 완강까지 좋은 강의로 보답하겠습니다.감사합니다.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 13
질문&답변
용어 발음법이 계속 바뀌는 것 같은데 이런 부분들 개선이 가능할까요...?
안녕하세요, 김상민 수강생님! 우선 저희 강의를 흥미롭게 들어주시고, 깊이 있는 주제에 대해 좋은 평가를 남겨주셔서 진심으로 감사드립니다.동시에 강의 내에서 libuv 라이브러리의 발음이 통일되지 못하고 명칭이 섞여 나와 학습하시는 데 불편함을 느끼게 해 드린 점 정말 죄송합니다. 이번 강의는 비개발자 편집자분과 함께 최종 영상 편집 및 추가 녹음 작업을 진행하는 과정이 있었습니다. 그 과정에서 중간중간 급하게 수정을 요청하고 반영하느라 제가 정확한 가이드를 제대로 전달하지 못해 명칭이 혼용되는 문제가 발생했습니다. 세심하게 체크하지 못한 모두 제 불찰이며, 상민 님께 강의의 몰입도를 떨어뜨리게 된 점 다시 한번 고개 숙여 사과드립니다.참고로 말씀해 주신 libuv 공식 명칭의 경우, 제가 알고 있는 한 '립유브이'가 가장 정확한 발음이 맞습니다. 앞으로는 이런 디테일한 부분까지 꼼꼼히 챙겨서 강의의 퀄리티를 더욱 높일 수 있도록 확실하게 보완하겠습니다. 학습하시면서 조심스러우셨을 텐데도 강의를 아끼시는 마음으로 이렇게 진심 어린 피드백을 남겨주셔서 정말 감사드립니다. 상민 님의 소중한 의견 덕분에 부족한 부분을 정확히 인지하고 고쳐나갈 수 있게 되었습니다.제가 이 감사한 마음을 조금이나마 표현하고 싶은데 다른 방법이 마땅치 않아, 혹시 괜찮으시다면 jeony0535@naver.com 이메일로 간단히 연락을 주실 수 있으실까요? 연락을 주시면 감사의 뜻을 담아 저희 강의의 전 강의 최대 할인율 쿠폰을 전달해 드리고자 합니다. 보내주신 의견을 발판 삼아 앞으로 더 퀄리티 높은 강의로 보답하겠습니다. 다시 한 번 불편을 드린 점 사과드리겠습니다. 감사합니다!
- 좋아요수
- 0
- 댓글수
- 1
- 조회수
- 27
질문&답변
call stack 표현이 잘못표현된것이 아닌가요?
안녕하세요 hanumoka님, 남겨주신 질문에 대해 답변드립니다.먼저 결론부터 말씀드리면 hanumoka님께서 짚어주신 내용이 100% 맞습니다. 자바스크립트 코드가 처음 실행될 때부터 마지막 동기 코드가 끝날 때까지 콜 스택은 엄밀히 말해 완전히 비어있는 상태가 될 수 없거든요. 코드가 처음 실행되면 가장 먼저 콜 스택 바닥에 전역 실행 컨텍스트가 깔리게 되는데, 에러 스택 트레이스에서 주로 main이나 anonymous로 표기되는 바로 그 부분입니다. 이 전역 실행 컨텍스트는 전체 스크립트의 동기적 실행이 모두 끝날 때까지 스택에 계속 남아있게 되죠.그래서 강의 노트의 두 번째 단계에서 콜 스택이 비어있다고 표현했던 것은, setTimeout이나 Promise 같은 개별 비동기 함수들이 블로킹을 일으키지 않고 호출 직후 스택에서 즉시 빠져나간다는 점을 강조하려다 보니 전역 컨텍스트를 의도적으로 생략하고 조금 단순화해서 설명한 부분이었습니다. 이 부분을 컴퓨터 공학적으로 엄밀하게 다시 살펴보면, hanumoka님 말씀대로 개별 비동기 함수들은 지시를 마치고 스택에서 빠져나가지만 전역 컨텍스트인 main은 여전히 콜 스택을 차지하고 있는 상태가 맞습니다.Plaintext[Step 2] setTimeout과 Promise 등록 직후 (엄밀한 상태) ----------------------------------------------------- * 개별 비동기 함수들은 지시를 마치고 스택에서 Pop 되지만, 전역 컨텍스트는 남아있음. [ Call Stack ] : [ Global Execution Context (main) ] 그리고 나서 이후에 console.log("D")까지 모두 실행되고 나서야 비로소 전역 실행 컨텍스트인 main이 스택에서 빠져나가게 되고, 바로 이때 처음으로 콜 스택이 완전히 비워진 상태가 되는 것이죠.이 과정을 조금 더 깊이 이해하시는 데 도움이 될까 해서 V8 엔진의 내부 개념인 'Microtask Checkpoint'에 대한 내용도 살짝 덧붙여 드릴게요. 콜 스택에서 전역 컨텍스트가 빠져나가며 스택이 완전히 텅 비는 바로 그 찰나의 순간에, 자바스크립트 엔진은 마이크로태스크 체크포인트를 발생시킵니다. 이 체크포인트가 발생하면 엔진은 이벤트 루프의 일반 정거장(타이머 등)으로 넘어가기 전에, 무조건 VIP 결재함인 마이크로태스크 큐부터 확인해서 대기 중인 모든 프로미스 콜백을 콜 스택으로 끌어올려 실행하게 됩니다. 즉, 강의에서 말씀드렸던 이벤트 루프의 'VIP 새치기'가 최초로 발동하는 트리거 자체가 바로 동기 코드의 실행이 모두 끝나고 main 컨텍스트가 종료되면서 콜 스택이 비워지는 순간인 것입니다.강의에서 추상적으로 표현하다 보니 생길 수 있었던 맹점을 이렇게 정확하게 짚어주셔서 정말 감사드립니다. 엔진의 내부 동작을 실행 컨텍스트 단위까지 추적하시는 시야를 가지신 만큼, 앞으로 백엔드 병목 분석이나 디버깅을 하실 때도 분명 큰 강점을 발휘하실 거라 생각해요.끝으로 혹시 저희 교육 과정 중에 추가로 수강을 희망하시는 강의가 있다면 문의 이메일인 jeony0535@naver.com으로 편하게 연락해 주십시오. 확인 후 즉시 할인 쿠폰을 전달해 드리겠습니다. 다시 한번 귀한 시간과 마음 내어 주셔서 감사합니다. hanumoka님의 성장을 진심으로 응원하겠습니다! :D
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 76
질문&답변
stopPropagation()에 대해서 질문 있습니다.
안녕하세요 Eddie님! 남겨주신 질문을 보니 DOM 이벤트 전파 흐름의 핵심을 아주 깊이 있게 파고들고 계시네요. 질문해주신 내용 중 용어에 대한 약간의 오해가 있으셨던 것 같아 이 부분부터 명확히 짚어드리고, 어떻게 동작하는지 상세히 설명해 드리겠습니다.강의에서 말씀드린 상위로 전파 차단이라는 표현은 캡처링이 아니라 버블링을 의미하는 것입니다. 브라우저의 이벤트 흐름은 최상단 부모에서 실제 클릭된 타겟 요소로 파고 내려가는 캡처링 단계와, 타겟에서 다시 부모 요소들을 타고 위로 올라가는 버블링 단계로 나뉩니다. 즉, 부모 요소라는 상위로 이벤트가 전파된다는 것은 곧 버블링을 뜻합니다. 여기에 딥다이브를 좋아하시는 Eddie님을 위해 아주 사소한 팁을 하나 더 얹어드리자면, DOM 이벤트 흐름은 엄밀히 말해 최상위에서 내려오는 캡처링 단계, 실제 클릭한 요소에 도달하는 타겟 단계, 그리고 다시 위로 올라가는 버블링 단계라는 세 가지 과정으로 이루어집니다. 비록 흐름의 방향성을 설명하기 위해 타겟 단계를 생략하고 말씀드렸지만, 이 세 단계를 모두 알아두시면 이해가 훨씬 깊어지실 겁니다.Eddie님이 강의를 모두 들으신 후 이해하신 내용이 아주 정확합니다. stopPropagation 함수는 단순히 버블링만 막는 것도 아니고 캡처링만 막는 것도 아닙니다. 이 함수의 정확한 동작 원리는, 현재 이벤트가 캡처링 단계에 있든 버블링 단계에 있든 상관없이 이 함수가 호출된 바로 그 시점부터 더 이상의 이벤트 전파를 완전히 끊어버리는 것입니다.만약 캡처링 단계의 이벤트 리스너에서 이 메서드를 호출하면 어떻게 될지 아래 코드로 살펴보겠습니다.// 부모 요소의 캡처링 단계에서 전파를 막는 예시입니다. parentElement.addEventListener('click', (e) => { e.stopPropagation(); console.log('부모 캡처링 단계'); }, { capture: true }); // 자식 요소의 이벤트 리스너입니다. childElement.addEventListener('click', (e) => { console.log('자식 클릭 이벤트'); }); 위의 예시처럼 캡처링 옵션을 켠 부모 요소에서 stopPropagation을 호출하게 되면, 이벤트가 타겟인 자식 요소까지 채 내려가기도 전에 흐름이 멈춰버리게 되어 자식 요소의 클릭 이벤트는 절대 실행되지 않습니다. 반대로 우리가 흔히 작성하는 기본 이벤트 리스너는 버블링 단계에서 동작하므로, 자식 요소에서 호출하게 되면 부모 요소로 클릭 이벤트가 올라가는 상위 전파를 막아주게 되는 것입니다.따라서 강의에서 상위로 전파 차단이라고 설명해 드린 것은, 실무에서 우리가 작성하는 대부분의 이벤트 핸들러가 기본적으로 버블링 단계에서 동작하기 때문에 가장 흔하게 마주하는 맥락을 강조해서 말씀드린 것입니다. 실제로 실무에서는 모달 창이나 팝업 메뉴를 구현할 때 이 방식을 아주 유용하게 활용합니다. 예를 들어, 어두운 배경 영역을 클릭하면 모달이 닫히도록 구현해 두었는데 사용자가 모달 내부의 버튼이나 폼을 클릭했을 때도 이벤트가 배경으로 버블링되어 모달이 의도치 않게 닫혀버리는 문제가 발생할 수 있습니다. 이때 모달 내부 요소에 stopPropagation을 걸어주면, 클릭 이벤트가 배경으로 전파되는 것을 완벽하게 막아주어 이러한 이슈를 깔끔하게 해결할 수 있습니다.하지만 Eddie님이 파악하신 것처럼 이벤트의 위아래 방향에 국한되지 않고, 이벤트 흐름 자체를 그 자리에서 정지시킨다는 것이 이 메서드에 대한 가장 완벽한 이해입니다. 이렇게 꼼꼼하게 강의를 수강하시고 핵심을 꿰뚫는 좋은 질문 남겨주셔서 진심으로 감사드립니다. 추가로 우리동네코딩스튜디오에서 관심 있는 강의가 있으시다면 jeony0535@naver.com 이메일로 연락해 주시면 할인 쿠폰을 전달해 드리겠습니다!
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 52
질문&답변
27강 Context내 RSC 사용 관련 문의
안녕하세요 Eden님! 남겨주신 질문을 보니 React Server Component, 즉 RSC의 아키텍처와 Composition 패턴에 대해 깊이 있게 이해하고 계시네요. 결론부터 말씀드리면 Eden님이 알고 계신 내용이 맞습니다. Client Component인 Provider로 감싸더라도 children prop을 통해 Composition 형태로 넘기면, 해당 children으로 전달된 컴포넌트들은 Server Component의 속성을 그대로 유지하며 렌더링 최적화 기회를 잃지 않습니다.질문해주신 것처럼 실습 자료의 "정밀 아키텍처 분석"에 적힌 "App.tsx를 10개의 Provider로 감싸는 순간 그 아래의 모든 컴포넌트는 서버 컴포넌트로서의 최적화 기회를 잃게 됩니다"라는 설명은, Eden님의 예상대로 Composition Pattern을 적용하지 않고 내부에서 직접 import하는 일반적이고 잘못된 안티 패턴을 전제로 설명한 것이 맞습니다. 더불어 패턴을 올바르게 사용하더라도 피할 수 없는 Context API의 근본적인 한계와 Client Boundary의 연쇄적인 오염을 함께 경고하기 위한 맥락이었습니다. 이 부분이 실무에서도 자주 발생하는 아키텍처적 실수이므로 왜 그런 표현이 나왔는지 이면의 동작 원리와 실무 예제, 그리고 추가적인 응용 방안까지 상세히 풀어드리겠습니다.RSC 아키텍처에서 Server Component를 Client Component로 만드는 조건은 단순히 물리적으로 내부에 위치하는가에 달려있는 것이 아니라, 어떻게 import 되고 렌더링 되는가에 달려 있습니다. 이 과정에서 가장 흔히 하는 실수는 Client Component 내부에서 Server Component를 직접 import하여 렌더링하는 경우입니다.'use client'; import { ThemeContext } from './context'; import Header from './Header'; // Server Component로 의도했으나 Client로 강제 변환됨 import MainContent from './MainContent'; // 마찬가지로 Client 영역으로 편입됨 export default function GlobalProvider() { return ( ); } 위의 예시 코드처럼 'use client'가 선언된 파일 내부에서 직접 import된 컴포넌트들은 트리 구조상 Client Boundary 안으로 끌려 들어가게 되어, 서버에서 렌더링될 기회를 잃고 전부 클라이언트 번들에 포함됩니다. 앞서 말씀드린 강의에서 경계했던, Composition Pattern을 적용하지 않은 가장 위험한 패턴이 바로 이것입니다. 반면에 Eden님이 말씀하신 대로 Next.js의 Root Layout에서 흔히 사용하는 children 패턴을 쓰면 이 문제를 우회할 수 있습니다.// Providers.tsx 'use client'; export default function Providers({ children }: { children: React.ReactNode }) { return {children}; } // layout.tsx (Server Component) import Providers from './Providers'; import Header from './Header'; // Server Component 유지 export default function RootLayout({ children }) { return ( {children} {/* Server Component 유지 */} ); } 이 올바른 패턴이 동작하는 핵심 원리는 평가, 즉 Evaluation 시점의 차이에 있습니다. Header와 children은 Server Component인 layout.tsx에서 미리 평가되어 직렬화된 React 객체(RSC Payload) 형태로 Providers의 prop으로 전달됩니다. 클라이언트 입장에서는 컴포넌트 함수를 직접 실행하는 것이 아니라, 서버가 전달해 준 렌더링 결과물을 그대로 화면에 얹기만 하므로 RSC의 이점을 유지할 수 있는 것입니다.이렇게 Composition 패턴을 쓰면 RSC가 유지됨에도 불구하고 Root 레벨을 Provider로 감싸는 것을 지양하라고 강조했던 더 깊은 이유가 있는데, 이는 바로 데이터 소비의 문제 때문입니다. RSC는 서버에서 실행되므로 브라우저의 API나 상태를 가질 수 없으며, 트리 하단 깊숙한 곳에 있는 Server Component가 Root에 정의된 Context 값을 읽기 위해 useContext를 호출하는 순간 에러가 발생합니다. 결국 이를 해결하려면 해당 컴포넌트를 불가피하게 'use client'로 전환해야 하는데, Root에 거대한 Provider를 두면 그 하위에 있는 수많은 컴포넌트들이 데이터를 읽기 위해 연쇄적으로 Client Component로 변질될 위험성, 즉 Client Boundary가 확장될 가능성이 커집니다.물론 다크모드나 로그인 상태처럼 불가피하게 Root Layout을 감싸야 하는 전역 Provider도 존재하지만, 강의에서 경계한 것은 습관적인 Root Provider 래핑을 지양하자는 의미였습니다. 이러한 문제를 방지하고 Server Component의 이점을 극대화하기 위해 실무에서는 크게 두 가지 전략을 사용합니다.첫 번째 전략은 Provider를 상태가 필요한 트리 최하단으로 밀어내는 것입니다. 전역 상태가 아니라 특정 도메인이나 UI 영역에서만 필요한 상태라면, 필요 없는 페이지까지 상태에 노출되도록 최상단 Layout을 감싸는 것이 아니라 해당 상태를 소비하는 가장 가까운 노드에서 Provider를 감싸야 합니다. 예를 들어 장바구니 기능이 필요한 특정 페이지나 레이아웃에서만 CartProvider로 랩핑하는 식입니다.두 번째 전략은 Server Component에서 Client Component로 상태를 주입하는 Hydration 패턴을 사용하는 것입니다. RSC의 강점을 살리면서 클라이언트 상태 관리를 매끄럽게 하려면, 데이터 페칭은 서버에서 처리하고 그 초기값을 Zustand나 Jotai 같은 클라이언트 스토어에 주입하는 방식을 많이 사용합니다.// Server Component import { fetchUserSession } from '@/lib/api'; import ClientStoreProvider from './ClientStoreProvider'; export default async function UserDashboard() { const sessionData = await fetchUserSession(); return ( ); } 이 예시처럼 서버에서 데이터 패칭을 진행하여 초기 상태를 Client Provider에 Prop으로 전달하게 되면, 보안에 민감한 데이터나 무거운 페칭 로직은 서버에 남겨두고 클라이언트에서는 가벼운 인터랙션 상태만 관리하는 이상적인 분리가 가능해집니다.이와 관련하여 실무에서 RSC 아키텍처의 장점을 극대화하기 위해 적극적으로 권장하는 접근 방식과 라이브러리도 몇 가지 덧붙여 드립니다. 우선 서버와 클라이언트의 상태 동기화를 위해 복잡한 데이터 패칭과 캐싱이 필요한 경우에는 TanStack Query(구 React Query)의 HydrationBoundary 패턴을 추천합니다. 서버에서 데이터를 프리패치하여 클라이언트로 직렬화해 넘겨주면, RSC의 초기 렌더링 이점을 챙기면서도 클라이언트에서의 데이터 동기화를 매끄럽게 처리할 수 있습니다. 다음으로 경량화된 클라이언트 전역 상태를 다룰 때는 앞서 예시로 언급한 Zustand나 Jotai를 활용하는 것이 좋습니다. 이 라이브러리들은 React Context API 특유의 불필요한 리렌더링 이슈를 피할 수 있고, 트리 외부에서 상태를 관리할 수 있어 컴포넌트를 습관적으로 'use client'로 만드는 일을 줄여줍니다. 마지막으로 탭, 필터링, 정렬, 검색어 같은 상태는 전역 스토어 대신 URL Search Params를 활용하여 URL에 저장하는 방식을 권장합니다. 최근 nuqs 같은 라이브러리를 활용하면 Client Boundary를 넓히지 않고도 Server Component가 URL 파라미터를 읽어 렌더링을 제어하는 구조를 쉽게 구현할 수 있어 상태 관리의 복잡도를 크게 낮출 수 있습니다.결론적으로 Eden님이 짚어주신 Composition Pattern은 Next.js App Router 생태계에서 중요하게 활용되는 패턴이며, 강의의 해당 표현은 Provider 사용 시 필연적으로 따라오는 데이터 소비의 부작용과 Client Component의 연쇄적 전파를 경계하자는 의미로 받아들여 주시면 좋을 것 같습니다.좋은 질문 남겨주셔서 감사드립니다. 추가로 우리동네코딩스튜디오에서 관심 있는 강의가 있으시다면 jeony0535@naver.com 이메일로 연락해 주시면 할인 쿠폰을 전달해 드리겠습니다!
- 좋아요수
- 0
- 댓글수
- 3
- 조회수
- 86
질문&답변
혹시 다음 강의 제작 예정된 것들이 있을까요?
안녕하세요 관태님, 우선 이렇게 정성스러운 수강 후기와 문의를 남겨주셔서 진심으로 감사합니다. AI 시대에 프론트엔드와 백엔드의 경계가 허물어지고 있다는 관태님의 통찰에 저 역시 깊이 공감합니다. 그 변화의 흐름 속에서 제 강의가 관태님의 성장에 실질적인 도움이 되었다니, 지식을 나누는 사람으로서 가장 뿌듯하고 감사한 순간이네요.말씀해 주신 것처럼 Node.js 시리즈는 Part 3까지 이어질 예정이며, 이어지는 강의의 방향성에 대해 최대한 상세하게 답변해 드리겠습니다. 다가올 Part 3는 관태님께서 Part 1과 Part 2를 거치며 단단하게 다지신 버퍼와 스트림, 그리고 운영체제 레벨의 이해를 네트워크라는 더 넓은 영역으로 확장하는 과정입니다. 단순히 프레임워크를 가져다 코드를 실행하면 서버가 열리고 네트워크가 연결된다는 식의 겉핥기식 접근이 아니라, 정말 밑바닥 근본부터 하나하나 이론과 구현을 모두 다루게 됩니다. 단순한 전기 신호가 물리적으로 어떻게 흘러 프레임, 패킷, 세그먼트 등의 형태로 변환되고, 이것이 우리가 자주 사용하는 네트워크에 어떻게 전달되는지 그 본질적인 과정을 파헤칩니다.이 과정을 통해 OSI 7계층과 TCP/IP 모델을 피상적인 이론으로만 암기하는 것을 넘어, 우리가 작성하는 코드의 어느 부분에서 데이터가 어떻게 캡슐화되고 전송되는지 바닥부터 이해하게 될 것입니다. 또한, 이미 만들어진 HTTP 서버에 의존하는 대신 Node.js의 내장 net 모듈을 활용해 TCP와 UDP 기반의 서버를 처음부터 직접 코딩하여 구현해 봅니다. 이렇게 직접 서버를 만들면서 3-way handshake나 흐름 제어는 물론, 버퍼에 데이터가 청크 단위로 쌓이고 처리되는 과정을 직접 눈으로 확인하시게 됩니다. 나아가 우리가 직접 만든 서버와 클라이언트가 통신할 때, 네트워크 선을 타고 흐르는 데이터를 Wireshark로 캡처하여 패킷 단위로 뜯어보는 과정도 거칩니다. 눈에 보이지 않던 네트워크 트래픽을 시각화하고 로그를 분석함으로써 완벽한 네트워크 이해도와 문제 해결 능력을 갖추게 될 것입니다. 이를 바탕으로 기존의 HTTP 규격에만 의존하는 것을 넘어, 헤더와 바디의 규격을 직접 정의하고 들어오는 스트림 데이터를 버퍼 레벨에서 파싱하여 처리하는 우리만의 독자적인 커스텀 프로토콜까지 설계하고 만들어 볼 것입니다.관태님께서 제안해 주신 운영과 배포 노하우, 확장 가능한 아키텍처, npm 패키지 제작, 디자인 패턴 등의 실무적인 주제들 역시 정말 훌륭한 포인트입니다. 사실 현재 대외적인 소개는 Part 3까지만 진행했지만, 강의를 제작하면서 저 역시 조금 더 욕심이 생겼고, 현재 구상 중인 내용들을 모두 고려했을 때 향후 시리즈는 Part 4에서 Part 5까지는 이어져 나올 것 같습니다. 그래서 향후에는 Part 1, 2, 3에 걸쳐 학습한 모든 내용을 기반으로 하여 우리만의 서버 프레임워크를 직접 만들어보는 과정까지 기획하고 있습니다. 여기에 데이터 압축의 원리는 물론이고, 현대 개발에서 필수적인 보안과 인증, 그리고 암호학 관련 내용까지 빠짐없이 모두 다룰 예정입니다. 더 나아가 Redis 등의 기술을 적극적으로 활용하여 백만 개의 엄청난 요청을 거뜬히 처리해 내는 대규모 트래픽 처리 시스템까지 모두 계획 중에 있습니다.이러한 큰 그림을 놓고 보면, 마침 관태님께서 관심 있어 하시는 확장 가능한 프로젝트 구조 설계와 실무 노하우 같은 주제들을 향후 커리큘럼에서 모두 깊이 있게 다룰 수 있을 것 같습니다. Part 3에서 다루게 될 바닥부터의 CS 및 네트워크 학습이 곧 확장 가능한 프로젝트 구조와 디자인 패턴을 완벽하게 이해하기 위한 가장 강력한 무기가 되기 때문입니다. 튼튼한 기초 위에 실무 노하우를 쌓을 때 그 시너지가 폭발하는 만큼, 저도 최대한 박차를 가해 관태님께서 기대하시는 좋은 강의를 빠르게 제공할 수 있도록 최선을 다하겠습니다. 앞으로 이어질 Part 2도 지금처럼 즐겁고 치열하게 수강해 주시길 바라며, 더 깊고 넓은 지식의 세계에서 관태님과 함께 끝없이 성장해 나가길 진심으로 기대하겠습니다. 정말 감사합니다.
- 좋아요수
- 0
- 댓글수
- 1
- 조회수
- 92
질문&답변
useReducer와 커스텀훅
안녕하세요 TAESUN님. 질문해주신 내용을 보니 리액트의 상태 관리와 아키텍처, 그리고 그 기반이 되는 원리까지 깊이 있게 고민하고 계신 것 같습니다. 결론부터 말씀드리면 제시해주신 의견이 정확히 맞습니다. 현대 리액트 개발 환경에서 useReducer가 제공하는 장점 중 상당 부분은 커스텀 훅으로 충분히 대체가 가능하며 때로는 더 직관적이기도 합니다. 리액트 18부터 도입된 자동 배칭(Automatic Batching) 기능 덕분에 여러 개의 setState를 연달아 호출해도 성능상의 불이익이나 불필요한 렌더링 폭포 현상이 발생하지 않기 때문입니다.그럼에도 불구하고 실무에서 useReducer가 갖는 고유한 입지를 이해하려면 먼저 상태를 변경하는 방식, 즉 '어떻게(How)'와 '무엇을(What)'의 차이인 명령적 접근과 선언적 접근의 차이를 단계적으로 살펴봐야 합니다. 커스텀 훅 내부에 여러 setState를 묶어두는 방식은 철저히 명령적인 접근에 가깝습니다.// 커스텀 훅을 활용한 명령적 접근의 예시 const useDataFetch = () => { const [isLoading, setIsLoading] = useState(false); const [data, setData] = useState(null); const [error, setError] = useState(null); const handleSuccess = (fetchedData) => { setIsLoading(false); // 1. 로딩 상태를 false로 변경해라 setData(fetchedData); // 2. 데이터를 업데이트해라 setError(null); // 3. 에러를 초기화해라 }; return { isLoading, data, error, handleSuccess }; } 위의 코드처럼 setLoading(true), setData(data), setError(null)과 같이 상태를 어떻게 바꾸어야 하는지 컴포넌트단에서 구체적인 지시를 내리는 방식이 우리가 자주 사용하는 패턴입니다. 반면 useReducer는 철저히 선언적인 접근을 지향합니다.// useReducer를 활용한 선언적 접근의 예시 const [state, dispatch] = useReducer(reducer, initialState); // 컴포넌트는 단지 '무슨 일이 일어났는지'만 알립니다. dispatch({ type: 'FETCH_SUCCESS', payload: fetchedData }); 데이터 로딩 성공이라는 사건이 발생했다는 의미로 위와 같이 디스패치를 호출하면, 상태가 어떻게 변할지는 컴포넌트 외부의 리듀서가 결정하게 됩니다. 실무에서 비즈니스 로직이 고도로 복잡해지면 컴포넌트 내부에서는 어떤 일이 일어났는지라는 액션만 신경 쓰고, 실제 상태의 변환은 리듀서라는 순수 함수에 온전히 위임하는 것이 유지보수에 훨씬 유리할 수 있습니다.이와 더불어 참조 동등성과 성능 최적화 관점에서도 중요한 차이가 존재합니다. 커스텀 훅 안에서 상태를 업데이트하는 여러 함수를 만들어 하위 컴포넌트로 전달할 때는 렌더링될 때마다 함수가 재생성되는 것을 막기 위해 많은 useCallback과 의존성 배열 관리가 필요해집니다. 반면 useReducer가 반환하는 dispatch 함수는 리액트 컴포넌트의 생명주기 동안 메모리 참조가 절대 변하지 않음을 리액트 자체적으로 보장받습니다. 그렇기 때문에 dispatch를 Context API에 담아 깊은 하위 컴포넌트로 전달하더라도 불필요한 리렌더링이나 의존성 배열의 지옥에 빠지지 않고 매우 안정적인 아키텍처를 구성할 수 있습니다.협업 프로젝트에서 useReducer를 많이 보지 못하신 것은 현재 프론트엔드 생태계의 현실을 그대로 반영하고 있으며, 여기에는 실무를 관통하는 명확하고 현실적인 이유들이 있습니다. 현업에서는 로컬 상태가 useReducer를 도입해야 할 만큼 복잡해지기 전에 이미 이를 훨씬 더 잘 다루는 전문 라이브러리들에게 역할을 위임하는 것이 압도적으로 유리하기 때문입니다.이를 구체적인 실무 팁과 함께 상세히 나누어 살펴보자면, 첫째로 서버 상태(Server State) 관리 패러다임의 완벽한 전환입니다. 과거에는 API 통신 시 발생하는 로딩, 데이터, 에러 상태를 FETCH_START, FETCH_SUCCESS, FETCH_ERROR 등의 액션으로 나누어 useReducer로 관리하는 것이 정석이었습니다. 하지만 실제 서비스에서는 단순한 상태 저장을 넘어 캐싱, 포커스 시 자동 재요청, 낙관적 업데이트(Optimistic Update), 에러 재시도(Retry) 같은 매우 고도화된 기능이 필수적입니다. 이 엄청난 양의 보일러플레이트를 useReducer로 직접 구현하는 것은 실무 관점에서 명백한 인력 낭비이며, 현재는 TanStack Query(React Query)나 SWR 같은 라이브러리가 단 한두 줄의 코드로 이 모든 것을 완벽하게 대체하고 서비스의 안정성을 크게 높여줍니다.둘째는 전역 상태(Global State) 관리 도구들의 경량화와 최적화입니다. useReducer를 Context API와 결합하면 훌륭한 상태 관리가 가능하지만, 실무에서는 상태가 조금만 변해도 Context를 구독하는 모든 하위 컴포넌트가 리렌더링되는 성능 이슈에 직면하게 됩니다. 이를 막기 위해 상태용 Context와 디스패치용 Context를 쪼개고 React.memo를 곳곳에 씌워야 하는 수고로움이 따릅니다. 반면 현대 실무의 표준으로 자리 잡은 Zustand나 Jotai, Redux Toolkit 같은 라이브러리들은 보일러플레이트가 거의 없으면서도 리액트의 렌더링 사이클 외부에서 상태를 효율적으로 관리하여 이런 성능 최적화 고민을 원천적으로 해결해 줍니다. 그래서 상태가 조금이라도 복잡해질 기미가 보이면 useReducer를 쓰기보다 곧바로 Zustand 같은 전역 스토어로 로직을 올려버리는 팀이 대다수입니다.셋째는 폼(Form) 상태 관리의 위임입니다. 회원가입이나 상품 등록 같은 복잡한 입력 폼은 입력값, 에러 상태, 터치 여부, 유효성 검사 등 관리해야 할 상태가 방대하여 과거 useReducer의 단골 소재였습니다. 그러나 매 타이핑마다 무거운 리듀서 로직이 실행되고 컴포넌트가 리렌더링되는 것은 사용자 경험(UX)에 치명적일 수 있습니다. 현재는 React Hook Form 같은 라이브러리가 비제어(Uncontrolled) 컴포넌트 기반으로 렌더링을 최소화하며 이 역할을 훨씬 우수한 성능과 개발자 경험으로 손쉽게 해결해 주고 있습니다. 결국 실무에서 useReducer의 포지션은, 외부 라이브러리를 도입하기에는 과하고 useState만 쓰기에는 상태 간의 꼬임이 심각하게 우려되는 아주 좁고 특수한 로컬 컴포넌트 영역으로 축소된 것이 자연스러운 현실입니다.이러한 상황에서도 현업에서 useReducer를 전략적으로 선택하는 구체적인 예외 케이스들이 분명 존재합니다. 먼저 상태 간의 의존성이 극도로 높은 경우를 들 수 있습니다. 예를 들어 A 상태가 바뀔 때 반드시 B와 C 상태도 이전 상태값을 참조하여 정교하게 동기화되어 바뀌어야 하는 복잡한 데이터 그리드나 드래그 앤 드롭 엔진, 혹은 노션 같은 리치 텍스트 에디터의 로컬 UI 로직을 설계할 때 그 진가를 발휘합니다. 다음으로 상태 머신 패턴을 구현할 때입니다.// 상태 머신 패턴을 활용한 견고한 로직 예시 function dragReducer(state, action) { switch (state.status) { case 'IDLE': if (action.type === 'START') return { status: 'DRAGGING', position: action.payload }; break; case 'DRAGGING': if (action.type === 'MOVE') return { ...state, position: action.payload }; if (action.type === 'DROP') return { status: 'DROPPED', position: action.payload }; break; case 'DROPPED': if (action.type === 'RESET') return { status: 'IDLE', position: { x: 0, y: 0 } }; break; } return state; // 현재 상태에서 허용되지 않은 액션은 무시되어 버그를 원천 차단함 } 이 코드처럼 UI가 가질 수 있는 상태가 IDLE, DRAGGING, DROPPED와 같이 명확히 정해져 있고, 특정 상태에서만 특정 액션이 허용되어야 하는 견고한 로직을 짤 때는 switch 문을 활용하는 리듀서가 가장 안전한 선택지가 됩니다. 물론 이조차도 로직이 거대해지면 XState 같은 전문 상태 머신 라이브러리로 대체되곤 합니다.실무에서 직접적인 사용 빈도가 이렇게 줄어들었음에도 불구하고 우리가 useReducer를 반드시 깊이 있게 학습하고 이해해야 하는 명확한 이유가 있습니다. 가장 큰 이유는 useReducer가 현대 프론트엔드 상태 관리의 근간이 되는 '단방향 데이터 흐름(Unidirectional Data Flow)'과 '플럭스(Flux) 아키텍처'의 핵심을 리액트 내장 훅으로 완벽하게 구현해 둔 훌륭한 교보재이기 때문입니다. 앞서 말씀드린 Zustand나 Redux 같은 전역 상태 관리 라이브러리들은 내부적으로 형태만 조금 다를 뿐, 결국 '액션(Action)'을 발생시키고 이를 중앙에서 받아 '순수 함수(Reducer)'를 통해 예측 가능한 새로운 상태를 반환한다는 useReducer의 철학과 패턴을 그대로 계승하고 있습니다. 따라서 useReducer의 동작 원리를 명확히 이해하고 있다는 것은 단순히 훅 하나를 더 아는 것을 넘어, 상태가 어떻게 예측 가능하게 전이되어야 하는지, 그리고 UI 컴포넌트와 비즈니스 로직을 어떻게 완벽히 분리할 수 있는지에 대한 깊은 아키텍처적 통찰력을 갖추게 됨을 의미합니다. 이는 향후 거대한 프로젝트의 상태 관리 시스템을 직접 설계하거나 복잡한 오픈소스 코드를 분석할 때 가장 강력한 무기가 됩니다.여기에 프로젝트 설계를 위한 또 다른 현실적인 팁을 덧붙이자면, 프로젝트 초기 설계 단계부터 이 로직은 복잡하니까 무조건 useReducer를 써야지라고 결정하기보다는, 일단 가장 익숙한 useState와 커스텀 훅의 조합으로 속도감 있게 개발을 시작하시는 것을 적극 권장합니다. 코드를 작성해 나가다 상태 업데이트 함수 내부에 if문이 너무 많아지고 여러 상태값들이 서로 얽혀서 사소한 수정에도 버그가 발생하기 쉬운 임계점이 오면, 그때 해당 컴포넌트의 비즈니스 로직만 추출하여 useReducer로 리팩토링하거나 앞서 말씀드린 Zustand 등의 라이브러리로 승격시키는 것이 실무에서 겪을 수 있는 가장 유연하고 이상적인 흐름입니다.결론적으로 대부분의 일상적인 개발에서는 생각하신 대로 커스텀 훅과 useState의 조합으로 충분하며 실질적으로는 취향 차이의 영역이 맞으므로 억지로 useReducer를 도입할 필요는 없습니다. 다만 앞서 설명해 드린 대로 상태 변경 로직을 UI와 완벽히 격리하고 액션과 상태 전이를 분리한다는 이 근본적인 설계 철학을 깊이 이해하고 체화하신다면, 앞으로 어떤 상태 관리 도구를 마주하더라도 컴포넌트가 무분별하게 비대해지는 것을 막는 탄탄한 밑거름이 될 것입니다. 강의를 들으시면서 또 궁금한 점이나 현업의 시각이 필요한 부분이 있다면 언제든 질문을 남겨주시기 바랍니다.감사합니다!
- 좋아요수
- 0
- 댓글수
- 1
- 조회수
- 60
질문&답변
미션 14에서 StockButton의 memo는 어떤 역할인가요
안녕하세요 이태관님. 질문해주신 정답 코드에 생략된 컴포넌트의 위치와 보호 함수의 역할에 대해 코드를 바탕으로 로직의 흐름에 따라 자연스럽게 설명해 드리겠습니다.정답 코드에서 생략된 ProductList 컴포넌트는 상태 채널인 ProductStateContext를 구독하여 상품 목록 데이터를 받아오게 됩니다. 그리고 각 상품 항목을 렌더링할 때 StockButton을 자식 컴포넌트로 포함하는 구조를 가집니다.import React, { useContext } from 'react'; import { ProductStateContext } from '../contexts/ProductContext'; import { StockButton } from './StockButton'; export default function ProductList() { const items = useContext(ProductStateContext); return ( {items.map((item) => ( {item.name} - {item.isSoldOut ? '품절' : '판매중'} ))} ); } 이 코드에서 볼 수 있듯이 StockButton은 개별 상품을 식별하여 상태를 변경해야 합니다. 따라서 ProductList 내부에서 map 함수를 통해 반복 렌더링되는 각 상품 항목과 동일한 레벨에 위치하는 것이 로직상 가장 적절합니다.이렇게 버튼이 배치된 상태에서 StockButton을 감싸고 있는 React.memo가 어떤 역할을 하는지 살펴보겠습니다. 이 함수의 주된 역할은 부모 컴포넌트가 리렌더링될 때, 자식 컴포넌트가 전달받는 속성인 props, 즉 여기서는 id 값이 변하지 않았다면 해당 자식 컴포넌트의 리렌더링을 막아주는 것입니다. 현재 구조에서는 특정 상품의 품절 여부와 같은 상태가 변경되면 items 배열 전체가 업데이트됩니다. 이렇게 되면 이를 직접 구독하고 있는 부모 컴포넌트인 ProductList에 리렌더링이 발생하게 됩니다. React의 기본 동작 원리상 부모 컴포넌트가 렌더링되면 그 내부에 있는 자식 컴포넌트들도 함께 다시 그려집니다.하지만 여기서 매우 중요한 실무적 함정이자 아키텍처 개선 포인트를 짚고 넘어가야 합니다. 현재 구조처럼 ProductList 내부에서 items.map을 돌려 개별 요소와 StockButton을 직접 배치할 경우, ProductList가 다시 실행될 때 map 함수도 재실행됩니다. 이때 StockButton은 React.memo로 감싸져 있어 버튼 자체의 연산은 방어할 수 있겠지만, 그 바깥의 태그와 텍스트(상품명, 품절 여부)들은 렌더링될 때마다 불필요하게 다시 생성됩니다.따라서 실무에서 이를 완벽하게 최적화하려면 map 내부의 요소 전체를 이라는 별도의 단일 컴포넌트로 분리하고, 이 컴포넌트 자체를 React.memo로 감싸는 구조로 변경해야 합니다. 이를 통해 부모로부터 전파되는 간접적인 리렌더링 폭포를 완벽히 차단하는 견고한 최적화를 완성할 수 있습니다.import React, { useContext, memo } from 'react'; import { ProductStateContext } from '../contexts/ProductContext'; import { StockButton } from './StockButton'; // 1. 개별 상품 항목을 독립된 컴포넌트로 분리하고 React.memo로 감싸기 const ProductItem = memo(({ item }) => { return ( {item.name} - {item.isSoldOut ? '품절' : '판매중'} ); }); // 2. 부모 컴포넌트인 ProductList는 items 배열이 바뀌어 리렌더링 되더라도, // 내용이 바뀌지 않은 다른 ProductItem들의 렌더링을 건너뛰게 됩니다. export default function ProductList() { const items = useContext(ProductStateContext); return ( {items.map((item) => ( ))} ); } 여기에 더해 실무 적용과 깊이 있는 이해를 위한 내용도 함께 짚어보겠습니다. 먼저 실무 팁으로 무분별한 React.memo 사용을 주의해야 한다는 점을 말씀드리고 싶습니다. 이 함수는 컴포넌트가 다시 그려지기 전에 이전 속성과 새로운 속성이 같은지 얕은 비교를 수행하게 됩니다. 만약 렌더링 자체가 아주 가벼운 단순한 UI 컴포넌트라면 이 비교 연산에 드는 리소스가 렌더링 비용보다 오히려 더 클 수 있습니다. 따라서 무거운 연산이 포함된 컴포넌트나 렌더링 빈도가 매우 높은 리스트의 하위 항목에 선택적으로 적용하는 것이 올바른 실무 방식이며, 앞서 말씀드린 분리 구조 같은 케이스가 바로 그 적절한 예시입니다.또한 Context와 React.memo가 만드는 시너지 측면에서도 접근해 볼 수 있습니다. 실무에서는 전역 상태가 변경될 때 발생하는 광범위한 렌더링 폭포를 제어하는 것이 성능 최적화의 핵심으로 꼽힙니다. 상태와 명령 채널을 분리하고, 명령이나 고정된 데이터만 사용하는 하위 컴포넌트(혹은 리스트 아이템)를 React.memo로 한 번 더 보호하는 패턴은 대규모 React 애플리케이션 설계 시 필수적으로 활용되는 견고한 아키텍처입니다.더 깊은 학습에 도움이 될 만한 내용으로 참조 동등성의 함정에 대해서도 생각해 볼 필요가 있습니다. 컴포넌트가 속성을 비교할 때 숫자나 문자열 같은 원시 타입은 값이 같으면 동일하다고 판단하지만, 객체나 함수 같은 참조 타입은 메모리 주소가 다르면 값이 변했다고 판단하게 됩니다. 만약 하위 컴포넌트에 id라는 원시 타입 대신 부모에서 생성한 함수를 직접 속성으로 넘겨주었다면 부모가 렌더링될 때마다 함수가 새로 생성되어 방어막이 뚫리게 됩니다. 이러한 현상을 막기 위해 짝꿍처럼 쓰이는 훅이 바로 useCallback입니다.import React, { useCallback, memo } from 'react'; const StockButton = memo(({ id, onClick }) => { return onClick(id)}>재고 변경; }); export default function ParentComponent() { // 부모가 리렌더링되더라도 함수 참조(메모리 주소)를 동일하게 유지하여 // 하위 컴포넌트의 React.memo 방어막이 뚫리지 않도록 보호합니다. const handleStockChange = useCallback((id) => { console.log(`${id}번 상품 재고 변경 로직`); }, []); // 의존성 배열이 비어있어 최초 한 번만 함수 생성 return ( ); } 마지막으로 렌더링 건너뛰기 현상에 대한 개념도 중요합니다. React 공식 문서 및 동작 원리에서는 조건 충족이나 동일한 상태 업데이트로 인해 렌더링 작업이 취소되고 건너뛰어지는 현상을 'Bailout'이라고 부릅니다. 이 키워드로 React의 렌더링 최적화 로직을 추가 학습하시면 프레임워크의 내부 동작을 완벽히 이해하는 데 큰 도움이 될 것입니다. 학습에 유용한 참고가 되시길 바랍니다.
- 좋아요수
- 0
- 댓글수
- 2
- 조회수
- 47
질문&답변
실습 가이드: 16강 에서 useMemo의 역할은 무엇인가요?
안녕하세요 이태관님. 남겨주신 질문을 보면, 강의에서 다룬 '상태와 상태 변경 컴포넌트 분리를 통한 리렌더링 최적화'라는 목적을 정확하게 파악하셨습니다.제가 제공해드린 실습 코드에서 useMemo가 정확히 어떤 역할을 하도록 의도했는지, 그리고 실무에서는 이 코드를 어떻게 바라보고 적용하는지 코드를 통해 단계별로 설명해 드리겠습니다.제가 실습 가이드 상에서 useMemo를 통해 의도한 역할은 참조 유지를 통한 리렌더링 방어입니다. React의 렌더링 엔진은 Context Provider의 값으로 전달되는 속성의 참조 주소가 변경될 때마다, 해당 Context를 구독하는 모든 하위 컴포넌트를 강제로 리렌더링하는 특성이 있습니다.// 실습 코드에서의 useMemo 사용 // 상태 객체의 참조를 유지하려는 '의도' const memoizedState = useMemo(() => state, [state]); return ( {/* ... */} ); 따라서 제가 실습 코드에서 useMemo를 사용한 근본적인 목적은, 상태 객체가 가지고 있는 값이 실제로 변하지 않았다면 컴포넌트가 다시 호출되더라도 이전과 동일한 메모리 주소를 반환하도록 강제하는 데 있습니다. 하위 컴포넌트들이 불필요하게 리렌더링되는 것을 방어하는 역할을 하도록 의도한 것입니다.하지만 참조 동등성을 유지해야 한다는 개념을 교육하기 위해 명시적으로 작성해둔 이 useMemo 코드는, React의 내부 동작 원리를 고려했을 때 사실상 아무런 역할도 하지 않는 코드에 가깝습니다. 그 이유는 useReducer가 가진 고유의 특성 때문입니다.const [state, dispatch] = useReducer(authReducer, { user: null, isLoading: false, error: null, }); // useReducer는 이미 상태가 안 변하면 참조를 유지합니다. // 상태가 변하면 어차피 memoizedState도 새로 갱신되므로, // 여기서의 useMemo는 사실상 동작에 영향을 주지 않습니다. const memoizedState = useMemo(() => state, [state]); React 내부적으로 useReducer나 useState가 반환하는 상태 객체는 새로운 상태로 업데이트되지 않는 이상, 렌더링 간에 이미 동일한 참조를 유지하도록 보장됩니다. 만약 dispatch 명령이 호출되어 상태가 변경된다면 useReducer는 새로운 상태 객체를 반환하게 되고, 이때 useMemo의 의존성 배열([state])에 등록된 상태 값이 변경되었으므로 useMemo 역시 콜백을 재실행하여 새로운 객체를 반환하게 됩니다.즉, 상태가 변하지 않으면 애초에 참조가 같고, 상태가 변하면 useMemo도 어차피 새로운 참조를 만들어내기 때문에 해당 위치에서의 useMemo 연산은 실질적인 의미가 없습니다. 결과적으로 제가 구성한 이 아키텍처에서 리렌더링 최적화를 완성한 진짜 이유는 useMemo가 아니라 Context를 상태용과 명령용 두 개로 쪼갠 구조 그 자체입니다. useReducer가 반환하는 dispatch 함수는 컴포넌트의 생애주기 동안 절대 참조가 변하지 않기 때문에 채널을 분리한 것만으로도 최적화가 이루어집니다.그렇다면 실무에서 Context를 최적화할 때 useMemo가 반드시 필요한 상황은 언제일까요? 보통 useReducer를 쓰지 않고 여러 개의 개별 상태를 조합해서 하나의 객체로 내려보낼 때 사용됩니다.예를 들어 Provider 내부에 유저 정보와 로딩 상태라는 두 개의 독립적인 상태가 있다고 가정해 보겠습니다.// 나쁜 예 (렌더링 폭탄 발생) export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(false); // 객체 리터럴 { user, isLoading }을 직접 전달하면 // AuthProvider가 렌더링될 때마다 완전히 새로운 메모리 주소를 가진 객체가 생성됩니다. return ( {children} ); } 위 코드처럼 값 속성에 바로 객체 형태로 전달하게 되면 컴포넌트가 렌더링될 때마다 새로운 객체가 메모리에 계속 생성됩니다. 이렇게 되면 이 Context를 구독하는 모든 하위 컴포넌트는 실제 값이 바뀌지 않았음에도 불구하고 무조건 리렌더링이 발생하는 문제가 생깁니다.이러한 문제를 해결하기 위한 useMemo의 진짜 역할은, 내부 값이 실제로 바뀔 때만 새로운 객체를 생성하도록 묶어주는 것입니다.// 해결책 (useMemo의 올바른 활용) export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(false); // user나 isLoading 값이 '실제로' 바뀔 때만 새로운 객체를 생성하고, // 값이 같다면 이전 렌더링에서 만들어둔 객체의 참조 주소를 그대로 재사용합니다. const value = useMemo( () => ({ user, isLoading, setUser }), [user, isLoading] ); return ( {children} ); } 변수에 useMemo를 사용하여 반환할 객체와 의존성 배열을 설정해두면, 내부 값이 같을 경우 이전 렌더링에서 만들어둔 객체의 참조 주소를 그대로 재사용하게 됩니다. 이를 통해 Provider에 여러 상태를 객체로 묶어 전달할 때 참조가 깨지지 않도록 방어할 수 있습니다.요약하자면 채널을 분리하여 리렌더링을 막는다는 아키텍처의 방향성은 정확합니다. 실습 코드에 포함된 useMemo는 "객체를 넘길 때는 참조가 깨지지 않게 주의해야 한다"는 점을 설명하기 위해 제가 추가해둔 교육용 장치로 이해하시면 됩니다.현업 실무 환경에서는 위와 같이 Context API를 쪼개고 useMemo로 감싸는 작업이 번거롭고 보일러플레이트 코드가 길어질 수 있습니다. 그래서 전역 상태 관리가 복잡해지는 프로젝트에서는 Zustand나 Jotai 같은 상태 관리 라이브러리를 도입하는 경우가 많습니다. 이 라이브러리들은 필요한 상태만 골라서 구독할 수 있도록 내부적으로 렌더링 최적화가 적용되어 있습니다.이처럼 Context API의 한계와 렌더링 최적화 원리(참조 동등성)를 이해하고 상태 관리 도구를 활용하는 것은 실무에서 발생하는 렌더링 이슈를 트러블슈팅하는 데 핵심적인 기반이 됩니다. 학습에 참고가 되시길 바랍니다.
- 좋아요수
- 0
- 댓글수
- 3
- 조회수
- 67
질문&답변
실습 가이드: 16강 에서 useMemo의 역할은 무엇인가요?
안녕하세요 이태관님. 남겨주신 질문을 보면, 강의에서 다룬 '상태와 상태 변경 컴포넌트 분리를 통한 리렌더링 최적화'라는 목적을 정확하게 파악하셨습니다.제가 제공해드린 실습 코드에서 useMemo가 정확히 어떤 역할을 하도록 의도했는지, 그리고 실무에서는 이 코드를 어떻게 바라보고 적용하는지 코드를 통해 단계별로 설명해 드리겠습니다.제가 실습 가이드 상에서 useMemo를 통해 의도한 역할은 참조 유지를 통한 리렌더링 방어입니다. React의 렌더링 엔진은 Context Provider의 값으로 전달되는 속성의 참조 주소가 변경될 때마다, 해당 Context를 구독하는 모든 하위 컴포넌트를 강제로 리렌더링하는 특성이 있습니다.// 실습 코드에서의 useMemo 사용 // 상태 객체의 참조를 유지하려는 '의도' const memoizedState = useMemo(() => state, [state]); return ( {/* ... */} ); 따라서 제가 실습 코드에서 useMemo를 사용한 근본적인 목적은, 상태 객체가 가지고 있는 값이 실제로 변하지 않았다면 컴포넌트가 다시 호출되더라도 이전과 동일한 메모리 주소를 반환하도록 강제하는 데 있습니다. 하위 컴포넌트들이 불필요하게 리렌더링되는 것을 방어하는 역할을 하도록 의도한 것입니다.하지만 참조 동등성을 유지해야 한다는 개념을 교육하기 위해 명시적으로 작성해둔 이 useMemo 코드는, React의 내부 동작 원리를 고려했을 때 사실상 아무런 역할도 하지 않는 코드에 가깝습니다. 그 이유는 useReducer가 가진 고유의 특성 때문입니다.const [state, dispatch] = useReducer(authReducer, { user: null, isLoading: false, error: null, }); // useReducer는 이미 상태가 안 변하면 참조를 유지합니다. // 상태가 변하면 어차피 memoizedState도 새로 갱신되므로, // 여기서의 useMemo는 사실상 동작에 영향을 주지 않습니다. const memoizedState = useMemo(() => state, [state]); React 내부적으로 useReducer나 useState가 반환하는 상태 객체는 새로운 상태로 업데이트되지 않는 이상, 렌더링 간에 이미 동일한 참조를 유지하도록 보장됩니다. 만약 dispatch 명령이 호출되어 상태가 변경된다면 useReducer는 새로운 상태 객체를 반환하게 되고, 이때 useMemo의 의존성 배열([state])에 등록된 상태 값이 변경되었으므로 useMemo 역시 콜백을 재실행하여 새로운 객체를 반환하게 됩니다.즉, 상태가 변하지 않으면 애초에 참조가 같고, 상태가 변하면 useMemo도 어차피 새로운 참조를 만들어내기 때문에 해당 위치에서의 useMemo 연산은 실질적인 의미가 없습니다. 결과적으로 제가 구성한 이 아키텍처에서 리렌더링 최적화를 완성한 진짜 이유는 useMemo가 아니라 Context를 상태용과 명령용 두 개로 쪼갠 구조 그 자체입니다. useReducer가 반환하는 dispatch 함수는 컴포넌트의 생애주기 동안 절대 참조가 변하지 않기 때문에 채널을 분리한 것만으로도 최적화가 이루어집니다.그렇다면 실무에서 Context를 최적화할 때 useMemo가 반드시 필요한 상황은 언제일까요? 보통 useReducer를 쓰지 않고 여러 개의 개별 상태를 조합해서 하나의 객체로 내려보낼 때 사용됩니다.예를 들어 Provider 내부에 유저 정보와 로딩 상태라는 두 개의 독립적인 상태가 있다고 가정해 보겠습니다.// 나쁜 예 (렌더링 폭탄 발생) export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(false); // 객체 리터럴 { user, isLoading }을 직접 전달하면 // AuthProvider가 렌더링될 때마다 완전히 새로운 메모리 주소를 가진 객체가 생성됩니다. return ( {children} ); } 위 코드처럼 값 속성에 바로 객체 형태로 전달하게 되면 컴포넌트가 렌더링될 때마다 새로운 객체가 메모리에 계속 생성됩니다. 이렇게 되면 이 Context를 구독하는 모든 하위 컴포넌트는 실제 값이 바뀌지 않았음에도 불구하고 무조건 리렌더링이 발생하는 문제가 생깁니다.이러한 문제를 해결하기 위한 useMemo의 진짜 역할은, 내부 값이 실제로 바뀔 때만 새로운 객체를 생성하도록 묶어주는 것입니다.// 해결책 (useMemo의 올바른 활용) export function AuthProvider({ children }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(false); // user나 isLoading 값이 '실제로' 바뀔 때만 새로운 객체를 생성하고, // 값이 같다면 이전 렌더링에서 만들어둔 객체의 참조 주소를 그대로 재사용합니다. const value = useMemo( () => ({ user, isLoading, setUser }), [user, isLoading] ); return ( {children} ); } 변수에 useMemo를 사용하여 반환할 객체와 의존성 배열을 설정해두면, 내부 값이 같을 경우 이전 렌더링에서 만들어둔 객체의 참조 주소를 그대로 재사용하게 됩니다. 이를 통해 Provider에 여러 상태를 객체로 묶어 전달할 때 참조가 깨지지 않도록 방어할 수 있습니다.요약하자면 채널을 분리하여 리렌더링을 막는다는 아키텍처의 방향성은 정확합니다. 실습 코드에 포함된 useMemo는 "객체를 넘길 때는 참조가 깨지지 않게 주의해야 한다"는 점을 설명하기 위해 제가 추가해둔 교육용 장치로 이해하시면 됩니다.현업 실무 환경에서는 위와 같이 Context API를 쪼개고 useMemo로 감싸는 작업이 번거롭고 보일러플레이트 코드가 길어질 수 있습니다. 그래서 전역 상태 관리가 복잡해지는 프로젝트에서는 Zustand나 Jotai 같은 상태 관리 라이브러리를 도입하는 경우가 많습니다. 이 라이브러리들은 필요한 상태만 골라서 구독할 수 있도록 내부적으로 렌더링 최적화가 적용되어 있습니다.이처럼 Context API의 한계와 렌더링 최적화 원리(참조 동등성)를 이해하고 상태 관리 도구를 활용하는 것은 실무에서 발생하는 렌더링 이슈를 트러블슈팅하는 데 핵심적인 기반이 됩니다. 학습에 참고가 되시길 바랍니다.
- 좋아요수
- 0
- 댓글수
- 3
- 조회수
- 67




