Releases: boostcampwm-2024/refactor-web07-Ask-It
1.0.1 (2025. 01. 23)
Release 1.0.1 (2025. 01. 23)
프로젝트 전반적으로 성능 개선과 유지보수성, 그리고 확장 가능한 설계를 목표로 Docker 기반 CI/CD, Redis 캐싱, 좋아요 동시성 문제 해결, 프론트엔드 렌더링 버그 수정, 코드 품질 및 프로젝트 구조 개선 등을 중점적으로 진행했습니다.
BE
Docker CI/CD (PR #28)
[배경]
- 기존에는 PM2를 사용해 애플리케이션을 관리했으나, 컨테이너 기반의 배포 방식으로 전환하여 일관된 환경에서 애플리케이션을 실행하고 관리하고자 했습니다.
- Docker 기반으로 CI/CD를 구성하여 개발, 테스트, 배포의 효율성을 높이고자 했습니다.
[문제]
- 환경 일관성 부족
- PM2는 서버 환경에 의존적이며, 배포 과정에서 환경별 차이에 따른 문제가 종종 발생했습니다.
- 복잡한 배포 프로세스
- 서버 설정과 배포 스크립트의 관리가 점점 복잡해지고, 유지 보수 부담이 증가했습니다.
[해결]
- Docker 컨테이너 기반으로 전환
- 애플리케이션과 모든 의존성을 컨테이너로 패키징.
- Dockerfile을 작성해서, 개발 및 배포 환경 간의 차이를 제거.
- CI/CD 파이프라인 개선
- GitHub Actions를 사용하여 자동화된 빌드, 테스트, 배포 프로세스를 구현.
- PR 머지 시 자동으로 컨테이너 이미지를 빌드하고, 서버에 배포.
[결과]
- 일관된 실행 환경
- 모든 서버에서 동일한 컨테이너 환경을 사용해 실행 오류가 감소.
- 배포 프로세스 간소화
- GitHub Actions와 Docker를 사용한 CI/CD로, 배포 시간을 단축하고 효율성을 향상.
- 확장성 및 유지 보수성 증가
- 컨테이너화된 애플리케이션을 통해 서버 스케일링과 업데이트가 용이.
- 개발자의 로컬 운영체제와 무관한 실행 테스트
- Docker는 애플리케이션을 컨테이너화하므로, 로컬 개발 환경(OS에 상관없이)에서도 프로덕션과 동일한 환경에서 테스트가 가능합니다.
- 예: 개발자가 Windows, macOS, Linux 등 다른 OS를 사용하더라도 동일한 컨테이너를 실행할 수 있습니다.
Redis 캐싱을 통한 API 성능 개선 (PR #34)
[배경]
- 서비스의 핵심기능 중 '질문 목록 조회', '질문 작성', '답변 작성', '좋아요 토글'에 대해 부하테스트를 진행하였습니다.
- 테스트는 200명의 가상 사용자를 대상으로 5분간 진행되었으며, 질문 목록 조회(40%), 질문 작성(30%), 답변 작성(20%), 좋아요 토글(50%)의 비중으로 구성되었습니다.
[문제]
- 특정 요청의 경우, 사용자의 요청 속 세션의 정보, 사용자의 토큰 등을 GUARD에서 검사하며, 이 작업은 많은 DB의 접근과 처리시간을 필요로 합니다.
- 부하테스트 결과, 응답 시간이 중간값 기준 118.6ms, 95퍼센타일 기준 204.2ms로 나타났습니다.
- 특히 읽기 작업이 많은 서비스 특성상(질문 목록 조회 40%), 매 요청마다 DB에 접근하는 것이 성능 병목의 주요 원인이 되었으며, 이로 인해 DB Connection Pool이 고갈되어 Timeout으로 인한 500 응답이 발생하기도 하였습니다.
[ERROR] https://ask-it.site/api/questions - Status: 500, Duration: 27ms Error response: <html> <head><title>500 Internal Server Error</title></head> <body> <center><h1>500 Internal Server Error</h1></center> <hr><center>nginx/1.18.0 (Ubuntu)</center> </body> </html>
[해결]
- Redis를 통해 GUARD 부분에 캐시를 도입하여 자주 사용되는 사용자의 정보, 세션의 토큰 등을 저장합니다.
- Cache hit가 발생할 경우 DB에 접근하지 않고 바로 반환해주는 방법을 통해 성능을 개선하였습니다.
- 특히 읽기 작업이 많은 서비스 특성을 고려하여, 캐시 적용 대상과 전략을 최적화하였습니다.
[결과]
- 전반적인 응답 시간이 평균 80% 이상 감소했습니다:
- 중간값 기준: 118.6ms → 22.9ms (80.7% 감소)
- 95퍼센타일 기준: 204.2ms → 37.0ms (81.9% 감소)
- 시스템 처리량이 18.3% 향상되었습니다 (캐싱 시나리오 기준)으로 1.69 RPS → 2.00 RPS)
좋아요 동시성 문제 해결 (PR #39)
[배경]
- Ask-It 서비스에서는 유저가 질문에 좋아요를 남길 수 있는 기능을 제공합니다.
- 유저는 질문당 하나의 좋아요를 남길 수 있으며, 재클릭 시 좋아요 취소가 가능합니다. (토글 방식)
[문제]
-
부하 테스트 결과 좋아요 토글 기능에서 동일 유저가 여러 번 Like를 누르는 경합(Race Condition) 발생했습니다.
-
문제의 원인을 다음 코드에서 찾았습니다.
// apps/server/src/questions/questions.service.ts async toggleLike(questionId: number, createUserToken: string) { const exist = await this.questionRepository.findLike(questionId, createUserToken); if (exist) await this.questionRepository.deleteLike(exist.questionLikeId); else await this.questionRepository.createLike(questionId, createUserToken); return { liked: !exist }; }
[해결]
model QuestionLike {
questionLikeId Int @id @default(autoincrement())
questionId Int
createUserToken String
question Question @relation("QuestionLikes", fields: [questionId], references: [questionId], onDelete: Cascade)
createUserTokenEntity UserSessionToken @relation("TokenQuestionLikes", fields: [createUserToken], references: [token])
@@unique([questionId, createUserToken])
}
- DB 스키마 수정
@@unique([questionId, createUserToken])
를 추가해 중복 삽입을 DB 레벨에서 강제로 막았습니다.
async toggleLike(questionId: number, createUserToken: string) {
try {
// 1) 무조건 create를 시도
await this.questionRepository.createLike(questionId, createUserToken);
return { liked: true };
} catch (error) {
// 2) UNIQUE_CONSTRAINT_VIOLATION 에러면 이미 존재하므로 delete 수행
if (
error instanceof PrismaClientKnownRequestError &&
error.code === PRISMA_ERROR_CODE.UNIQUE_CONSTRAINT_VIOLATION
) {
await this.questionRepository.deleteLike(questionId, createUserToken);
return { liked: false };
}
throw error;
}
}
- 애플리케이션 로직 수정
- 유니크 제약이 있는 상태에서
create
를 시도하고, 에러가 나면(이미 존재) 그때delete
수행하도록 하였습니다.
- 유니크 제약이 있는 상태에서
[결과]
- 이전 부하테스트에서 발생하던 500에러 4건이 0건으로 완전히 개선되었습니다. 즉, Race Condition으로 인한 데이터 불일치 문제가 해결되었습니다.
리프레시 토큰 (PR #44)
[배경]
- 레디스의 다양한 기능을 활용해 리프레시 토큰을 관리해 보았습니다.
[문제]
- 인메모리 방식의 refresh token 관리는 다음과 같은 한계가 있었습니다:
- 서버 재시작 시 모든 토큰 정보가 손실됨
- 주기적인 cleanup 로직을 직접 구현하고 관리해야하는 부담
[해결]
- Redis를 도입하여 refresh token 저장소로 활용하였습니다:
- Redis의 TTL(Time To Live) 기능을 활용하여 토큰의 자동 만료 처리
- 토큰 저장 및 관리를 Redis에 위임하여 애플리케이션 로직 단순화
[결과]
- 서버가 재시작되더라도 사용자는 불편함없이 그대로 유지할 수 있습니다.
FE
렌더링 버그 수정 (PR #32)
사용자가 로그인 이후, 화면이 렌더링이 하지만 되지 않는 문제를 발견했습니다.
프로젝트에서 로그인 여부를 판단하는 isLogin
이라는 함수를 사용하고 있습니다. 해당 함수는 AuthStore
라는 스토어에서 액션으로 제공되고 있습니다.
사용자가 로그인 이후에 액세스 토큰 값이 변경되지만, isLogin
함수는 재생성되지 않아 해당 함수를 사용하는 코드는 리렌더링이 발생되지 않았습니다.
따라서, isLogin
함수를 사용하지 않고, 스토어에서 액세스 토큰 값을 이용한 파생 상태를 만들어 액세스 토큰 값의 변경에 렌더링이 일어나도록 수정했습니다.
const { isLogin, clearAuthInformation: clearAccessToken } = useAuthStore(
useShallow((state) => ({
- isLogin: state.isLogin,
+ isLogin: state.accessToken != null,
clearAuthInformation: state.clearAuthInformation,
})),
);
최종적으로 사용자가 로그인 이후, 로그인 여부에 따른 UI들이 전부 정상적으로 렌더링 되도록 수정되었습니다.
코드 품질 개선 (PR #36)
프로젝트가 확장되면서 코드 베이스가 복잡해지고 유지보수 비용이 증가했습니다.
특히, 깊게 중첩된 컴포넌트는 로직을 파악하기 어려울 뿐 아니라 재사용성도 떨어졌고, 복잡도가 높은 함수는 디버깅 및 기능 추가 과정에서 오류 발생 확률을 높였습니다.
- 이에 따라 개발 생산성과 품질을 동시에 개선하기 위해, 코드 전반에 걸쳐 구조적·도구적 리팩토링을 진행했습니다.
- 우선, 중복 구현되거나 불필요하게 복잡했던 모달 컴포넌트의 구조를 간소화하고, 4단계를 초과하는 컴포넌트 중첩을 모두 정리하여 가독성과 유지보수성을 높였습니다.
- 또한, 복잡도가 10을 넘거나 지나치게 긴 함수를 분리·재설계해 로직 이해도를 높이고, 수정 시 발생하는 리스크를 줄였습니다.
- ESLint 설정 파일은 기존
json
형식에서config.js
형식으로 전환하며 규칙을 수정하였고, 이 과정을 통해 약 30건의 린트 에러도 정정하여 코드 품질과 일관성을 확보했습니다.
결과적으로, 모달 컴포넌트를 비롯한 전반적인 컴포넌트 계층 구조가 단순해져 유지보수가 한결 수월해졌고, 복잡도가 높은 로직을 분리함으로써 오류 발생 가능성을 효과적으로 줄였습니다.
프로젝트 구조 개선 (PR #40)
프로젝트가 확장되는 과정에서 파일 구조가 지나치게 복잡해지고, 역할이 중복되거나 모호해지는 문제가 드러났습니다. 이를 해결하기 위해 폴더 구조를 일관된 기준으로 재정비하고, 책임이 지나치게 크거나 복잡도가 높은 폴더들을 분리·재설계함으로써 개발 효율을 높였습니다.
- 일관성 있는 구조 확립: 역할별 폴더를 재조정하여 프로젝트 전반의 구조적 일관성을 확보
- 혼동 방지: 중복 또는 모호한 폴더를 정리해 구조 파악을 용이하게 개선
- 향후 확장 대비: 기능 추가 시 폴더 구조를 명확히 유지하면서도 유연하게 수정 가능
프로젝트 내부의 파일 구조에서 다음과 같은 문제가 관찰되었습니다
- 일관성 부족: 유사한 기능임에도 서로 다른 위치에 존재하거나, 책임이 명확치 않은 폴더 구조
- 복잡도 증가: 특정 폴더나 경로에 지나치게 많은 기능이 몰려 있어 수정 시 충돌이 잦고 유지보수가 어려움
- 역할 구분 모호: 동일한 기능이나 역할이 여러 폴더에 걸쳐 분산·중복돼, 구조 파악이 어려워짐
.
├── public
├── src
│ ├── components
│ │ ├── modal
│ │ ├── my
│ │ └── qna
│ │ └── hooks
│ ├── features
│ │ ├── auth
│ │ │ └── validation
│ │ ├── modal
│ │ ├── session
│ │ │ ├── chatting
│ │ │ └── qna
│ │ ├── socket
│ │ ├── toast
│ │ └── user
│ ├── pages
│ ├── routes
│ │ └── session
│ │ └── $sessionId
│ │ └── $questionId
│ └── shared
├── test-results
└── tests
기존에 FSD(Feature-Sliced Design) 구조를 어느 정도 차용한 상태였으나, 이번 개편을 통해 FSD 구조를 더욱 엄격하고 일관되게 적용했습니다. 데이터와 행위(기능)를 분리하는 레이어를 두고, 이를 필요에 따라 상위에서 조합해 사용하는 방식을 채택했습니다. 개선된 구조는 다음과 같습니다
apps/client/src/
├── app
│ └── config
├── entities
│ └── session
│ ├── model
│ └── ui
├── features
│ ├── auth
│ │ ├── api
│ │ ├── model
│ │ └── ui
│ ├── close-question
│ │ └── api
- 생략 -
│ ├── terminate-session
│ │ ├── api
│ │ └── ui
│ └── update-session-host
│ └── api
├── pages
│ ├── home
│ │ └── ui
│ ├── my
│ │ └── ui
│ └── session
│ ├── model
│ └── ui
├── routes
│ └── session
│ └── $sessionId
│ └── $questionId
├── shared
│ ├── model
│ │ └── validation-status
│ └── ui
│ ├── InputField
│ ├── button
│ ├── modal
│ └── toast
│ ├── model
│ └── ui
└── widgets
├── chatting-list
├── header
├── question-list
│ └── ui
└── reply-list
└── ui
- 유지보수성 향상
- 기능을 기준으로 코드를 분리함으로써, 해당 기능에 필요한 로직이 한 곳에 모이도록 구조화되었습니다.
- 따라서, 문제가 발생했을 때 어디서 해결해야 하는지가 명확해져, 디버깅과 수정 작업이 훨씬 수월해질 것으로 기대합니다.
- 높은 응집도와 낮은 결합도
- 각 기능 내에서 관련된 로직만 모아두어 응집도가 높아졌으며, 다른 기능과의 의존성은 필요한 최소 수준으로 관리됩니다.
- 기능 변경이 다른 부분에 미치는 영향을 최소화하여 유지보수 부담을 줄였습니다.
- 확장성 및 규모 확장 용이
- 새 기능을 추가할 때 독립된 슬라이스를 생성하면 되므로, 기존 코드에 대한 대규모 수정 없이 기능 확장이 가능합니다.
- 기능을 나누어 개발하기에도 용이해 충돌이 줄어들고, 확장 요구 사항에 유연하게 대응할 수 있습니다.
- 예컨대, AI 기능을 새롭게 추가해야 할 때도 기존 코드를 대폭 수정할 필요 없이 손쉽게 확장할 수 있습니다.
- 코드 가독성 및 구조화 향상
- 기능 별로 코드가 명확히 분리되어 있어 설계 의도가 직관적으로 드러납니다.
- 재사용...
1.0.0 (2025. 01. 16)
Release 1.0.0 (2025. 01. 16)
프로젝트 전반적으로 성능 개선과 유지보수성과 확장 가능한 설계를 위한 작업을 주로 이뤘습니다.
BE
RBAC 도입 (PR #9) (Wiki)
[배경]
Ask-It 각 사용자에게 아래와 같은 역할을 부여합니다
-
슈퍼호스트
- Q&A 세션 페이지를 생성한 사용자
-
서브호스트
- SuperHost에 의해 host가 되어 Q&A 세션페이지의 관리(댓글 삭제 등)를 맡는 사용자
-
참가자
- 세션 페이지에 참여하여 질문과 채팅을 남기는 일반 참여자
각 참가자는 아래와 같은 권한을 행사할 수 있습니다.
권한 | 슈퍼 호스트 | 서브 호스트 | 참가자 |
---|---|---|---|
1. 세션 종료 | O | X | X |
2. 타인에게 호스트 권한 부여 | O | X | X |
3. 타인의 호스트 권한 해제 | O | X | X |
4. 질문 삭제 | O | O | X |
5. 답변 삭제 | O | O | X |
6. 질문 고정 | O | O | X |
7. 질문에 대한 답변 완료 기능 | O | O | X |
[문제]
기존의 권한 처리 로직은 IF 분기문을 통해 처리하였습니다.
예를 들어 사용자가 질문 삭제를 요청하였다면 아래와 같이 처리합니다.
async deleteQuestion(questionId: number, question: Question, { token }: BaseDto) {
const { isHost } = await this.sessionAuthRepository.findByToken(token);
if(!isHost)
throw new ForbiddenException('권한이 없습니다.');
// 이하 생략...
}
그러나 서비스의 권한정책이 다음과 같이 변한다고 가정해봅니다.
- 서브호스트가 서브서브호스트를 생성합니다.
- 질문의 삭제는 서브서브호스트만이 가능합니다.
그렇다면 다음과 같은 추가 처리가 필요합니다.
- DB 수정
- DB 수정에 따른 DB 접근 로직 변경(어떤 테이블에서 어떤 데이터를 가져올 것인가)
- 서비스 로직 변경(사용자가 서브서브호스트일때만 질문 삭제가 가능하도록)
즉, 서비스의 정책이 변경되어 역할과 권한이 수정 및 확대된다면 많은 코드가 변경되어야 하며, 이는 유지보수 및 확장에 장애가 됩니다.
[해결]
- 기존에 퍼져있는 권한 조건을 DB 스키마 변경을 통해 RBAC기반 권한 관리로 수정했습니다.
- 역할과 권한 테이블을 만든 후에 다대다 관계를 맺어줬습니다.
- 이를 통해 다음과 같은 권한 관리를 구현했습니다.
async deleteQuestion(questionId: number, question: Question, { token }: BaseDto) {
const { role } = await this.sessionAuthRepository.findByTokenWithPermissions(token);
const granted = role.permissions.some(
({ permissionId }) => permissionId === Permissions.DELETE_QUESTION,
);
if (!granted)
throw new ForbiddenException('권한이 없습니다.');
// 이하 생략...
}
[결과]
- 단순 분기문을 통한 기존의 권한 관리를 RBAC 기반의 권한 관리로 변경하였습니다.
- 차후 서비스 정책상 권한이 수정되어도 코드의 변경 없이 DB 데이터만을 변경하여 해결할 수 있습니다.
- 이를 통해 권한의 수정과 역할의 확장이 용이해지는 결과를 얻을 수 있었습니다.
FE
모달 애니메이션 추가 (PR #11)
모달을 이용한 사용자 경험이 많은 Ask-It 서비스 특성상 모달의 사용자 경험은 중요합니다.
모달이 열리고 닫히는 부분에 자연스러운 애니메이션을 추가하여 사용자 경험을 향상시켰습니다.
Lighthouse 성능 개선 (PR #19)
개선 전 | 개선 후 |
---|---|
랜딩페이지에서 초기 렌더링 부분에서 아쉬운 성능을 보이는 것을 확인했습니다.
폰트 로딩 최적화와 코드 스플리팅을 통한 초기에 다운로드 해야하는 번들 크기가 감소되어 초기 렌더링 부분에서 성능을 향상시킬 수 있었습니다.
렌더링 성능 최적화 (PR #22, PR #24)
개선 전 | 개선 후 |
---|---|
부하테스트 과정 중, 채팅창의 변화가 다른 컴포넌트의 렌더링에 영향을 미치는 것을 확인했습니다.
질문이 많아 질문 목록의 컴포넌트가 굉장히 커져있는 상태에서 새로운 채팅이 올라와 새롭게 렌더링되는 것은 사용자 경험에도 부정적인 영향을 미치는 것을 확인했습니다.
zustand
이용하여 전역 상태를 관리하고 있는 상황에서, 많은 부분에서 스토어의 특정 상태, 액션만을 구독하는 것이 아닌 객체 자체를 구독하고 있어 객체의 변경에 쉽게 영향을 받는 것을 확인할 수 있었습니다.
이를 스토어의 특정 상태를 가져오는 selector
함수를 메모이제이션하는 useShallow
라는 훅을 이용하여 렌더링을 최적화하였습니다.
- const { sessionId, sessionToken, expired, addQuestion, updateQuestion } = useSessionStore();
+ const { sessionId, sessionToken, expired, addQuestion, updateQuestion } = useSessionStore(
+ useShallow((state) => ({
+ sessionId: state.sessionId,
+ sessionToken: state.sessionToken,
+ expired: state.expired,
+ addQuestion: state.addQuestion,
+ updateQuestion: state.updateQuestion,
+ })),
+ );
현재, 질문 목록, 답변 목록, 채팅창 부분에서 굉장히 많은 요소들이 렌더링 되는 것에 대해서는 현재 아무런 처리가 되어 있지 않아 가상 스크롤과 같은 기법을 추가하여 성능 최적화를 추가적으로 진행하고자 하고 있습니다.
What's Changed
- fix(fe): modify tailwind config file by @cjeongmin in #4
- refactor: refactor test code by @yu-yj215 in #2
- fix: modify deploy.yml by @yu-yj215 in #3
- fix: Update deploy.yml by @cjeongmin in #5
- refactor(fe): add loadSessionData utility for session management by @cjeongmin in #7
- refactor(be): rbac by @wlgh1553 in #9
- feat(fe): add animation effect to modal by @cjeongmin in #11
- refactor(be): separate client, server, and socket-server apps by @yu-yj215 in #14
- fix: modify deploy.yml by @yu-yj215 in #16
- fix: modify deploy.yml by @yu-yj215 in #17
- refactor(fe): improve lighthouse performance by @cjeongmin in #19
- fix(fe): fix build error by @cjeongmin in #20
- refactor(fe): optimize state access by @cjeongmin in #22
- refactor(fe): use useShallow access for improved performance by @cjeongmin in #24
New Contributors
Full Changelog: https://github.com/boostcampwm-2024/refactor-web07-Ask-It/commits/v1.0.0