개발 기간: 2주(2023.2.11~2023.2.25)
취얼업
: 개발자 취업 준비생을 위한 공부 루틴 만들기 및 자기계발 웹사이트
프론트엔드 2명, 백엔드 2명
사용한 기술 스택: React(프론트), NestJS(백엔드), MySQL(DB), TypeORM
: 새로운 기술을 사용해서 프로젝트를 진행하고자 했기 때문에, nestjs와 tyeporm, typescript를 프로젝트 진행 기간 동안 새로 배우면서 진행했다. 그렇기 때문에 시간이 그냥 nodejs와 express를 사용해서 진행하는 것보다 더 오래 걸려 여유있게 배포를 하지 못했던 점이 아쉬웠다. 외래키 설정하고 서버와 db를 연결하는 것도 nest로 처음해보는 것이었기 때문에 시간이 배로 걸렸다. 하지만 nest, ts 새로운 언어를 경험해보았기에 매우 값진 프로젝트였다고 생각한다.
curtime()
으로 설정하였고, 게시판와 User의 경우 시간도 같이 나올 수 있도록 DATETIME, default는 CURRENT_TIMESTAMP
으로 설정하였다.: 아쉬웠던 점 - puppeteer를 처음 사용해서 크롤링을 해보았는데, 일단 크롤링 시간이 10초 정도 걸리는 것이 아쉬웠다. 아마 직접 서버에서 인프런 사이트에 접속해서 긁어오기 때문에 시간이 걸리는 것 같다. 하지만 구글링해보니 크롤링 속도를 높이는 법도 있는 것 같아 추후 개선해야할 부분이다. 그리고 headless 모드를 true로 설정하였음에도 불구하고, 맥이 아닌 윈도우의 경우 팝업창이 뜨는 문제가 발생했다. 구글링해보니 headless: true
가 잘 작동하지 않는 경우도 있는 것 같아 이부분도 추후 개선 사항이다. 또한 배포 모드로 했을 때도 크롤링 get요청 axios에러가 발생해서 작동하지 않았다. http이기 때문에 작동하지 않는 것일지 다른 설정을 해줘야 하는지 알아보아야 한다.
//scrapper.service.ts
혹시 참고하실 분이 있을까봐
@Injectable()
export class ScrapperService {
async getDataViaPuppeteer() {
const URL = `https://www.inflearn.com/community/studies`;
const browser = await puppeteer.launch({
headless: true,
args: ['--fast-start', '--disable-extensions', '--no-sandbox'],
ignoreHTTPSErrors: true,
});
const page = await browser.newPage();
const data = [];
for (let index = 1; index < 3; index++) {
await page.goto(
'https://www.inflearn.com/community/studies?page=' +
index +
'&order=recent',
{
waitUntil: 'networkidle2',
},
);
const lists = await page.$$('div.question-list-container > ul > li');
for (let i = 0; i < lists.length; i++) {
const list = lists[i];
const title = await list.$eval('h3', (element) => element.innerText);
const url = await list.$eval('a', (element) => element.href);
const badge = await list.$eval(
'a > div > div > div.question__title > div > div > span',
(element) => element.innerText,
);
const dataArr = {
title: title,
url: url,
badge: badge,
};
data.push(dataArr);
}
}
await page.close();
await browser.close();
return data;
}
}
: Nestjs를 사용해서 클라이언트로 데이터를 전달하고, 요청받는 것이 처음에는 너무 생소해서 오류가 많이 났다. axios 요청을 주고 받는 것조차 너무 어려웠던 기억이 난다. 덕분에 postman과 insomnia를 사용하는 이유를 잘 체감하게 되었지만 말이다.
무튼 nestjs 사용에 있어서 가장 좋았던 점은 도메인을 관리하기 편했다는 점이다. controller에서 기본 도메인을 설정하면 get, post, patch, delete 등 다른 요청의 도메인은 기본 도메인에 맞춰서 설정할 수 있다. 그리고 controller, module, service로 나누어진 점도 흥미로웠다. 컨트롤러는 요청을 반환하고, 서비스는 데이터베이스와 관련된 부분을 다루는 점이 확실히 분리되어 있어서 사용법에 더 익숙해진다면 기존의 nodejs로만 하던 방법보다 더 효율적이고 정돈된 방법이라고 생각했다. 그리고 dto를 설정해서 사용해보았는데 확실히 데이터의 값이 string인지, 아니면 숫자인지를 검증하기 때문에 확실하게 오류를 더 줄여줄 수 있는 방법같다.
문제는 nest에서 postman, insomnia로 데이터 주고받기를 성공했다면 어떻게 클라이언트에서 이를 전달하냐였는데, 이것 때문에 처음에 걱정과 막연한 두려움이 컸다. 하지만 그냥 클라이언트 단에서는 기존의 방식과 같이 axios로 데이터를 입력해주면 되는 간단한 문제였다! (새로운 방식이라고 해서 괜히 미리 겁먹지 말자는 교훈을 얻었다.)
예시:
useEffect(() => {
axios.get(`${process.env.REACT_APP_SERVER_HOST}/${id}`).then((res) => {
...
const postDataArr = {
post_id: res.data.post_id,
title: res.data.title,
content: res.data.content,
date: convertDate,
userId: res.data.userId,
nickname: res.data.nickname,
};
setPost(postDataArr);
});
}, []);
const formData = new FormData();
formData.append('id', String(userID));
formData.append('pw', String(user.pw));
formData.append('nickname', String(user.nickname));
formData.append('file', pickedFile);
const Data = {
id: formData.get('id'),
pw: formData.get('pw'),
nickname: formData.get('nickname'),
file: formData.get('file'),
};
axios
.patch(
`${process.env.REACT_APP_SERVER_HOST}/user/upload/${userID}`,
Data,
{
headers: {
'Content-Type': 'multipart/form-data',
},
},
)
그리고 서버단에서 @Body로 데이터를 받아오려니 계속 오류가 나서 @Req를 이용해서 req.body로 데이터를 받아왔다. 또한 파일의 이름의 중복도 방지하고자 따로 파일이름을 설정해주었다. 하지만 upload
폴더에 계속 이미지가 생성되는 걸 따로 삭제처리를 해주지 않았기 때문에 이 또한 추후 개선해야할 점이다.
//user.controller.ts
@Patch('/upload/:id')
@UseInterceptors(
FilesInterceptor('file', 1, {
storage: diskStorage({
destination: './upload',
filename: (request, file, callback) => {
callback(null, `${Date.now()}${extname(file.originalname)}`);
},
}),
}),
)
async uploadFile(
@Param('id') userId: string,
@Req() req,
) {
await this.userService.uploadImg(req.files[0], req.body, userId);
}
- 게시판에서 해당 유저의 프로필 사진 보여주기, 미리보기
먼저 get요청에 대한 코드를 미리 만들어줘서 결국 서버에서 get요청을 받아와서 이미지를 미리 보여주는 방식을 이용했다.
@Get(':imgpath')
seeUploadedFile(@Param('imgpath') image, @Res() res) {
return res.sendFile(image, { root: './upload' });
}
이를 통해 파일 고르기에서 선택한 이미지를 미리 보여줄 때도 useState를 이용해서 그냥 이미지를 가져오는 도메인과 이미지가 저장된 파일 이름을 db에서 가져와서 불러오는 방식을 사용했다.
++
function formatDate(string) {
var options = {
year: 'numeric',
month: 'numeric',
day: 'numeric',
};
return new Date(string).toLocaleDateString([], options);
}
Entity 외래키 설정
: 외래키 설정도 애를 많이 먹었다. 결론적으로 manytoone에 joincolumn, 그리고 이의 옵션인 name, referencedColumnName을 설정해줌으로써 무사히 연결할 수 있었다.
//Comment.entity.ts
@Column({ nullable: false })
userId: string;
@ManyToOne(() => User, (user) => user.comments)
@JoinColumn([{ name: 'userId', referencedColumnName: 'id' }])
user: User;
//User.entity.ts
@OneToMany(() => Study, (comment) => comment.user, { cascade: true })
comments: Comment[];
app.module.ts 설정
: 추가적으로 .env를 불러오는 것도 시간이 꽤 많이 걸렸는데, env 파일을 src 바깥 폴더에 저장함으로써 해결할 수 있었다.
또한 맥과 윈도우의 파일을 읽어오는 방식 때문인지 entities를 [__dirname + '/../**/*.entity.{js,ts}'
이렇게 입력하면 metadata를 찾을 수 없다는 오류가 떴었다. 그래서 결국 직접 import하여 입력해주는 방식으로 해결했다.
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST,
port: +process.env.DB_PORT,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWD,
database: process.env.DB_DATABASE,
entities: [User, Post, Comment, Study, Chat, Life],
logging: true,
}),
MulterModule.register({
dest: './upload',
}),
그리고 초반에 옵션을 synchronize : true로 해놨기 때문에 mysql 컬럼을 다 지우고 nestjs, tyeporm이 새로 생성하여 metadata를 못읽는다는 에러가 계속 발생했었다. 그래서 synchronize: true를 지워서 해결했고, logging: true로 설정하여 query 상황을 알 수 있어서 개발할 때 편했다.
createQueryBuilder
를 사용해서 데이터를 불러왔다..where
을 2번 쓰지말고, .andWhere
을 쓸 것을 기억하자! //study.service.ts
async getStudiesByUserId(userId: string) {
const TIME_ZONE = 9 * 60 * 60 * 1000; // 9시간
const koreatime = new Date(Date.now() + TIME_ZONE)
.toISOString()
.split('T')[0];
//userId 일치 & 날짜 일치하는 데이터 불러오기
const studyDatabyUser = await this.studyRepository
.createQueryBuilder('s')
.select(['s.study_id', 's.done', 's.date', 's.content', 's.user_id'])
.where('s.user_id = :user_id', {
user_id: String(userId),
})
.andWhere('s.date = :date', {
date: koreatime,
})
.getMany();
return studyDatabyUser;
}
++
Anson the developer 이분의 영상을 많이 참고했다!
매우 감사한 분...
https://www.youtube.com/watch?v=W1gvIw0GNl8