Post

TIL(20240801) [프론트와 백엔드 렌더링 과정에서 발생한 트러블 슈팅]

TIL(20240801) [프론트와 백엔드 렌더링 과정에서 발생한 트러블 슈팅]

🔒 문제발생 : 프론트 연결시 토큰오류

현재 로컬스토리지 쪽에 토큰이 저장되고 있다.

백엔드쪽에서 아래 에러 발생

1
Token Error: Illegal base64url character: ' '

해당오류는 Base64 URL 인코딩 토큰을 디코딩하는 데 문제가 있으며 잘못된 문자, 특히 공백 ‘ ‘이 발견되었음을 나타내는 오류라고 한다.

그래서 토큰이 잘못 인코딩되었거나 손상되어 공백이나 기타 유효하지 않은 문자가 포함되었기에 발생하는 문제라고 생각해서 프론트 쪽에서 토큰의 공백을 제외하고 잘라서 가져오도록 수행

1
2
3
4
getToken() {
      const tokenWithPrefix = localStorage.getItem('accessToken')?.trim();
      return tokenWithPrefix ? tokenWithPrefix.replace('Bearer ', '').trim() : '';
    },

vue.js store/index.js

1
2
3
4
5
6
7
8
9
async fetchToken({commit}) {
      try {
        const token = localStorage.getItem('accessToken');
        commit('setAccessToken', token);
      } catch (error) {
        console.error('Failed to fetch token:', error);
        throw error;
      }
    }

🔒 문제발생 : 프론트쪽에서 파일 업로드시 타입이 맞지 않아 발생하는 상황

백엔드에서 아래 에러 발생

1
 Content-Type 'application/octet-stream' is not supported
  • application/octet-stream: 바이너리 데이터를 나타내는 데 사용되는 일반 MIME 으로 파일이나 데이터의 유형을 알 수 없거나 명시적으로 지정하지 않은 경우에 자주 사용됨

두 가지 원인으로 예측

  1. 요청에서 지원되지 않는 콘텐츠 유형: 특정 콘텐츠 유형(예: application/json 또는 application/x-www-form-urlencoded)을 기대하는 엔드포인트에 요청을 보내지만 Content-Type 헤더를 application/octet-stream, 서버가 요청을 적절하게 처리하는 방법을 알지 못할 수 있다.

  2. 클라이언트측 문제: 클라이언트가 요청에 대해 ‘Content-Type’을 잘못 지정했을 경우, 파일을 업로드하거나 바이너리 데이터를 전송 중이지만 엔드포인트가 다른 형식을 요구하는 경우 이런 일이 발생할 수 있음

1
2
3
4
5
6
7
8
9
10
11
 @PostMapping(value = "/challenges/{challengeId}/verifications")
  public ResponseEntity<CommonResponseDto<VerificationResponseDto>> requestVerification(
	  @PathVariable("challengeId") Long challengeId,
	  @RequestPart(value="image") MultipartFile image,
	  @RequestPart("request") VerificationRequestDto requestDto,
	  @AuthenticationPrincipal UserDetailsImpl userDetails
  ) {
	VerificationResponseDto verification = verificationService.requestVerification(challengeId, image, requestDto, userDetails.getUser());
	return ResponseEntity.ok().body(new CommonResponseDto<>
		(HttpStatus.OK.value(), "챌린지 인증 요청 성공", verification));
  }

vue.js 이전 코드

1
2
3
4
5
6
7
8
9
10
11
 let formData = new FormData();
      formData.append('image', this.verificationImage);
      formData.append('request', JSON.stringify({
        title: this.verificationTitle,
        content: this.verificationContent
      }));
      fetch(`http://localhost:8080/api/challenges/${this.challengeId}/verifications`, {
        method: 'POST',
        body: formData,
        headers: {
          'Authorization': `Bearer ${token}`

수정한 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try {
        let formData = new FormData();
        formData.append('image', this.verificationImage);
        const requestDto = {
          title: this.verificationTitle,
          content: this.verificationContent
        };
        formData.append('request', new Blob([JSON.stringify(requestDto)], { type: 'application/json' }));
        const response = await fetch(`http://localhost:8080/api/challenges/${this.challengeId}/verifications`, {
          method: 'POST',
          body: formData,
          headers: {
            'Authorization': `Bearer ${this.token}`
          }
        });

해당 오류는 2번에 해당하는 경우 같았다. Content-Type을 적절하게 설정하거나 요청 본문을 올바르게 포맷하지 않았기 때문에 발생한 문제.

requestPart(FormDate)로 같이 보내기 때문에 서버쪽에서 구별을 못하고 있다고 생각했다.

FormData를 사용하여 파일과 JSON 데이터를 동시에 전송할 때 Blob을 활용하면 서버가 JSON 형식을 인식할 수 있다는 점을 알게되어

1
formData.append('request', new Blob([JSON.stringify(requestDto)], { type: 'application/json' }));

type: ‘application/json’ 명시해주고 Blob로 한번 더 감싸주었다.

**‘FormData’를 사용하여 데이터를 보낼 때 데이터의 콘텐츠 유형은 ‘FormData’ API 자체에서 관리됩니다. JSON 데이터를 문자열로 직접 포함하는 경우 해당 부분의 Content-Type은 기본적으로 application/octet-stream으로 설정될 수 있으며, 서버가 이를 올바르게 처리하도록 구성되지 않았을 수 있습니다.

🔒 문제발생 : 프론트 어떤 에러인지 알 수 없는 상황 발생

에러코드에 따라 프론트에서 alert창이 다르게 보이는 부분이 필요했다. 콘솔창에는 해당 에러를 알 수 있지만 유저 쪽에서는 어떤에러가 발생했는지 프론트쪽에서 처리를 해주지 않으니 알 수 없기에

1
2
3
4
5
6
7
8
9
10
11
12
13
14
catch (error) {
        if (error.response) {
          const status = error.response.status;
          if (status === 409) {
            alert('이미 신청하신 챌린지입니다.');
          } else if (status === 400) {
            alert('마감된 챌린지로 참여할 수 없습니다.');
          } else {
            alert('알 수 없는 오류가 발생했습니다. 나중에 다시 시도해 주세요.');
          }
        } else {
          alert('서버와의 연결에 문제가 발생했습니다.');
        }
      }

백엔드에서 예외처리한 에러코드에 따라 다르게 정의하였다.

🔒 문제발생 : 프론트에서 페이지 처리 안되는 상황 발생

컨트롤러 쪽

1
2
3
4
5
6
7
8
9
 @GetMapping
  public ResponseEntity<CommonResponseDto<List<ChallengeSummaryResponseDto>>> getAllChallenge(
	  @RequestParam(value = "page", defaultValue = "1") int page
  ) {
	List<ChallengeSummaryResponseDto> challengeList = challengeService.getAllChallenges(
		page - 1);
	return ResponseEntity.ok().body(new CommonResponseDto<>
		(HttpStatus.OK.value(), "챌린지 전체 조회 성공", challengeList));
  }

레포지토리 임플쪽

1
2
3
4
5
6
7
8
9
10
11
12
13
 public List<ChallengeSummaryResponseDto> getChallengeList(int page) {
	QChallenge challenge = QChallenge.challenge;
	Pageable pageable = PageRequest.of(page, PAGE_SIZE);

	List<ChallengeSummaryResponseDto> challengeList = queryFactory
		.select(Projections.constructor(ChallengeSummaryResponseDto.class, challenge))
		.from(challenge)
		.orderBy(challenge.createdAt.desc())
		.offset(pageable.getOffset())
		.limit(pageable.getPageSize())
		.fetch();
	return challengeList;
  }

도대체 나는 무엇을 페이지 처리하였는가…? 이 코드를 막상 프론트랑 연결하려고 하니 프론트가 당연히 알 수가 없지 .. 해당 데이터가 얼마인지 알 수가 없는딩… 구지 QueryDSL로 한 이유도 지금 생각해보니 왜그랬는가 싶다..전체조회면 findAll로 가져와도 될텐데,

QueryDSL이 직관적으로 어떻게 데이터를 가져올지에 대해서 잘 보이기 때문에 익숙해져 그런거 같다. 하지만 JPA쿼리랑 상황에 맞게 적절히 잘 사용하도록 노력해야겠다.

현재 코드를 리팩토링 하니 N+1문제는 발생하지 않는 것 같다 하나의 쿼리문만 나가는 상태

image image

그런데 가만히 밑에 들여다보니 이 것이 무엇인고..?

1
Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!

PageImpl 인스턴스를 있는 그대로 직렬화하는 것은 지원되지 않습니다. 즉, 결과 JSON 구조의 안정성이 보장되지 않습니다! 라는 경고 메세지…..ㅎㅎㅎㅎㅎㅎㅎ……………….

리팩토링했던 코드(service)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional(readOnly = true)
    public Page<ChallengeSummaryResponseDto> getAllChallenges(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        Page<Challenge> challengeList = challengeRepository.findAll(pageable);
        if (challengeList.isEmpty()) {
            throw new GlobalException(ErrorCode.NOT_FOUND_CHALLENGE);
        }
        List<ChallengeSummaryResponseDto> dtoList = new ArrayList<>();

        for (Challenge challenge : challengeList) {
            ChallengeSummaryResponseDto dto = new ChallengeSummaryResponseDto(challenge);
            dtoList.add(dto);
        }
        return new PageImpl<>(dtoList, pageable, challengeList.getTotalElements());
    }

다시 리팩토링한 코드…😂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Transactional(readOnly = true)
    public PaginationResponse<ChallengeSummaryResponseDto> getAllChallenges(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        Page<Challenge> challenges = challengeRepository.findAll(pageable);
        if (challenges.isEmpty()) {
            throw new GlobalException(ErrorCode.NOT_FOUND_CHALLENGE);
        }
        List<ChallengeSummaryResponseDto> challengeList = challenges.getContent().stream()
            .map(ChallengeSummaryResponseDto::new)
            .toList();

        return new PaginationResponse<>(
            challengeList,
            challenges.getTotalPages(),
            challenges.getTotalPages(),
            challenges.getNumber(),
            challenges.getSize()
        );
    }

image

경고메세지 사라졌다…하하

  • challengePage.getTotalPages(): (총 페이지 수)
  • challengePage.getTotalElements(): (총 레코드 수)
  • challengePage.getNumber(): (현재 페이지 번호)
  • challengePage.getSize(): (페이지당 레코드 수)

Third-party cookie will be blocked in future Chrome versions as part of Privacy Sandbox.

chrome://settings/trackingProtection

image

This post is licensed under CC BY 4.0 by the author.