nhcodingstudio
@nhcodingstudio
Học viên
938
Đánh giá khóa học
48
Đánh giá khóa học
4.8
Bài viết
Hỏi & Đáp
강의랑 강의 자료랑 내용이 다른 것 같아요
안녕하세요 TAESUN님, 먼저 강의 수강 중 자료와 영상 내용이 달라 혼란을 드려 대단히 죄송합니다.말씀해주신 대로 강의 영상에서는 폼을 입력받는 화면(ejs)과 이를 보여주는 라우트(/add)를 다루고 있는데, 제공해 드린 깃허브 링크와 텍스트 자료에는 해당 부분이 누락되어 있는 것을 확인했습니다. 꼼꼼하게 확인해주신 덕분에 빠르게 파악할 수 있었습니다. 정말 감사합니다.누락된 새 메모 작성 폼 라우트(GET /add)와 EJS 뷰 파일(views/new-memo.ejs) 코드를 아래와 같이 정리해 드립니다. 이 코드를 기존 프로젝트에 추가하시면 정상적으로 실습하실 수 있습니다.1. routes/memos.js 수정 (GET 라우트 추가)기존 코드에 router.get("/add", ...) 부분이 추가되어야 합니다. 이 코드가 있어야 사용자가 메모를 입력할 수 있는 화면(HTML 폼)을 볼 수 있습니다.const express = require("express"); const { v4: uuidv4 } = require("uuid"); const fs = require("fs"); const path = require("path"); const DATA_FILE = path.join(__dirname, "..", "data", "memos.json"); const router = express.Router(); // [추가된 부분] 새 메모 작성 폼 페이지 렌더링 (GET /memos/add) router.get("/add", (req, res) => { res.render("new-memo"); // views/new-memo.ejs 파일을 찾아서 응답 }); // 새 메모 작성 API (POST /memos) router.post("/", (req, res) => { const { title, content, userId } = req.body; const memos = JSON.parse(fs.readFileSync(DATA_FILE)); const newMemo = { id: uuidv4(), title, content, userId, }; memos.push(newMemo); fs.writeFileSync(DATA_FILE, JSON.stringify(memos, null, 2)); // 성공 시 목록 페이지나 상세 페이지로 리다이렉트하는 것이 일반적이나, // API 테스트 중이라면 JSON 응답을 유지해도 됩니다. res.status(201).json(newMemo); }); module.exports = router; 2. views/new-memo.ejs 생성 (누락된 파일)views 폴더 안에 new-memo.ejs 파일을 새로 만드시고 아래 코드를 작성해 주세요. 새 메모 작성 새 메모 작성하기 제목: 내용: 작성자 ID: 메모 저장 목록으로 돌아가기 3. 참고 사항 (index.js)EJS 템플릿을 사용하기 위해서는 index.js에 뷰 엔진 설정이 되어 있어야 합니다. 만약 설정되어 있지 않다면 아래 코드를 app.use 부분 윗줄에 추가해 주세요.// index.js app.set("view engine", "ejs"); // 뷰 엔진을 ejs로 설정 app.set("views", path.join(__dirname, "views")); // 뷰 파일 경로 설정 학습 흐름이 끊기게 해 드려 다시 한번 죄송한 마음을 전합니다. 해당 내용은 강의 자료에도 즉시 업데이트하여 다른 분들도 혼란을 겪지 않도록 조치하겠습니다.실습하시면서 추가로 궁금한 점이나 잘 안되는 부분이 있다면 언제든 편하게 질문 남겨주세요!
- 0
- 1
- 16
Hỏi & Đáp
로그인과 로그아웃 처리 강의 2:00 질문 드려요
안녕하세요 TAESUN님, 질문 주신 res.cookie 설정에서 httpOnly: true 옵션은 웹 보안, 특히 사용자 인증 정보를 다룰 때 가장 핵심이 되는 방어 기제 중 하나이기에 그 원리를 깊이 이해하고 넘어가는 것이 좋습니다.우선 TAESUN님께서 언급하신 ‘브라우저 쿠키’와 ‘클라이언트 사이드 스크립트 쿠키’가 별개의 저장소나 다른 종류의 쿠키를 의미하는 것은 아니라는 점을 먼저 짚고 넘어가야 하는데, 이는 동일한 쿠키 저장소에 위치하지만 자바스크립트를 통한 ‘접근 권한’이 있느냐 없느냐의 차이로 이해하시는 것이 정확합니다. 기본적으로 브라우저 콘솔에서 document.cookie를 입력했을 때 조회가 가능한 쿠키들은 자바스크립트로 언제든지 값을 읽거나 탈취할 수 있는 상태인데, httpOnly: true 옵션을 붙여서 서버가 쿠키를 내려주게 되면 브라우저는 이 쿠키를 저장하되 document.cookie와 같은 자바스크립트 명령어로 조회하는 것을 기술적으로 차단하게 되며 오직 HTTP 통신(서버로 요청을 보낼 때) 상에서만 이 쿠키를 포함시켜 전송하게 됩니다.우리가 이렇게까지 자바스크립트의 접근을 막으려 하는 가장 큰 이유는 바로 XSS(Cross Site Scripting)라고 불리는 교차 사이트 스크립팅 공격 때문입니다. 만약 해커가 게시판이나 댓글 등을 통해 악성 자바스크립트 코드를 웹페이지에 심어놓는 데 성공했다고 가정했을 때, 일반적인 사용자가 그 페이지에 접속하여 해당 스크립트가 실행되는 순간 해커는 사용자의 브라우저 저장소에 접근할 수 있게 됩니다. 이때 로그인 토큰이 담긴 쿠키에 자바스크립트 접근이 허용되어 있거나 혹은 자바스크립트로 접근이 매우 자유로운 로컬 스토리지(LocalStorage)나 세션 스토리지(SessionStorage)에 토큰을 저장해 두었다면, 해커는 아주 손쉽게 사용자의 인증 토큰(JWT 등)을 탈취하여 그 사용자 행세를 할 수 있게 됩니다.흔히 프론트엔드 개발 시 편의성을 이유로 로컬 스토리지에 JWT와 같은 인증 토큰을 저장하는 경우가 많지만, 로컬 스토리지는 태생적으로 자바스크립트로 데이터를 다루기 위해 만들어진 공간이기에 XSS 공격에 무방비로 노출될 수밖에 없다는 치명적인 단점을 가지고 있습니다. 반면에 쿠키는 httpOnly 설정을 통해 자바스크립트의 접근을 원천 봉쇄할 수 있으므로, 설령 해커가 XSS 공격을 통해 스크립트를 실행시킨다 하더라도 브라우저 내부에 숨겨진 인증 쿠키의 값 자체는 읽어낼 수 없어 토큰 탈취를 방지할 수 있게 됩니다.결국 우리가 JWT와 같은 중요한 인증 토큰을 다룰 때 굳이 쿠키를 사용하고 httpOnly 옵션을 켜는 이유는, 브라우저가 가진 ‘요청 시 자동으로 쿠키를 포함하여 서버로 전송하는 특성’을 활용해 개발의 편의를 챙기는 동시에, ‘스크립트 접근 차단’이라는 강력한 보안 기능을 활용하여 해커의 공격으로부터 사용자의 계정 정보를 안전하게 지키기 위함입니다. 따라서 질문하신 코드는 서버가 클라이언트에게 "이 토큰은 인증에 필요하니 저장해두고 나한테 요청 보낼 때만 보여줘, 대신 자바스크립트로는 절대 열어보지 못하게 막아둬"라고 브라우저에게 명령을 내리는 과정이라고 이해하시면 전체적인 인증 처리의 흐름이 명확히 보이실 겁니다.마지막으로 실무에서 JWT와 같은 토큰을 실제로 어디에 가장 많이 보관하는지에 대한 팁을 드리자면, 단순히 개발하기 편하고 구현이 빠르다는 이유로 ‘로컬 스토리지’에 Access Token을 저장하는 경우가 꽤 흔하지만, 앞서 말씀드린 보안 이슈 때문에 금융권이나 대형 서비스와 같이 보안이 중요한 환경에서는 지양하는 추세입니다. 가장 보안적으로 견고하다고 평가받는 방식은 Refresh Token은 httpOnly 쿠키에 담아 XSS로부터 보호하고, Access Token은 메모리(자바스크립트 변수)에 담아 관리하는 방식 혹은 Access Token과 Refresh Token 모두를 httpOnly 쿠키로 관리하는 방식입니다. 물론 쿠키를 사용할 경우 CSRF(사이트 간 요청 위조) 공격에 대한 별도의 방어 처리가 필요해지지만, 토큰 자체가 탈취되는 최악의 상황을 막기 위해 실무에서는 httpOnly 쿠키를 적극적으로 활용하는 것을 권장합니다.참고해주세요!
- 0
- 2
- 20
Hỏi & Đáp
27과목 ejs로 todo list 만들기에서 todo를 여러 개 항목 만들었을 때
안녕하세요 Edwards님, 올려주신 내용을 확인해보니 어떤 상황인지 바로 이해가 되었습니다. 결론부터 말씀드리면 이는 코드가 틀렸다기보다는, 컴퓨터에게 '누구'를 건드려야 할지 정확히 지시하지 않아 발생하는 자연스러운 현상입니다.우선 클릭 시 엉뚱하게 맨 뒤 항목만 반응하거나 동작이 꼬이는 현상의 근본적인 원인을 살펴보면, 이는 브라우저가 각 항목을 구별할 수 있는 '고유 식별자(Unique ID)'가 없기 때문입니다. HTML과 웹 표준의 규칙상 하나의 웹 페이지 내에서 id라는 속성값은 주민등록번호처럼 유일해야 합니다. 하지만 현재 작성하신 코드의 흐름을 보면 반복문(forEach)이 돌면서 생성되는 모든 체크박스와 라벨이 똑같은 id 이름을 갖게 되는 구조입니다. 예를 들어 교실에 '철수'라는 학생이 5명 있을 때 선생님이 "철수야!"라고 부르면 누구를 부르는지 알 수 없어 혼란이 생기는 것과 같습니다. 브라우저 또한 마찬가지여서, 사용자가 첫 번째 항목의 라벨을 클릭하더라도 내부적으로는 "이 ID를 가진 요소를 찾아라"라는 명령을 수행할 때 중복된 ID들 사이에서 길을 잃고, 브라우저의 처리 방식에 따라 맨 마지막에 생성된 요소를 선택하거나 첫 번째 요소만 바라보는 등 의도치 않은 동작을 하게 되는 것입니다.이 문제를 근본적으로 해결하기 위해서는 서버인 app.js에서 데이터가 생성될 때부터 각 할 일(Todo)마다 세상에 하나뿐인 고유한 ID를 부여해주어야 합니다. 기존에는 사용자가 입력한 텍스트(문자열)만 단순히 배열에 담았다면, 이제는 텍스트와 ID, 그리고 완료 여부까지 포함된 '객체(Object)' 형태로 데이터를 관리해야 합니다. 아래 코드는 이러한 로직을 반영하여 수정한 app.js의 전체 내용입니다.const path = require("path"); const express = require("express"); const app = express(); // 데이터를 저장할 배열입니다. // 기존에는 문자열만 저장했지만, 이제는 {id, content, isDone} 형태의 객체가 저장됩니다. let todos = []; app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static("public")); app.set("view engine", "ejs"); app.set("views", path.join(__dirname, "views")); // 메인 페이지 라우트: ID가 포함된 todo 목록을 화면(ejs)으로 전달합니다. app.get("/", (req, res) => { res.render("index.ejs", { todos: todos }); }); // 할 일 추가 라우트 (가장 중요한 변경점입니다) app.post("/", (req, res) => { const todoContent = req.body.todo; // 빈 내용이 들어오는 것을 방지하기 위한 유효성 검사입니다. if (todoContent) { const newTodo = { // 1. 고유 ID 생성: Date.now()는 현재 시간을 밀리초 단위 숫자로 반환하므로 // 겹칠 확률이 매우 낮아 간단한 ID로 쓰기에 적합합니다. id: Date.now(), // 2. 사용자가 입력한 할 일 내용 content: todoContent, // 3. 완료 여부 상태 (기본값은 false) isDone: false }; todos.push(newTodo); } res.redirect("/"); }); // 삭제 기능 추가: ID를 기준으로 특정 항목만 정확히 삭제하는 라우트입니다. app.post("/delete/:id", (req, res) => { // URL 파라미터로 넘어온 ID는 문자열이므로 숫자로 변환해줍니다. const deleteId = parseInt(req.params.id); // 배열의 filter 함수를 사용해 해당 ID가 아닌 것들만 남깁니다 (즉, 해당 ID 삭제). todos = todos.filter(todo => todo.id !== deleteId); res.redirect("/"); }); app.listen(3000, () => { console.log("Server is Listening on port 3000"); }); 서버에서 데이터를 체계적으로 관리할 준비가 되었다면, 이제 사용자에게 보여지는 화면인 views/index.ejs 파일에서도 이 ID를 적극적으로 활용해야 합니다. 단순히 데이터를 뿌려주는 것을 넘어, HTML 태그의 속성에 서버에서 받은 ID를 결합하여 모든 태그가 서로 다른 이름을 갖도록 만들어야 합니다. 아래 코드는 수정된 index.ejs의 내용으로, id 속성과 for 속성에 ID를 동적으로 할당하는 방식에 주목해주시면 됩니다. To-Do List /* 완료된 항목에 취소선을 긋는 스타일입니다 */ .completed { text-decoration: line-through; color: gray; } My To-Do List 추가 "> "> " method="POST" style="display:inline;"> 삭제 이렇게 코드를 수정하시면 각 항목이 자신만의 고유한 주소(ID)를 갖게 되어 클릭 시 정확하게 해당 항목만 반응하게 됩니다. 여기서 한 걸음 더 나아가 실제 현업 개발 환경에서는 이 로직을 어떻게 발전시키는지 실무적인 관점도 덧붙여 드리겠습니다.우선 ID 생성 방식에 있어 학습용으로는 Date.now()가 훌륭하지만, 초당 수천 건의 요청이 들어오는 대규모 서비스에서는 우연히 같은 밀리초에 요청이 들어와 ID가 충돌할 수 있습니다. 그래서 실무에서는 UUID(범용 고유 식별자) 라이브러리를 사용하여 100%에 가깝게 중복을 방지합니다. 또한 현재 방식은 서버 메모리(변수)에 데이터를 저장하기 때문에 서버가 재시작되면 모든 데이터가 사라집니다. 이를 방지하기 위해 MySQL이나 MongoDB 같은 데이터베이스를 연동하여 데이터를 영구적으로 보존하는 것이 필수적입니다. 마지막으로 사용자 경험(UX) 측면에서, 현재는 항목을 지우거나 추가할 때마다 페이지가 새로고침되어 깜빡이는 현상이 있습니다. 실무에서는 AJAX(비동기 통신) 기술이나 React, Vue와 같은 프런트엔드 프레임워크를 도입하여, 페이지 전체를 다시 로드하지 않고 필요한 부분만 부드럽게 업데이트하는 방식을 사용합니다.지금 겪으신 이 과정은 단순한 오류 해결이 아니라, 웹이 데이터를 어떻게 식별하고 관리하는지에 대한 가장 핵심적인 원리를 이해하는 과정입니다. 안내해 드린 코드를 차근차근 적용해 보시면서 데이터의 흐름을 느껴보시면 실력이 한층 더 성장하실 것입니다. 도움이 되셨기를 바랍니다.감사합니다!
- 0
- 2
- 26
Hỏi & Đáp
학습 방향성
안녕하세요 코딩님 고민이 많으실 것 같습니다.강의를 끝까지 듣고 완벽하게 준비해서 시작해야 할지, 아니면 부족하더라도 지금 당장 부딪혀야 할지 망설여지는 그 마음, 개발자라면 누구나 한 번쯤 겪는 과정이기에 깊이 공감합니다. 하지만 결론부터 말씀드리면 Express Part 2나 타 강의의 완강을 기다리지 마시고 지금 당장 첫 번째 프로젝트를 시작하시는 것이 훨씬 효율적입니다. 강의는 수영을 배우기 위한 이론서와 같아서 아무리 완벽하게 이해했다고 느껴도 막상 물에 들어가면 숨 쉬는 것조차 버거운 것이 당연하며, 코딩님께서 걱정하시는 '배운 내용이 기억나지 않는 현상'은 10년 차 시니어 개발자에게도 일상적인 일이니 그 부분은 전혀 염려하지 않으셔도 됩니다. 오히려 프로젝트를 진행하며 마주치는 에러와 비효율적인 코드들이야말로 코딩님이 왜 자료구조를 공부해야 하고, 왜 특정 알고리즘이 필요한지를 뼈저리게 느끼게 해주는 가장 훌륭한 스승이 될 것입니다.특히 첫 번째 프로젝트로 계획하신 바닐라 JS와 로우 쿼리(Raw Query)를 활용한 개발은 요즘처럼 프레임워크가 모든 것을 추상화해주는 시대에 아키텍트 관점에서 볼 때 '기본기(Fundamental)'를 다질 수 있는 가장 훌륭한 선택인데, 이는 React나 Nest.js 같은 도구들이 결국 자바스크립트와 HTTP 프로토콜, 그리고 SQL 위에서 돌아가는 것이기에 직접 DOM(Document Object Model)을 조작해보며 브라우저의 렌더링 과정인 Reflow와 Repaint가 성능에 미치는 영향을 체감해보고, ORM 없이 직접 SQL을 작성하며 인덱스(Index)가 쿼리 성능에 어떤 차이를 만드는지, 조인(Join) 연산이 데이터베이스 부하에 어떻게 작용하는지를 경험해보는 것은 훗날 대규모 트래픽을 처리할 때 남들과 차별화되는 강력한 무기가 되기 때문입니다.더 나아가 두 번째, 세 번째 프로젝트를 진행하시면서 Docker나 RabbitMQ, Redis 같은 다양한 인프라 및 미들웨어 도구들을 필연적으로 마주하게 될 텐데, 이때 단순히 '남들이 쓰니까' 혹은 '강의에서 시키니까' 사용하는 것과 그 원리를 이해하고 쓰는 것은 천지 차이입니다. 개발을 하다 보면 "도대체 왜 Docker 컨테이너 간 통신이 안 되는 거지?" 혹은 "RabbitMQ에 쌓인 메시지가 왜 순서대로 처리되지 않지?"라는 근본적인 의문이 생기는 순간이 반드시 오는데, 이 의문을 해결하고 넘어갈 수 있느냐는 전적으로 운영체제의 네트워크 네임스페이스나 프로세스 스케줄링, 자료구조의 큐(Queue)와 같은 CS 기반 지식의 유무에 달려 있습니다. 만약 이러한 베이스가 없다면 단순히 블로그 코드를 복사해서 해결하는 '도구 사용자'에 머물게 되고, 어떤 상황에 어떤 기술을 적재적소에 배치해야 하는지 판단하는 아키텍트의 시야는 가질 수 없게 됩니다.이후 Next.js와 Nest.js, 그리고 AWS 인프라를 본격적으로 도입하실 때에도 단순히 최신 기술을 사용했다는 것에 만족하지 마시고 시스템 아키텍처 관점에서 깊이 있는 고민을 병행하셔야 하는데, 예를 들어 대규모 트래픽을 가정했을 때 Node.js의 싱글 스레드 이벤트 루프(Event Loop) 모델이 CPU 집약적인 작업에서 어떤 취약점을 가지는지 이해하고 이를 해결하기 위해 워커 스레드(Worker Threads)나 메시지 큐(Message Queue) 같은 아키텍처 패턴을 도입해보거나, 여러 사용자가 동시에 재고를 차감하는 동시성 이슈(Concurrency Issue)가 발생했을 때 데이터베이스의 트랜잭션 격리 수준(Isolation Level)이나 비관적/낙관적 락(Lock)을 활용해 데이터의 무결성을 보장하는 방법 등을 고민해보는 것이 진짜 실력입니다. 이때 Sentry나 Datadog 같은 모니터링 툴 또한 단순히 에러 로그를 수집하는 것을 넘어, 메모리 릭(Memory Leak)이나 슬로우 쿼리(Slow Query)를 추적하여 운영체제 레벨의 리소스 관리와 네트워크 TCP/IP 핸드셰이크 과정에서의 지연 시간을 최적화하는 데 활용되어야 합니다.결국 이 모든 과정에서 가장 근본이 되는 것은 자료구조, 알고리즘, 운영체제, 네트워크와 같은 CS 기초 체력이며, 이는 단순히 코딩 테스트를 통과하기 위한 암기식 공부가 아니라 "왜 이 상황에서 배열(Array) 대신 해시 테이블(Hash Table)을 사용하여 데이터 조회 성능을 O(n)에서 O(1)로 최적화했는가?", "왜 프로세스(Process) 대신 스레드(Thread)를 사용하여 컨텍스트 스위칭(Context Switching) 비용을 줄였는가?"와 같은 기술적인 의사결정의 근거를 마련하기 위함입니다. 프레임워크나 라이브러리는 3년, 5년이 지나면 바뀌거나 사라질 수 있지만 이러한 컴퓨터 과학의 원리는 변하지 않으며 시니어 개발자가 되었을 때 시스템의 안정성과 확장성을 담보하는 가장 강력한 무기가 됩니다.마지막으로 최근의 개발 환경에서 빼놓을 수 없는 AI 활용 역량 또한 결국 이러한 근본적인 시야가 갖춰졌을 때 비로소 빛을 발하게 됩니다. 지금은 AI를 통해 어떤 제품이든 순식간에 뚝딱 만들어낼 수 있는 시대이지만, 그렇게 생성된 코드의 품질을 관리하고 전체적인 아키텍처의 흐름 속에 자연스럽게 녹여내는 역량은 오직 시스템의 전반적인 메커니즘을 꿰뚫어 볼 수 있는 사람만이 가능합니다. AI가 짜준 코드가 왜 성능 병목을 일으키는지, 혹은 보안상 어떤 허점이 있는지 판단하지 못한 채 결과물만 내놓는다면 그것은 모래 위에 성을 쌓는 것과 같으므로, AI를 단순히 코드를 대신 써주는 도구가 아니라 자신의 설계 철학을 실현하는 강력한 보조 수단으로 다루기 위해서라도 더욱 근본적인 학습에 집중하시기를 권장합니다.그러니 코딩님, 지금 당장 완벽한 코드를 짜지 못할까 봐, 혹은 보안에 취약한 코드를 만들까 봐 느끼는 부담감 때문에 강의 수강으로 준비 시간을 늘리기보다는, 과감하게 첫 번째 프로젝트의 첫 줄을 작성해 보시기를 권장합니다. 처음에는 스파게티 코드와 N+1 문제로 가득 찬 쿼리, 보안에 취약한 인증 로직을 작성하게 될지라도, 그것이 시스템에 어떤 악영향을 미치는지 직접 겪어보고 깨져보는 경험이 없다면 리팩토링의 필요성도, 클린 아키텍처의 가치도 결코 깨달을 수 없기 때문입니다. 현업에서 인정받는 개발자는 처음부터 완벽한 코드를 짜는 사람이 아니라, 엉망인 코드에서 출발했더라도 탄탄한 CS 지식과 AI 활용 능력을 바탕으로 병목을 찾아내고, 논리적인 근거를 가지고 코드를 개선해 나가며 결국에는 견고한 시스템을 만들어내는 사람이라는 점을 꼭 기억해 주셨으면 합니다.참고해주세요!
- 0
- 1
- 37
Hỏi & Đáp
useRef를 활용한 이전 상태 추적 시 발생하는 ESLint 에러(react-hooks/refs)에 대해 질문드립니다.
안녕하세요 관태님! 질문 주셔서 감사합니다. 관태님께서 마주하신 react-hooks/refs 에러인 "Cannot access refs during render"는 React 18 이후 동시성 모드(Concurrent Mode)가 도입되면서 더욱 엄격해진 규칙인데, 이는 렌더링 단계(Render Phase)가 사진을 찍는 순간처럼 순수해야 하기 때문입니다. React의 렌더링 과정은 크게 컴포넌트 함수를 호출하고 JSX를 만들어내는 계산 단계인 렌더 단계와 실제 DOM에 변경 사항을 반영하고 useEffect 등을 실행하는 적용 단계인 커밋 단계로 나뉘는데, 여기서 중요한 점은 렌더 단계는 언제든 멈추거나, 취소되거나, 여러 번 재실행될 수 있다는 점입니다. 그런데 useRef는 값이 바뀌어도 렌더링을 유발하지 않는 가변적인 저장소이기 때문에 만약 렌더링 도중에 ref.current 값을 읽거나 쓰게 되면 React가 렌더링을 잠깐 멈춘 사이 값이 바뀌어버려 같은 렌더링 사이클 안에서도 화면의 위쪽은 값 A를 보여주고 아래쪽은 값 B를 보여주는 '티어링(Tearing)' 현상이 발생할 수 있어 ESLint가 이를 엄격하게 경고하는 것입니다.실무에서는 이 문제를 해결하고 재사용성을 높이기 위해 usePrevious라는 커스텀 훅을 만들어 사용하는 것이 표준이며, 이 훅의 핵심은 Ref의 업데이트 시점을 렌더링 도중이 아닌 렌더링이 다 끝난 후인 이펙트 단계로 미루는 것입니다. 기존 코드에서 직접 ref.current를 읽는 부분을 커스텀 훅으로 분리하면 다음과 같이 깔끔하고 안전해집니다.import React, { useState, useEffect, useRef } from "react"; // [실무 패턴] 별도의 유틸리티 파일(hooks/usePrevious.js)로 분리해서 쓰는 것을 추천합니다. function usePrevious(value) { const ref = useRef(); // 1. 렌더링이 다~ 끝나고 화면이 그려진 뒤(Commit Phase)에 값을 저장합니다. useEffect(() => { ref.current = value; }, [value]); // 2. 렌더링 도중에는 아직 useEffect가 실행되기 전이므로 // ref에는 '이전 렌더링'에서 저장해둔 값이 들어있습니다. return ref.current; } export default function ExchangeRateTracker() { const [rate, setRate] = useState(1300); // 커스텀 훅을 사용해 '이전 값'을 안전하게 가져옵니다. // 내부적으로 useEffect를 쓰기 때문에 렌더링 흐름을 방해하지 않습니다. const prevRate = usePrevious(rate); const isFirstRender = useRef(true); useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; return; } // 환율 변동 알림 alert(`환율이 ${rate}원으로 변동되었습니다.`); }, [rate]); return ( 💹 실시간 환율 추적기 현재 환율: {rate}원 {/* 이제 prevRate를 일반 변수처럼 편하게 사용하면 됩니다 */} 직전 환율: {prevRate !== undefined ? `${prevRate}원` : "데이터 수집 중..."} 변동: { prevRate === undefined ? "-" : rate > prevRate ? "▲ 상승" : rate setRate(prev => prev + 10)}>환율 올리기 (+10) setRate(prev => prev - 10)}>환율 내리기 (-10) ); } 이 패턴이 안전한 이유는 작동하는 타이밍 덕분인데, 예를 들어 첫 번째 렌더링 시 rate가 1300이라면 usePrevious는 undefined를 반환하고 화면이 그려진 후 useEffect가 실행되어 ref.current에 1300을 저장하게 되며, 이후 rate가 1310으로 바뀌어 두 번째 렌더링이 일어날 때 usePrevious를 호출하면 아직 두 번째 useEffect가 실행되기 전이므로 ref.current에는 아까 저장해둔 1300이 들어있게 되어 이것이 바로 prevRate가 되는 것입니다. 추가로 여기서 한 가지 더 중요한 점은 왜 굳이 useState가 아닌 useRef를 쓰는지에 대한 성능적인 이유입니다. 만약 이전 값을 저장하기 위해 useState를 사용한다면 useEffect 내부에서 setPrevState를 호출하는 순간 리액트는 상태가 바뀌었다고 판단하여 불필요한 재렌더링을 한 번 더 유발하게 되지만, 반면 useRef는 값이 바뀌어도 렌더링을 유발하지 않기 때문에 성능 저하 없이 조용히 데이터를 기록하는 '그림자 저장소' 역할을 수행하기에 가장 적합한 도구입니다. 실무에서는 매번 usePrevious를 직접 구현하기보다 잘 관리되는 훅 라이브러리를 사용하는 경우가 많은데, 대표적으로 가장 방대한 훅 모음집인 react-use나 알리바바에서 만든 고품질 훅 라이브러리인 ahooks가 업계 표준에 가깝습니다. 결론적으로 관태님의 코드가 틀린 것은 아니지만 React의 렌더링 순수성을 지키고 ESLint 경고를 해결하면서 성능까지 챙기기 위해서는 usePrevious 커스텀 훅 패턴으로 로직을 분리하는 것이 가장 우아한 해결책이며, 이 개념까지 이해하신다면 React의 상태 관리를 훨씬 더 효율적으로 하실 수 있을 겁니다.참고해주세요. 감사합니다!
- 0
- 1
- 45
Hỏi & Đáp
Express 에러 처리 관련 질문 드려요.
안녕하세요 TAESUN님, 남겨주신 질문은 단순히 프레임워크의 사용법을 넘어 언어의 동작 원리와 아키텍처 설계까지 닿아 있는 아주 수준 높은 질문입니다. 우선 Express가 비동기 에러를 잡지 못하는 현상(Express 4 기준)은 싱글 스레드 자체의 문제라기보다는 자바스크립트의 비동기 처리 방식과 Express의 설계 시점 간의 차이에서 기인한다고 볼 수 있습니다. Express 4가 만들어질 당시에는 Promise나 async/await가 표준이 아니었기 때문에 Express의 라우터는 비동기 함수가 반환하는 Promise를 기다려주지 않고 바로 다음 로직으로 넘어가 버리거나 리턴해 버리게 됩니다. 따라서 비동기 로직 내부에서 에러가 발생하더라도, 즉 Promise Rejection이 일어나더라도 이미 동기적 실행 흐름이 끝난 Express의 에러 처리 미들웨어는 이를 감지할 수 없게 되어 서버가 멈추거나 타임아웃이 발생하게 되는 것입니다. 이를 해결하기 위해서는 비동기 함수 내부에서 try-catch 블록을 사용하여 에러를 잡은 뒤 반드시 next(err)를 호출하여 Express에게 에러가 발생했음을 명시적으로 알려주어야 하는데, 이를 코드로 확인해보면 다음과 같이 문제 상황과 해결책을 비교해 볼 수 있습니다.// [케이스 1: Express가 잡지 못하는 비동기 에러 (문제 상황)] // 이 코드는 클라이언트에게 아무런 응답을 주지 못하고 서버 콘솔에 Unhandled Rejection 경고만 남긴 채 요청이 타임아웃됩니다. app.get('/async-error', async (req, res, next) => { const user = await User.findById(req.params.id); // 여기서 에러가 발생하면? if (!user) throw new Error('User not found'); res.json(user); }); // [케이스 2: try-catch를 사용한 수동 처리 (해결책)] // 비동기 안에서 발생한 에러를 잡아서 next()로 넘겨주면 Express가 인지합니다. app.get('/async-fixed', async (req, res, next) => { try { const user = await User.findById(req.params.id); if (!user) throw new Error('User not found'); res.json(user); } catch (err) { next(err); // 핵심: 에러를 명시적으로 다음 미들웨어(에러 핸들러)로 넘겨야 함 } }); 반면 Spring 같은 멀티 스레드 기반 프레임워크는 요청마다 독립적인 스레드가 할당되고 해당 스레드 내의 스택 프레임에서 예외가 발생하면 상위 호출 스택으로 예외가 전파되는 구조를 가지기 때문에, 비동기나 동기 여부와 상관없이 트라이-캐치 혹은 전역 예외 처리기가 이를 안정적으로 잡아낼 수 있다는 차이점이 있습니다.두 번째로 질문 주신 에러 처리 미들웨어의 우선순위는 말씀하신 '코드 위치'가 맞는데, 정확히는 코드가 작성된 물리적 거리라기보다 app.use를 통해 등록된 순서가 중요합니다. Express는 미들웨어를 배열 형태의 스택으로 관리하므로 요청이 들어오면 등록된 순서대로 미들웨어를 거치게 되고, 에러가 발생하여 next(err)가 호출되면 그 시점 이후에 등록된 미들웨어 중 인자가 4개(err, req, res, next)인 미들웨어를 찾아 이동하기 때문입니다. 이러한 흐름을 직관적으로 이해하기 위해 next(err)가 호출되었을 때 중간에 있는 일반 미들웨어를 건너뛰고 바로 에러 핸들러로 '점프'하는 과정을 아래 코드로 확인해 볼 수 있습니다.// [미들웨어 스택 및 에러 전파 순서 시각화] // 1. 가장 먼저 등록된 일반 미들웨어 (실행됨 O) app.use((req, res, next) => { console.log('1. 첫 번째 미들웨어: 실행됨'); next(); // 다음 미들웨어로 이동 }); // 2. 에러를 발생시키는 라우터 (실행됨 O) app.get('/test', (req, res, next) => { console.log('2. 라우터: 에러 발생 시도'); // next()에 인자를 넣으면 Express는 이를 에러로 간주합니다. next(new Error('Something went wrong!')); }); // 3. 라우터와 에러 핸들러 사이에 있는 일반 미들웨어 (건너뜀 X) // 중요: 위에서 next(err)가 호출되었기 때문에 이 미들웨어는 무시되고 건너뜁니다. app.use((req, res, next) => { console.log('3. 두 번째 미들웨어: 실행되지 않음 (Skip)'); next(); }); // 4. 에러 처리 미들웨어 (실행됨 O) // Express는 인자가 4개(err, req, res, next)인 미들웨어를 찾아 이곳으로 점프합니다. app.use((err, req, res, next) => { console.log('4. 에러 핸들러: 도착! 에러 메시지 ->', err.message); res.status(500).send('에러 처리 완료'); }); 마지막으로 실무에서의 권장 방식에 대해 말씀드리면, 수강생님이 우려하신 대로 에러 처리 미들웨어가 라우터 사이사이에 흩어져 있는 것은 유지보수 측면에서 최악의 패턴 중 하나로 간주되어 실무에서는 거의 사용하지 않습니다. 일반적으로는 '전역 에러 처리(Global Error Handling)' 패턴을 사용하여 모든 비즈니스 로직을 등록한 후 app.js의 가장 마지막 부분에 단 하나의 전역 에러 처리 미들웨어를 두는 방식을 표준처럼 사용합니다. 이때 라우터 내부에서 발생하는 비동기 에러를 놓치지 않기 위해 express-async-errors 라이브러리를 사용하거나 모든 라우터 함수를 감싸는 래퍼(Wrapper) 함수를 만들어 에러 발생 시 강제로 next(err)를 호출하도록 처리합니다. 또한, 단순히 에러를 잡는 것을 넘어 AppError나 HttpException 같은 커스텀 에러 클래스를 정의하여 에러의 종류와 메시지를 체계적으로 관리하고, 최종적으로 전역 핸들러에서 이 에러 타입에 따라 적절한 응답을 보내거나 Sentry 같은 모니터링 도구로 로그를 전송하는 것이 시니어 레벨에서 설계하는 견고한 에러 처리 아키텍처이며, 이를 실제 코드로 구현하면 다음과 같은 구조가 됩니다.// 1. 커스텀 에러 클래스 정의 (utils/AppError.js) // 실무 팁: Error 클래스를 상속받아 상태 코드와 운영(Operational) 에러 여부를 구분합니다. class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; this.isOperational = true; // 프로그래밍 버그가 아닌, 우리가 예측한 에러임을 표시 } } // 2. 비동기 에러 래퍼 함수 (utils/catchAsync.js) // 실무 팁: 반복되는 try-catch 블록을 제거하기 위한 고차 함수입니다. const catchAsync = fn => { return (req, res, next) => { fn(req, res, next).catch(next); // Promise가 reject되면 자동으로 next(err)를 호출 }; }; // 3. 실제 컨트롤러 사용 예시 (controllers/userController.js) // 코드가 훨씬 깔끔해지고 비즈니스 로직에만 집중할 수 있습니다. const getUser = catchAsync(async (req, res, next) => { const user = await User.findById(req.params.id); if (!user) { // 여기서 던진 에러는 자동으로 Global Handler로 전달됩니다. // 404 같은 구체적인 상태 코드를 지정할 수 있습니다. return next(new AppError('해당 ID의 사용자를 찾을 수 없습니다.', 404)); } res.status(200).json({ status: 'success', data: user }); }); // 4. 전역 에러 처리 미들웨어 (app.js 하단) // 실무 팁: 개발 환경과 운영 환경(Production)의 에러 응답을 다르게 주는 것이 보안상 중요합니다. app.use((err, req, res, next) => { err.statusCode = err.statusCode || 500; err.status = err.status || 'error'; // 개발 환경: 디버깅을 위해 상세한 스택 트레이스 포함 if (process.env.NODE_ENV === 'development') { res.status(err.statusCode).json({ status: err.status, error: err, message: err.message, stack: err.stack }); } else { // 운영 환경: 사용자에게는 정제된 메시지만 전달하고 내부 정보는 숨김 if (err.isOperational) { res.status(err.statusCode).json({ status: err.status, message: err.message }); } else { // 프로그래밍 버그나 라이브러리 에러 등은 로그로 남기고 사용자에겐 일반 메시지 전달 console.error('ERROR 💥', err); // Sentry 등으로 전송 가능 res.status(500).json({ status: 'error', message: '서버 내부에서 문제가 발생했습니다. 잠시 후 다시 시도해주세요.' }); } } }); 답변이 도움이 되었기를 바라며, 실무적인 관점에서 고민하는 모습이 매우 인상적입니다.참고해주세요!
- 0
- 2
- 40
Hỏi & Đáp
commonJS 방식
안녕하세요 TAESUN님, 질문 주셔서 감사합니다!강의에서는 Node.js의 가장 전통적이고 널리 쓰이는 방식인 CommonJS를 기준으로 require 문법을 사용하여 진행했지만, 말씀하신 대로 최신 Node.js 환경에서는 ESM(ECMAScript Modules) 방식을 공식적으로 지원하고 있기 때문에 설정만 조금 바꿔주면 브라우저에서 쓰던 import와 export 문법을 그대로 사용할 수 있습니다. 이를 프로젝트에 적용하기 위해서는 가장 간단한 방법으로 프로젝트의 루트에 있는 package.json 파일 최상단 객체에 "type": "module"이라는 속성을 한 줄 추가해주시면 Node.js가 해당 프로젝트를 ESM 환경으로 인식하게 됩니다.// package.json { "type": "module", // ... 나머지 설정들 } 이렇게 설정을 마치셨다면 이제 기존 강의에서 작성했던 const express = require('express')와 같은 코드를 import express from 'express'와 같은 모던 자바스크립트 형태로 변경하여 작성하실 수 있습니다.import express from 'express'; const app = express(); 다만 Express로 서버를 구축할 때 주의하셔야 할 중요한 차이점이 하나 있는데, CommonJS 환경에서는 전역 변수처럼 자유롭게 사용할 수 있었던 __dirname과 __filename 변수가 ESM 환경에서는 기본적으로 제공되지 않는다는 점입니다. 만약 강의 내용 중 정적 파일 제공이나 경로 설정을 위해 이 변수들이 필요한 경우에는 Node.js 내장 모듈인 url과 path를 활용하여 다음과 같이 직접 변수를 선언해주셔야 기존과 동일하게 경로를 제어할 수 있습니다.import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // 이후 app.use(express.static(path.join(__dirname, 'public'))) 등을 사용 가능 이러한 몇 가지 설정과 변수 선언 방식의 차이만 인지하고 계신다면 ESM 방식으로도 강의의 모든 기능을 문제없이 구현하실 수 있으니, TAESUN님께서 더 익숙하고 선호하시는 방식으로 코드를 작성해보시는 것도 훌륭한 학습 방법이 될 것입니다.참고해주세요!
- 0
- 1
- 27
Hỏi & Đáp
EJS 관련 질문드려요
안녕하세요, TAESUN 님! 답변이 조금 늦었습니다. 기다려 주셔서 감사합니다.질문하신 내용은 실무에서 백엔드 아키텍처를 설계할 때 반드시 마주하게 되는 핵심적인 부분입니다. 결론부터 말씀드리면, Express에서 EJS로 HTML을 만드는 코드(로직)는 전혀 달라지지 않습니다. 다만, 그 서버를 감싸는 '외벽'의 구성이 달라진다고 이해하시면 됩니다.이해를 돕기 위해 '유명한 맛집(식당)'에 비유해 볼게요.ex) 주방장(Express)과 지배인(Nginx)강의에서 우리가 배운 방식은 주방장이 입구에서 주문도 받고, 요리도 하고, 서빙까지 직접 하는 1인 식당과 같습니다. 반면, Nginx나 Apache를 앞단에 두는 것은 전문 지배인을 고용하는 것과 같습니다.Express (주방장): 손님의 요청에 맞춰 신선한 재료를 볶고 지져서 HTML이라는 '요리'를 만들어냅니다. (이게 바로 SSR이죠.)Nginx (지배인): 식당 입구에서 손님을 맞이합니다. 주방장이 요리에만 집중할 수 있게 잡무(보안, 서빙, 주차 관리 등)를 대신 처리해 줍니다.Nginx/Apache를 사용할 때 실무에서 반드시 적용하는 세 가지 표준 방식입니다.정적 자원(Static Files)의 외주화강의에서는 express.static을 썼지만, 실무에서는 이미지, CSS, JS 파일을 Nginx가 직접 클라이언트에게 던져줍니다.이유: "콜라(이미지)"는 냉장고에서 꺼내기만 하면 되는데, 굳이 "주방장(Express)"을 불러서 시킬 필요가 없기 때문입니다. 주방장은 오직 요리(EJS 렌더링)에만 에너지를 써야 성능이 올라갑니다.보안 강화 (SSL Termination)HTTPS 보안 설정을 Express 코드 안에서 구현하지 않습니다. Nginx라는 입구에서 '보안 검사'를 다 끝내고, 내부 주방(Express)에는 편안하게 요청을 전달합니다. 코드 관리가 훨씬 깔끔해지죠.무중단 서비스와 로드 밸런싱주방장이 너무 힘들지 않게 주방을 여러 개(Express 서버 여러 대) 만들고, Nginx가 손님을 비어 있는 주방으로 적절히 배정해 줍니다. 혹시 한쪽 주방에 문제가 생겨도 손님은 식사를 계속할 수 있게 만드는 실무의 핵심 기술입니다.요약하자면 TAESUN 님이 강의에서 배운 EJS 렌더링 방식은 실무에서도 그대로 쓰입니다. 다만 실무에서는 그 앞에 Nginx라는 '전문 매니저'를 세워 성능과 보안을 업그레이드할 뿐입니다.개발 단계에서는 지금처럼 Express 하나로 구현하시고, 나중에 배포 단계에서 Nginx 설정법을 한 스푼만 더하면 완벽한 실무형 서비스가 됩니다.궁금한 점이 더 생기면 언제든 질문 주세요. TAESUN 님의 성장을 응원합니다!
- 0
- 2
- 40
Hỏi & Đáp
41 번 강좌 이미 있는 가입자 존재하는 경우에서..
안녕하세요 Edwards 님, 답변이 조금 늦었습니다. 기다려 주셔서 감사합니다!공유해주신 내용을 보니 "/"를 제외한 "users/register" 방식으로 해결하셨는데, 사실 이 방식이 Express.js의 Best Practice가 맞습니다. 강의와 다른 부분에서 혼선을 드려 죄송하며, 실무적인 관점에서 왜 이 코드가 더 정확한지 핵심만 짚어드릴게요.1. res.render는 'URL'이 아니라 '파일'을 찾습니다res.render("users/register"): Express는 설정된 views 폴더 내에서 해당 파일을 찾습니다. 이때 앞에 /가 없어야 "현재 views 디렉토리 기준"으로 안전하게 탐색합니다.res.render("/users/register"): 경로 앞에 /가 붙으면 운영체제에 따라 시스템 루트(최상위) 경로로 오해할 소지가 있어 에러가 발생하곤 합니다.2. 실무 팁: status(400)를 함께 활용해 보세요현재 작성하신 res.render(..., { error: "..." }) 코드는 아주 훌륭합니다. 여기에 실무적인 디테일을 한 뼘만 더하자면, 상태 코드를 명시해 주는 것이 좋습니다.return res.status(400).render("users/register", { error: "..." });이렇게 하면 브라우저나 개발자 도구에서도 이 요청이 '실패(Bad Request)'했음을 명확히 알 수 있어 디버깅과 협업에 큰 도움이 됩니다.3. 헷갈리지 않는 명확한 기준render: 파일을 그려낼 때 사용 → / 생략 (View Engine 기준)redirect: 주소를 옮길 때 사용 → / 포함 (브라우저 주소창 기준)강의 자료보다 Edwards 님이 직접 수정하신 코드가 더 견고한 방식이니, 그대로 믿고 진행하셔도 좋습니다. 덕분에 저도 강의 노트를 다시 점검해 볼 수 있었습니다. 감사합니다!또 진행하시다가 막히는 부분이 생기면 언제든 말씀해 주세요.
- 0
- 2
- 31
Hỏi & Đáp
jwt
안녕하세요, 코딩님! 먼저 궁금한 점을 남겨주셔서 감사드립니다. 강의를 수강하시면서 설명이 부족한 부분을 직접 찾아보시느라 학습 흐름이 깨지셨다니, 공부에 대한 열의가 높으신 만큼 고충이 크셨을 것 같습니다.이번 Express Part 1 강의는 말 그대로 Express의 핵심 기능을 어떻게 사용하는지에 가장 큰 포커스를 두고 있습니다. 그래서 수업의 목적에 따라 JWT, Session, Cookie와 같은 디테일한 인증 메커니즘이나 보안 관련 요소들을 상세히 설명해 드리지 못한 점 양해 부탁드립니다.질문 주신 JWT와 같은 부분들을 Express Part 2에서 제대로 다루는지 말씀드리자면, Part 2에서는 json이나 urlencoded 등 웹에서 자주 사용되는 데이터를 직접 받아와 데이터를 이벤트 형태로 읽고 이를 파싱하는 엔진을 직접 구현해보게 됩니다. 이 과정을 통해 서버가 데이터를 처리하는 근본적인 원리를 배우게 되므로, 질문하신 JWT 등의 개념에 대한 이해도를 높이는 데에도 큰 도움이 될 것입니다.따라서 지금은 전반적인 구현 흐름과 CRUD의 감을 잡는 정도로 넘어가셔도 좋습니다. 또한 현재 서버 쪽에서 자주 사용되는 보안, 인증, 데이터베이스 등 이론 및 배경은 물론이고, 더 나아가 CS 관련 강의를 준비 중에 있으니 이를 통해 부족한 부분을 확실히 채우실 수 있을 것입니다. 해당 강의는 최대한 빠르게 준비하여 출시할 예정입니다.직접 자료를 찾아보며 고생하신 만큼, Part 2와 이어질 강의들이 코딩님의 학습 흐름을 잡는 데 큰 힘이 되었으면 좋겠습니다. 열공하시느라 고생 많으셨으며, 또 궁금한 점이 생기면 언제든 질문 남겨주세요!
- 0
- 2
- 39




