• 각 라우터에서 처리해야하는 함수 코드가 길면 유지보수가 어려우니
  • 라우트에서 처리하는 함수들만 모아서 컨트롤러(controller)를 만들자
# yarn add koa-bodyparser

src/api/posts/posts.ctrl.js

let postId = 1; // init

// posts data

const posts = [
  {
    id: 1,
    title: '제목',
    body: '내용',
  },
];

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

/* 포스트 목록 조회
GET /api/posts
*/
exports.list = ctx => {
  ctx.body = posts;
};

/* 특정 포스트 조회
GET /api/posts/:id
*/

exports.read = ctx => {
  const { id } = ctx.params; // default datatype: string
  const post = posts.find(p => p.id.toString() === id);
  if (!post) {
    ctx.status = 404;
    ctx.body = {
      message: '포스트가 존재하지 않습니다',
    };
    return;
  }
  ctx.body = post;
};

/* 특정 포스트 제거
DELETE /api/posts/:id
*/
exports.remove = ctx => {
  const { id } = ctx.params;
  const index = posts.findIndex(p => p.id.toString() === id);

  if (index === -1) {
    ctx.status = 404;
    ctx.body = {
      message: '포스트가 존재하지 않습니다',
    };
    return;
  }
  posts.splice(index, 1);
  ctx.status = 204; // No Content
};

/* 포스트 수정 (교체)
PUT /api/posts/:id
{ title, body }
*/
exports.replace = ctx => {
  const { id } = ctx.params;
  const index = posts.findIndex(p => p.id.toString() === id);
  if (index === -1) {
    ctx.status = 404;
    ctx.body = {
      message: '포스트가 존재하지 않습니다',
    };
    return;
  }
  posts[index] = {
    id,
    ...ctx.request.body,
  };
  ctx.body = posts[index];
};

/* 포스트 수정 (특정 필드 변경)
PATCH /api/posts/:id
{ title, body }
*/
exports.update = ctx => {
  const { id } = ctx.params;
  const index = posts.findIndex(p => p.id.toString() === id);
  if (index === -1) {
    ctx.status = 404;
    ctx.body = {
      message: '포스트가 존재하지 않습니다',
    };
    return;
  }
  posts[index] = {
    ...posts[index],
    ...ctx.request.body,
  };
  ctx.body = posts[index];
};

src/api/posts/index.js

const Router = require('koa-router');
const postsCtrl = require('./posts.ctrl');

const posts = new Router(); 

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; 
  • 여러개의 Router를 생성하게 될텐데, 이렇게 되면 유지보수가 이후에는 어려워질수 있으니 router 모듈화를 진행
  • src/api/index.js 에 api에 관련된 호출을 나열

src/index.js

const Koa = require('koa');
const Router = require('koa-router');

const api = require('./api');

const app = new Koa();
const router = new Router();

// 라우터 설정
router.use('/api', api.routes()); // add api router

// app instance에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());

app.listen(4000, () => {
  console.log('Listening to port 4000');
});

src/api/index.js

const Router = require('koa-router');
const api = new Router();

api.get('/test', ctx => {
  ctx.body = 'test 성공';
});

module.exports = api;
  • src/api/index.js를 생성하고, src/index.jsrouter.use를 통해 router를 등록해주면 된다.
  • 이때 주의해야할점은 src/api/index.js에서 반드시 exports를 해줘야 한다.

posts API 생성하기

  • src/api/posts/index.js에 아래와 같이 posts에 관련된 api의 목록들을 정의한다.
  • 정의한 route를 src/api/index.js에 적용을 해주면 된다.

src/api/index.js

const Router = require('koa-router');
const posts = require('./posts');

const api = new Router();

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

module.exports = api;

src/api/posts/index.js

const Router = require('koa-router');
const posts = new Router();

const printInfo = ctx => {
  ctx.body = {
    method: ctx.method,
    path: ctx.path,
    params: ctx.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;
  • 위 같이 정의하고 http://localhost:4000/api/posts/12을 호출하면 아래와 같은 결과가 나온다.

$ yarn add koa-router
const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

// 라우터 설정
router.get('/', ctx => {
  ctx.body = '홈';
});
router.get('/about', ctx => {
  ctx.body = '소개';
});

// app instance에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());

app.listen(4000, () => {
  console.log('Listening to port 4000');
});

Router Parameter, Query

  • /about/:name의 형식으로 :을 사용하여 라우트 경로 설정
  • ctx.params의 객체에서 조회
  • /posts/?id=10 처럼 조회하면 ctx.query에서 조회
    • 문자열 형태는 ctx.querystring
router.get('/about/:name?', ctx => {
  const { name } = ctx.params;
  ctx.body = name ? `${name}의 소개` : '소개';
});

router.get('/posts', ctx => {
  const { id } = ctx.query;
  ctx.body = id ? `포스트 #${id}` : `포스트 없어`;
});
  • http://localhost:4000/about/icecream
  • http://localhost:4000/posts?id=10

코드를 수정하고 반영하기 위해서는 서버를 재시작을 해야하는데 nodemon을 이용하면 코드를 변경할때마다 자동으로 서버를 재시작을 해준다. yarn을 통해 설치가 가능하다

$ yarn add --dev nodemon
  • 패키지를 설치한 이후에는 package.json에 아래와 같이 추가
{
  (...)
  ,
  "scripts": {
    "start": "node src",
    "start:dev": "nodemon --watch src/ src/index.js"
  }
}

서버시작하기

$ yarn start # 재시작이 필요 없을때
$ yarn start:dev # 코드 변경시 재시작 하도록

적용전

app.use((ctx, next) => {
  console.log(ctx);
  console.log(1);
  next();
});

적용후

app.use(async (ctx, next) => {
  console.log(ctx);
  console.log(1);
  await next();
  console.log('END');
});
const Koa = require('koa');

const app = new Koa();

app.use(ctx => {
  ctx.body = 'hello word';
});

app.listen(4000, () => {
  console.log('Listening to port 4000');
});
$ node src

미들웨어

  • Koa App은 미들웨어의 배열로 구성
  • app.use 함수를 이용해 미들웨어 함수를 app에 등록
  • 파라미터 ctx는 조금 전에 사용한, 두번째 파라미터는 next
  • ctx는 Context의 줄임말
    • 웹요청과 응담에 대한 정보
  • next는 현재 처리중인 미들웨어의 다음 미들웨어를 호출하는 함수
    • next 함수를 호출하지 않으면 다음 미들웨어 처리를 하지 않음
const Koa = require('koa');

const app = new Koa();

app.use((ctx, next) => {
  console.log(ctx);
  console.log(1);
  next();
});

app.use((ctx, next) => {
  console.log(2);
  next();
});

app.use(ctx => {
  ctx.body = 'hello world';
});

app.listen(4000, () => {
  console.log('Listening to port 4000');
});

  • next를 중간에 뺀다면 그 이후의 미들웨어는 모두 무시
  • 특정 조건으로 미들웨어를 처리하도록 가능
app.use((ctx, next) => {
  console.log(ctx);
  console.log(1);
  if (ctx.query.authorized !== '1') {
    ctx.status = 401; // Unauthorrized
    return;
  }
  next();
});
  • http://localhost:4000/?authorized=1로 조회해야지만 hello world의 결과가 나옴
  • 웹 요청의 쿠키 혹은 헤더를 통해 처리가 가능
  • next 함수는 Promise를 반환
    • Express와 차별되는 부분
    • next 함수가 반환하는 Promise는 다음에 처리해야하는 미들웨어가 끝나야 완료

node 프로젝트 시작하기 전에 아래와 같이 환경을 구성하고 구현하자

yarn init -y  
yarn add koa  
yarn add --dev eslint  // dev는 개발용 의존 모듈로 설치, package.json에서 dev dependencies 쪽이 모듈의 버전 입력
yarn run eslint --init // javascript 문법 검사, 깔끔한 코드 작성

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80
}

을 추가하고 Prettier에서 관리하는 코드 스타일은 ESLint에서 관리하지 않도록 설치

$ yarn add eslint-config-prettier 

.eslintrc.json의 파일은 아래와 같이 설정

{
  "env": {
    "browser": true,
    "commonjs": true,
    "es6": true,
    "node": true
  },
  "extends": ["eslint:recommended", "prettier"],
  "globals": {
    "Atomics": "readonly",
    "SharedArrayBuffer": "readonly"
  },
  "parserOptions": {
    "ecmaVersion": 2018
  },
  "rules": {
    "no-unused-vars": "warn", // 설정하지 않으면 사용하지 않는 변수에 대해서 Error
    "no-console": "off" // console.log를 사용하는것을 지양함.
  }
}
  • Context API는 리액트 프로젝트에서 전역적으로 사용할 데이터가 있을 때 유용한 기능
  • 사용 예
    • 로그인 정보
    • 애프리케이션 환경 설정
    • 테마
    • redux, react-router, styled-components 의 라이브러리
  • 컴포넌트 간에 데이터를 props로 저달하기 때문에 여기저기서 필요한 데이터가 있을 때는 주로 최상위 컴포넌트인 App의 state에 넣어서 관리
  • 하지만 props으로 아주 ~ 먼 컴포넌트에 전달하려면 여러개의 컴포넌트를 지나가야 하니... 유지 보수성이 낮아질 가능성이 있음
  • 위 문제점을 해결하기 위해서 redux나 MobX 같은 상태 관리 라이브러리를 사용하면 전역 상태 관리를 더 편하게 처리
  • 리액트 v16.3 업데이트 이후에는 Context API가 많이 개선되었기 때문에 별도의 라이브러리 사용할 필요 없음
import { createContext } from 'react';

const ColorContext = createContext({ color: 'black' });

export default ColorContext; 
import ColorBox from './components/ColorBox';
<ColorBox></ColorBox>

Provider 이용하기

  • Provider를 사용하면 Context의 값을 변경할 수 있다.
  • 아래와 같이 컴포넌트 ColorBoxColorContext.Provider를 통해 color 값을 변경할 수 있다.
  • Provider를 사용할때는 value 값을 명시해줘야 제대로 작동
      <ColorContext.Provider value={{ color: 'red' }}>
        <div>
          <ColorBox></ColorBox>
        </div>
      </ColorContext.Provider>

동적으로 사용하기

  • context의 value에는 상태값, 함수를 전달할 수 있다.
  • 아래와 같이 state, actions을 따로 정의한 ColorContext를 정의
  • ColorProvider 를 정의
import React, { createContext, useState } from 'react';

// 객체에서 state, actions을 분리해서 정의하면 나중에 사용하기가 편함
const ColorContext = createContext({
  state: { color: 'black', subcolor: 'red' },
  actions: {
    setColor: () => {},
    setSubcolor: () => {}
  }
});

const ColorProvider = ({ children }) => {
  const [color, setColor] = useState('black');
  const [subcolor, setSubcolor] = useState('red');

  const value = {
    state: { color, subcolor },
    actions: { setColor, setSubcolor }
  };

  return (
    <ColorContext.Provider value={value}>{children}</ColorContext.Provider>
  );
};

const { Consumer: ColorConsumer } = ColorContext;
export { ColorProvider, ColorConsumer };
export default ColorContext;
import React from 'react';
import { ColorConsumer } from '../contexts/color';

const ColorBox = () => {
  return (
    <ColorConsumer>
      {(
        { state } // 객체 비구조화 할당 문법
      ) => (
        <>
          <div
            style={{
              width: '64px',
              height: '64px',
              background: state.color
            }}
          />
          <div
            style={{
              width: '32px',
              height: '32px',
              background: state.subcolor
            }}
          />
        </>
      )}
    </ColorConsumer>
  );
};

export default ColorBox;

사용하는 쪽에서는 아래와 같이

      <ColorProvider>
        <div>
          <ColorBox></ColorBox>
        </div>
      </ColorProvider>

색상 선택하는 컴포넌트 생성

  • Context에 들어있는 actions에 넣어준 함수를 호출하는 컴포넌트 생성
# SelectColor.js
import React from 'react';
import { ColorConsumer } from '../contexts/color';

const colors = [
  'red',
  'oragne',
  'yellow',
  'green',
  'blue',
  'indigo',
  'violoet'
];

const SelectColors = () => {
  return (
    <div>
      <h2>색상을 선택하세요.</h2>
      <ColorConsumer>
        {({ actions }) => (
          <div style={{ display: 'flex' }}>
            {colors.map(color => (
              <div
                key={color}
                style={{
                  background: color,
                  width: '24px',
                  height: '24px',
                  cursor: 'pointer'
                }}
                onClick={() => actions.setColor(color)}
                onContextMenu={e => {
                  e.preventDefault(); // 기존 오른쪽 메뉴가 뜨는 것을 방지
                  actions.setSubcolor(color);
                }}
              />
            ))}
          </div>
        )}
      </ColorConsumer>
      <hr />
    </div>
  );
};

export default SelectColors;
  • App.js에서는 SelectColors의 컴포넌트를 추가
      <ColorProvider>
        <div>
          <SelectColors></SelectColors>
          <ColorBox></ColorBox>
        </div>
      </ColorProvider>

useContext Hook 사용

  • 리액트 내장되어 있는 Hooks 중에서 useContext를 사용해보자
  • useContext Hook은 함수형 컴포넌트에서만 사용할 수 있다.
  • ColorBox를 아래와 같이 수정
import React, { useContext } from 'react';
import { ColorContext } from '../contexts/color';

const ColorBox = () => {
  const { state } = useContext(ColorContext);
  return (
    <>
      <div
        style={{
          width: '64px',
          height: '64px',
          background: state.color
        }}
      />
      <div
        style={{
          width: '32px',
          height: '32px',
          background: state.subcolor
        }}
      />
    </>
  );
};

export default ColorBox;

static contextType

  • 클래스형 컴포넌트에서 Context를 좀 더 쉽게 사용하기 위해서 static contextType을 정의
  • 기존에 SelectColors의 컴포넌트를 클래스형으로 변경
  • static contextType을 정의하면 클래스 메소드에서도 Context에 있는 함수 호출이 가능
  • 단점이라면 한 클래스에서 하나의 context만 사용할 수 있음
  • useContext를 사용하는게 더 권장됨
  • 아래가 기존
import React from 'react';
import { ColorConsumer } from '../contexts/color';

const colors = [
  'red',
  'oragne',
  'yellow',
  'green',
  'blue',
  'indigo',
  'violoet'
];

const SelectColors = () => {
  return (
    <div>
      <h2>색상을 선택하세요.</h2>
      <ColorConsumer>
        {({ actions }) => (
          <div style={{ display: 'flex' }}>
            {colors.map(color => (
              <div
                key={color}
                style={{
                  background: color,
                  width: '24px',
                  height: '24px',
                  cursor: 'pointer'
                }}
                onClick={() => actions.setColor(color)}
                onContextMenu={e => {
                  e.preventDefault(); // 기존 오른쪽 메뉴가 뜨는 것을 방지
                  actions.setSubcolor(color);
                }}
              />
            ))}
          </div>
        )}
      </ColorConsumer>
      <hr />
    </div>
  );
};

export default SelectColors;
  • 이후가 클래스 컴포넌트로 변경 후
import React, { Component } from 'react';
import ColorContext from '../contexts/color';

const colors = [
  'red',
  'oragne',
  'yellow',
  'green',
  'blue',
  'indigo',
  'violoet'
];

class SelectColors extends Component {
  static contextType = ColorContext;

  handleSetColor = color => {
    this.context.actions.setColor(color);
  };

  handleSetSubcolor = subcolor => {
    this.context.actions.setSubcolor(subcolor);
  };

  render() {
    return (
      <div>
        <h2>색상을 선택하세요.</h2>
        <div style={{ display: 'flex' }}>
          {colors.map(color => (
            <div
              key={color}
              style={{
                background: color,
                width: '24px',
                height: '24px',
                cursor: 'pointer'
              }}
              onClick={() => this.handleSetColor(color)}
              onContextMenu={e => {
                e.preventDefault(); // 기존 오른쪽 메뉴가 뜨는 것을 방지
                this.handleSetSubcolor(color);
              }}
            />
          ))}
        </div>
        <hr />
      </div>
    );
  }
}

export default SelectColors; 

+ Recent posts