티스토리 뷰
nodejs로 웹서버 만드는 가장 기초적인 방법인 express를 사용하는 방식에 대해 알아보고자 한다. 웹서버를 구축하는 방법은 아래의 내용을 순차적으로 따라하면 쉽게 구축할 수 있을 것이다. 기존에 구글링을 하다보면, express를 만드는 방식이 너무 다양한다. 초심자 기준으로 다양한 방식에 대한 이유를 판단 할 수 있도록 여러가지 예시를 담았다. node.js 프로젝트 구조를 어떻게 잡아야 할까? 이러한 질문을 시작으로 시작해 보려한다. 그리고 기본적으로 express.js를 활용하여 REST API를 사용할 것이다. 기본적으로 코드 중복을 피하고, 서비스 확장에 도움이 주는 방식을 적어보려한다.
로그인 구현 시 여기를 눌러서 확인하자. 추가로 nodejs에 대한 기초와 다른 실무 예시를 참고하려면 아래의 링크를 확인해보자.
> frontend, backend를 포함한 nodejs 총 정리
1. 시작하기
> express를 시작하는 방식은 두 가지가 있다. 첫 번째는 express-generator를 사용하여 기초 구조를 다 잡는 방식이 있고, 두 번째는 직접 구조를 잡는 방식이 있다.
직접 구조를 만들려면, 아래의 url을 참고하자.
$ https://heodang-repository.tistory.com/m/28
첫 번째 방법인 express-generator를 활용하여 구조를 잡아보자. 이 방법의 장점은, express 프레임워크는 express 외에도 많은 패키지를 사용하기 때문에 필요 패키지를 찾아서 설치하고 package.json을 만들기가 어렵다. 이러한 부분을 생각할 필요 없이 기본 폴더 구조까지 잡아 주는 패키지가 바로 express-generator이다.
우선 express-generator를 설치하자
npm i -g express-generator
- -g 는 전역 설치를 할 수 있는 것이다. 전역 설치를 한다면, PATH 환경변수에 포함 시킬 수 있다. 프로젝트 내부에서만 실행한다면, -g를 빼고 실행하면 된다.
설치후 express 프로젝트를 생성하는 명령어는 아래와 같다.
$ express [pj 이름을 적어주는 곳] --view=pug
- pug 옵션 : express-generator는 기본적으로 Jade 템플릿을 설치 합니다. 이때 Jade의 최신 버전이 pug이다. (Jade는 Pug로 개명 했다.) Pug 대신 EJS 템프릿 엔진을 사용한다면, --view=ejs를 입력하면 된다. 입력을 안하면, 자동으로 jade view engine이 선택된다. 만약 리액트나 앵귤러 등을 프런트로 사용한다면, 선택을 신경쓰지 않아도 된다.
# basepj를 만들었다.
$ express basepj --view=pug
# 들어가서 살펴 보면 아래와 같다.
$ cd basepj
$ ls
app.js bin/ package.json public/ routes/ views/
# dependencies(의존성) 설치
$ npm i
# 서버 시작 확인
# localhost:3000 접속가능
$ npm start
서버 시작 시 디폴트는 3000 포트를 사용한다. 포트를 수정하려면, 아래에 bin/www 부분의 코드를 참고하자.
설치 이후에 VScode로 열어서 구조를 하나씩 보자.
기본 setting 코드 해석
기본적인 폴더 구조는 아래와 같다.
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
app.js
: express앱의 본체로 핵심적인 서버 역할을 담당하고 여러 미들웨어를 관리한다. app.js는 node 서버의 entry point(진입점)으로 서버 시작 시 app.js에서 시작한다.
코드 시작부분
var express = require('express');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
첫 두줄은 express 패키지를 호출하여 app 변수 객체를 만드는 로직이다. 만들어진 app 객체에 기능을 하나씩 연결을 한다고 할 수 있다. app.set으로 설정을 하나씩 세팅한다고 할 수 있다.
만약 파일 구조를 나누지 않고 node init으로 프로젝트 시작 시 아래와 같이 간단하게도 get으로 호출이 가능하다.
const express = require('express')
const app = express()
const port = process.env.PORT || 3000
app.get('/', (req, res) => {
res.json({
success: true,
});
});
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))
위와 같은 코드는 로직을 파일로 나누지 않고, router의 내용을 코드 아래 부분에 적은것이다. 이렇게 파일 하나에 적어도 된다는 것을 참고만 하고 넘어가자.
(유지보수가 쉽게 아키텍쳐(파일 분리)를 만드는 것이다.)
다음 코드부분
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
app.use는 미들웨어를 연결하는 부분이라고 할 수 있다.
module.exports = app;
위 코드는 app 객체를 모듈로 만드는 코드이다. 이렇게 만들어진 코드가 bin/www 에서 사용된 app 모듈이라고 할 수 있다.
bin/www
: 서버를 실행하는 스크립트로 http 모듈에 express 모듈을 연결하고 프로젝트가 실행되는 포트를 지정한다.
#!/usr/bin/env node
var app = require('../app');
var debug = require('debug')('basepj:server');
var http = require('http');
#!/usr/bin/env node
: www 파일을 콘솔 명령어로 만들 때, 이 주석이 사용된다.
var app, var debug, var http
: app, debug, http 모듈을 불러온다. debug모듈은 콜솔에 로고 남기는 모듈이다.
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
> app.set(키, 값)으로 데이터를 저장가능하다. 이후에 app.get(키)로 가져오는 것이 가능하다.
var port = normalizePort(process.env.PORT || '3000');
: process.env.PORT 객체에 값이 있다면 그 값을 사용하고, 없다면 3000번 포트를 이용하도록 한다.
app.set('port', port);
: 서버가 실행될 포트를 설정하는 것이다. 두번째 인자는 바로 위에서 받아온 포트가 들어간다.
var server = http.createServer(app);
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
var server = http.createServer(app);
: http.createServer에 app 모듈을 넣어주면, app 모듈은 createServer메서드의 콜백 함수 역할을 한다. (콜백함수란 특정함수에 전달되어 특정함수가 어떤조건에 의해 호출하는 함수로 이벤트 처리같은 것을 할 떄 사용가능하다.)
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
: http 웹 서버와 동일하게 listen한다. 즉, 기존 서버를 구동하는 것과 동일하게 포트를 연결하고 서버를 실행한다고 할 수 있다. 다만, express는 콜백 함수 부분을 기존과 다르게 핸들링 하는 것이다.
public
: 브라우저 등의 클라이언트들이 접근 가능한 폴더로 이미지, 자바스크립트, css 파일을 넣을 수 있다. 여기에 담긴 파일은 외부(브라우저와 같은 client)에서 접근 가능한 파일들이 모여있다. 앱 서버에서는 딱히 건들일이 없다.
- /images : 그림파일을 저장
- /javascripts : 자바스크립트 파일들을 저장
- /stylesheets : CSS 파일들을 저장
routes
: 주소별 라우터가 들어있다. 서버의 로직이 들어간다. 즉, 마지막 end point(소프트웨어나 제품의 최종 목적지)가 되는 파일들을 저장해 놓는 폴더이다.
- /index.js 파일로 라우팅을 관리한다.
#routes/index.js
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;
기본적으로 routing 매핑 시 client 쪽의 url과 프로토콜 메소드를 사용한다.
- GET(조회): router.get()
- POST(저장): router.post()
- PUT(수정): router.put()
- DELETE(삭제): router.delete()
request
- req.query : url?name=hanpy 로 들어올 시 console.log(req.query.name) => hanpy 로출력
- req.params : url/5 로 들어 올 시 console.log(req.params.id) => 5로 출력
예시)
/*
localhost:5000/hanpy/5로 들어오면,
{id : 5} 로 return한다.
*/
router.get('/hanpy/:id', (req,res) => {
res.json({id: req.params.id});
});
- req.body : body로 넘어온 값
- req.file : 파일로 넘어온 값
response
> res 파라미터는 클라이언트로 응답을 하는 객체이다. 아래는 응답을 위한 함수이다.
- res.status() : status code로 정수 값 들어감
- res.send() : 문자열로 응답
- res.json() : Json 객체로 응답
- res.render() : html 변환 템플릿을 렌터(ejs)
- res.sendfile() : file 다운로드
views
: 템플릿(html) 파일을 모아둔다.
- jade, ejs 파일 들 템플릿 파일을 모아둔다.
- 웹 서버 사용 시 이 폴더의 파일들을 사용해 렌더링 한다.
package.json
: 프로젝트의 실행 명령어, 사용 패키지들과 같은 기록을 담고 있다.
{
"name": "basepj",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"pug": "2.0.0-beta11"
}
}
기본적으로 name, version은 필수로 입력되어야 패키지 설치가 가능하다.
name : 대문자 불가. 점(.)과 밑줄(_)로 시작 불가.
version: name과 version으로 각 패키지의 고유성을 판별하므로, 패키지 내용이 변경되면, version도 변경해야만 한다.
private : true라는 것은 공식적인 배포, 즉, npm 레파지토리에 배포를 막는다.
"scripts" : { "start" : "node ./bin/www" }
: $ npm start 입력 시 start 이번트가 발생한다. 그리고 script에 정의 된 "start"의 값인 "node ./bin/www"가 실행된다.
package-lock.json
: 사용하는 패키지들의 버전 정보와 같은 것들을 담고 있다. 즉, 'npm install' 실행 했을 때의 의존모듈을 다운 받으면서 실제로 받은 버전을 기록한 파일이다. node_modules나 package.json 파일을 수정하면, 자동으로 생성되고, 변경되는 시점의 의존성 트리에 대한 정확한 정보를 가진다. 이것이 없으면, github의 pull 후 실치시에 트존성 트리의 일부 버전이 다르게 설치된다.
2. nodemon
서버를 킨 후에 수정 사항이 있으면, 서버를 껐다 켜야한다. 이게 귀찮다면, nodemon을 설치한다.
$ npm i -g nodemon
실행 방법은 간단하다. 기존에 npm start으로 서버를 실행했다면, 관련 코드는 package.json 내부에 아래와 같이 정의되어 있다.
"scripts": {
"start": "node ./bin/www"
},
그렇다면 node 뒤에 부분을 보고 아래와 같이 서버를 실행시키면 된다.
$ nodemon ./bin/www
결과는 아래와 같이 실행이 잘되는 것을 알 수 있다.
3. CORS(Cross-Origin Resource Sharing)
> 자신이 속하지 않은 다른 도메인, 다른 프로토콜, 혹은 다른 포트에 있는 리소스를 요청하는 cross-origin HTTP 요청 방식이다. 쉽게 말해서 이거 설정을 안하면, front와 통신 주고 받다가, 외부 서버에서 통신 주고 받다보면 CORS error가 발생한다. 그러한 문제가 발생 전에 미리 문제를 해결하자.
3.1 CORS 패키지 설치
$ npm i cors
3.2 모두 허용
> 어려운 것 없이 cors를 불러와서 app에 use로 미들웨어를 추가하는 2줄만 적으면 된다.
// index.js
const http = require('http');
const express = require('express');
const app = express();
const server = createServer(app);
const cors = require('cors');
const PORT = 8080;
app.use(cors());
server.listen(PORT, () => {
console.log(`Server running on ${PORT}`);
});
3.3 특정 도메인만 허용
// index.js
const http = require('http');
const express = require('express');
const app = express();
const server = createServer(app);
const cors = require('cors');
const PORT = 8080;
let corsOptions = {
origin: 'https://www.domain.com',
credentials: true
}
app.use(cors(corsOptions));
server.listen(PORT, () => {
console.log(`Server running on ${PORT}`);
});
4. MySQL 연동
먼저 설치를 진행하자.
$ npm i mysql
쿼리 실행
CREATE DATABASE IF NOT EXISTS mediation_server;
USE mediation_server;
CREATE TABLE IF NOT EXISTS Users (
id VARCHAR(45) NOT NULL,
password VARCHAR(45) NOT NULL,
PRIMARY KEY (id));
INSERT INTO Users (id, password) VALUES ('hanpy', '1234');
INSERT INTO Users (id, password) VALUES ('dupy', '7788');
SELECT password FROM Users WHERE id='hanpy';
mysql workbench로 보면 아래와 같다
생성된, 스키마를 보면 아래와 같다.
mysql 과의 연결을 확인해 보자.
index.js 파일에서 우선적으로 연결을 확인하자. local 환경이기 때문에, workbench에서 설정한 user, password, database 부분만 내가 설정한 것과 같이 바꿔주면 된다.
var express = require('express');
var router = express.Router();
/* mysql setting */
const mysql = require('mysql');
const connection = mysql.createConnection({
host : '127.0.0.1',
port : '3306',
user : 'root',
password : 'root',
database : 'mediation_server'
});
connection.connect();
/* GET home page. */
router.get('/', function(req, res, next) {
connection.query('SELECT * from Users', (error, rows) => {
console.log('id/pw: ', rows);
res.send(rows);
});
});
module.exports = router;
서버를 npm start로 실행하고 localhost:3000으로 들어가면 아래와 같은 결과 값이 잘 나오는 것을 알 수 있다.
보통 mysql과 연결하고 연결을 끊어줄 때, 아래와 같이 connection()으로 연결하고, end()로 끊어주는 명령어를 사용한다.
const mysql = require('mysql');
const connection = mysql.createConnection({
host : '127.0.0.1',
port : '3306',
user : 'root',
password : 'root',
database : 'mediation_server'
});
mysql.connection();
/* mysql로직 */
mysql.end();
그렇다면, mysql을 사용할 때는 연결 했다가 사용 하지 않을 때는 end()로 매번 연결을 종료 해야할까? 매번 호출 시 연결한다면, 자원도 많이 잡아먹는다. 따라서 전역으로 연결을 하는 것이 좋다. 아래와 같이 파일변경한다.
우선은 index.js내용을 아래와 같이 수정한다.
var express = require('express');
var router = express.Router();
/* mysql */
var mysql = require('../mysql/db');
/* GET home page. */
router.get('/', function(req, res, next) {
mysql.query('SELECT * from Users', (error, rows) => {
console.log(rows);
res.send(rows);
});
});
module.exports = router;
그리고 ./mysql 내부에 db.js 파일을 만들어 위에서 mysql 연결부분을 넣어준다.
/* mysql setting */
const mysql = require('mysql');
const connection = mysql.createConnection({
host : '127.0.0.1',
port : '3306',
user : 'root',
password : 'root',
database : 'mediation_server'
});
module.exports = connection;
마지막으로 app.js에서 mysql 연결 부분을 추가해준다.
/* connect mysql */
var mysqlDB = require('./mysql/db');
mysqlDB.connect();
그러면 전역 상태로 연결이 되는 것을 확인 할 수 있다. 위 방식 말고, 설정 정보는 config파일로 담는 것도 가능하다. 그렇다면 config에 넣는 방식은 우리가 mysql 내부의 db.js를 config 파일로 옮겨주기만 하면 된다. 아래와 같이 다른 방식도 가능하다. 참고만 하자.
설정 정보 분리 코드
const mysql = require('mysql');
const dbconfig = require('./config/db.js');
const connection = mysql.createConnection(dbconfig);
// configuration =========================
app.get('/users', (req, res) => {
connection.query('SELECT * from Users', (error, rows) => {
res.send(rows);
});
});
app.listen(app.get('port'), () => {
console.log('Express server listening on port ' + app.get('port'));
});
// config/db.js
module.exports = {
host : 'localhost',
user : 'root',
password : 'root',
database : 'mediation_server'
};
5. Nodejs + mybatis
> 위에서는 mysql을 연결을 했다. 이제 mybatis를 연결해 보자. 간단히 말하면, 아래의 'SELECT * from Users'와 같은 명령문을 다른 곳으로 모아서 관리하는 것이라고 생각하면 좋을 것 같다.
// 기존 코드
router.get('/', function(req, res, next) {
mysql.query('SELECT * from Users', (error, rows) => {
res.json(rows);
});
});
// mybatis 적용 코드
// 'SELECT * from Users' => query
router.get('/', function(req, res, next) {
mysql.query(query, (error, rows) => {
res.json(rows);
});
});
우선은 library를 설치하자.
$ npm i mybatis-mapper
index.js에 아래와 같은 내용을 추가한다.
/* mybatis $ npm i mybatis-mapper */
const mybatisMapper = require('mybatis-mapper');
mybatisMapper.createMapper(['./mybatis/testMapper.xml'])
/* 조히회 내용 */
var param = {
id : 'hanpy'
}
/* mybatis query */
var format = {language: 'sql', indent: ' '}
var query = mybatisMapper.getStatement('testMapper', 'testBasic', param, format)
마지막 줄의 getStatement를 보면, 첫번째는 xml의 name값, 두번째는 해당 xml의 id값, 세번째는 파라미터, 마지막은 포맷값이다. 이는 아래의 xml 파일을 보면 좋다.
mybatisMapper.createMapper(['./mybatis/testMapper.xml'])
주의할 점은 경로가 현재위치의 상대경로가 아니라 최상위 app.js 가 있는 경로를 기준으로 해야 에러가 안난다.
그리고 ./mybatis 폴더를 만들고 내부에 testMapper.xml 파일을 만들어 준다. 아래의 내용을 보면, 위에서 적은 testMapper, testBasic들이 적혀 있다. 저 값들을 보고 어느 SQL 명령문을 사용할지 정할 수 있다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="testMapper">
<select id="testBasic">
SELECT
*
FROM
users
WHERE
id =#{id}
</select>
</mapper>
mybatis를 이용한 간단한 예제를 만들어 봤다.
코드는 아래와 같다.
index.js
var express = require('express');
var router = express.Router();
/* mysql */
var mysql = require('../mysql/db');
/* mybatis $ npm i mybatis-mapper */
const mybatisMapper = require('mybatis-mapper');
mybatisMapper.createMapper(['./mybatis/testMapper.xml'])
/* mybatis query */
var format = {language: 'sql', indent: ' '}
router.get('/', function(req, res, next) {
var param = {
id : req.params.id
}
var query = mybatisMapper.getStatement('sqlMapper', 'getAllQuery', param, format)
mysql.query(query, (error, rows) => {
console.log(rows);
// res.send(rows);
res.json(rows);
});
});
router.get('/:id', function(req, res, next) {
var param = {
id : req.params.id
}
var query = mybatisMapper.getStatement('sqlMapper', 'getIdQuery', param, format)
mysql.query(query, (error, rows) => {
console.log(rows);
// res.send(rows);
res.json(rows);
});
});
module.exports = router;
일반적으로 전체 db를 가져오는 것과, params를 받아서 검색하는 코드이다.
testMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="sqlMapper">
<select id="getAllQuery">
SELECT
*
FROM
users
</select>
<select id="getIdQuery">
SELECT
*
FROM
users
WHERE
id =#{id}
</select>
</mapper>
결과값은 아래와 같다.
위에서 부터 천천히 읽어 왔다면 어려운 것은 없어 보인다.
6. 아키텍처
> 위의 파일 구조는 기본적인 구조라고 할 수 있다. 여러가지 구조들을 변경해보자.
현재 지금까지 위의 로직은 기본적으로 routes 부분에서 api요청에 대해 라우팅을 하면서 동시에 비지니스 로직을 작성한 것을 알 수 있다.
그렇다면, 비지니스 로직은 무엇일까? 로그인 시 password를 확인하고 관련 데이터베이스 값들을 저장하는 것처럼, 내부에서 데이터에 대한 가공을 하는 것을 의미한다.
현재 routes/index.js 의 내부를 보면 내부 로직을 controllers/index.js과 models/indes.js로 옮겨서 가독성을 높이고 유지보수에 용이하게 만들어 볼 것이다. (정답은 없다. 아래를 참고해서 응용해서 자신만의 구조를 만들어보자. )
routes/index.js 부터 확인을 하자.
변경 전 코드 (위의 mybatis 까지 적용한 내용이다.)
var express = require('express');
var router = express.Router();
/* mysql */
var mysql = require('../mysql/db');
/* mybatis $ npm i mybatis-mapper */
const mybatisMapper = require('mybatis-mapper');
mybatisMapper.createMapper(['./mybatis/testMapper.xml'])
/* mybatis query */
var format = {language: 'sql', indent: ' '}
router.get('/', function(req, res, next) {
var param = {
id : req.params.id
}
var query = mybatisMapper.getStatement('sqlMapper', 'getAllQuery', param, format)
mysql.query(query, (error, rows) => {
console.log(rows);
// res.send(rows);
res.json(rows);
});
});
module.exports = router;
after 코드
var express = require('express');
var router = express.Router();
// controllers로 옮긴다.
var index = require('../controllers/index')
//after
router.get('/', index.getAllUser);
module.exports = router;
위에서 알 수 있 듯, 코드의 가독성이 엄청 올라간다.
controllers/index.js 코드
/* mybatis $ npm i mybatis-mapper */
const mybatisMapper = require('mybatis-mapper');
mybatisMapper.createMapper(['./models/mybatis/testMapper.xml'])
/* mybatis query */
var format = {language: 'sql', indent: ' '}
/* mysql */
var mysql = require('../config/mysql/db');
module.exports ={
getAllUser: function (req, res, next) {
var param = {
id : req.params.id
}
var query = mybatisMapper.getStatement('sqlMapper', 'getAllQuery', param, format)
mysql.query(query, (error, rows) => {
console.log(rows);
// res.send(rows);
res.json(rows);
});
}
}
기존의 코드를 분리해여 가져온 것이다. 코드 내용은 변경전과 비슷하다고 할 수 있다. 그리고 sql setting 부분을 config 파일에 넣었다.
/config/mysql/db.js
/* mysql setting */
const mysql = require('mysql');
const connection = mysql.createConnection({
host : '127.0.0.1',
port : '3306',
user : 'root',
password : 'root',
database : 'mediation_server'
});
module.exports = connection;
전체 구조를 사진으로 보면 아래와 같다.
정리하면, routers/index.js에서 작성한 코드를 분리해서 controllers/index.js에 넣은 것이라고 할 수 있다. 그리고 db 를 가져오는 로직은 models 내부에 넣어서 로직의 흐름을 나누어 복잡한 로직을 쉽게 만들었다고 할 수 있다.
'Web > JAVASCRIPT' 카테고리의 다른 글
[nodejs] dotenv로 중요키 분리하기 (0) | 2021.12.13 |
---|---|
[nodejs/expess] backend에 session 붙여 login하기 (0) | 2021.12.06 |
[nodeJS] AWS EC2 배포 ALL-IN-ONE (1) | 2021.11.21 |
[nodejs error] UnhandledPromiseRejectionWarning: SequelizeAccessDeniedError: Access denied for user 'root'@'localhost' (using password: NO) (0) | 2021.11.15 |
[nodejs / express] POST 안받아지는 문제 (0) | 2021.11.04 |
- Total
- Today
- Yesterday
- UserCreationForm
- Queue
- JavaScript
- Express
- nodejs
- 클라우데라
- logout
- TensorFlow
- read_csv
- next.config.js
- 자연어처리
- react
- pandas
- Vue
- typescript
- Python
- django
- login
- mongoDB
- NextJS
- DFS
- BFS
- 자료구조
- nextjs autoFocus
- vuejs
- react autoFocus
- useHistory 안됨
- error:0308010C:digital envelope routines::unsupported
- useState
- Deque
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |