스터디 포스트 >  웹 개발 실전 프로젝트

Yelp Camp 활용한 프로젝트-2

조재홍 멘토
이로운 개발자가 되고 싶은 조재홍입니다.

웹 개발 실전 프로젝트 - 4주차

 
💡
Yelp Camp 활용한 프로젝트
 

404 page Setting

지금까지 app.use 는 모든 요청에 상관없이 함수를 실행하고 req 를 전달하는데 사용했다.
하지만 use 는 문자열이나 경로, regex로 라우트를 설정할 수 있음.
app.use('/dogs', (req, res, next) => {
    console.log("I love dogs");
    next();
})

app.get('/dogs', (req, res) => {
    console.log(req.requstTime);
    res.send('WOOF WOOF');
})
notion image
여기서 POST /dogs 로 요청했으면 404 error 가 발생하지만 I love dogs 는 출력 된다.
특정 경로에만 미들웨어를 실행할 때 이 방법을 사용할 수 있음 아무것도 매칭되지 않았을 때 404 로 이동하게 될 때, 맨 아래에 정의할 수 있음
//index.js의 라우터 핸들러 메소드들 맨 아래에...

app.use((req, res)=>{
    res.status(404).send("NOT FOUND");
})
 

Authentication Demo

실제로는 인증을 만드는 건 많은 노력이 필요하지만 이번에는 간단하게 만들어본다. /secret 라는 경로에 인증 기능을 추가해본다.
쿼리 문자열로 인증을 넣어보기 위해 아래와 같은 코드를 생성한다.
app.use((req, res, next)=>{
    console.log(req.query);
    next();
})
http://localhost:3000/secret?food=chicken 실행시
notion image
이제 쿼리문자열에 password 를 받도록 해본다.
app.use((req, res, next)=>{
    const { password } = req.query;
    if(password === "chickennugget"){
        next();
    } 
    res.send("SORRY YOU NEED A PASSWORD!!")
})
이 코드는 모든 라우트 핸들러에 대해 실행을 하기 때문에 queryString 으로 항상 패스워드를 전달 해야 한다. 비밀번호가 틀렸다면 여기서 종료되고 맞다면, next 가 실행된다.
 
기존처럼 http://localhost:3000/dogs 를 실행하면 SORRY YOU NEED A PASSWORD!! 가 출력 된다. 여기서 http://localhost:3000/dogs?password=chickennugget 로 접근해야 next 가 출력되서 WOOF WOOF 가 출력 되었다.
 

Credential only Secret page

위의 경우는 모든 페이지에 대해서 패스워드를 요구하는데, 실제로는 특정한 라우트 에서만 비밀번호를 요구하도록 만들어야 한다.
req.path 가 특정 경로일 때 하도록 if 문을 추가하는 방법이 있지만 최선의 방법은 아니다. 특정한 경로일 때만 미들웨어를 사용하도록 설정하는 방법을 쓰면 된다.
const verifyPassword = (req, res, next)=>{
    const { password } = req.query;
    if(password === "chickennugget"){
        next();
    }
    res.send("SORRY YOU NEED A PASSWORD!!")
}
기존 use 미들웨어를 해당 함수로 선언하고
app.get('/secret', verifyPassword, (req, res)=> {
	res.send('MY SECRET');
})
이렇게 설정하면 secret에 접근할 때 verifyPassword 가 먼저 실행되고 다음 매개변수의 함수가 Callback으로 실행된다. 이런 식으로 secret 에만 인증 기능이 적용되었다.
 

41. YelpCamp: 기본 스타일 추가하기

421. Layouts를 위한 새로운 EJS 툴

  • 패키지로 여러 기능을 설치할 수 있는데 그 중 layout을 사용할 것이다.
    • boilerplate 를 통해 콘텐츠 사이에 짧은 변수 사용으로 코드를 삽입하는 것이 가능 (head, footer 파일 분할보다 간편)
  • 설치 npm i ejs-mate
    • const ejsMate = require('ejs-mate');
      // ...
      app.engine('ejs', ejsMate);
  • ejs 파일을 실행하거나 파싱할 때 쓰이는 Express의 디폴트 엔진 대신 ejsMate를 설정
  • views 디렉터리에 layouts 폴더 생성 후 boilerplate.ejs 파일 추가
    • 모든 페이지의 기본 상용구 역할
    • <!DOCTYPE html>
      <html lang="ko">
      
      <head>
          <meta charset="UTF-8">
          <meta http-equiv="X-UA-Compatible" content="IE=edge">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>BOILERPLATE!!!</title>
      </head>
      
      <body>
      		<main>
      		    <%- body %>
      		</main>
      </body>
      
      </html>
      views/layouts/boilerplate.ejs
      <% layout('layouts/boilerplate') %> 
      <h1>All Campgrounds</h1>
      <div>
          <a href="/campgrounds/new">Add Campground</a>
      </div>
      <ul>
          <% for (let campground of campgrounds) { %>
          <li><a href="/campgrounds/<%= campground._id %>"><%= campground.title %></a></li>
          <% } %>
      </ul>
      views/campgrounds/index.ejs
  • ejs에 작성한 콘텐츠들을 layout이 찾아내 boilerplate에 body로 전달한다.
  • 스타일 시트나 JavaScript 등을 삽입해 common layout을 만들 수 있다.
    • navbar를 삽입한다면 boilerplate.ejs 에만 추가하면 된다.
 

422. 부트스트랩5! 상용구 코드

 

423. 내비게이션 바 부분

<nav class="navbar navbar-expand-lg bg-dark navbar-dark sticky-top">
    <div class="container-fluid">
        <a class="navbar-brand" href="#">YelpCamp</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
            aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNavAltMarkup">
            <div class="navbar-nav">
                <a class="nav-link" href="/">Home</a>
                <a class="nav-link" href="/campgrounds">Campgrounds</a>
                <a class="nav-link" href="/campgrounds/new">New Campground</a>
            </div>
        </div>
    </div>
</nav>
views/partials/navbar.ejs
<%- include('../partials/navbar') %>
<main class="container mt-5">
    <%- body %>
</main>
views/layouts/boilerplate.ejs
 

424. Footer 부분

  • body vh-100 view height 설정 후 footer mt-auto
 

425. 이미지 추가하기

  • Campground의 시드 파일 수정
    • const seedDB = async () => {
        await Campground.deleteMany({});
        for (let i = 0; i < 50; i++) {
          const random1000 = Math.floor(Math.random() * 1000);
          const price = Math.floor(Math.random() * 20) + 10;
          const camp = new Campground({
            location: `${cities[random1000].city}, ${cities[random1000].state}`,
            title: `${sample(descriptors)} ${sample(places)}`,
            image: 'https://source.unsplash.com/collection/483251',
            description:
              'Lorem ipsum dolor sit amet consectetur adipisicing elit. Earum a asperiores rerum iusto dolor ipsam iure totam laboriosam reiciendis impedit voluptatibus molestiae, nisi obcaecati officiis molestias, unde in, dolorem tempora?',
            price,
          });
          await camp.save();
        }
      };
      seedDB().then(() => {
        mongoose.connection.close();
      });
      node seeds/index.js
데이터가 생성되어 DB에 추가됨
데이터가 생성되어 DB에 추가됨

426. 캠프그라운드 인덱스 스타일링


427. 새로운 (폼)양식 스타일링


 
 

438. YelpCamp의 다음 목표는?

  • 오류 처리
  • 클라이언트 측 유효성 검사
  • 서버 측 유효성 검사
    • 들어오는 req.body의 유효성을 검사해 패턴과 맞는지 확인
  • Joi
  • 오류 템플릿 설정
 

439. 클라이언트 양식 유효성 검사

  • 폼에 클라이언트 측 유효성 검사 추가하기
  • input에 required 어트리뷰트를 추가해 브라우저에서 처리시키는 방법
    • 브라우저마다 구현 방법이 다르고 표준화되어 있지 않다.

부트스트랩으로 폼 유효성 검사하기

<form action="/campgrounds" method="POST" novalidate class="validated-form">
  • 유효성 검사가 필요한 input들에 required를 추가한 후, 부모 form에 novalidate 어트리뷰트를 추가하면 브라우저 대신 부트스트랩이 유효성 검사를 수행할 수 있다.
  • 유효성 검사가 필요한 폼에 클래스를 추가한다. validated-form
const forms = document.querySelectorAll('.validated-form');

// Array.prototype.slice.call(forms)
Array.from(forms).forEach(function (form) {
    form.addEventListener('submit', (event) => {
        if (!form.checkValidity()) {
            event.preventDefault();
            event.stopPropagation();
        }
        form.classList.add('was-validated')
    }, false)
})
  • 이벤트리스너를 추가하고 submit 시 form.checkValidity 호출
부트스트랩의 유효성 검사가 추가된 것을 볼 수 있다.
부트스트랩의 유효성 검사가 추가된 것을 볼 수 있다.

valid feedback 추가하기

  • 유효성 검사를 수행할 input 마다 밑에 valid-feedback 클래스를 가진 div를 추가한다.
<div class="valid-feedback">Looks good!</div>
notion image
 

440. 기본 오류 처리기

  • Price에 숫자가 아닌 값을 입력했을 때 유효성 오류 처리 및 Promise 처리
    • async 함수에서 try…catch문 사용
    • 시도 및 저장할 때 오류가 있으면 검출하고, 해당 오류로 next를 호출하면 기본 오류 핸들러를 실행시켜 오류 메시지를 출력한다.
app.post('/campgrounds', async (req, res, next) => {
  try {
    const campground = new Campground(req.body.campground);
    await campground.save();
    res.redirect(`/campgrounds/${campground._id}`);
  } catch (e) {
    next(e);
  }
});

// ...
app.use((err, req, res, next) => {
  res.send('Oh boy, something went wrong!');
});
Price로 문자열을 전달하고 submit 하면
Price로 문자열을 전달하고 submit 하면
⇒ 오류 메시지를 출력
⇒ 오류 메시지를 출력
 

441. ExpressError Class 정의하기

  • 의도적으로 발생시킬 수 있는 표준화된 오류들 알아보기
  • ExpressError 클래스 생성
class ExpressError extends Error {
  constructor(message, statusCode) {
    super();
    this.message = message;
    this.statusCode = statusCode;
  }
}

module.exports = ExpressError;
utils/ExpressError.js
  • async 오류 검출을 위해 래퍼(wrapper) 함수 추가하기
module.exports = (func) => {
  return (req, res, next) => {
    func(req, res, next).catch(next);
  };
};
utils/catchAsync.js
  • 앱 라우트에 추가하기
const catchAsync = require('./utils/catchAsync');

// ...

app.post('/campgrounds', catchAsync(async (req, res, next) => {
  const campground = new Campground(req.body.campground);
  await campground.save();
  res.redirect(`/campgrounds/${campground._id}`);
}));
 

442. 오류 더 알아보기

  • 404 오류 추가
  • 상단의 모든 코드에 요청이 닿지 않는 경우 실행되어야 함 ⇒ 하단에 위치
app.all('*', (req, res, next) => {
  res.send('404!!!!');
});
  • ExpressError 클래스 사용하기
const ExpressError = require('./utils/ExpressError');
// ...
app.all('*', (req, res, next) => {
	res.status();
  next(new ExpressError('Page Not Found', 404));
});
  • app.use로 메시지와 상태 코드 가져오기
    • new ExpressError를 next로 전달하기 때문에, 오류 핸들러가 실행되면 ExpressError가 err가 된다. (혹은 다른 코드의 오류가 될 수도 있다.)
app.use((err, req, res, next) => {
  const { statusCode = 500, message = 'Something went wrong' } = err;
  res.status(statusCode).send(message);
});
  • 유효하지 않은 URL을 입력하면 Page Not Found가 뜨고, 오류를 발생시킬 만한 페이지에서 유효하지 않은 ID를 입력하면 오류 메시지가 출력된다.
유효하지 않은 URL
유효하지 않은 URL
→ 상태 코드(statusCode) 404
→ 상태 코드(statusCode) 404
유효하지 않은 ID
유효하지 않은 ID
→ 상태 코드(statusCode) 500
→ 상태 코드(statusCode) 500
  • 어떤 라우트에서든 new ExpressError를 작성해서 next로 전달하면, 그 코드에 있는 상태 코드가 적용된다.
  • 입력된 캠프 정보가 없는 상황을 가정하고 오류 처리하기
    • 데이터를 잘못 기입하는 등의 클라이언트 오류 상태 코드인 400으로 지정
app.post('/campgrounds', catchAsync(async (req, res, next) => {
    if (!req.body.campground) throw new ExpressError('Invaild Campground Data', 400);
    const campground = new Campground(req.body.campground);
    await campground.save();
    res.redirect(`/campgrounds/${campground._id}`);
  })
);
  • 폼에는 현재 유효성 검사가 적용되어 있어 제출할 수 없으므로, Postman으로 데이터를 누락한 채 POST 요청을 보내면 다음과 같이 의도대로 메시지와 상태 코드가 출력된다.
notion image
 

443. 오류 템플릿 정의하기

<% layout('layouts/boilerplate')%>
<div class="row">
    <div class="col-6 offset-3">
        <div class="alert alert-danger" role="alert">
            <h4 class="alert-heading">Error!</h4>
            <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Exercitationem itaque, dolores vero molestias quis
                doloremque amet consequatur quisquam ipsum, rem sapiente quae!</p>
            <hr>
            <p class="mb-0">Whenever you need to, be sure to use margin utilities to keep things nice and tidy.</p>
        </div>
    </div>
</div>
views/error.ejs
app.use((err, req, res, next) => {
  const { statusCode = 500, message = 'Something went wrong' } = err;
  res.status(statusCode).render('error');
});
.send(message)를 수정
notion image
  • 오류 객체 error.ejs로 전달하기
    • err 구조분해 시 statusCode를 꺼내는 건 상관없지만, 설정한 message는 오류 객체에 업데이트 되지 않기 때문에 디폴트 값 설정을 바꿔야 한다.
app.use((err, req, res, next) => {
  const { statusCode = 500 } = err;
  if(!err.message) err.message = 'Oh No, Something Went Wrong!';
  res.status(statusCode).render('error', { err });
});
<h4 class="alert-heading"><%= err.message %></h4>
notion image
  • 개발 목적이라면 <%= err.stack %> 을 적용할 수도 있다.
 

444. JOI 스키마 유효성 검사

JavaScript 유효성 검사 도구 Joi

  • 스키마를 간단히 설정하고, 스키마를 통해 데이터 유효성 검사를 할 수 있어 직접 작성하지 않아도 된다.
  • 설치 npm i joi
const Joi = require('joi');

// ...

app.post('/campgrounds', catchAsync(async (req, res, next) => {
    // if (!req.body.campground) throw new ExpressError('Invaild Campground Data', 400);
    const campgroundSchema = Joi.object({
			campground: Joi.object({
        title: Joi.string().required(),
        price: Joi.number().required().min(0),
        image: Joi.string().required(),
        location: Joi.string().required(),
        description: Joi.string().required(),
    }).required(),
    // const result = campgroundSchema.validate(req.body);
		// if (result.error) {
    //  throw new ExpressError(result.error.details, 400);
    // }
		const { error } = campgroundSchema.validate(req.body);
		if (error) {
      const msg = error.details.map(el => el.message).join(',');
      throw new ExpressError(msg, 400);
    }
    const campground = new Campground(req.body.campground);
    await campground.save();
    res.redirect(`/campgrounds/${campground._id}`);
  })
);
  • 이제 Postman이나 AJAX 등을 활용하려 해도 서버 측에서 데이터를 검증할 수 있다.
notion image
 

445. JOI 유효성 검사 미들웨어

  • 스키마 유효성을 검사하는 재사용 가능한 미들웨어 만들기
const validateCampground = (req, res, next) => {
  const campgroundSchema = Joi.object({
    campground: Joi.object({
      title: Joi.string().required(),
      price: Joi.number().required().min(0),
      image: Joi.string().required(),
      location: Joi.string().required(),
      description: Joi.string().required(),
    }).required(),
  })
  const { error } = campgroundSchema.validate(req.body);
  if (error) {
    const msg = error.details.map(el => el.message).join(',');
    throw new ExpressError(msg, 400);
  } else {
    next();
  }
}

// ...

app.post('/campgrounds', validateCampground, catchAsync(async (req, res, next) => {
// ...
  • 라우트 자체, 라우트 핸들러에는 유효성 검사 로직이 없고, 인수로 validateCampground를 전달해 호출하면 유효성 검사를 할 수 있다.
notion image
  • validateCampground의 스키마를 파일로 분리한다.
const Joi = require('joi');
module.exports.campgroundSchema = Joi.object({
  campground: Joi.object({
    title: Joi.string().required(),
    price: Joi.number().required().min(0),
    image: Joi.string().required(),
    location: Joi.string().required(),
    description: Joi.string().required(),
  }).required(),
});
schemas.js
  • 리뷰나 사용자 등 원하면 다른 스키마들을 schemas 파일에 추가할 수 있다.
  • 다수의 스키마를 내보내기 할 예정이므로 불러올 때도 구조분해한다.
const { campgroundSchema } = require('./schemas');

// ...

const validateCampground = (req, res, next) => {
  const { error } = campgroundSchema.validate(req.body);
  if (error) {
    const msg = error.details.map((el) => el.message).join(',');
    throw new ExpressError(msg, 400);
  } else {
    next();
  }
};

// ...
app.post('/campgrounds', validateCampground, catchAsync(async (req, res, next) => {
// ...
app.put('/campgrounds/:id', validateCampground, catchAsync(async (req, res) => {
💡
무언가를 create 하거나 update 하기 전에 유효성 검사를 위해 사용하므로 model, mongoose Schema와는 다르다. Joi 메서드를 사용하는 스키마 파일에 정의한 JavaScript 객체용 패턴.
 

446. 섹션 주제

  • 데이터베이스를 구조화 및 저장하는 방법과 관계(relationships) 이해하기
    • 데이터는 독립적으로 존재하지 않고 다양한 엔티티 사이에서 서로 연결됨
  • SQL
  • 세 가지 유형의 관계와 모델링을 위한 접근법
    • One to Few
    • One to Many
    • One to Bajillions
  • Mongoose 메서드 populate
  • Mongo 스키마 디자인
 

447. Mongo의 관계 개요

  • Mongo의 다양한 데이터간 관계 중 몇 가지 유형을 모델링하는 방법과 어떤 패턴을 사용할 수 있는지 알아보자.
 

448. SQL 관계 개요

  • 관계형 데이터베이스
  • MySQL, PostgreSQL Lite, Microsoft SQL Server 등의 SQL 데이터베이스들은 모두 테이블에 정보를 저장한다.
  • 테이블은 엄격한 스키마를 설정해야 하는 견고한 구조
  • 정보 중복의 비효율성 때문에 한 테이블에서 다른 테이블을 참조하고, 정보를 합칠 수 있다.
  • One to Many의 예: 사용자 ID와 여러 게시물(또는 댓글) 관계가
  • Many to Many의 예: 영화와 영화배우의 관계
💡
Mongo가 Node, Express와 잘 어울린다고 해서, NoSQL 데이터베이스가 절대적인 정답이라고는 할 수 없다. SQL도 배워야 한다. 하지만 Mongo의 데이터 구조화에는 유연성과 자율성이라는 강점이 있다.
 

449. One to Few 관계

notion image
  • 웹 상에서 사용자(users)와 그들이 만든 것(게시물, 댓글, …)과 연관이 깊고, 흔한 형태
  • One to Many에서 ‘Many’가 얼마나 많은가에 따라 세 가지 유형으로 나뉜다.
  • 그 중 One to ‘Few’는 사용자와 주소를 생각하면 이해하기 쉽다.
  • 사용자가 두 가지나 그 이상의 여러 항목을 가질 때 좋은 방법
const mongoose = require('mongoose');

mongoose
  .connect('mongodb://localhost:27017/relationshipDemo', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log('MONGO CONNECTION OPEN!!!');
  })
  .catch((err) => {
    console.log('OH NO MONGO CONNECTION ERROR!!!!');
    console.log(err);
  });

const userSchema = new mongoose.Schema({
  first: String,
  last: String,
  addresses: [
    {
      _id: { id: false },
			// address에 id가 생성되지 않게 옵션으로 끌 수 있다.
      street: String,
      city: String,
      state: String,
      country: String,
    },
  ],
});

const User = mongoose.model('User', userSchema);
const makeUser = async () => {
  const u = new User({
    first: 'Harry',
    last: 'Potter',
  });
  u.addresses.push({
    street: '123 Sesame St.',
    city: 'New York',
    state: 'NY',
    country: 'USA',
  });
  const res = await u.save();
  console.log(res);
};
const addAddress = async (id) => {
  const user = await User.findById(id);
  user.addresses.push({
    street: '99 3rd St.',
    city: 'New York',
    state: 'NY',
    country: 'USA',
  });
  const res = await user.save();
  console.log(res);
};
id 값으로 user를 찾아 address 데이터 추가
  • 하위 문서나 정보를 부모 문서 안에 직접 임베드하는 구조는 임베드하려는 정보 집합의 크기가 작을 때 더 적합하다.
 

450. One to Many 관계

notion image
  • One to Few의 데이터 크기가 small, One to Bajillions의 데이터 크기가 large라면, One to Many의 데이터 크기는 medium에 해당된다고 볼 수 있다.
  • One to Many 방식은 부모 문서에 정보를 직접 임베드하는 것이 아니라, 다른 곳에 정의되어 있는 참조(reference)를 임베드하는 방식이다.
  • 보통 객체 ID를 활용하고, SQL 방식과 매우 유사하다.
Product 스키마 정의 및 모델 생성
const mongoose = require('mongoose');
const { Schema } = mongoose; // 구조 분해 해두면 편리

mongoose
  .connect('mongodb://localhost:27017/relationshipDemo', {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => {
    console.log('MONGO CONNECTION OPEN!!!');
  })
  .catch((err) => {
    console.log('OH NO MONGO CONNECTION ERROR!!!!');
    console.log(err);
  });

const productSchema = new Schema({
  name: String,
  price: Number,
  season: {
    type: String,
    enum: ['Spring', 'Summer', 'Fall', 'Winter'],
  },
});

Product.insertMany([
    { name: 'Goddess Melon', price: 4.99, season: 'Summer' },
    { name: 'Sugar Baby Watermelon', price: 4.99, season: 'Summer' },
    { name: 'Asparagus', price: 3.99, season: 'Spring' },
])

const Product = mongoose.model('Product', productSchema);

Farm 모델 생성

  • Mongoose 공식 문서 populate 항목을 보면, 레퍼런스를 문서에 넣기 위해 거쳐야 하는 작업 단계가 설명되어 있다.
  • 제일 먼저 할 일은 각 상품의 타입을 객체 ID로 설정해 주는 것
  • Schema.Types.ObjectId
const farmSchema = new Schema({
  name: String,
  city: String,
  products: [{ type: Schema.Types.ObjectId, ref: 'Product' }],
});

const Farm = mongoose.model('Farm', farmSchema);
  • ref 옵션을 통해서는 Mongoose에게 채우기 작업 시 사용할 모델을 알려줄 수 있다.
    • 모델 이름인 Product를 전달한다.
    • 데이터베이스를 쿼리하고 지정된 농장에 다양한 상품들을 채워 넣을 수 있게 된다.
const makeFarm = async () => {
  const farm = new Farm({ name: 'Full Belly Farms', city: 'Guinda, CA' });
  const melon = await Product.findOne({ name: 'Goddess Melon' });
  farm.products.push(melon);
  await farm.save();
  console.log(farm);
};

makeFarm();
notion image
const addProduct = async () => {
  const farm = await Farm.findOne({ name: 'Full Belly Farms' });
  const watermelon = await Product.findOne({ name: 'Sugar Baby Watermelon' });
  farm.products.push(watermelon);
  await farm.save();
  console.log(farm);
};

addProduct();
notion image
  • 위와 같이 상품이 ObjectId 형태로 farms의 products 배열에 들어가는 것을 볼 수 있다.
  • farms는 객체 ID 말고는 상품 자체에 대한 정보를 가지고 있지 않다.
 

451. Mongoose의 Populate 명령어

Farm.findOne({ name: 'Full Belly Farms' })
  .populate('products')
  .then((farm) => console.log(farm));
 

452. One to “Bajillions” 관계

notion image
 
 
 

 
 
본 스터디는 Udemy의 <【한글자막】 The Web Developer 부트캠프 2022> 강의를 활용해 진행됐습니다. 강의에 대한 자세한 정보는 아래에서 확인하실 수 있습니다.
 
 
프밍 스터디는 Udemy Korea와 함께 합니다.
 
 

 
 
원하는 스터디가 없다면? 다른 스터디 개설 신청하기
누군가 아직 원하는 스터디를 개설하지 않았나요? 여러분이 직접 개설 신청 해 주세요!
이 포스트는
"웹 개발 실전 프로젝트" 스터디의 진행 결과입니다
진행중인 스터디
웹 개발 실전 프로젝트
JavaScript를 이용한 DOM 조작으로 프론트엔드 분야를 배우고, Node JS와 Express, DB, HTTP, REST API 등 백앤드 지식을 학습할수 있습니다. 이론만 공부하는 스터디가 아니라 13개 이상의 프로젝트로 이루어진 실습 위주의 커리큘럼으로 개인 프로젝트 결과물을 가져갈 수 있다는 메리트가 있습니다.
조재홍 멘토
이로운 개발자가 되고 싶은 조재홍입니다.