nhcodingstudio
@nhcodingstudio
Students
2,423
Reviews
142
Course Rating
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.
Courses
Reviews
seongbiny
·
React Master Class: Part 1 - Understanding the Essence of Rendering and Design Through MissionsReact Master Class: Part 1 - Understanding the Essence of Rendering and Design Through Missionsjos502752669322
·
"The Era of AI Clicks" Breaking Through with Principles: Node.js and CS Part 1 - V8 and Core Deconstruction"The Era of AI Clicks" Breaking Through with Principles: Node.js and CS Part 1 - V8 and Core Deconstructionnggag
·
"The Era of AI Clicks" Breaking Through with Principles: Node.js and CS Part 1 - V8 and Core Deconstruction"The Era of AI Clicks" Breaking Through with Principles: Node.js and CS Part 1 - V8 and Core Deconstructionpotato9801245279
·
React Master Class: Part 2 - High-Performance Hooks and Real-World Architecture Completed Through MissionsReact Master Class: Part 2 - High-Performance Hooks and Real-World Architecture Completed Through Missionsn5i4
·
Next.js Master Class: Part 1 Learning the Essence of App Router and Rendering Design through MissionsNext.js Master Class: Part 1 Learning the Essence of App Router and Rendering Design through Missions
Posts
Q&A
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 이메일로 연락해 주시면 할인 쿠폰을 전달해 드리겠습니다!
- Likes
- 0
- Comments
- 2
- Viewcount
- 38
Q&A
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 이메일로 연락해 주시면 할인 쿠폰을 전달해 드리겠습니다!
- Likes
- 0
- Comments
- 3
- Viewcount
- 64
Q&A
혹시 다음 강의 제작 예정된 것들이 있을까요?
안녕하세요 관태님, 우선 이렇게 정성스러운 수강 후기와 문의를 남겨주셔서 진심으로 감사합니다. 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도 지금처럼 즐겁고 치열하게 수강해 주시길 바라며, 더 깊고 넓은 지식의 세계에서 관태님과 함께 끝없이 성장해 나가길 진심으로 기대하겠습니다. 정말 감사합니다.
- Likes
- 0
- Comments
- 1
- Viewcount
- 60
Q&A
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와 완벽히 격리하고 액션과 상태 전이를 분리한다는 이 근본적인 설계 철학을 깊이 이해하고 체화하신다면, 앞으로 어떤 상태 관리 도구를 마주하더라도 컴포넌트가 무분별하게 비대해지는 것을 막는 탄탄한 밑거름이 될 것입니다. 강의를 들으시면서 또 궁금한 점이나 현업의 시각이 필요한 부분이 있다면 언제든 질문을 남겨주시기 바랍니다.감사합니다!
- Likes
- 0
- Comments
- 1
- Viewcount
- 46
Q&A
미션 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의 렌더링 최적화 로직을 추가 학습하시면 프레임워크의 내부 동작을 완벽히 이해하는 데 큰 도움이 될 것입니다. 학습에 유용한 참고가 되시길 바랍니다.
- Likes
- 0
- Comments
- 2
- Viewcount
- 33
Q&A
실습 가이드: 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의 한계와 렌더링 최적화 원리(참조 동등성)를 이해하고 상태 관리 도구를 활용하는 것은 실무에서 발생하는 렌더링 이슈를 트러블슈팅하는 데 핵심적인 기반이 됩니다. 학습에 참고가 되시길 바랍니다.
- Likes
- 0
- Comments
- 3
- Viewcount
- 43
Q&A
실습 가이드: 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의 한계와 렌더링 최적화 원리(참조 동등성)를 이해하고 상태 관리 도구를 활용하는 것은 실무에서 발생하는 렌더링 이슈를 트러블슈팅하는 데 핵심적인 기반이 됩니다. 학습에 참고가 되시길 바랍니다.
- Likes
- 0
- Comments
- 3
- Viewcount
- 43
Q&A
1강 질문
안녕하세요 madwolves98님. 질문 주셔서 감사합니다!우선 Apache나 전형적인 Spring Boot 같은 전통적인 서버 모델은 사용자의 요청이 들어올 때마다 일종의 작업자인 Thread를 한 명씩 전담시키는 방식을 사용합니다. 이런 구조에서는 데이터베이스에서 정보를 찾아오거나 외부 결제 시스템을 호출하고, 또 파일을 읽고 쓰는 등의 데이터 입출력(I/O) 작업이 발생할 때 문제가 생깁니다. 컴퓨터의 두뇌인 CPU는 할 일이 없어서 놀고 있는데도, 해당 작업자는 데이터베이스나 네트워크의 응답이 올 때까지 아무것도 하지 않고 가만히 멈춰 서서 기다리게 됩니다. 우리는 이렇게 작업자가 꼼짝 못 하고 대기하는 상태를 Blocking 상태라고 부릅니다.반면 Node.js는 메인 작업자를 딱 한 명만 두고 이 작업자가 절대 대기하지 않고 계속 움직이는 Non-blocking 방식을 사용합니다. 여기서 Event Loop라는 개념이 등장하는데, 이는 들어오는 요청을 끊임없이 접수하는 단 한 명의 멈추지 않는 점원과 같습니다. 만약 사용자의 요청에 시간이 오래 걸리는 데이터베이스 조회 같은 I/O 작업이 포함되어 있다면, Event Loop는 이 작업을 운영체제의 백그라운드 시스템이나 자체적인 보조 작업 공간(엄밀히 말하면 libuv가 관리하는 내부 스레드 풀)에 던져버리고 즉시 다음 손님의 요청을 받으러 이동합니다. 이후 백그라운드 시스템이 일을 끝내서 완료 신호를 보내면, Event Loop는 다음 손님의 주문을 받는 틈틈이 그 결과값을 가지고 그다음으로 해야 할 후속 조치, 즉 Callback 처리를 진행하게 됩니다.이를 현실 세계의 식당에 비유해 보면 이해가 쉽습니다. 전통적 방식은 손님이 메뉴를 고르고 음식을 먹고 나갈 때까지 점원 한 명이 그 손님 테이블 옆에 서서 계속 대기하는 구조입니다. 손님이 많아지면 점원을 계속 고용해야 하는데, 컴퓨터 세상에서는 점원이 많아질수록 이 점원들을 일일이 관리하고 교대시키는 데 드는 Context Switching 비용이 기하급수적으로 증가합니다. 결국 서버의 메모리가 버티지 못하고 고갈되어 다운되어 버리죠. 반면에 Node.js 방식은 메인 점원 한 명이 주문만 계속 받아서 주방에 넘기고, 주방에서 음식을 만들어 내오면 점원은 새로운 주문을 받는 틈틈이 서빙만 하는 구조입니다. 그렇기 때문에 점원 한 명이 수백, 수천 명의 손님을 거뜬히 커버할 수 있는 것입니다.실제 기업들이 서비스를 개발하고 운영하는 현장에 빗대어 시장의 도구들을 냉정하게 비교해 보면 이 특징들이 더욱 선명해집니다. Node.js를 기반으로 하는 Express나 NestJS는 앞서 말씀드린 주문만 빠르게 쳐내는 구조 덕분에 수만 명의 동시 접속을 가볍게 처리합니다. 배달 앱에서 수많은 라이더와 고객의 위치 정보를 1초 단위로 끊임없이 중계하거나, 카카오톡처럼 수만 명이 동시에 메시지를 주고받는 실시간 채팅 서버를 만들 때 아주 강력합니다. 다만, 동영상 인코딩이나 복잡한 암호화 알고리즘 계산처럼 메인 점원 스스로 머리를 써서 오랫동안 풀어야 하는 무거운 수학적 계산 작업이 들어오면, 단 한 명뿐인 점원이 그 계산을 하느라 서빙 자체를 멈춰버려 전체 서버가 먹통이 될 수 있다는 약점이 있습니다.이와 달리 Java 진영의 Spring Boot는 여러 명의 점원을 체계적으로 관리하는 방식을 바탕으로 거대한 기업용 시스템에 맞는 강력한 뼈대와 엄격한 규칙을 제공합니다. 토스나 은행 앱처럼 1원의 오차도 허용되지 않는 복잡한 송금 결제 시스템을 개발하거나, 수백 개의 데이터베이스 테이블을 복잡하게 엮어서 정밀하게 계산하고 통제해야 하는 대기업의 사내 인사 및 재무 관리 시스템을 만들 때 매우 적합합니다. (물론 최근에는 WebFlux 같은 논블로킹 모델이나 Java 21의 가상 스레드 도입으로 이러한 전통적인 구조의 한계를 극복해 나가고 있긴 합니다만,) 여전히 수많은 점원을 관리하고 교대하는 데 상대적으로 시스템적 비용이 많이 든다는 점은 분명합니다.한편 Go 언어는 아주 가볍고 작은 작업자들을 엄청나게 많이 만들어내는 방식을 사용하여 압도적인 동시 처리 성능과 빠른 실행 속도를 자랑합니다. 이미 클라우드 인프라나 마이크로서비스 생태계에서는 주류로 자리 잡았지만, 아직 일반적인 웹 비즈니스 로직 개발에 있어서는 Java나 Node.js에 비해 실무 현장에서 이 언어를 능숙하게 다룰 줄 아는 백엔드 개발자를 채용하기 어렵다는 현실적인 제약이 있습니다.여기까지 흐름을 따라오셨다면, 그렇다면 컴퓨터가 가장 잘 이해하는 빠르고 근본적인 언어인 C나 C++로 처음부터 모든 걸 만들면 제일 좋은 것 아닌가 하는 날카로운 의문이 생기실 수 있습니다. 실제로 Node.js를 돌아가게 하는 심장부인 V8 엔진과 내부 보조 작업 공간인 libuv도 모두 C와 C++로 만들어져 있습니다. 이론적으로 이 언어들을 사용해 앞서 칭찬했던 멈추지 않는 Non-blocking 구조를 개발자가 직접 완벽하게 만들어낸다면 Node.js보다 훨씬 빠르고 메모리도 적게 쓸 것입니다.하지만 현실적으로 이를 바닥부터 직접 구현한다는 것은 불가능에 가깝습니다. Node.js의 기반이 되는 libuv 엔진은 전 세계 최고 수준의 천재적인 개발자들이 수많은 예외 상황과 엣지 케이스를 치열하게 고민하고 고려해서 만들었으며, 지금 이 순간에도 꾸준히 버전 업데이트를 진행하며 정교하게 다듬어가고 있는 거대한 시스템입니다. 개인이나 작은 회사의 개발자들이 이 사람들이 수십 년간 깎아온 수준의 안정성과 성능을 가진 엔진을 직접 만들어서 서비스를 런칭한다는 것은 사실상 불가능합니다.게다가 현업에서 비즈니스 웹 서버를 C나 C++로 직접 짜지 않는 가장 큰 이유는 비즈니스의 기회비용과 개발 생산성 때문입니다. 현대 IT 기업은 아이디어를 빠르게 프로그램으로 구현해서 시장에 출시하고 고객의 반응을 살피는 것이 생명입니다. 개발팀이 C++로 석 달 동안 밤새워 메모리를 일일이 관리해가며 서버 응답 속도 100점짜리 서비스를 만드는 것보다, Node.js를 이용해 2주일 만에 속도 80점짜리 서비스를 빠르게 출시해서 먼저 시장을 선점하고 수익을 내는 것이 비즈니스 관점에서는 압도적인 승리입니다.또한 C나 C++에서는 개발자의 아주 작은 실수로 사용이 끝난 메모리를 제때 비우지 않아 메모리 누수가 발생하거나 허락되지 않은 엉뚱한 공간을 건드리면, 멀쩡히 돌아가던 서버 전체가 치명적인 에러와 함께 강제로 꺼져버립니다. 반면 Node.js는 JavaScript라는 사람이 쓰기 편한 언어를 사용하므로, 안 쓰는 메모리 찌꺼기들을 시스템이 알아서 주기적으로 청소해 주는 Garbage Collector 기능이 탑재되어 있어 훨씬 안전하고 유연하게 돌아갑니다. 여기에 전 세계에서 가장 거대한 부품 창고인 npm 생태계가 더해져, 데이터베이스 연결이나 소셜 로그인 연동 등 당장 필요한 거의 모든 기능이 이미 만들어진 무료 오픈소스로 존재합니다. C++로 바닥부터 바퀴를 깎아야 하는 상황과는 확연히 다르죠. 즉, 기계가 이해하기 가장 좋은 언어와 인간이 비즈니스 문제를 가장 빠르게 풀기 좋은 언어 사이에서 완벽한 타협점을 찾은 결과가 바로 Node.js입니다.결국 현업 개발 세계에서 무조건 이 기술이 정답이다라는 말은 결코 존재하지 않습니다. 모든 Framework와 언어는 저마다 하나를 얻으면 반드시 다른 하나를 잃는 Trade-off 관계를 가지고 있습니다.물론 기술적으로 억지를 부리자면, Java Spring 환경에서도 설정을 조작해 싱글 스레드처럼 동작하게 만들 수 있고, 반대로 Node.js에서도 워커 스레드(Worker Thread)라는 기능을 사용해 멀티 스레딩을 흉내 낼 수는 있습니다. 하지만, 결국 비즈니스의 개발 생산성과 서버 운영의 효율성을 고려했을 때 각 프레임워크가 태생적으로 가장 잘하는 것을 하도록 두는 것이 맞습니다. 억지로 맞지 않는 옷을 입히느라 끙끙대는 것보다, 각자의 장점을 살려 적재적소에 배치하는 것이 현명하죠. Node.js는 압도적인 개발 생산성과 효율적인 네트워크 I/O 처리 능력을 얻은 대신 무거운 수학적 연산 능력을 양보했고, C와 C++은 극한의 컴퓨터 성능을 얻은 대신 사람의 개발 편의성과 시스템의 런타임 안전성을 양보한 것처럼 말입니다.우리가 지향해야 하는 궁극적인 목적은 유행하는 특정 기술을 맹신하는 것이 아니라, 현재 접속하는 사용자의 규모, 개발팀의 실력, 비즈니스 목표 등 눈앞에 주어진 상황과 서버 인프라의 물리적 한계를 명확히 인식하고 그 상황에 가장 알맞은 도구를 냉정하게 판단하여 선택하는 시야를 갖추는 것입니다.더 나아가 어떤 화려하고 새로운 Framework를 쓰더라도 그 밑바탕이 되는 동작 원리는 결국 같습니다. 운영체제가 어떻게 컴퓨터의 한정된 자원을 여러 프로그램에게 공평하게 빌려주는지인 System Call부터, 깊숙한 Kernel 수준에서 파일과 네트워크 데이터를 제어하는 방식, 그리고 메모리라는 임시 바구니인 Buffer에 데이터를 담아 끊임없는 물결인 Stream처럼 흘려보내고 받아내는 컴퓨터 과학의 근본 원리는 Java든 Python이든 C++이든 모두 완벽하게 동일합니다.우리가 이 수업에서 Node.js라는 직관적인 도구를 선택한 이유도 결국 이 수면 아래에 숨겨진 컴퓨터 과학의 근본 원리를 파헤치기 위함입니다. 단순히 도구의 껍데기 같은 사용법을 외우는 것을 넘어 이 본질적인 뼈대를 이해하게 된다면, 훗날 Node.js가 아닌 완전히 새로운 도구가 시장을 지배하더라도 아주 손쉽고 빠르게 그 기술의 핵심을 장악하실 수 있을 것입니다.감사합니다!
- Likes
- 0
- Comments
- 2
- Viewcount
- 53
Q&A
ai가 만든 강의인가요?
안녕하세요 고건호 님, 먼저 강의에 관심을 가져주시고 문의를 남겨주셔서 진심으로 감사드립니다. 우려하시는 부분에 대해 오해를 풀어드리고자 솔직하고 상세하게 답변을 드리고자 합니다.먼저, 본 강의는 TTS가 아닌 제 목소리로 직접 녹음한 것이 맞습니다. 다만, 강의를 한 번에 연속해서 녹음하는 것이 아니라 틈틈이 시간을 내어 여러 차례로 나누어 녹음을 진행했습니다. 예를 들어 10분짜리 영상이라면 2분은 먼저 찍고, 또 2분은 시간이 날 때 찍는 방식이었습니다. 그러다 보니 녹음할 때마다 주변 환경이나 제 컨디션에 따라 목소리의 톤과 질감이 많이 달라지는 경우가 많았습니다. 이를 보완하기 위해 최종적으로 녹음을 완료하고 전체적인 음성의 높낮이와 톤을 균일하게 맞추는 과정을 거쳤는데, 이 과정에서 음성이 다소 불편하고 기계음처럼 들리게 된 것 같습니다. 학습하시면서 불편함을 느끼셨다면 너른 양해를 부탁드립니다.또한, 강의 자료를 대충 AI로 만들었다는 말씀에 대해서는 절대 그렇지 않다는 점을 분명히 말씀드리고 싶습니다. 이 강의는 'AI 딸깍의 시대 원리로 돌파하는 Node.js와 CS'라는 이름에 걸맞게, 얕은 지식이 아닌 깊은 원리를 전달하기 위해 오랜 기간 철저하게 준비했습니다. 수많은 공식 문서와 신뢰할 수 있는 기술 블로그들을 꼼꼼히 살펴보고 최대한 논란의 여지가 없도록 심혈을 기울였습니다. 수강생분들의 시각적 이해를 돕기 위해 일부 이미지 요소에 AI 도구를 활용하는 경우는 있으나, 전체 슬라이드쇼는 제가 직접 하나하나 꼼꼼하게 기획하고 제작했습니다. 더불어 강의 영상뿐만 아니라 내용 전반을 정리한 노트까지 하나부터 열까지 제 손을 거쳐 다듬었습니다. 고건호 님께서 이 강의를 위해 지불해 주신 비용, 그 이상의 값어치를 반드시 얻어 가실 수 있도록 진심을 다해 준비한 강의입니다.확인해 보니 아직 1강밖에 수강하지 않으신 것으로 보이는데, 만약 이 강의 수강을 계속 희망하신다면 꼭 여러 강의를 이어서 수강해 보시는 것을 권장해 드립니다. 그럼에도 불구하고 강의가 만족스럽지 않고 도저히 납득이 되지 않으신다면, 제가 제 개인 사비로 전액 환불해 드리겠습니다. 환불 및 기타 문의를 위한 제 이메일 주소는 jeony0535@naver.com입니다.끝으로 강의를 들으시면서 내용에 대해 궁금한 점이 생기신다면 언제든 편하게 질문 남겨주세요. 상세히 답변해 드리겠습니다. 감사합니다.
- Likes
- 0
- Comments
- 1
- Viewcount
- 106
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가 가장 안전한 위치에서 통제하게 되는 것입니다.탄탄한 컴퓨터 공학적 기반 지식과 네트워크 흐름에 대한 이해를 바탕으로 아키텍처를 설계하고 계시니, 분명 구조적으로 아주 훌륭하고 견고한 프로젝트가 완성될 것이라 확신합니다. 프로젝트를 진행하시면서 더 깊이 있는 고민이 생기거나 궁금한 점이 있다면 언제든지 편하게 질문 남겨주시길 바랍니다. 져니님의 멋진 프로젝트 완성을 진심으로 응원합니다.참고해주세요!
- Likes
- 0
- Comments
- 2
- Viewcount
- 55




