2023.12-2024.1월까지 진행한 FE/BE 협업 팀프로젝트 회고록
줌 회의를 통해 식재료 관리 및 레시피 추천 웹 사이트 개발 프로젝트가 결정되었다.
이번 프로젝트에서 프론트엔드 개발자가 주력해야 할 주요 과제는 3가지라고 생각했다.
첫째, JWT 토큰을 이용한 사용자 로그인 상태에 따른 라우팅 처리.
둘째, 식재료와 레시피 등록 시 이미지 업로드와 저장을 위한 이미지 처리 기능.
셋째, Server-Sent Events(SSE)를 활용한 실시간 알림 서비스.
이 외에도 편리한 UI/UX 디자인, 반응형 웹 지원, 접근성 확보 등 프론트엔드 측 전반적인 개발 과제가 있겠지만, 이 문제에서 요구하는 3가지 핵심적인 기술적 과제를 충실히 처리해나가는 것이 중요하다고 생각했다.
동료 FE개발자분과 같이 상의하여 기술스택을 선정하였다. 주요 기술스택은 아래와 같다.
React.js
TypeScript
Recoil
React-Query
Styled-Components
React-Hook-Form
이 프로젝트에서 전역 상태 관리 라이브러리를 선택하는 문제가 가장 큰 고민거리였다.
처음에는 Redux를 사용해 보려고 했지만, Redux는 보일러플레이트 코드가 많고 학습 곡선도 비교적 높았다.
6주라는 짧은 프로젝트 기간을 감안했을 때 이를 효율적으로 소화하는 것이 어려울 것으로 예상되었다.
반면 Recoil은 상대적으로 사용이 간단하고 학습 곡선이 낮다고 생각했다. 또한 보일러플레이트 코드도 적었다.
이러한 점을 고려하여 프로젝트 기간과 효율성을 높이기 위해 Recoil을 상태 관리 라이브러리로 선택하였다.
참고로 React-Hook-Form은 초기에는 사용하지 않았으나, 레시피 등록, 식재료등록, 회원가입 등 form을 많이 사용하는 프로젝트 특성을 고려하여 렌더링 최적화와 유효성 검사의 편의를 위하여 프로젝트 중간 도입하였다.
우리 팀은 레시피 작성을 단계별 스텝 추가 방식으로 구현하였다.
처음에는 forEach문을 사용하여 formData에 이미지를 순차적으로 추가했다.
recipeData.stepImage.forEach((item,index) => {
formData.append('stepImages[${index}]', item.image);
})
하지만 이 방식에서는 이미지 파일의 순서가 보장되지 않는 문제가 있었다.
예를 들어, 사용자가 특정 스텝에만 설명을 입력하고 이미지는 첨부하지 않았을 경우, 이후 서버에서 처리시 스텝 이미지가 밀리는 현상이 발생했다.
레시피는 요리 순서가 중요하므로 이미지 순서도 정확히 맞춰야 했다.
백엔드 팀과 문제를 공유하다 이미지가 없는 스텝의 경우 빈 Blob 객체를 전송하는 방식으로 문제를 해결하였다.
recipeData.stepImage.forEach((item) => {
formData.append(`stepImages`, item.image || new Blob());
});
이렇게 하면 이미지 파일이 없는 경우 빈 Blob 객체가, 있는 경우 이미지 파일 자체가 전송된다.
서버에서는 빈 Blob이 들어오면 이미지가 없는 스텝으로 판단하고 처리할 수 있게 되었다.
백엔드 개발자 분들과 문제 공유를 통해 이 같은 해결 방안을 찾을 수 있어 프로젝트에 활용하였다.
레시피 작성 프로세스에서 이미지 처리 방식을 돌아보니 한꺼번에 이미지를 모아서 벌크로 전송하는 것이 아니라,
각 스텝별로 이미지 업로드가 완료될 때마다 바로 서버에 전송하도록 구현했다면 좋았을 것 같다는 반성점이 있었다.
스텝별 실시간 업로드 방식이 이미지 순서 보장이나 중간 저장 문제 등을 방지하는 효과가 있었을 것이다.
프로젝트 초기 단계에서 이런 방식도 고려해볼 수 있었다는 생각이 든다.
앞으로 유사한 상황에서는 다양한 옵션을 미리 검토해볼 필요가 있을것 같다.
401 에러 응답을 받았을 때 무한으로 리프레쉬 토큰 요청이 발생하는 문제가 있었다.
이 문제의 근본 원인은 인터셉터 파일 내에 구현된 handleTokenError 함수에서 발생했다.
함수에서 401 에러 발생 시 axiosAuth를 통해 리프레시 토큰으로 새로운 토큰 발급을 요청 하지만, 이 요청 자체도 리프레쉬 토큰의 만료등의 이유로 401 에러를 반환할 경우
axios 내부에서 에러가 잡히면서 handleTokenError 함수가 다시 호출되는 구조였다.
이를 해결하기 위해 isRefreshing 변수를 도입하여 현재 리프레시 토큰 요청이 진행 중인지 체크하도록 수정했다.
변수의 값이 true일 때만 리프레시 토큰을 요청하도록 했기 때문에, 한 번 요청이 시작되면 두 번째 요청이 발생하지 않아
무한 반복문제를 성공적으로 해결할 수 있었다.
그때 상황을 돌이켜 보니, 리프레시 토큰 요청에서 발생하는 오류도 종류별로 구분했어야 했다는 생각이 들었다.
리프레시 토큰 자체가 만료된 경우에는 401에러가 아닌 403에러를 반환하도록 팀원들과 이야기해보았다면, 오류 처리 로직을 더 정밀하게 구현할 수 있었을 것 같았다.
401에러와 403에러를 구분하여 각각 적절한 수행 동작을 정의했다면, 리프레시 토큰 요청 시 발생하는 문제에 대한 이해와 대처가 보다 체계적으로 이루어졌을 것으로 생각된다.
앞으로 이 같은 오류 코드의 의미와 구분에 대한 고려가 필요하다는 교훈을 얻었다.
정확한 오류 처리를 통해 더 나은 사용자 경험을 제공할 수 있도록 개선 방안을 모색해볼 필요가 있다는 점에서 팀원들 간 소통과 의견 교환의 중요성도 깨달았다.
React/Vite 프로젝트를 진행하면서 동적으로 API 호출 시 CORS(Cross-Origin Resource Sharing) 오류가 발생하는 문제에 부딪혔다.
SPA의 특성상 다른 도메인의 API를 호출할 일이 많은데, 이를 해결하기 위해 Vite의 강력한 기능 중 하나인 프록시 설정을 활용했다.
//vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react-swc';
const env = loadEnv(process.env.NODE_ENV, process.cwd(), '');
// https://vitejs.dev/config/
// eslint-disable-next-line import/no-default-export
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: env.VITE_SERVER_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
우선 vite.config.ts에서 프록시 설정은 server 옵션 내부의 proxy 프로퍼티에서 정의한다.
여기서 핵심은 /api 라우트 밑의 요청들을 모두 target으로 지정한 VITE_SERVER_URL 환경변수 서버 주소로 프록시 처리한다는 것이다.
이를 통해 프론트엔드에서 /api/user 호출 시, 실제로는 VITE_SERVER_URL/user로 요청이 가도록 매핑시켜준다.
changeOrigin 옵션을 통해서 요청 헤더의 Origin을 target URL로 교체하여, 서버 입장에서는 동일 출처인 것처럼 인식하게 해준다.
이를 통해 CORS 이슈를 프록시 설정 하나로 효과적으로 해결할 수 있었다.
리액트 프로젝트에서 백엔드 개발자와 협업할 때 CORS문제로 어려움이 많았는데, 이를 vite 하나로 해결한 경험을 얻었다.
JWT 토큰을 어디에서 어떻게 관리할지는 이번 프로젝트의 중요한 문제 중 하나였다.
클라이언트의 로컬스토리지에 토큰을 저장하면 XSS 공격에 취약하다는 단점이 있었고, 쿠키에 저장하면 CSRF 공격 위험성이 높다는 문제점을 확인했다.
결국 우리 팀이 세션스토리지를 선택한 이유는 첫째, 사용자 세션마다 고유한 저장공간을 제공한다는 장점이 있었다. 둘째, 서버를 우회하지 않고도 클라이언트에서 토큰을 관리할 수 있다는 이점이 컸다.
다만 세션스토리지도 XSS나 CSRF 공격에 취약하다는 점을 명확히 인식하고 있었다. 추가적인 보안 절차를 거치고 토큰의 유효기간도 짧게 설정하는 등 보완하는 절차가 반드시 필요했다.
백엔드 팀과의 협업을 통해 쿠키에 httpOnly, secure, SameSite 옵션을 사용한다면 더욱 안전했을 것이며, 다음 프로젝트시에는 이러한 방식으로의 개선도 고려해볼 필요가 있다는 생각이 든다.
이번 팀프로젝트를 통해 많은 것을 배울 수 있었다.
첫째, 팀원들과의 협업이 프로젝트 성공에 얼마나 중요한지 실감했다. 다양한 의견을 나누고 상호 보완적으로 문제를 해결할 수 있었기 때문이다.
둘째, 문제해결에 있어 체계적이고 모든 관점을 고려하는 것이 중요하다는 것을 알 수 있었다. 직관적인 해결보다는 깊이 있는 접근이 필요하다 느꼈다.
셋째, 개발 프로세스 전반을 이해하고 관리하는 것이 얼마나 중요한지 깨달았다. 제한된 일정 속에서 계획과 일정관리를 잘할 때 효율적으로 프로젝트를 수행할 수 있었던 것 같다.
넷째, 팀원들과 원활한 소통을 통해 시너지 효과를 낼 수 있다는 것을 배웠다.
이번 프로젝트를 통해 팀워크와 문제해결력, 프로세스관리 능력을 기를 수 있었다. 앞으로 이러한 경험은 미래를 위한 소중한 자산이 될 것이라 믿는다.