728x90

이미지 압축 및 s3 업로드는

정말 많이 쓰이지만 의외로 포스팅이 없어

간단히 정리해본다.

 

 

처리절차

1. 클라이언트 요청 (생략)

2. multer-s3로 aws 업로드

3. key로 s3 object를 불러와 압축 후 재 업로드

4. 기존 파일 key로 제거

 

 

1. 클라이언트 요청 (생략)

요청은 포스트맨(Postman)으로 요청,

테스트하면 됨으로 생략 (form-data 방식 활용)

 

 

2. multer-s3로 aws 업로드

express router에서 미들웨어 처리를 해줄것이다.

그전에 multer-s3 미들웨어를 만들어주자.

// aws.ts
const AWS = require('aws-sdk');
const multer = require('multer');
const multerS3 = require('multer-s3');

const aws_config = {
    accessKeyId: process.env.AWS_ACCESS_KEY,
    secretAccessKey: process.env.AWS_SECRET_KEY,
    region: 'ap-northeast-2',
    signatureVersion: 'v4',
};

export const s3 = new AWS.S3();

export const uploadImage = multer({
  storage: multerS3({
    s3,
    bucket: 'test.bucket',
    metadata: (req, file, cb) => {
      cb(null, { fieldName: file.fieldname });
    },
    key: (req, file, cb) => {
      const ext = path.extname(file.originalname);
      const timestamp = new Date().getTime().valueOf();
      const filename = 'test' + timestamp + ext;
      cb(null, filename);
    },
  }),
});

이제 사용자 요청을 받는 router부분에서

uploadImage를 미들웨어로 넣어주면 된다.

// image.router.ts
router.post('/image/upload', uploadImage.single('photo'), (req, res) => {
  ...
})

 

 

3. key로 s3 object를 불러와 압축 후 재 업로드 & 기존 파일 key로 제거

uploadImage 처리 후 req.file에서

업로드된 이미지 객체의 key를 확인할 수 있다.

 

이 key를 활용해 객체를 불러와

압축하여 재업로드 및 삭제를 진행할것이다.

 

// aws.ts
export const compressImageUploadByKey = async (key: string, width?: number) => {
  try {
    const compressedKey = `compressed_${key}`;
    const config = {
      Bucket: 'test.bucket',
      Key: key
    }
        
    let resizedConfig: any = {
      Bucket: 'test.bucket',
      Key: compressedKey
    }

    // fetch
    const imageData: any = await s3.getObject(config).promise();

    // resizing
    const imageBuffer = await sharp(imageData.Body).resize({ width: width || 640 }).toBuffer();
    resizedConfig.Body = imageBuffer;
    
    // upload
    await s3.putObject(resizedConfig).promise();

    // origin image delete
    await s3.deleteObject(config).promise();

    return compressedKey;
  } catch(error) {
    console.log('Get image by key from aws: ', error);
  }
}

불러오고, 압축하고, 업로드하고, 지우고

4가지 작업이 진행되었다.

 

 

새로 업로드된 이미지의 Key를

DB에 저장하거나 하는 작업을 

각 작업 환경에 맞게 진행하면 끝

728x90
반응형
728x90

"늘 그랬듯이"

라는 생각은 개발자에게 치명적이다.

 

그 현상을 더 이상

해결해야될 문제로 보지않고

안주하게 만들어버린다.

 

나에게 server.js 스크립트는

그런 대상이다.

 

수 많은 MVP 모델을 만들면서

server.js를 어떻게 구조화할지는

크게 고민하지않앗다.

 

express가 너무나도 쉽게 서버를 만들어주기에

필요할때 route만 더 붙여준다던지

cors를 설정해준다던지 하면 끝이었다.

 

// server.js
const express = require('express');
const api_v1 = require('../api/v1');

const app = express();
const http = require('http').createServer(app);

app.use('/api/v1/', api_v1);
http.listen(process.env.PORT, () => {
  console.log('server is listening on', process.env.PORT);
});

 

간단하게는

이정도면 충분히 훌륭한 앱서버가 된다.

 

문제는

서버에서 담당하는 역할이 많아지면서

여러가지가 붙다보면

코드가 지저분해지고

더이상 보기 싫은 스크립트가 되어버린다.

 

// server.js
const express = require('express');
const { Server } = require('socket.io');
const api_v1 = require('../api/v1');
const config = require('./config/server');

const app = express();
const http = require('http').createServer(app);
const io = new Server(http, {});

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cookieParser());

app.use(function (req, res, next) {
  const allowedOrigins = config.allowedOrigin;
  const origin: any = req.headers.origin;
  if (allowedOrigins.indexOf(origin) > -1) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  res.header('Access-Control-Allow-Headers', 'Authorization, X-Requested-With, Content-Type, Accept');
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS,PATCH');
  res.header('Access-Control-Allow-Credentials', 'true');
  req.method === 'OPTIONS' ? res.sendStatus(200) : next();
});

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

io.sockets.on('connection', (socket) => {
  console.log('socket is connected..!');
});

http.listen(process.env.PORT, () => {
  console.log('server is listening on', process.env.PORT);
});

 

이정도만 되어도 보기싫어지기 시작한다.

우리는 class로 스크립트를 새로 짜보도록 하자.

 

// server.js
const express = require('express');
const { Server, createServer } = require('http');
const { Server as ioServer } = require('socket.io');
const api_v1 = require('../api/v1');
const config = require('./config/server');

const app = express();
const http = require('http').createServer(app);
const io = new Server(http, {});

class Server {
  private app: express.Aplication;
  private server: Server;
  private io: ioServer;
  
  constructor() {
    this.createApp();
    this.createServer();
    this.sockets();
    this.configure();
    this.routes();
    this.listen();
  }
  
  private createServer = (): void => {
    this.server = createServer(this.app);
  }
  
  private createApp = (): void => {
    this.app = express();
  }
  
  private sockets = (): void => {
    this.io = new ioServer(this.server);
  }
  
  private routes = (): void => {
    app.use('/api/v1/', api_v1);
  }
  
  private configure = (): void => {
    app.use(express.urlencoded({ extended: true }));
    app.use(express.json());
    app.use(cookieParser());

    app.use(function (req, res, next) {
      const allowedOrigins = config.allowedOrigin;
      const origin: any = req.headers.origin;
      if (allowedOrigins.indexOf(origin) > -1) {
        res.setHeader('Access-Control-Allow-Origin', origin);
      }
      res.header('Access-Control-Allow-Headers', 'Authorization, X-Requested-With, Content-Type, Accept');
      res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS,PATCH');
      res.header('Access-Control-Allow-Credentials', 'true');
      req.method === 'OPTIONS' ? res.sendStatus(200) : next();
    });
  }
  
  private listen = (): void => {
    this.server.listen(process.env.PORT, () => {
      console.log('server is listening on', process.env.PORT);  
    });
    
    io.sockets.on('connection', (socket) => {
      console.log('socket is connected..!');
    });
  }
}

const App = new Server();

 

이렇게 정리해놓고 보니

목적에 맞는 코드끼리 분리되어

유지보수가 확실히 수월해진듯한 느낌이든다.

 

코드를 짤때는 내 코드를

함께 공유할 개발자도 배려하는 자세를 갖도록 해야겠다.

728x90
반응형
728x90

pm2를 이용해서

앱서버를 백그라운드 환경으로 운영이 가능하다.

 

여기서 한발 더 나아가

cluster mode로 실행중인 node.js서버를

pm2를 이용해 돌리곤 했었다.

 

하지만 배포할때마다 일시적으로 중단되어버리는 서버.

원인은 restart로 배포하기때문.

 

reload 명령어를 통해

kill -> restart

reset 형식으로 서버 중단없이

무중단 배포가 가능하다.

pm2 reload index.js

 

근데 이를 제대로 활용하려면

몇가지 설정이 필요하다

 


 

pm2 ecosystem

 

위 명령어로 ecosystem.config.js 파일을 얻을수 있다.

다양한 설정들이 기본으로 세팅되어있는데

docs를 보면서 입맛에 맞게 설정하면 된다.

 

진짜 간단히는

module.exports = {
    apps: [
        {
            name: 'server',
            script: './index.js',
            watch: '.',
            instances: -1, // 클러스터 모드
            // exec_mode: 'cluster', <-- 이것도 클러스터 모드
        },
    ],
};

이렇게만 설정해도 된다

 

package.json을 수정하면

npm 명령어로 서버에서

간편히 실행 혹은 reload 하는것도 가능하다.

scripts: {
  "start": "pm2 start ./ecosystem.config.js --only server",
  "reload": "pm2 reload server"
}

 

고객에게 불편을 주지말자

 


여기서 더 나아간다면

실행중인 process가 덮어씌워지는 경우를

방지하고

reload할 수 있다.

 

// server.js

const app = express();


let disableKeepAlive = false;

// 중단이 감지되면 Keep 상태인 요청 닫음
app.use((req, res, next) => {
    if (disableKeepAlive) {
        res.set('Connection', 'close');
    }
    next();
});

const service = app.listen(process.env.PORT, () => {
    console.log(`The application is listening on port ${process.env.PORT}`);
    if (process.send) {
        console.log('send')
        process.send('ready');
    }
});

process.on('SIGINT', async () => {
    disableKeepAlive = true;
    service.close();
    process.exit(0);
});

이런식으로 구성해주면 된다.

728x90
반응형
728x90

취미생활로 브이로그 제작을 시작하였습니다.

 

파일정리가 필요해 지다보니

 

귀찮은 작업을 최소화 하기위해

 

파일 정리 라이브러리를 만들었습니다. (비디오 파일 정리 세팅 되어있음)

 

필요 하신 분들은 이용하시면 될것같습니다.

 

https://github.com/jaekwangLee/organize_video

 

GitHub - jaekwangLee/organize_video

Contribute to jaekwangLee/organize_video development by creating an account on GitHub.

github.com

 

728x90
반응형
728x90

base64? buffer? stream?

일반적으로 자주 사용하지는 않으나

파일 처리를 할때면 나타나 곤란한 상황을 만드는 녀석들이다.

 

이번에 우연한기회에

AWS에 업로드된 파일 URL을 노출시키지 않으면서

클라이언트에서 파일을 내려받게 해줘야하는 상황이 발생했다.

 

해당 과정에서 삽질하며 얻은 조각 지식들을 공유한다.

 

[ 가정 ]

1. 클라우드 저장소에 파일이 있다.

2. 클라이언트에서 파일 다운로드 요청 발생.

3. 서버에서 해당 파일을 불러온다.

4. 클라이언트에 해당 파일을 전송한다.

5. 다운로드 실시

 

1.  클라우드 저장소에 파일이 있다.

- AWS의 S3에 특정 파일이 저장되어있는 상황입니다.

- 해당 파일은 비문은 아니지만 공유를 원치않아 파일의 링크는 노출되지 않기를 원합니다.

- 마찬가지로 해당 파일의 버킷은 퍼블릭 버킷이 아닙니다.

- 따라서, 허가된 key를 가진 사용자만 파일을 호출할 수 있습니다. (즉, 서버에서만 요청 가능)

 

2. 파일을 필요로 하는 클라이언트는 지정된 서버로 요청을 보냅니다.

- 지정된 서버는 당연히 해당 AWS계정의 접근권한, key를 가진 서버겠죠.

 

3. 서버는  S3로부터 파일을 가져옵니다.

const AWS = require('aws-sdk')

const confg = {
  accessKeyId: '',
  scretAccessKey: '',
  region: '',
  signatureVersion: 'v4',
};

AWS.config.update(config);
const S3 = new AWS.S3();

function getObjectFromS3(key_name) {
  return new Promise((resolve, reject) => {
    S3.getObject({ Bucket: '', Key: key_name }, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    })
  });
}

 

getObjectFromS3를 이용하면 쉽게 파일을 읽어올 수 있습니다.

자, 지금부터가 집중이 필요합니다.

불러온 데이터는 JSON 형태인데

파일은 Body에 Buffer형태로 따라옵니다.

 

이, Buffer를 어떤식으로 클라이언트로 반환해줄건지

이걸 받으려면 클라이언트는 어떻게 해야하는지

주목해야합니다.

 

다시, 돌아가 클라이언트에서 파일을

어떻게 요청해야되는지 살펴봅시다.

 

4. 클라이언트에서 서버에 특정 파일 요청하기

const requestPrivateFile = () => {
  axios.post(url, {}, { responseType: 'arrayBuffer' })
  .then(resp => {
    const { data } = resp;
    console.log(data);
    ...
  });
}

 

자, 일반적인 API요청과 다르게  responseType으로 arrayBuffer을 지정해주었습니다.

이걸 놓치면 백날 삽질해도 소용없습니다.

 

서버에서도 그러면 값을 맞춰서 반환해줘야겠죠?

 

5. 서버에서 불러온 파일 반환해주기

const getPrivateFile = async (req, res) => {
  ...
  
  const file = await getObjectFromKey('something.pdf');
  
  res.writeHead(200, [ 
    ['Content-Type', 'application/pdf'], //  다른 형식의 파일이면 알맞는 mime/type을 지정해주세요.
  ]);
  res.end(Buffer.from(file.Body, 'base64'));
}

sendFile이나 send 등이 아니라 위와 같은 방식으로

파일을 리턴해주게됩니다.

 

6. 자, 그러면 파일을 받아서 한번 다운로드해볼까요?

const requestPrivateFile = () => {
  axios.post(url, {}, { responseType: 'arrayBuffer' })
  .then(resp => {
    const { data } = resp;

    downloadFile(data, 'something.pdf');
  });
}

function downloadFile(buffer, filename) {
  const blob = new Blob([buffer], { type: 'application/pdf' });
  const url = window.URL.createObjectURL(blob);
  
  const a = document.createElement('a');
  a.download = filename; // download될때의 파일명을 지정해줍니다.
  a.href = url;
  
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a); // 더 이상 필요없으므로 삭제
}

 

자, 여기까지 "서버로부터 buffer 형식으로 파일 전송받아 처리하기"를 해보았습니다.

알고나면 간단하지만 모를때는 조각조각 흩어져있는

토막지식들로인해 어려움을 주는 사항이었습니다...

 

여러가지 상황에 응용이 가능하니 알아두면 좋을것같습니다.

728x90
반응형
728x90

nodejs에서 pdfjs-dist 라이브러리를 사용하기위해 canvas가 필요했다.

(pdfjs가 canvas에 의존적이기 때문)

 

그런데 linux기반 서버에서 canvas설치 후 실행시 다음과 같은 에러가났다.

Cannot find module '../build/Release/canvas'

 

한참을 헤맸다.

node-gyp rebuild니 permission 에러니...

 

정답은 늘 가까이에 있고, 많이 사용하는 라이브러리는 늘 답안이 나와있다. (감사해요 stackoverflow)

I just use "npm uninstall canvas" and then install using "npm i canvas"

 

이 단 한줄의 문장으 내 머리를 후려쳤다.

생각해보니 canvas에서 지시한 지시사항을 실행하기 전에 canavs를 먼저 설치한것이다.

 

sudo apt-get install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev

 

위는 canvas에서 ubuntu환경일 경우 먼저 설치하라고 안내해준 library들이다.

 

나는 이미 진행했으므로

npm uninstall canvas
npm install canvas

 

canvas를 다시 설치해주는것만으로 해결되었다.

 

pdfjs는 pdf to png작업을 위해 사용하였는데, 곧 관련내용을 정리해볼 예정이다.

728x90
반응형
728x90

오랜만에 포스팅을 합니다.

 

운영중인 프로그램이 대규모 업데이트가 연속적으로 이루어지고있어

 

포스팅할 시간이 없네요.

 

대규모 업데이트를 하다본니, 이것만큼은 꼭 공유해주고 싶다! 라고 생각이 든 파트가 있어 포스팅하게되었습니다.

 

바로 로깅 시스템인데요.

 

 

다양한 라이브러리에서 로깅시스템을 지원해주지만,

내가 만들어 자유자재로 쓰는 가벼운 로깅시스템은 디버깅에서 최고의 효율은 발휘해주는것 같습니다.

 

예를들어, try/catch구문에서  해당 코드의 디렉토리 위치부터 에러발생 시간, 에러 내용을 모두 표시해준다면
에러복구에 정말 유용하겠죠?

따라서, fs시스템을 이용해 간단한 로깅메서드를 만들어 해당 시스템을 구축해 보도록 하겠습니다.

 

# 순서

1. 로깅 메서드 만들기

2. 활용 예제

3. 응용 예제

 

 


 

1. 로깅 메서드 만들기

const fs = require('fs');
const moment = require('moment');

export const logging = log => {
  const today = moment().format('YYYYMMDD');
  const date = moment().format('YYYY.MM.DD HH:mm:ss');

  const file = `./log/${today}_server.log`;
  const newLine = `[${date}] - ${log}`;

  fs.readFile(file, (err, data) => {
    if (err) {
      return console.error('read log err: ', err);
    }
    
    fs.writeFile(file, data + '\n' + newLine, err => {
      if (err) {
        return console.error('write log err: ', err);
      }
    });
  });
  
};

# 예제 키워드: fs, moment, 백틱

파일을 읽어오고 줄을 바꿔 내용을 덧붙이는 예제입니다.
writeFile은 해당 파일이 존재하지 않으면 기본적으로 create & write하게 되어있습니다.

 

(추가적으로, 해당 옵션은 flag옵션으로 변경가능합니다.)


2. 활용 예제

const { logging } = require('./util');

try {
  // something ...
  
} catch(error) {
  console.log('check system error log: ', error); // 콘솔에서 에러 확인 (개발에 활용)
  logging(error); // 로그파일에 기록 (나중에 확인하기 위함)
  return 'error;
}

 

우리의 logging 예제코드가 util파일에 있다고 가정했을때의 예제입니다.

이렇게, logging(error) 한줄 추가해주는것만으로

" [날짜] - 에러내용 "

 

구조의 에러 리포트를 누적해둘수있습니다.

예외처리에 빠짐없이 잘 써둔다면 디버깅에서 아주 유용하게 쓰일것으로 보입니다.

 

좀더 실용적이고 쓸만한 메서드로 개선해볼까요

 

 

3. 응용예제

# util
const fs = require('fs');
const moment = require('moment');

export const logging = (log, dir) => {
  const today = moment().format('YYYYMMDD');
  const date = moment().format('YYYY.MM.DD HH:mm:ss');

  const file = `./log/${today}_server.log`;
  const newLine = `[${date}] - ${log} ${ dir ? '\n'+ 'directory: ' + dir : ''}`;

  fs.readFile(file, (err, data) => {
    if (err) {
      return console.error('read log err: ', err);
    }
    
    fs.writeFile(file, data + '\n' + newLine, err => {
      if (err) {
        return console.error('write log err: ', err);
      }
    });
  });
  
};


# server
const { logging } = require('./util');

try {
  // something ...
  
} catch(error) {
  console.log('check system error log: ', error); // 콘솔에서 에러 확인 (개발에 활용)
  logging(error, __filename); // 로그파일에 기록 (나중에 확인하기 위함)
  return 'error;
}

# 예제 키워드: __filename

 

 

이렇게 간단한 수정만으로 파일의 디렉토리까지 남겨가며 모든 에러를 추적하게됩니다.

 

728x90
반응형
728x90

Node는 기본적으로 싱글스레드 기반이기 때문에

내부적으로 서버를 Fork해서 여러 스레드를 쓰는 방법을 지원해준다.

바로 cluster.

기본적으로 내장되어있으며 그 위력은 어마어마하다.

 

그런데 linux에서 cluster를 실행한 코드를 pm2로 실행하다보니

난관에 봉착했다.

 

에러가 발생한다.

cluster가 설정된 코드를 실행할수없었다.

 

그런데 node의 공식문서에서 방법을 찾았다.

문서의 일부 내용을 발췌했다.

 

======================================================

PM2 사용

애플리케이션을 PM2에 배치하면, 애플리케이션 코드를 수정하지 않고도 클러스터링을 활용할 수 있습니다. 먼저 여러분의 애플리케이션이 stateless인지 확실하게 해야합니다. 어떠한 로컬 데이터도 프로세스에 저장되지 않아야 합니다. (세션이나 웹소켓 같은 것들 말입니다).

PM2로 애플리케이션을 실행하고 있을 때, 특정한 수의 인스턴스에 실행하는 클러스터 모드를 켤 수 있습니다. 머신의 가용 CPU 수같은 것들이 특정한 수입니다. 애플리케이션을 끌 필요 없이 pm2 커맨드라인 명령을 이용해 클러스터에 있는 프로세스의 수를 직접 바꿀 수도 있습니다.

 

아래와 같은 방법으로 클러스터 모드를 킵니다.

# Start 4 worker processes

$ pm2 start app.js -i 4 # Auto-detect number of available CPUs and start that many worker processes

$ pm2 start app.js -i max

이 수는 PM2 프로세스 파일 (ecosystem.config.js나 그와 유사한 파일) 안의 exec_mode를 cluster나 instances를 설정해서 수정될 수 있습니다.

 

실행이 시작되면, app으로 이름지어진 애플리케이션을 아래와 같은 방법으로 스케일링 할 수 있습니다.

# Add 3 more workers

$ pm2 scale app +3 # Scale to a specific number of workers

$ pm2 scale app 2

PM2의 클러스터링에 관한 추가 정보는 PM2 문서의 Cluster Mode를 참고해주세요.

======================================================

 

 

728x90
반응형
728x90

암호화에는 대칭키와 비대칭키 방식이 있다.

 

대칭키 방식은 암호화와 복호화에 같은 키를 이용하는 방식이다.

주로 서버에서 암/복화하를 모두 하는 경우에 사용한다.

 

 

비대칭키 방식은 암호화 복호화에 각기 다른 키를 사용하는 방식이다.

먼저,  (복호화를 위한)개인키를 생성하고

개인키를 가지고  (복화화를 위한)공개키를  생성한다.

 

클라이언트와의 통신간에 암호화를 필요로 하는 경우 비대칭키를 사용하게되는데,

이는 대칭키 방식에 비해 속도가 다소 떨어진다고 한다.

 

나는 이번에 계좌번호, 카드번호 등의 데이터 교환을 위해 비대칭키 방식을 사용하게 되었다.

 

 

실제로, 어떻게 구현해 나가면 되는지 알아보자.

1. 개인키를 생성한다.

 - openssl genrsa -out private.key 2048 // 나는 이거로는 안심이 안된다. 4096을 사용

 

2. 개인키를 가지고 공개키를 생성한다.

 - openssl rsa -in private.key -out public.key -pubout (PKCS#8 표준)

 

3. PKCS#1 을 사용하고자 한다.

 - openssl rsa -pubin -in public.key -RSAPublicKey_out

 

4. 환경변수를 쉽게 사용하게 해주는 dotenv를 정의하는 .env파일에 다음과 같이 정의한다.

 - private키의 줄 끝마다 \n를 붙여 한줄로 합쳐준다.

// before
asdfasdf
asdfasdf
asdfasdf

// after
asdfasdf\nasdfasdf\nasdfasdf

 

5. 사용할 때는 다음과 같은 코드를 통해 사용할 수 있다

export const getPrivateKey = () => {
    return process.env.PRIVATE_KEY.replace(/\\n/g, '\n');
};

 

6. 이후의 암복화하는 일상 하던것과 다르지 않다...

728x90
반응형
728x90

Youtube API와 관련된 Nodejs 레퍼런스가 많이 부족한것 같습니다.

Youtube가 현재 대세인 것을 고려하면 너무나 안티까워 간단하게 Youtube api사용법을 공유하려 합니다.

 

* API로 유튜브 영상을 지우기

Youtube.videos.delete를 사용하게되며 영상의 id값만을 parameter로 보내줍니다.

Youtube.videos.delete({ id }, (err, data) => { ...something });

 

이전의 게시물에서 충분히 사전내용을 기록해두었기에 간단히 기록해보았습니다. (이전 게시물에 Youtube API서용법이 상세히 나와있으니 참고하시길 바랍니다..^^)

 

* 다만, 기억할것이 있습니다.

다른 api들과 같이 YoutubeAPI 역시 일일할당 트래픽량이 존재랍니다.

1인당 하루 10000트래픽이 허용되며, 초과시 사용이 더 이상 불가능합니다.

 

영상 업로드 api를 사용하다보면 금방 초과되버립니다 (7~8개쯤 영상업로드 테스트중 초과해버렸네요

..)

list를 가져오는 경우에는 거의 소요되지않지만 업로드 api를 사용해야 한다면,

정말 꼭 필요한 경우에만 쓰도록 하는게 좋을듯 합니다...

 

 

 


웹사이트 개발 / 홈페이지 제작 / android앱 개발 / ios 앱 개발 / server / client / aws / fullstack / buisness partner / 외주 / 용역

https://open.kakao.com/o/sNETgUJb

http://self-made.cloud

 

 

728x90
반응형