2019. 9. 5. 10:59

1. mongodb , mongoose 모듈

1-1. mongodb

- Nosql 특성상 문서 객체 안에 들어간느 속성을 마음대로 바꿀 수 있으므로 매우 유연하게 데이터를 저장할 수 있다. 

- 하지만 컬렉션 안에 들어 있는 여러개의 객체를 조회할 때는 제약이 생길 수도 있음

ex) 같은 user 컬렉션 안에 들어 있는 문서 객체라도 어떤 문서객체는 age속성이 있는데, 다른 문서 객체에는 age속성이 없을 수 있음

-> 조회 조건을 공통 적용하기 어려움

 

1-2. mongoose

MongoDB 기반 ODM (object data modelling) 라이브러리 

- 스키마를 만들어, 이에 맞는정 자바스크립트 객체 그대로 db에 저장하거나 db의 문서 객체를 그대로 자바스크립트 객체로 바꿀 수 있게 함

 

2. mongoose 적용 

 

2-1. .env환경변수파일 생성

//.env
PORT=3000
MONGO_URI=mongodb://localhost:27017/blog

- MongoDB에 접속할 때, 서버주소, 포트번호, 계정, 비밀번호들의 민감하고 환경별로 달라지는 값은 코드에 작성하지 않고 환경변수로 설정하여 .env 환경변수 파일에 저장

- github등의 서비스에 올릴 때는 .gitignore를 작성하여 환경변수 파일 제외

- blog는 프로젝트에서 사용할 데이터베이스 이름

 

yarn add dotenv

 

//src/index.js
require('dotenv').config();

//...

const { PORT, MONGO_URI} = process.env;

//...

//웹 서버의 port속성 설정
http.createServer(app).listen(PORT, () => {
    console.log('express server starts : ' + PORT);
});

- dotenv는 환경변수들을 파일에 넣고 사용할 수 있게함

- require('dotenv').config() 호출 후 환경변수는 process.env값을 통해 조회 가능

 

2-2. mongoose로 서버와 데이터베이스 연결

//src/index.js

//...
const { PORT, MONGO_URI} = process.env;

mongoose.connect(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }).then(() => {
    console.log('connected to mongodb');
}).catch((e) => {
    console.error(e);
});

//...

 

2-3. import/export구문과 Node.js

- 리액트 프로젝트에서 쓰던 ES모듈 import/export 문법은 Node.js에서 정식으로 지원되지 않음

- require(), exports, module.exports 문법 사용

- Node.js v12부터는 정식 지원

 

3. 스키마와 모델

3.1 스키마

- 컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체

//src/models/post.js
const mongoose = require('mongoose');

//스키마 :컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체
// 스키마 생성 
const {Schema} = mongoose;
const Post = new Schema({
  title: String,
  body:String,
  tags:[String], // 문자열 배열
  publishedDate:{
      type:Date,
      default:new Date() // 현재 날짜를 기본 갑 으로 설정
  }
});

 

3.2 모델

- 스키마를 사용하여 만드는 인스턴스

- 데이터베이스에서 실제 작업을 처리할 수 있는 함수들을 지니고 있는 객체

//src/modes/post.js
const mongoose = require('mongoose');

//스키마 :컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체
// 스키마 생성 
const {Schema} = mongoose;
const Post = new Schema({
  title: String,
  body:String,
  tags:[String], // 문자열 배열
  publishedDate:{
      type:Date,
      default:new Date() // 현재 날짜를 기본 갑 으로 설정
  }
});

//모델 : 스키마를 사용하여 만든 인스턴스
//모델 생성 
module.exports = mongoose.model('Post', Post);

- mongoose.model()의 첫번째 파라미터 : 스키마 이름 -> 데이터베이스는 스키마 이름의 소문자, 복수형태로 데이터베이스에 컬련섹 생성

- mongoose.model()의 두번째 파라미터 : 스키마 객체

 

 

4. 데이터 생성, 조회, 삭제, 수정

4-1. 컨트롤러 수정

//src/api/posts/posts.ctrl.js
const Post = require('models/post');

//wrapAsync 메소드는 앞으로 async-await가 필요한 라우트 핸들러에 사용.
//try-catch를 이용해 상위 래퍼 메소드에서 에러 핸들링을 해주기 때문에 
//모든 메소드에 try-catch를 사용할 필요가 없어진다.
const wrapAsync = require('lib/wrapAsync');

express 에서 async/await 적용 : https://yongmin0000.tistory.com/category/Node.js/express

 

4-2. 데이터 생성

//src/api/posts/posts.ctrl.js
const Post = require('models/post');
const wrapAsync = require('lib/wrapAsync');

//데이터생성
//POST /api/posts
exports.write = wrapAsync(async (req, res, next) => {

    const { title, body, tags } = req.body;
    const post = new Post({
        title, body, tags
    });
    await post.save();  // 데이터베이스등록
    res.send(post);     //저장된 결과 반환
});

4-3. 데이터 조회

//src/api/posts/posts.ctrl.js

// ....

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {

    const posts = await Post.find().exec();
    
    res.send(posts);
});

4-4. 특정 데이터 조회

//src/api/posts/posts.ctrl.js

// ....

//특정포스트 조회 
//GET /api/posts/:id 
exports.read = wrapAsync(async (req, res, next) => {
    const { id } = req.params;
    const post = await Post.findById(id).exec();
    //특정포스트 존재x
    if (!post) {
        res.status(404).send("<h1>404 ERROR : 데이터 조회 실패, 해당 id의 데이터 존재 x</h1>");
        return;
    }
    res.send(post);
});

 

4-5. 데이터 삭제

//src/api/posts/posts.ctrl.js

// ....

//데이터삭제
// DELETE /api/posts/:id
exports.remove = wrapAsync(async (req, res) => {
    const { id } = req.params;
    await Post.findByIdAndRemove(id).exec();
    console.log("DELETE 성공");
    res.status(204).send("<h1>삭제 성공</h1>");
});

 

4-6. 데이터 수정

//src/api/posts/posts.ctrl.js

// ....

//데이터 수정
//PATCH /api/posts/:id
exports.update = wrapAsync(async (req, res) => {
    const { id } = req.params;
    const post = await Post.findByIdAndUpdate(id, req.body, {
        new: true   //이 갑을 설정해야 업데이트 된 객체 반환, 설정안하면 업데이트 전 객체 반환
    }).exec();
    if (!post) {
        res.status(404).send("<h1>404 ERROR : 데이터수정실패, 해당 id의 데이터 존재x</h1>")
        return;
    }
    res.send(post);
}); 

 

5. 요청 검증

5.1 ObjectId

- id가 올바른 ObjectId형식인지 아닌지 검증함으로써 형식이 올바르지 않을 때 에러를 서버에서 처리할 수 있도록 한다

//src/api/posts/posts.ctrl.js

//...

//ObjectId 검증
const { ObjectId } = require('mongoose').Types;
exports.checkObjectId = (req, res, next) => {
    const { id } = req.params;

    //검증실패
    if (!ObjectId.isValid(id)) {
        console.log("ObjectId 실패")
        res.status(400).send("400 error : ObjectId 검증 실패");
        return null;
    }
    return next();
}

 

//src//api/posts/index.js
//POSTS API
const express = require('express');
const posts = express.Router();
const postsCtrl = require('./posts.ctrl');

posts.get('/', postsCtrl.list);
posts.post('/', postsCtrl.write);
posts.get('/:id', postsCtrl.checkObjectId, postsCtrl.read);
posts.delete('/:id', postsCtrl.checkObjectId, postsCtrl.remove);
posts.patch('/:id', postsCtrl.checkObjectId, postsCtrl.update);

module.exports = posts;

 

5.2 Request Body

- write, update API에서 전달받은 요청 내용에 title, body, tags값이 모두 들어있는지 검증

- 객체 검증 라이브러리 @hapi/joi 이용

yarn add @hapi/joi

 

//src/api/posts/posts.ctrl.js

const Post = require('models/post');
const wrapAsync = require('lib/wrapAsync');
const Joi = require('@hapi/joi');

//Request body 검증
exports.checkRequestBody = (req,res,next) => {

    //객체가 지닌 갑들을 검증
    const schema = Joi.object({
        title: Joi.string().required(),
        body: Joi.string().required(),
        tags:Joi.array().items(Joi.string()).required()
    });

    const result = schema.validate(req.body);
    //오류가 발생하면 오류내용응답
    if (result.error) {
        console.log("checkRequestBody 실패")
        res.status(400).send(result.error);
        return;
    }
    return next();
}

 

//src//api/posts/index.js
//POSTS API
const express = require('express');
const posts = express.Router();
const postsCtrl = require('./posts.ctrl');

posts.get('/', postsCtrl.list);
posts.post('/',postsCtrl.checkRequestBody, postsCtrl.write);
posts.get('/:id', postsCtrl.checkObjectId, postsCtrl.read);
posts.delete('/:id', postsCtrl.checkObjectId, postsCtrl.remove);
posts.patch('/:id', postsCtrl.checkObjectId, postsCtrl.update);

module.exports = posts;

 

6. Pagination

6-1. 포스트 역순으로 불러오기 

- 최근 작성된 포스트를 먼저 보여주기

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .exec();
    
    res.send(posts);
});

 

6-2. 보이는 개수 제한

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {


    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .exec();
    
    
    res.send(posts);
});

 

6-3. 페이지 기능 구현

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {
    // 페이지 기능
    const page = parseInt(req.query.page||1,10); //10 진수
    if(page<1){                                 //잘못된 페이지 주어지면 오류
        res.status(400).send("<h1>wrong page</h1>");
        return;
    }

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .skip((page-1)*10)  //파라미터만큼의 데이터를 맨처음에서 제외하고 그 다음 데이터 불러옴
    .exec();
    
    res.send(posts);
});

 

6-4. 마지막 페이지 번호 알려 주기

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {
    // 페이지 기능
    const page = parseInt(req.query.page||1,10); //10 진수
    if(page<1){                                 //잘못된 페이지 주어지면 오류
        res.status(400).send("<h1>wrong page</h1>");
        return;
    }

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .skip((page-1)*10)  //파라미터만큼의 데이터를 맨처음에서 제외하고 그 다음 데이터 불러옴
    .exec();
    
    //마지막 페이지 번호 알려주기
    const postCount = await Post.countDocuments().exec();
    res.set('Last-Page',Math.ceil(postCount/10)); //응답 헤더에 Last-Page라는 커스텀http헤더 설정

    res.send(posts);
});

 

6-5. 내용 길이 제한

//src/api/posts/posts.ctrl.js

// 데이터 조회
//GET /api/posts
exports.list = wrapAsync(async (req, res, next) => {
    // 페이지 기능
    const page = parseInt(req.query.page||1,10); //10 진수
    if(page<1){                                 //잘못된 페이지 주어지면 오류
        res.status(400).send("<h1>wrong page</h1>");
        return;
    }

    const posts = await Post.find()
    .sort({_id:-1})     //포스트 역순으로 불러오기
    .limit(10)          //보이는 개수 제한
    .skip((page-1)*10)  //파라미터만큼의 데이터를 맨처음에서 제외하고 그 다음 데이터 불러옴
    .lean()             //반환형식을 JSON형태로 (내용길이 제한기능을 위해)
    .exec();
    
    //마지막 페이지 번호 알려주기
    const postCount = await Post.countDocuments().exec();
    res.set('Last-Page',Math.ceil(postCount/10)); //응답 헤더에 Last-Page라는 커스텀http헤더 설정

    // 내용길이 제한
    const limitBodyLength = (post)=>({
        ...post,
        body:post.body.length <160 ? post.body : `${post.body.slice(0,160)}...`
    });
    res.send(posts.map(limitBodyLength));
});
Posted by yongminLEE
2019. 9. 5. 10:58

1. 요청과 응답

- 클라이언트에서 서버로 request를 보내고, 서버에서는 request내용을 읽고 처리한뒤 클라이언트에게 response를 보낸다.

- 클라이언트로부터 요청이 왔을 때 어떤 작업을 수행할지 listen 메서드에 콜백함수를 넣거나, 서버에 listening 이벤트 리스너를 추가한다.

 

2. 쿠키와 세션

- 쿠키 : '키-값'의 쌍, reques와 response의 header를 통해 오고 감

- 서버는 요청에 대한 응답을 할 때 쿠키라는 것을 클라이언트에게 보냄 -> 서버로 부터 쿠키가 오면 '웹 브라우저'는 쿠키를 저장해 두었다가 요청할 때마다 쿠키를 동봉해서 보냄 -> 서버는 요청에 들어있는 쿠키를 읽어서 사용자가 누구인지 파악

- 브라우저는 쿠키가 있으면 자동으로 동봉해서 보내므로 따로 처리할 필요 없음, 서버에서 브라우저로 쿠키를 보낼 때만 코드 작성

 

- 세션 : 서버에 사용자 정보를 저장하고 클라이언트와는 세션 아이디로만 소통하는 방식

- 세션 아이디는 꼭 쿠키를 사용해서 주고 받을 필요는 없지만, 대부분의 서비스는 쿠키를 사용

 

 

3. REST API와 라우팅

 

 REST (Representational State Transfer) 정의

1. 자원을 이름(자원의 표현)으로 구분하여 해당 자원의 상태(정보)를 주고 받는 모든 것을 의미한다. -> 즉, 자원(resource)의 표현(representation) 에 의한 상태 전달

- 자원: 해당 소프트웨어가 관리하는 모든 것 -> Ex) 문서, 그림, 데이터, 해당 소프트웨어 자체 등
- 자원의 표현: 그 자원을 표현하기 위한 이름 -> Ex) DB의 학생 정보가 자원일 때, ‘students’를 자원의 표현으로 정한다.
- state(상태) 전달 : 데이터가 요청되어지는 시점에서 자원의 상태(정보)를 전달한다. -> JSON 혹은 XML를 통해 데이터를 주고 받는 것이 일반적

 

2. 월드 와이드 웹(www)과 같은 분산 하이퍼미디어 시스템을 위한 소프트웨어 개발 아키텍처의 한 형식
- REST는 기본적으로 웹의 기존 기술과 HTTP 프로토콜을 그대로 활용하기 때문에 웹의 장점을 최대한 활용할 수 있는 아키텍처 스타일이다.
- REST는 네트워크 상에서 Client와 Server 사이의 통신 방식 중 하나이다.


REST의 구체적인 개념

- HTTP URI(Uniform Resource Identifier)를 통해 자원(Resource)을 명시하고, HTTP Method(POST, GET, PUT, DELETE, PATCH)를 통해 해당 자원에 대한 CRUD Operation을 적용하는 것을 의미
-> 즉, REST는 자원 기반의 구조 (ROA, Resource Oriented Architecture) 설계의 중심에 Resource가 있고 HTTP Method를 통해 Resource를 처리하도록 설계된 아키텍쳐를 의미한다.

- 웹 사이트의 이미지, 텍스트, DB 내용 등의 모든 자원에 고유한 ID인 HTTP URI를 부여한다.
- CRUD Operation
1. Create : 생성(POST)
2. Read : 조회(GET)
3. Update : 수정(PUT)
4. Delete : 삭제(DELETE)
HEAD: header 정보 조회(HEAD)


REST의 장단점

- 장점
1. HTTP 프로토콜의 인프라를 그대로 사용하므로 REST API 사용을 위한 별도의 인프라를 구출할 필요가 없다.
2. HTTP 프로토콜의 표준을 최대한 활용하여 여러 추가적인 장점을 함께 가져갈 수 있게 해준다.
3. HTTP 표준 프로토콜에 따르는 모든 플랫폼에서 사용이 가능하다.
4. Hypermedia API의 기본을 충실히 지키면서 범용성을 보장한다.
5. REST API 메시지가 의도하는 바를 명확하게 나타내므로 의도하는 바를 쉽게 파악할 수 있다.
6. 여러가지 서비스 디자인에서 생길 수 있는 문제를 최소화한다.
7. 서버와 클라이언트의 역할을 명확하게 분리한다.


- 단점
1. 표준이 존재하지 않는다.
2. HTTP Method 형태가 제한적이다.
3. 브라우저를 통해 테스트할 일이 많은 서비스라면 쉽게 고칠 수 있는 URL보다 Header 값이 왠지 더 어렵게 느껴진다.
4. 구형 브라우저가 아직 제대로 지원해주지 못하는 부분이 존재한다. -> PUT, DELETE 사용불가, pushState 지원x


REST가 필요한 이유

1.‘애플리케이션 분리 및 통합’
2. ‘다양한 클라이언트의 등장’
3. 최근의 서버 프로그램은 다양한 브라우저와 안드로이폰, 아이폰과 같은 모바일 디바이스에서도 통신을 할 수 있어야 한다.
4. 이러한 멀티 플랫폼에 대한 지원을 위해 서비스 자원에 대한 아키텍처를 세우고 이용하는 방법을 모색한 결과, REST에 관심을 가지게 되었다.


REST 구성 요소

1. 자원(Resource): URI
  - 모든 자원에 고유한 ID가 존재하고, 이 자원은 Server에 존재한다.
  - 자원을 구별하는 ID는 ‘/groups/:group_id’와 같은 HTTP URI 다.
  - Client는 URI를 이용해서 자원을 지정하고 해당 자원의 상태(정보)에 대한 조작을 Server에 요청한다.
2. 행위(Verb): HTTP Method
  - HTTP 프로토콜의 Method를 사용한다.
  - HTTP 프로토콜은 GET, POST, PUT, DELETE 와 같은 메서드를 제공한다.
3. 표현(Representation of Resource)
  - Client가 자원의 상태(정보)에 대한 조작을 요청하면 Server는 이에 적절한 응답(Representation)을 보낸다.
  - REST에서 하나의 자원은 JSON, XML, TEXT, RSS 등 여러 형태의 Representation으로 나타내어 질 수 있다.
  - JSON 혹은 XML를 통해 데이터를 주고 받는 것이 일반적이다.


REST 특징

1. Server-Client(서버-클라이언트 구조)
  - 자원이 있는 쪽이 Server, 자원을 요청하는 쪽이 Client가 된다.
  - REST Server: API를 제공하고 비즈니스 로직 처리 및 저장을 책임진다.
  - Client: 사용자 인증이나 context(세션, 로그인 정보) 등을 직접 관리하고 책임진다.
    => 서로 간 의존성이 줄어든다.
2. Stateless(무상태)
  - HTTP 프로토콜은 Stateless Protocol이므로 REST 역시 무상태성을 갖는다.
  - Client의 context를 Server에 저장하지 않는다. -> 세션과 쿠키와 같은 context 정보를 신경쓰지 않아도 되므로 구현 단순
  - Server는 각각의 요청을 완전히 별개의 것으로 인식하고 처리한다. -> 각 API 서버는 Client의 요청만을 단순 처리한다.
  - 즉, 이전 요청이 다음 요청의 처리에 연관되어서는 안된다.
  - 물론 이전 요청이 DB를 수정하여 DB에 의해 바뀌는 것은 허용한다.
  - Server의 처리 방식에 일관성을 부여하고 부담이 줄어들며, 서비스의 자유도가 높아진다.
3. Cacheable(캐시 처리 가능)
  - 웹 표준 HTTP 프로토콜을 그대로 사용하므로 웹에서 사용하는 기존의 인프라를 그대로 활용할 수 있다.
  - 즉, HTTP가 가진 가장 강력한 특징 중 하나인 캐싱 기능을 적용할 수 있다.
  - HTTP 프로토콜 표준에서 사용하는 Last-Modified 태그나 E-Tag를 이용하면 캐싱 구현이 가능하다.
  - 대량의 요청을 효율적으로 처리하기 위해 캐시가 요구된다.
  - 캐시 사용을 통해 응답시간이 빨라지고 REST Server 트랜잭션이 발생하지 않기 때문에 전체 응답시간, 성능, 서버의 자원 이용률을 향상시킬 수 있다.
4. Layered System(계층화)
  - Client는 REST API Server만 호출한다.
  - REST Server는 다중 계층으로 구성될 수 있다.
  - API Server는 순수 비즈니스 로직을 수행하고 그 앞단에 보안, 로드밸런싱, 암호화, 사용자 인증 등을 추가하여 구조상의 유연성을 줄 수 있다.
  - 또한 로드밸런싱, 공유 캐시 등을 통해 확장성과 보안성을 향상시킬 수 있다.
  - PROXY, 게이트웨이 같은 네트워크 기반의 중간 매체를 사용할 수 있다.
5. Code-On-Demand(optional)
  - Server로부터 스크립트를 받아서 Client에서 실행한다.
  - 반드시 충족할 필요는 없다.
6. Uniform Interface(인터페이스 일관성)
  - URI로 지정한 Resource에 대한 조작을 통일되고 한정적인 인터페이스로 수행한다.
  - HTTP 표준 프로토콜에 따르는 모든 플랫폼에서 사용이 가능하다.
  - 특정 언어나 기술에 종속되지 않는다


REST API의 개념

REST API의 정의

1. 네트워크 구조의 한 형식. 서버의 자원을 정의하고, 자원에 대한 주소를 지정하는 방법을 가리킨다.
2. REST 기반으로 서비스 API를 구현한 것

  - 최근 OpenAPI(누구나 사용할 수 있도록 공개된 API: 구글 맵, 공공 데이터 등), 마이크로 서비스(하나의 큰 애플리케이션을 여러 개의 작은 애플리케이션으로 쪼개어 변경과 조합이 가능하도록 만든 아키텍처) 등을 제공하는 업체 대부분은 REST API를 제공한다.

 

REST API의 특징

  - 사내 시스템들도 REST 기반으로 시스템을 분산해 확장성과 재사용성을 높여 유지보수 및 운용을 편리하게 할 수 있다.
  - REST는 HTTP 표준을 기반으로 구현하므로, HTTP를 지원하는 프로그램 언어로 클라이언트, 서버를 구현할 수 있다.
  - 즉, REST API를 제작하면 델파이 클라이언트 뿐 아니라, 자바, C#, 웹 등을 이용해 클라이언트를 제작할 수 있다.


RESTful의 개념

  - RESTful이란?
    : RESTful은 일반적으로 REST라는 아키텍처를 구현하는 웹 서비스를 나타내기 위해 사용되는 용어이다.
  - ‘REST API’를 제공하는 웹 서비스를 ‘RESTful’하다고 할 수 있다.
  - RESTful은 REST를 REST답게 쓰기 위한 방법으로, 누군가가 공식적으로 발표한 것이 아니다.
  - 즉, REST 원리를 따르는 시스템은 RESTful이란 용어로 지칭된다.


RESTful의 목적

  - 이해하기 쉽고 사용하기 쉬운 REST API를 만드는 것
  - RESTful한 API를 구현하는 근본적인 목적이 성능 향상에 있는 것이 아니라 일관적인 컨벤션을 통한 API의 이해도 및 호환성을 높이는 것이 주 동기이니, 성능이 중요한 상황에서는 굳이 RESTful한 API를 구현할 필요는 없다.
  - RESTful 하지 못한 경우
Ex1) CRUD 기능을 모두 POST로만 처리하는 API

Ex2) route에 resource, id 외의 정보가 들어가는 경우(/students/updateName)

 

참고
https://gmlwjd9405.github.io/2018/09/21/rest-and-restful.html

Posted by yongminLEE
2019. 9. 5. 10:58

1. 웹서버 생성

yarn add express

 

//src/index.js
const http = require('http');
const express = require('express');

//익스프레스 서버 객체
const app = express();

//웹 서버의 port속성 설정
app.set('port', 3000);

http.createServer(app).listen(app.get('port'), ()=>{
    console.log('express server starts : ' + app.get('port'));
});

express server object의 메소드

- set(name, value) : 서버 설정을 위한 속성 지정

- get(name) : 지정한 속성 반환

- use([path], function) : 미들웨어함수 등록

- get([path], function) : 특정 패스로 요청된 정보 처리

 

 

2. 미들웨어

2-1. 미들웨어등록

//src/index.js
const http = require('http');
const express = require('express');

//익스프레스 서버 객체
const app = express();

//미들웨어 등록
app.use((req,res,next)=>{
    console.log('first middleware');

    //req객체에 user속성을 추가하고 문자열 할당
    req.user = 'mike';

    //다음 미들웨어 호출
    next();
});
app.use('/',(req,res,next)=>{
    console.log('second middleware');
    res.writeHead('200', {'content-type':'text/html;charset=utf-8'});
    res.write('<p>express server 응답 결과 : ' + req.user + '</p>');
    res.end();
})

//웹 서버의 port속성 설정
app.set('port', 3000);

http.createServer(app).listen(app.get('port'), ()=>{
    console.log('express server starts : ' + app.get('port'));
});

- app.use파라미터로 등록된 함수가 하나의 미들웨어

- 미들웨어 함수는 req, res, next 객체를 파라미터로 받음

- req : 요청객체

- res : 응답객체

- next() : 현재처리중인 미들웨어의 다음 미들웨어를 호출하는 함수

 

2-2. 응답객체 메소드

- writeHead(statusCode, statusMessage, headers)

- write(chunk, encoding, callback) : 여러번 작성가능

- end(data, encoding, callback)

+

express에서 추가로 사용 가능한 메소드

- send() : 클라이언트에 모든 종류의 응답데이터를 보낸다 (html, buffer객체, json객체, json배열 등등 -> 만능메서드)

//app.use('/',(req,res,next)=>{
//   res.writeHead('200', {content-type':'text/html;charset=utf-8'});
//	 res.write("<p>server response</p>");
//	 res.end();
//});

app.use('/',(req,res,next)=>{
    res.send('<p>express server response</p>');
});

- status(code) : http 상태코드 반환 -> send()메소드를 연속으로 호출해야 전송

- sendStatus(code) = status(code).send();

app.use('/',(req,res,next)=>{
    res.status(404).send();
})

//app.use('/',(req,res,next)=>{
//    res.sendStatus(404);
//})

- redirect([status], path) : 웹페이지 경로를 강제로 이동

app.use('/',(req,res,next)=>{
	//디폴트 상태값 : 302
    res.redirect(302,'http://www.naver.com');
});

 

2-3. 요청객체의 속성

- req.query : 클라이언트에서 get방식으로 전송한 데이터(querystring) 확인, url query로 전송한 데이터(querystring) 확인

- req.body : 클라이언트에서 post방식으로 전송한 데이터(querystring) 확인, (body-parser 외장모듈 필요)

- req.params : 라우트 파라미터를 확인할 수 있는 객체, 이 객체의 속성을 "토큰"이라고 한다.

- header(name) : header를 확인

 

2-4. static middleware

: 특정 폴더의 파일들을 특정패스로 접근할 수 있도록 만들어준다.

yarn add server-static

public 디렉토리를 만들고 그 안에 index.html파일을 만든다.

app.use('/public', static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

-> http://localhost:3000/public/index.html로 index.html 접근 가능

 

2-5. body-parser middleware

: 클라이언트가 post방식으로 요청할 때 본문 영역에 들어 있는 요청파라미터(querystring)들을 파싱하여

요청객체(req)의 "body" 속성에 넣어준다. ex) req.body.key

<!-- public/index.htrml -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>

<body>
    <h1>test page</h1>
    <!-- post방식 -->
    <form method="post">   
        <table>
            <tr>
                <td><label>아이디</label></td>
                <td><input type="text" name="id"></td>
            </tr>
            <tr>
                <td><label>비밀번호</label></td>
                <td><input type="password" name="pw"></td>
            </tr>
        </table>
        <input type="submit" value="submit" name="">
    </form>
</body>

</html>

 

//src/index.js
const http = require('http');
const express = require('express');
const static = require('serve-static');
const path = require('path');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();
app.set('port', 3000);

//미들웨어 등록

//body-parser를 사용해 application/x-www-from-urlencoded 파싱
app.use(bodyParser.urlencoded({extended:false}));

//body-parser를 사용해 application/json 파싱
app.use(bodyParser.json());

app.use('/public', static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

app.use((req,res)=>{
    const paramId = req.body.id;
    const paramPw = req.body.pw;

    res.send("id : " + paramId + "<br>" + "pw : " + paramPw);
    // res.writeHead('200', {'content-type':'text/html;charset=utf-8'});
    // res.write("<h1>response</h1>");
    // res.write("<p>id : " + paramId + "</p>");
    // res.write("<p>pw : " + paramPw + "</p>");
    // res.end();
})

//웹 서버의 port속성 설정
http.createServer(app).listen(app.get('port'), ()=>{
    console.log('express server starts : ' + app.get('port'));
});

 

3.  라우팅

: 다른주소로 요청이 들어올 때, 다른 작업을 처리할수 있도록 라우터 사용 -> 사용자가 요청한 기능이 무엇인지 패스를 기준으로 구별

3-1. 라우터 설정 및 등록

<!-- public/index.htrml -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>

<body>
    <!-- GET방식 -->
    <h1>GET</h1>
    <form method="GET" action="../src/index.js">
        <table>
            <tr>
                <td><label>아이디</label></td>
                <td><input type="text" name="gid"></td>
            </tr>
            <tr>
                <td><label>비밀번호</label></td>
                <td><input type="password" name="gpw"></td>
            </tr>
        </table>
        <input type="submit" value="submit" name="">
    </form>
    <hr>
    <!-- POST방식 -->
    <h1>POST</h1>
    <form method="POST">
        <table>
            <tr>
                <td><label>아이디</label></td>
                <td><input type="text" name="pid"></td>
            </tr>
            <tr>
                <td><label>비밀번호</label></td>
                <td><input type="password" name="ppw"></td>
            </tr>
        </table>
        <input type="submit" value="submit" name="">
    </form>
</body>

</html>

 

// src/index.js
const http = require('http');
const express = require('express');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();
//라우터 객체 참조
const router = express.Router();
//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우팅 함수 설정
router.get('/',(req, res) => {
    console.log("/ get방식으로 요청들어옴");
    res.send("<p>HOME</p>");
});
router.get('/src/index.js',(req, res) => {
    console.log("/src/index.js 에서 get방식으로 요청들어옴");
    const paramgId = req.query.gid;
    const paramgPw = req.query.gpw;
    res.writeHead('200', { 'content-type': 'text/html;charset=utf-8' });
    res.write("<h1>GET</h1>");
    res.write("<p>id : " + paramgId + "</p>");
    res.write("<p>pw : " + paramgPw + "</p>");
    res.end();
});
router.post('/public/index.html',(req, res) => {
    console.log("/public/index.html 에서 post방식으로 요청들어옴");
    const paramId = req.body.pid;
    const paramPw = req.body.ppw;
    res.writeHead('200', { 'content-type': 'text/html;charset=utf-8' });
    res.write("<h1>POST</h1>");
    res.write("<p>id : " + paramId + "</p>");
    res.write("<p>pw : " + paramPw + "</p>");
    res.end();
});

//라우터 등록
app.use('/', router);

//웹 서버의 port속성 설정
http.createServer(app).listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});

- 라우팅 함수 설정 (19~43행)

  - GET 방식으로 request 발생시 req.query 객체에서 속성조회가능

  - POST 방식으로 request 발생시 req.body 객체에서 속성조회가능 (body-parser 미들웨어 필수)

- 라우터 등록 (46행)

 

3-2. 클라이언트->서버로 데이터(querystring) 전송 방법 : URL query와 라우트 파라미터, form태그

- URL query(url parameter) : url 뒤에 ?를 붙이고 querysting을 추가하여 보내면 자동으로 객체 형태로 파싱되어 req.query에서 확인 가능

- 라우트파라미터 : 라우트경로 설정 시 맨뒤에 /:parameter? 와 같은 형식으로 라우터 경로 설정하면 req.params 객체에서 속성(토큰)으로 확인 가능. 이 속성을 '토큰'이라고 부른다.

- form 태그 : method속성으로 데이터를 보내는 방식을 설정 가능 (get, post, etc) 

=> get 방식으로 보낸 데이터는 req.query, post방식으로 보낸 데이터는 req.body에서 확인 가능

// src/index.js
//...
router.get('/src/index.js/:name?',(req, res) => {
    console.log("/src/index.js 에서 get방식으로 요청들어옴");
    const {name} = req.params;
    const paramgId = req.query.gid;
    const paramgPw = req.query.gpw;
    res.writeHead('200', { 'content-type': 'text/html;charset=utf-8' });
    res.write("<h1>GET</h1>");
    res.write("<p>id : " + paramgId + "</p>");
    res.write("<p>pw : " + paramgPw + "</p>");
    res.write("<p>name : " + name + "</p>");
    res.end();
});
/...

=> url :  http://localhost:3000/src/index.js/John?gid=123&gpw=456 

=> 결과 : id : 123 pw = 456 name : John (John은 토큰)

 

3-3. request path를 router 객체에 설정시 사용하는 메소드

- get('request path', callback) : get 방식으로 request path 에서 요청이 발생이 콜백함수 실행

- post('request path', callback) : post 방식으로 request path 에서 요청이 발생이 콜백함수 실행

- put('request path', callback) : put 방식으로 request path 에서 요청이 발생이 콜백함수 실행

- delete('request path', callback) : delete 방식으로 request path 에서 요청이 발생이 콜백함수 실행

- patch('request path', callback) : patch 방식으로 request path 에서 요청이 발생이 콜백함수 실행펭

- all('request path', callback) : 모든방식으로 request path 에서 요청이 발생이 콜백함수 실행 - > 페이지 오류 응답 처리하는데 사용

// src/index.js
//...
router.all('*', (req,res)=>{
    res.status(404).send("<h1>404 ERROR - page not found</h1>");
});
/...

 

 

3-4. 라우트 모듈화

- 모듈화 전

// src/index.js
const http = require('http');
const express = require('express');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();
//라우터 객체 참조
const router = express.Router();

//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우팅 함수 
router.get('/',(req, res) => {
    console.log("/ get방식으로 요청들어옴");
    res.send("<h1>HOME</h1>");
});
router.get('/about/:name?', (req,res)=>{
    const {name} = req.params;
    if(name){
        res.send(`${name} 소개`);
    }else{
        res.send("소게");
    }
});
router.get('/posts',(req,res)=>{
    const {id} = req.query;
    if(id){
        res.send(`포스트 #${id}`);
    }else{
        res.send("포스트id 없음");
    }
});
router.all('*', (req,res)=>{
    res.status(404).send("<h1>404 ERROR - page not found</h1>");
});

//라우터 등록
app.use('/', router);

//웹 서버의 port속성 설정
http.createServer(app)
.listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});

 

- 모듈화 후

// src/index.js
const http = require('http');
const express = require('express');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();

//라우터 설정
const router = require('./router');

//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우터 등록
app.use('/', router);

//웹 서버의 port속성 설정
http.createServer(app)
.listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});

 

//라우트 모듈화
// src/router/index.js
const express = require('express');

const router = express.Router();


router.get('/',(req, res) => {
    console.log("/ get방식으로 요청들어옴");
    res.send("<h1>HOME</h1>");
});
router.get('/about/:name?', (req,res)=>{
    const {name} = req.params;
    if(name){
        res.send(`${name} 소개`);
    }else{
        res.send("소게");
    }
});
router.get('/posts',(req,res)=>{
    const {id} = req.query;
    if(id){
        res.send(`포스트 #${id}`);
    }else{
        res.send("포스트id 없음");
    }
});
router.all('*', (req,res)=>{
    res.status(404).send("<h1>404 ERROR - page not found</h1>");
});


module.exports = router;

 

- posts 라우트

//src/api/posts/index.js
const express = require('express');
const posts = express.Router();

const printInfo = (req,res)=> {
    res.send(req.body = {
        method:req.method,
        path : req.originalUrl,
        params: req.params
    })
};

posts.get('/', printInfo);
posts.post('/', printInfo);
posts.get('/:id', printInfo);
posts.delete('/:id', printInfo);
posts.put('/:id', printInfo);
posts.patch('/:id', printInfo);

module.exports = posts;

 

//src/api
const express = require('express');
const api = express.Router();
const posts = require('./posts');

api.use('/posts', posts);

module.exports = api;

 

//src/index.js
const http = require('http');
const express = require('express');
// const static = require('serve-static');
// const path = require('path');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();

//라우터 설정
const api = require('./api');

//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우터 등록

app.use('/api', api);

//웹 서버의 port속성 설정
http.createServer(app)
.listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});

 

3-5. 컨트롤러 파일

- 컨트롤러 : 라우트 처리 함수만 모아놓은 파일

- 컨트롤러에서는 백엔드 기능을 구현

 

//src/api/posts/posts.ctrl.js
//컨트롤러 파일
let postId = 1; // id 초깃값

const posts = [{ id: 1, title: '제목', body: '내용' }];
//목록조회 GET /api/posts 
exports.list = (req, res) => {
    res.send(posts);
};

//특정포스트 조회 GET /api/posts/:id 
exports.read = (req, res) => {
    const { id } = req.params;

    const post = posts.find(p => p.id.toString() === id);

    //포스트 없으면 오류 반환
    if (!post) {
        res.status(404).send("포스트없음");
        return;
    }
    res.send(post);
};

//포스트 작성 POST /api/posts
exports.write = (req, res) => {
    const { title, body } = req.body;
    postId += 1;
    const post = { id: postId, title, body };
    posts.push(post);
    res.send(post);
};

//특정포스트 제거 DELETE /api/posts/:id
exports.remove = (req, res) => {
    const { id } = req.params;

    const index = posts.findIndex(p => p.id.toString() === id);
    if (index === -1) {
        res.status(404).send("포스트없음");
        return;
    }
    posts.splice(index, 1);
    res.status(204).send("콘텐츠 없음, no content");
};

// 포스트 수정 PUT /api/posts/:id
//put 메소드는 전체 포스트 정보를 입력하여 데이터를 통째로 교체할 때 사용
exports.replace = (req, res) => {
    const { id } = req.params;
    const index = posts.findIndex(p => p.id.toString() === id);
    if (index === -1) {
        res.status(404).send("포스트없음");
        return;
    }
    posts[index] = {
        id,
        ...req.body
    };
    res.send(posts[index]);
};

//포스트 수정 PATCH /api/posts/:id
exports.update = (req, res) => {
    const { id } = req.params;
    const index = posts.findIndex(p => p.id.toString() === id);
    if (index === -1) {
        res.status(404).send("포스트없음");
        return;
    }
    posts[index] = {
        ...posts[index],
        ...req.body
    }
    res.send(posts[index]);
};

 

//src/api/posts/index.js
// posts 라우터
const express = require('express');
const posts = express.Router();
const postsCtrl = require('./posts.ctrl');

posts.get('/', postsCtrl.list);
posts.post('/', postsCtrl.write);
posts.get('/:id', postsCtrl.read);
posts.delete('/:id', postsCtrl.remove);
posts.put('/:id', postsCtrl.replace);
posts.patch('/:id', postsCtrl.update);

module.exports = posts;

 

//src/api/index.js
//라우트 모듈화
const express = require('express');
const api = express.Router();
const posts = require('./posts');

api.use('/posts', posts);

module.exports = api;

 

//src/index.js
// 백엔드 프로그래밍
const http = require('http');
const express = require('express');
// const static = require('serve-static');
// const path = require('path');
const bodyParser = require('body-parser');

//익스프레스 서버 객체
const app = express();

//라우터 설정
const api = require('./api');

//포트설정
app.set('port', 3000);

//미들웨어 등
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// static 미들웨어 
app.use('/public', express.static("/home/yongmin/Desktop/React/blog/blog-backend/public"));

//라우터 등록

app.use('/api', api);

//웹 서버의 port속성 설정
http.createServer(app)
.listen(app.get('port'), () => {
    console.log('express server starts : ' + app.get('port'));
});
Posted by yongminLEE
2019. 9. 5. 10:56

1. 기본 개념

* Node.js

: V8-engine based runtime which allows using Javascript in server

V8 자바스크립트 엔진을 기반으로 하는 서버에서 자바스크립트를 사용하게 해주는 런타임

 

- 특징

1. single thread

2. non-blocking I/O

3. event-drive

 

* Global Object

1. console : 콘솔 창에 결과를 보여주는 객체

 - dir(object) : 객체의 속성들을 출력

 - time(id) : 실행시간 측정을 위한 시작시간 기록

 - timeEnd(id) :  실행시간 측정을 위한 종료시간 기록

 

2. process : 프로세스의 실행에 대한 정보를 다루는 객체

 - argv : 프로세스를 실행할 때 전달되는 파라미터 정보를 담고 있는 배열 객체

 - env : 환경변수 정보를 담고 있는 객체

 - exit() : 프로세스를 끝내는 메소드

 

3. exports : 모듈을 다루는 객체

 

* Global variable

- __dirname : 폴더의 전체 패스가 출력됨

- __filename : 파일의 전체 패스가 출력됨

 

* Modules

: 별도의 파일로 분리된 독립 기능의 모음

 

1. 외장 모듈 : 다른 사람이 만들어 둔 모듈

 

2. 내장 모듈

- os : 시스템 정보를 알려줌

- path : 파일 패스를 다루는 메소드 제공

  - join() : 여러개의 이름을 합쳐 하나의 파일 패스로 만들어 줌

  - dirname() : 파일 패스에서 디렉터리 이름을 반환

  - basename() : 파일 패스에서 파일의 확장자를 제외한 이름 반환

  - extname() : 파일 패스에서 파일의 확장자 반환

- url : 주소 문자열 안에 있던 정보를 나누고 객체를 만들어 객체의 속성으로 보관

- querystring : 요청 파라미터(query string)를 쉽게 분리

- fs : 파일시스템에 접근및 파일을 다루는 메소드 제공

-http : 웹 서버 기능을 담당하는 서버객체를 만들 수 있음

-events : 이벤트 핸들링 메소드 제공 

-util : 상속메소드 제공

 

* Handling url and request parameters

  - url.parse() : 주소문자열을 파싱하여 url객체를 생성

  - url.format() : url객체를 주소문자열로 변환

  - querystring.parse() : 주소문자열을 파싱하여 url객체를 생성

  - querystring.format() : url객체를 주소문자열로 변환

 

* Handling events

- require('events').EventEmitter.on(event, listener) : 지정한 이벤트 리스너를 추가

- require('events').EventEmitter.once(event, listener) : 지정한 이벤트 리스너를 추가하지만 한 번 실행후 자동으로 리스너 제거

- require('events').EventEmitter.removeListener(event, listener) : 지정한 이벤트에 대한 리스너 제거

- require('events').EventEmitter.emit(event) : 지정한 이벤트 발생

 

* request vs response

- request : 클라이언트가 서버에게 웹페이지를 보여달라고 말하는 것

- reponse : 서버가 클라이언트에게 요청받은 것에 대한 대답으로, 웹페이지 내용을 표현하기 위해 html문서로 주는것

 

* HTTP 패킷

 클라이언트가 서버로 요청을 했을때, 보내는 데이터

-  HTTP패킷의 구조는 크게 헤더  바디로 나뉘어진다.

- 헤더에는 7가지 HTTP 메서드 방식(GET, POST, PUT, DELETE, PATCH, ...)중 무엇을 썻는지, 클라이언트의 정보, 브라우저 정보, 접속할 URL 등등 과 같은 클라이언트 정보를 담는다. 

- 바디는 보통 비어있다. 하지만, 특정 데이터를 담아서 서버에게 요청을 보낼 수 있다.

 

* GET방식 vs POST방식 

두 방식 모두, 서버에 요청을 하는 메서드이다.

 

- get 방식 : 'HTTP패킷 헤더'부분에 요청 정보를 넣어 보낸다

- post 방식 : 'HTTP패킷 바디'부분에 요청 정보를 넣어 보낸다

 

get 방식은 주소 표시줄에 입력한 내용이 나타나며 256byte~4096byte까지의 데이터만을 서버로 전송할 수 있다. 주소 표시줄에 입력한 내용이 노출되기 때문에 보안상의 문제가 민감한 경우에는 사용하지 않는다.
- post 방식은 입력된 내용의 크기에 제한을 받지 않고 입력한 내용이 노출되지 않기 때문에 회원가입, 로그인 시 등에 많이 사용

 

 

2. 간단한 웹서버 구현

* 서버 구현

//src/index.js
const http = require('http');

//웹 서버 객체 생성
const server = http.createServer();

//웹 서버를 시작하며 3000번 포트에서 대기하도록 설정
const port = 3000;
server.listen(port,()=>{
    console.log('웹서버가 시작됨 [port : ' + port + ']');
});

//클라이언트 연결 이벤트(connection) 처리 : 클라이언트가 웹서버에 연결되면 connection 이번트 발생
server.on('connection', (socket)=>{

    //클라이언트의 ip와 port 정보 확인
    const addr = socket.address();
    console.log('클라이언트 접속 : %s, %d', addr.address, addr.port);
});

//클라이언트 요청 이벤트(request) 처리 : 클라이언트가 특정패스로 요청을 하면 request 이벤트 발생
server.on('request', (req,res)=>{
    console.log('request 이벤트 발생');
    console.dir(req);
    
    // 클라이언트로 응답을 보냄
    res.writeHead(200, {'Content-Type':'text/html; charset=utf-8'}); //응답으로 보낼 헤더
    res.write("<!doctype html>");
    res.write("<html>");
    res.write("<head>");
    res.write("<title>nodeTest</title>");
    res.write("</head>");
    res.write("<body>");
    res.write("<p>hello node</p>");
    res.write("</body>");   
    res.write("</html>");
    res.end();     //전송
});

//서버 종료 이벤트 처리
server.on('close', ()=>{
    console.log('서버가 종료 됨');
});

MIME Type (Multipurpose Internet Mail Extensions :내용이 어떤 형식인지 알려주기 위한 인터넷 표준) -> content-type의 값

- text/plain : 일반 텍스트 문서

- text/html

- text/css

- text/xml

- image/jpeg, image/png

- vedio/mpeg, audio/mp3

- application/zip

 

* 서버 실행

node src
Posted by yongminLEE
2019. 9. 3. 13:39

Webpack에서 Resolve 설정을 추가해주면 React 프로젝트의 루트디렉토리를 설정합니다.

// webpack.config.js
// webpack.dev.config.js
var path = require('path');
/* .. 코드 생략 .. */
resolve: {
    root: path.resolve('./src')
}

이렇게하면 코드에서 import 시 path에서 간단하게 접근할 수 있게 해줍니다.

 

resolve 셋팅 전

import Home from './components/Home';
// or
import Home from './../components/Home';

 

resolve 셋팅 후

import Home from 'components/Home';

'Javascript > React' 카테고리의 다른 글

#3 React Event Handling  (0) 2020.02.12
#2 React Component  (0) 2020.02.12
#1 basic react  (0) 2020.02.12
#0 React intro  (0) 2020.02.12
react-router  (0) 2019.09.03
Posted by yongminLEE
2019. 9. 3. 13:37

1. previous page VS SPA

- previous page

: 유저가 요청할 때마다 페이지를 새로고침하며, 페이지를 로딩할 때마다 서버에서 리소를 전달받아 해석한 후 렌더링. HTML파일 또는 템플릿 엔진(ex: pug)등을 사용해서 뷰를 어떻게 보일지도 서버에서 담당

=> 단점 : 서버에서 렌더링을 담당하므로 그만큼 서버자원 소모->불필요한 트래픽낭비

=> 해결 : 리액트와 같은 라이브러리나 프레임워크를 사용하여 뷰 렌더링을 유저의 웹브라우저가 담당하도록하고, app을 우선 웹 브라우저에 로드시킨 후 필요한 데이터만 전달

 

- SPA

: 서버에서 제공하는 페이지가 하나이지만, 웹브라우저에서 나머지 페이지들을 정의. 다른 페이지로 이동할 때는 서버에 새로운 페이지를 요청하는 것이 아니라, 새 페이지에서 필요한 데이터만 받아 와 그에 따라 웹브라우저가 다른 종류의 뷰를 만든다.

 

- routing

: 주소에 따라 다른 뷰를 보여주는 것

 

- react-router

: 서드 파티 라이브러리로, 페이지 주소를 변경했을 때 주소에 따라 다른 컴포넌트를 렌더링하고, 주소정보를 컴포넌트의 props로 전달해서 컴포넌트 단에서 주소 상태에 따라 다른 작업을 하도록 설정가능

 

2. react-router 라이브러리를 이용한 SPA 개발

2-1. 프로젝트 생성및 컴포넌트 준비

$ create-react-app react-router-project
$ yarn add creat-router-dom

 

2-2. 컴포넌트 준비

//src/App.js
import React from 'react';

function App() {
  return (
    <div className="App">
    	react-router
    </div>
  );
}

export default App;

 

//src/Root.js
import React from 'react';
import App from './App';

//BrowserRouter : HTML$의 history api를 사용하여 새로고침하지 않고도 페이지 주소를 교체
import {BrowserRouter} from 'react-router-dom'; 

const Root = () => {
    return (
        <BrowserRouter> 
            <App />
        </BrowserRouter>
    );
};

export default Root;

 

//src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Root from './Root';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<Root />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

 

2-3. 기본 라우트 설정

//src/pages/Home.js
import React from 'react';

const Home = () => {
    return (
        <div>
            <h1>HOME</h1>
        </div>
    );
};

export default Home;

 

//src/pages/About.js
import React from 'react';

const About = () => {
    return (
        <div>
            <p>this is about page</p>
        </div>
    );
};

export default About;

 

export {default as Home} from './Home';
export {default as About} from './About';

 

//src/App.js
import React from 'react';

// router
import {Route} from 'react-router-dom';
import {Home, About} from './pages';

function App() {
  return (
    <div className="App">
      <Route exact path="/" component={Home} />
      <Route path="/about" component={About} />
    </div>
  );
}

export default App;

 

2-4. 라우트 파라미터와 쿼리 읽기

//src/App.js
import React from 'react';

// router
import {Route} from 'react-router-dom';
import {Home, About} from './pages';

function App() {
  return (
    <div className="App">
      <Route exact path="/" component={Home} />
      <Route path="/about/:name?" component={About} /> 
    </div>
  );
}

export default App;

- params를 지정할 때는 :key형식으로 설정

- :name값을 선택적으로 입력받을 수 있게 뒷부분에 ?를 입력

//src/pages/About.js

import React from 'react';
import queryString from 'query-string';

const About = ({location, match}) => {
    const query = queryString.parse(location.search);
    const {color} = query //const color = query.color;

    return (
        <div>
            <h1 style={{color}}>About</h1>
            <p>this is about page</p>
            <p>params : {match.params.name}</p>
        </div>
    );
};

export default About;

- query-string 라이브러리는 문자열로 된 쿼리를 객체 형태로 파싱

 

2-5. 라우터이동 : Link컴포넌트, NavLink 컴포넌트, 자바스크립트에서 라우팅

//src/components/Menu.js
import React from 'react';
import {Link} from 'react-router-dom';
import {NavLink} from 'react-router-dom';

const Menu = () => {
    const activeStyle = {
        color:'green',
        fontSize:'2rem'
    }

    return (
        <div>
            <ul>
                <li><Link to="/">HOME</Link></li>
                <li><Link to="/about">ABOUT</Link></li>
                <li><Link to="/about/react">ABOUT REACT</Link></li>
                <li><a href="/about">ABOUT with 새로고칭</a></li>
                <li><a href="/about/react">ABOUT REACT with 새로고칭</a></li>
                <li><NavLink exact to="/" activeStyle={activeStyle}>HOME</NavLink></li>
                <li><NavLink exact to="/about" activeStyle={activeStyle}>ABOUT</NavLink></li>
                <li><NavLink exact to="/about/react" activeStyle={activeStyle}>ABOUT REACT</NavLink></li>
            </ul>
        </div>
    )
};

export default Menu;

- <a href>...</a> : 페이지를 새로고침하면서 로딩

- Link 컴포넌트 : 페이지를 새로고침하여 불러오지 않고, 주소 창 상태를 변경하고 원하는 라우트로 화면을 전환

- NavLink 컴포넌트 : 현재주소와 해당 컴포넌트의 목적지 주소가 일치하면 특정스타일 또는 클래스 지정 가능

 

//src/pages/Home.js
import React from 'react';

const Home = ({history}) => {
    return (
        <div>
            <h1>HOME</h1>
            <button onClick={()=>{history.push('./about/JS')}}>JS로 라우팅</button>
        </div>
    );
};

export default Home;

 

2-6. 서브라우터 : 라우트 안의 라우트 

//src/components/Menu.js
import React from 'react';
import {NavLink} from 'react-router-dom';

const Menu = () => {
    const activeStyle = {
        color:'green',
        fontSize:'2rem'
    }

    return (
        <div>
            <ul>
                <li><NavLink exact to="/" activeStyle={activeStyle}>HOME</NavLink></li>
                <li><NavLink exact to="/about" activeStyle={activeStyle}>ABOUT</NavLink></li>
                <li><NavLink exact to="/about/react" activeStyle={activeStyle}>ABOUT REACT</NavLink></li>
                <li><NavLink exact to="/postList" activeStyle={activeStyle}>POST LIST</NavLink></li>
            </ul>
        </div>
    )
};

export default Menu;

 

//src/App.js
import React from 'react';
import Menu from './components/Menu';

// router
import {Route} from 'react-router-dom';
import {Home, About, PostList} from './pages';

function App() {
  return (
    <div className="App">
      <Menu />
      <Route exact path="/" component={Home} />
      <Route path="/about/:name?" component={About} />
      <Route path="/postList" component={PostList} />
    </div>
  );
}

export default App;

 

//src/pages/PostList.js
import React from 'react';
import { Post } from 'pages';
import { Link, Route } from 'react-router-dom';

const PostList = ({ match }) => {
    return (
        <div>
            <h2>POST LIST</h2>
            <ul>
                <li><Link to={`${match.url}/1`}>POST #1</Link></li>
                <li><Link to={`${match.url}/2`}>POST #2</Link></li>
                <li><Link to={`${match.url}/3`}>POST #3</Link></li>
            </ul>
            <Route exact path={match.url} render={() => (<p>choose a post</p>)} />
            <Route exact path={`${match.url}/:id`} component={Post} />
        </div>
    );
};

export default PostList;

 

//src/pages/Post.js
import React from 'react';

const Post = ({match}) => {

    return (
        <div>
            <p>포스트 id : {match.params.id}</p>
        </div>
    );
};

export default Post;

 

//src/pages/index.js
export {default as Home} from './Home';
export {default as About} from './About';
export {default as Post} from './Post';
export {default as PostList} from './PostList';

 

2-7. 라우트로 사용된 컴포넌트가 전달받는 props : location, match, history

 

- location.pathname 은 현재 브라우저상의 위치를 알려줍니다. 이 값은 어떤 라우트에서 렌더링하던 동일합니다.

- match 관련은 설정한 Route 와 직접적으로 관계된 값만 보여줍니다.

  • Posts 를 보여주는 라우트에선 :id 값을 설정하지 않았으니 path 와 url 이 둘다 /posts 입니다.
  • Post 를 보여주는 라우트에선 path 의 경우엔 라우트에서 설정한 path 값이 그대로 나타납니다. url 의 경우엔 :id 부분에 값이 들어간 상태로 나타납니다.- histroy는 현재 라우터를 조작할 때 사용

'Javascript > React' 카테고리의 다른 글

#3 React Event Handling  (0) 2020.02.12
#2 React Component  (0) 2020.02.12
#1 basic react  (0) 2020.02.12
#0 React intro  (0) 2020.02.12
webpack - resolve  (0) 2019.09.03
Posted by yongminLEE
2019. 9. 2. 12:19

1. middleware?

: 미들웨어는 액션을 디스패치했을 때 리듀서에서 이를 처리하기 전에 사전에 지정된 작업들을 실행

-> 액션과 리듀서 사이의 중간자

 

1-1. loggerMiddleware.js 구현

const middleware = store => next => action => {
    // 현재 스토어 상태 값 기록
    console.log('현재 상태', store.getState());
    // 액션 기록
    console.log('액션',action);

    // 액션을 다음 미들웨어 또는 리듀서에 전달
    const result = next(action);

    // 액션 처리 후 스토어의 상태를 기록
    console.log('다음 상태', store.getState());
    
    return result; // 여기에서 반환하는 값은 store.dispatch(ACTION_TYPE)했을 때 결과로 설정한다.

}

export default middleware;// 내보내기

- next(action) 을 했을 때는 그다음 처리해야 할 미들웨어로 액션을 넘겨주고, 추가로 처리할 미들웨어가 없다면 바로 리듀서에 넘겨준다

- store.dispatch는 다음 미들웨어로 넘기는 것이 아니라 액션을 처음부터 디스패치한다

 

1-2. 미들웨어 적용

import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import middleware from './lib/middleware';

// 미들웨어가 여러 개일 경우 파라미터로 전달하면 된다. (applyMiddleware(a,b,c))
// 미들웨어 순서는 여기에서 전달한 파라미터 순서대로 지정한다.
const store = createStore(modules, applyMiddleware(middleware));

export default store;

 

1-3. redux-logger 라이브러리

import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import {createLogger} from 'redux-logger';

const logger = createLogger();
const store = createStore(modules, applyMiddleware(logger));

export default store;

 

1-4 . 결과

*크롬브라우저의 콘솔창 확인

 

 

2. 미들웨어로 비동기 액션 처리

: 여러 오픈소스 라이브러리 존재 : redux-thunk, redux-saga, redux-pender, redux-promise-middleware etc...

 

2-1. redux-thunk 모듈

- thunk : 특정작업을 나중에 할 수 있도록 함수 형태로 감싼 것

- redux-thunk : 함수를 디스패치함으로써 일반 액션 객체로는 할 수 없는 작업을 가능하게함

 

ex) 1초 뒤에 액션이 디스패치되는 코드

import { createStore, applyMiddleware } from 'redux';
import modules from './modules';
import {createLogger} from 'redux-logger';
import ReduxThunk from 'redux-thunk';

const logger = createLogger();
const store = createStore(modules, applyMiddleware(logger, ReduxThunk));

export default store;

 

import {handleActions, createAction} from 'redux-actions';

//action types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT';

//action creators
export const increment = createAction(INCREMENT);
export const decrement = createAction(DECREMENT);

//thunks
export const incrementAsync = () => dispatch => {
    // 1초 뒤 액션 디스패치
    setTimeout(
        () => { dispatch(increment()) }, //함수를 dispatch
        1000
    );
}
export const decrementAsync = () => dispatch => {
    // 1초 뒤 액션 디스패치
    setTimeout(
        () => { dispatch(decrement()) }, //함수를 dispatch
        1000
    );
}

//reducer
export default handleActions({
    [INCREMENT]: (state, action) => state +1,
    [DECREMENT]: (state, action) => state -1
}, 0);

 

3. Promise

- Promise : 비동기 처리를 다루는데 사용하는 객체

function printLater(number, fn) {
    setTImeout(
        function() {
            console.log(number);
            if(fn) fn();
        },
        1000
    );
}

//콜백지옥
printLater(1, function() {
    printLater(2, function() {
        printLater(3, function() {
            printLater(4);
        })
    })
});

위와 같은 콜백지옥( 비동기적 작업에 의한 깊고 복잡해진 코드)을 Promise를 통해 다음과 같이 해결

function printLater(number) {
    return new Promise( // Promise 생성 후 리턴
        (resolve, reject) => {
            if ( number > 4 ) {
                return reject('number is greater than 4'); // reject 는 오류를 발생시킨다.
            }
            setTimeout( // 1초 뒤 실행
                () => {
                    console.log(number);
                    resolve(number + 1); // 현재 숫자에 1을 더한 값을 반환한다.
                }, 1000
            );
        }
    );
}

printLater(1)
    .then( num => printLater(num) )
    .then( num => printLater(num) )
    .then( num => printLater(num) )
    .then( num => printLater(num) )
    .catch( e => console.log(e) );

Promise 에서 결과 값을 반환할 때는 resolve(결과 값) 을 작성하고, 오류를 발생시킬 때는 reject(오류)를 작성.

- 여기에서 반환하는 결과 값은.then(), 오류 값은.catch() 에 전달하는 함수의 파라미터로 설정됨.

 

4. redux-thunk와 axios를 사용한 웹 요청 처리

- axios : Promise 기반 HTTP 클라이언트 라이브러리

yarn add axios

 

 

4-1. post 모듈 : API함수, 액션, 액션생성함수, thunk, reducer

// src/modules/post.js
import {handleActions, createAction} from 'reudx-actions';

import axios from 'axios';

//API function
funciton getPostAPI(postId) {
    return axios.get(`url/${postId}`);	//Promise 객체형태로 반환
}

//action types
const GET_POST_PENDING = 'GET_POST_PENDING';
const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
const GET_POST_FAILURE = 'GET_POST_FAILURE';

//action creators
const getPostPending = createAction(GET_POST_PENDING);
const getPostSuccess = createAction(GET_POST_SUCCESS);
const getPostFailure = createAction(GET_POST_FAILURE);

//thunk
export const getPost = (postId) => dispatch => {
    dispatch(getPostPending()); // 요청 시작했다는 것을 알림
    
    // 요청 시작. 여기에서 만든 promise를 return해야 나중에 컴포넌트에서 호출할 때 getPost().then(...)을 할 수 있다.
    return getPostAPI(postId)
    .then( () => {
        // 요청이 성공했다면 서버 응답 내용을 payload로 설정하여 GET_POST_SUCCESS 액션을 디스패치한다.
        dispatch(getPostSuccess(response));
        
        // 후에 getPostAPI.then 을 했을 때 then에 전달하는 함수에서 response 에 접근할 수 있게 한다.
        return response;
    } )
    .catch( error => {
        // 오류가 발생하면 오류 내용을 payload로 설정하여 GET_POST_FAILURE 액션을 디스패치한다.
        dispatch(getPostFailure(error));
        
        // error를 throw하여 이 함수를 실행한 후 다시 한 번 catch를 할 수 있게 한다.
        throw(error);
    } );
}

const initialState = {
    pending: false,
    error: false,
    data: {
        title: '',
        body: ''
    }
}

//reducer
export default handleAction({
    [GET_POST_PENDING]: (state, action) => {
        return {
            ...state,
            pending: true,
            error: false
        };
    },
    [GET_POST_SUCCESS]: (state, action) => {
        const {title, body} = action.payload.data;
        return {
            ...state,
            pending: false,
            data: {
                title,
                body
            }
        };
    },
    [GET_POST_FAILURE]: (state, action) => {
        return {
            ...state,
            pending: false,
            error: true
        }
    }
}, initialState);

- thunk : 요청상태에 따라 액션을 디스패치

 

4-2. post 모듈의 리듀서를 루트 리듀서에 넣어준다

import {combineReducers} from 'redux';
import counter from './counter';
import post from './post';

export default combineReducers({
    counter,
    post
});

 

4-3. counter의 기본 값을 postId로 사용하므로 1로 설정, 0이면 오류 발생

// scr/modules/counter.js
(...)
export default handleActions({
    [INCREMENT]: (state, action) => state + 1,
    [DECREMENT]: (state, action) => state - 1
},1); // counter의 기본 값을 postId로 사용하므로 1로 설정, 0이면 오류 발생

 

4-4. App 컴포넌트에서 액션으로 웹 요청 시도

// src/App.js
import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as counterActions from './modules/counter';
import * as postActions from './modules/post';

class App extends Component {
    loadData = () => {
        const {PostActions, number} = this.props;
        PostActions.getPost(number);
    }

    componentDidMount() {
        this.loadData();
    }
    
    componentDidUpdate(prevProps, prevState) {
        // 이전 number 와 현재 number 가 다르면 요청을 시작
        if(this.props.number != prevProps.number) {
            this.loadData();
        }
    }

    render() {
        const {CounterActions, number, post, error, loading} = this.props;
        return (
            <div>
                <h1>{number}</h1>
                {
                    ( () => {
                        if (loading) return (<h2>로딩중...</h2>);
                        if (error) return (<h2>에러발생...</h2>);
                        return (
                            <div>
                                <h2>{post.title}</h2>
                                <p>{post.body}</p>
                            </div>
                        );
                    } )
                }
                <button onClick={CounterActions.increment}>+</button>
                <button onClick={CounterActions.decrement}>-</button>
            </div>
        );
    }
}

export default connect(
    (state) => ({
        number: state.counter,
        post: state.post.data,
        loading: state.post.pending,
        error: state.post.error
    }),
    (dispatch) => ({
        CounterActions: bindActionCreators(counterActions, dispatch),
        PostActions: bindActionCreators(postActions, dispatch)
    })
)(App);

 

4-5. 요청 완료후 작업및 오류 발생시 작업 추가

// es6
// loadData = () => {
//     const { PostActions, number } = this.props;
//    //this.props.Postactions.getPost(this.props.number);
//
//     PostActions.getPost(number).then(	// 요청 완료 후 작업
//         (response) => {
//             console.log(response);
//         }
//     ).catch(					// 오류 발생시 작업
//         (error) => {
//             console.log(error);
//         }
//     );
// };

//es7
loadData = async () => {
    const { PostActions, number } = this.props;
    // this.props.Postactions.getPost(this.props.number);
    
    try {
        const response = await PostActions.getPost(number); // 요청 완료 후 작업
        console.log(response);
    }
    catch (error) {                                        // 오류 발생시 작업
        console.log(error);
    }
};

- await를 쓸 함수의 앞부분에 async 키워드를 붙인다.

- 기다려야 할 Promise 앞에 await 키워드를 붙인다.

- await를 사용할 때는 꼭 try~catch문으로 오류를 처리해야 한다.

 

4-6. 요청취소기능 추가

 

 

*redux-pender, redux-promise-middleware는 잘 쓰이지 않으므로 스킵..

'Javascript > Redux' 카테고리의 다른 글

#13 Redux in React  (0) 2020.02.25
#12 Redux  (0) 2020.02.25
Redux Life Cycle  (0) 2019.08.29
Posted by yongminLEE
2019. 9. 1. 22:20

0. 디렉터리 구성

 

1. 패키지 설치

yarn add redux react-redux redux-actions immutable

 

2-1. input 모듈 생성

//dukcks structure

import {Map} from 'immutable';
import {handleActions, createAction} from 'redux-actions';

//action types
const SET_INPUT = 'input/SET_INPUT';

//action creator
export const setInput = createAction(SET_INPUT);
// ex: call -> setInput({value:'abc'}) 
// return value -> action object = {type:SET_INPUT, payload:{value:'abc'}}

//initialState
const initialState = Map({
    value:' '
});

//reducer
export default handleActions ({
    [SET_INPUT]: (state, action) => {
        return state.set('value', action.payload)
    }
}, initialState);

 

2-2. todos 모듈 생성

//dukcks structure

import {Map, List} from 'immutable';
import {handleActions, createAction} from 'redux-actions';

//action types
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

//action creators
export const insert = createAction(INSERT);
// ex: call -> insert({id:4, text:'abc', done:'false'}) 
// return value -> action object = {type:INSERT, payload:{id:4, text:'abc', done:'false'}}

export const toggle = createAction(TOGGLE);
// ex: call -> toggle(1) 
// return value -> action object = {type:toggle, payload:1}

export const remove = createAction(REMOVE);
// ex: call -> remove(2) 
// return value -> action object = {type:remove, payload:2}

//initialState
const initialState = List([
    Map({id:0, text:'todo1', done:true}),
    Map({id:1, text:'todo22', done:false}),
    Map({id:2, text:'todo333', done:false})
]);

//reducer
export default handleActions({
    [INSERT] : (state,action) => {
        console.dir(action);
        return state.push(Map({            
            id:action.payload.id,
            text:action.payload.text,
            done:action.payload.done
        }));
    },
    [TOGGLE]: (state,action) => {
        console.dir(action);
       // const {payload:id} = action;
        //List의 내장함수 get
       // console.dir(action);
        const index = state.findIndex(todo => todo.get('id') === action.payload);
        //List의 내장함수 update
        return state.update(index, item=>item.set('done',!item.get('done')));
    },
    [REMOVE]:(state,action) => {
        const {payload:id} = action;
        const index = state.findIndex(todo => todo.get('id') === id);
        return state.delete(index);
    }
}, initialState);

 

2-3. 모듈 index 생성

import input from './input';
import todos from './todos';
import {combineReducers} from 'redux';

export default combineReducers({
    input,
    todos
});

//  state { 
//     input : Map({ value:' ' }), 
//     todos :[ Map({id:0,text:" ",done: }), Map({...}), ...]
// }

 

3. 스토어 생성

import React from 'react';
import ReactDOM from 'react-dom';
import './styles/main.scss';
import App from './components/App';
import * as serviceWorker from './serviceWorker';

//creating store
import modules from './modules';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
const store = createStore(modules, window.devToolsExtension && window.devToolsExtension());

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider >
    , document.getElementById('root')
);

serviceWorker.unregister();

 

4-1. TodoInputContainer 생성

import React, {Component} from 'react';
import TodoInput from '../components/TodoInput';

import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import * as inputActions from '../modules/input';
import * as todosActions from '../modules/todos';

const mapStateToProps = (state) => ({
    value:state.input.get('value')
});

const mapDispatchToProps = (dispatch) => ({
    InputActions : bindActionCreators(inputActions, dispatch),
    TodosActions : bindActionCreators(todosActions, dispatch)
});

class TodoInputContainer extends Component {
    id=1

    getId = () => {
        return ++this.id;
    };
	
    handleChange = (e) => {
        this.props.InputActions.setInput(e.target.value);	//setInput() : action creator
    };

    handleInsert = () => {
        const todo = {
            id: this.getId(),
            text:this.props.value,
            done:false
        };
        this.props.TodosActions.insert(todo);			//insert() : action creator	
        this.props.InputActions.setInput(' ');			//setInput() : action creator
    };


    render() {
        return (
            <div>
                <TodoInput
                    onChange={this.handleChange}
                    onInsert={this.handleInsert}
                    value={this.props.value}
                />
            </div>
        );
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(TodoInputContainer);

 

4-2. TodoListContainer 생성

import React, {Component} from 'react';
import TodoList from '../components/TodoList';

import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import * as todoActions from '../modules/todos';

const mapStateToProps = (state) => ({
    todos:state.todos
});

const mapDispatchToProps = (dispatch) => ({
    TodoActions : bindActionCreators(todoActions, dispatch)
});

class TodoListContainer extends Component {
    handleToggle = (id) => {
        this.props.TodoActions.toggle(id);  //toggle() : action creator
    }
    handleRemove = (id) => {
        this.props.TodoActions.remove(id);  //remove() : action creator
    }

    render() {
        return (
            <div>
                <TodoList
                    todos={this.props.todos}
                    onToggle={this.handleToggle}
                    onRemove={this.handleRemove}
                />
            </div>
        );
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoListContainer);

 

5. TodoList 수정

import React, { Component } from 'react';
import TodoItem from '../TodoItem';

class TodoList extends Component {
    
    render() {
        const { todos, onToggle, onRemove } = this.props;
        const todoList = todos.map((todo) => {
            return (
                <TodoItem
                    key={todo.get('id')}
                    done={todo.get('done')}
                    onToggle={() => onToggle(todo.get('id'))}
                    onRemove={() => onRemove(todo.get('id'))}
                >
                    {todo.get('text')}
                </TodoItem>
            )
        })
        return (
            <div>
                {todoList}
            </div>
        );
    }
}

export default TodoList;

 

6. App.js 수정

import React, { Component } from 'react';
import PageTemplate from './PageTemplate';
import TodoInputContainer from '../containers/TodoInputContainer';
import TodoListContainer from '../containers/TodoListContainer';


class App extends Component {

    render() {
       
        return (
            <div>
                <PageTemplate>
                    <TodoInputContainer />
                    <TodoListContainer />
                </PageTemplate>
            </div>
        );
    };
};

export default App;

'Javascript > projects' 카테고리의 다른 글

blog 1-1  (0) 2019.09.05
Counter (2) : 멀티 카운터  (0) 2019.08.29
Counter (1) : 카운터 만들기  (0) 2019.08.29
Todo-list (4) : 리렌더링 최적화 하기  (0) 2019.08.29
Todo-list (3) : 데이터 추가, 수정, 삭제  (0) 2019.08.29
Posted by yongminLEE
2019. 8. 29. 21:50

0. 디렉터리 생성

 

1. action types 수정

//src/actions/ActionTypes.js

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const SET_COLOR = 'SET_COLOR';
export const CREATE = 'CREATE';
export const REMOVE ='REMOVE';

 

2. action creators 수정

//src/actions/index.js

import * as types from './ActionTypes';

export const increment = (index) => ({
    type:types.INCREMENT,
    index
});

export const decrement = (index) => ({
    type:types.DECREMENT,
    index
    
});

export const setColor = (color, index) => ({
    type:types.SET_COLOR,
    color,
    index
});

export const create = (color) => ({
    type:types.CREATE,
    color
});

export const remove = () => ({
    type:types.REMOVE
});

 

3. reducers 수정

//src/reducers/index.js

import * as types from '../actions/ActionTypes';

const initialState = { counters: [{ color: 'black', number: 1 }] };


const reducers = (state = initialState, action) => {

    const {counters} = state;

    switch (action.type) {
        case types.CREATE:
            return {
                counters:[...counters,{color:action.color, number:1}]
            };
        case types.REMOVE:
            return {
                counters:counters.slice(0,counters.length-1)
            };
        case types.INCREMENT:
            return { 
                counters:[...counters.slice(0,action.index),
                {
                    ...counters[action.index],
                    number:counters[action.index].number+1
                },
                ...counters.slice(action.index+1, counters.length)
                ]
            };
        case types.DECREMENT:
            return { 
                counters:[...counters.slice(0,action.index),
                {
                    ...counters[action.index],
                    number:counters[action.index].number-1
                },
                ...counters.slice(action.index+1, counters.length)
                ]
            };
        case types.SET_COLOR:
            return { 
                counters:[...counters.slice(0,action.index),
                {
                    ...counters[action.index],
                    color:action.color
                },
                ...counters.slice(action.index+1, counters.length)
                ]
            };
        default:
            return state;
    }
}


 export default reducers;

 

4. Button 컴포넌트 생성

//src/components/Button.js

import React from 'react';
import PropTypes from 'prop-types';
import './Button.css';

const Button = ({onCreate, onRemove}) => {
    return (
        <div className="Button">
            <div className="btn add" onClick={onCreate}>CREATE</div>
            <div className="btn rm" onClick={onRemove}>REMOVE</div>
        </div>
    );
};

Button.propTpes = {
    onCreate:PropTypes.func,
    onRemove:PropTypes.func
};

Button.defaultProps = {
    onCreate: () => console.warn("onCreate is not defined")
}

export default Button;

 

/* src/components/Button.css */
.Button {
    display:flex;
}

.Button .btn{
    flex:1;
    display:flex;
    align-items: center;
    justify-content: center;
    height: 3rem;
    color: white;
    font-size: 1rem;
    cursor: pointer;
}

.Button .add{
    background: green;
}

.Button .add:hover{
    background: yellow;
}

.Button .rm{
    background: red;
}

.Button .rm:hover{
    background: yellow;
}

 

5. CounterList 컴포넌트 생성

//src/components/CounterList.js

import React from 'react';
import Counter from './Counter';
import PropTypes from 'prop-types';

const CounterList = ({ counters, onIncrement, onDecrement, onSetColor }) => {
    const counterList = counters.map((counter, i) => (
        <Counter
            key={i}
            index={i}
            {...counter}
            onIncrement={onIncrement}
            onDecrement={onDecrement}
            onSetColor={onSetColor} 
        />
    ));

    return (
        <div className="counterList">
            {counterList}
        </div>
    );
};

CounterList.propTypes = {
    onIncrement: PropTypes.func,
    onDecrement: PropTypes.func,
    onSetColor: PropTypes.func
};

Counter.defaultProps = {
   counters:[],
    onIncrement: () => console.warn('onIncrement function is not defined'),
    onDecrement: () => console.warn('onDecrement function is not defined'),
    onSetColor: () => console.warn('onSetColor function is not defined')
}

export default CounterList;

 

6. Counter 컴포넌트 수정

//src/components/Counter.js

import React from 'react';
import PropTypes from 'prop-types';
import './Counter.css';


const Counter = ({ number, color, index, onIncrement, onDecrement, onSetColor }) => {
    return (
        <div
            className="counter"
            onClick={()=>onIncrement(index)}
            onContextMenu={(e) => {           //마우스 오른쪽 버튼을 눌렀을 때 메뉴가 열리는 이벤트
               e.preventDefault();         //메뉴가 열리는 것을 방지
                onDecrement(index);              // => 마우스 오른쪽 클릭 : 감소
            }}
            onDoubleClick={()=>onSetColor(index)}
            style={{ backgroundColor: color }}
        >
            {number}
        </div>
    );

};

//{ number, color, onIncrement, onDecrement, onSetColor 
Counter.propTypes = {
    index: PropTypes.number,
    number: PropTypes.number,
    color: PropTypes.string,
    onIncrement: PropTypes.func,
    onDecrement: PropTypes.func,
    onSetColor: PropTypes.func
};

Counter.defaultProps = {
    index:0,
    number: 0,
    color: 'black',
    onIncrement: () => console.warn('onIncrement function is not defined'),
    onDecrement: () => console.warn('onDecrement function is not defined'),
    onSetColor: () => console.warn('onSetColor function is not defined')
}
export default Counter;

 

7. getRandomColor 라이브러리 생성

// src/lib/getRandomColor.js

const getRandomColor = () => {
    const colors = ['green', 'yellow', 'blue'];
    const random = Math.floor(Math.random() * 3);
    return colors[random];
};

export default getRandomColor;

 

8. CounterContainer 삭제, CounterListContainer 생성

// src/containers/CounterListContainer.js

import CounterList from '../components/CounterList';
import * as actions from '../actions';
import {connect} from 'react-redux';
import getRandomColor from '../lib/getRandomColor';

const mapStateToProps = (state) => ({
    counters:state.counters
});

const mapDispatchToProps = (dispatch) => ({
    onIncrement:(index)=>dispatch(actions.increment(index)),
    onDecrement:(index)=>dispatch(actions.decrement(index)),
    onSetColor:(index) => {
        const color = getRandomColor();
        dispatch(actions.setColor(color, index));
    }
});

const CounterListContainer = connect(mapStateToProps, mapDispatchToProps)(CounterList);
export default CounterListContainer;

 

9. App 컨테이너 컴포넌트 수정

// src/containers/App.js

import React, {Component} from 'react';
import CounterListContainer from './CounterListContainer';
import Button from '../components/Button';

import {connect} from 'react-redux';
import * as actions from '../actions';
import getRandomColor from '../lib/getRandomColor';

class App extends Component {
    render() {
        const {onCreate, onRemove} = this.props;
        return (
            <div className="App">
                <Button
                    onCreate={onCreate}
                    onRemove={onRemove}
                />
                <CounterListContainer />
            </div>
        );
    }
};

const mapDispatchToProps = (dispatch) => ({
    onCreate: ()=>dispatch(actions.create(getRandomColor())),
    onRemove:()=>dispatch(actions.remove())
});

export default connect(null,mapDispatchToProps)(App);

- AppContainer 안에서 App과 redux를 connect하지 않고 App 컴포넌트에서 바로 연결

- store.state값을 필요로 하지 않으므로 connect()의 ampStateToProps는 null로 설정

'Javascript > projects' 카테고리의 다른 글

blog 1-1  (0) 2019.09.05
Todo-list (5) : redux 적용  (0) 2019.09.01
Counter (1) : 카운터 만들기  (0) 2019.08.29
Todo-list (4) : 리렌더링 최적화 하기  (0) 2019.08.29
Todo-list (3) : 데이터 추가, 수정, 삭제  (0) 2019.08.29
Posted by yongminLEE