답변, 받았습니다! v1.1.0 개발 회고록

한중일 개발자·2024년 1월 31일

들어가며...

답변 받았습니다! 는 북경대학교 한국인 유학생들을 위한 강의 정보공유/익명 커뮤니티 웹사이트입니다.

답변, 받았습니다! (https://honeycourses.com) 이 겨울방학을 맞이하여 새로운 업데이트로 찾아왔습니다! 금번 업데이트는 커뮤니티 기능 오픈, 백엔드 스프링 마이그레이션, 강의평가 기능 개선 등 정말 많은 변화들이 있었는데요, 프런트엔드만 놓고봐도 1월 20일 업데이트 이후 60개가 넘는 커밋들이 이루어졌습니다.

잔뜩 쌓여있는 커밋들!

재학생 개발팀인 특성 상 방학에만 개발이 가능한 우리잘했조는, 많은 기능들을 버그없이 개발해내기 위해 개발진 7명 모두- 그리고 특히 팀장인 저, 프론트/백엔드 팀장인 김혜원, 박건호 학우는 밤샘도 마지않으며 개발을 진행했습니다.

방학인데 학기중보다도 뜨거웠던 우리의 새벽..

팀장으로써, 종강 이후 약 한달간 진행한 신 버전 개발을 몇가지 주요 부분으로 나눠 회고해볼까 합니다.

Spring Boot로의 백엔드 마이그레이션

답변, 받았습니다는 최초에 Express.js를 사용한 백엔드로 개발되었습니다. 이를 AWS Amplify 서비스와 연계해서, AWS API Gateway에 올려 Lambda 서비스로 서버리스 API를 구축했었습니다. (해당 백엔드 배포 과정은 Amplify가 자동으로 진행해주었습니다) 당시 구조는 아래와 같습니다:

Amplify 서비스가 Cognito 유저 인증 풀과의 연계, 그리고 Lambda로의 백엔드 배포 관리까지 자동으로 해주었기 때문에 상당히 편리한 구조였으나, 프로젝트의 규모가 커질수록 백엔드 코드가 어수선해지는 문제가 발생하기도 하고, 결정적으로 백엔드 팀장이 백엔드 방향 취업을 결정하여 이번 방학의 첫 목표는 Spring Boot로의 백엔드 마이그레이션이 되었습니다.

백엔드 마이그레이션은 24개의 티켓으로 나누어, 팀장인 제가 강의평가 관련 엔드포인트 기능 구현, 박건호 백엔드 팀장이 커뮤니티 관련 엔드포인트 기능 구현을 맡았습니다.

Spring REST Docs 사용 문서화

백엔드 개발 담당인 저와 박건호 학우는 학기 중에도 네트워크 수업 과제로 프런트/백엔드 구현 과제를 완료한 적이 있습니다.

하지만 당시 저희 둘다 스프링으로 제대로 된 프로젝트를 해보는건 처음이었고, Node.js로는 Swagger를 사용한 문서화를 진행해본 적이 있지만 스프링으로는 상당히 번잡하다는 걸 확인하고 노션에 손으로 직접 (...) 문서화를 진행하는 방식으로 진행했었습니다. 문서화의 목적 자체는 이루었지만, 이번에는 제대로 Spring REST Doc을 사용하여 문서화를 진행해보기로 했습니다.

Spring REST Docs는 Swagger와 다르게, 테스트 코드에 문서화를 진행하여 백엔드 코드의 변경을 바로 반영할 수 있는 장점이 있어 Swagger 대신 선택할 이점은 확실했습니다. 다만 설정, 그리고 API 엔드포인트로의 배포는 다소 번거로운 점도 많았고, 시행착오도 많았습니다.

저희는 주로 해당 링크를 참고하며 초기 작업을 진행했습니다. 찾는 자료들마다 build.gradleasciidoc.adoc형태가 약간씩 달라 애를 먹었지만, 아래와 같이 구현해 냈습니다.

...

ext {
    set('snippetsDir', file("build/generated-snippets"))  // snippetsDir : 테스트 실행시 생성되는 응답을 저장할 디렉토리 지정
}

configurations {
    asciidoctorExt
}

test {
    useJUnitPlatform()
    outputs.dir snippetsDir
}

asciidoctor {
    inputs.dir snippetsDir
    configurations 'asciidoctorExt'
    dependsOn test
}

bootJar {
    dependsOn asciidoctor
    from ("${asciidoctor.outputDir}") {
        into 'static/docs'
    }
}

task copyDocument(type: Copy) { // 생성된 html 파일을 옮긴다
    dependsOn asciidoctor // Gradle의 asciidoctor Task 이후 수행
    from file("${asciidoctor.outputDir}")
    into file("src/main/resources/static/docs")
}

build {
    dependsOn copyDocument // build 이후 html 파일 복사
}

위의 build.gradle은 테스트 실행시 adoc 스니펫들이 build/generated-snippets에 저장되도록 하고, asciidoc이 생성한 html이 빌드 시 static/docs에 옮겨져, 배포 후 API 엔드포인트에서 접근 가능하게 하고 있습니다.

= 답변 받았습니다! API 명세
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
:sectnums:
:docinfo: shared-head

== 강의 관리

=== 모든 강의 조회 (GET /courses)
==== 성공
operation::courses/find/success[snippets='http-request,http-response']
...
@Test
    @DisplayName("모든 강의 조회 요청을 받으면 강의를 반환한다.")
    void getCourses() {
        Mockito.when(courseService.findAll()).thenReturn(responses);

        restDocs
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .header("Authorization", "Bearer aws-cognito-access-token")
                .when().get("/courses")
                .then().log().all()
                .apply(document("courses/find/success"))
                .statusCode(HttpStatus.OK.value());
    }

위는 저희의 index.adoc와 컨트롤러 테스트 코드 예제입니다. 해당 파일로 asciidoc이 html로 문서화를 진행하고, build.gradle에 쓴대로 static/docs에 살포시 이쁘게 포장된 API 명세 html을 넣어주는 방식입니다. 처음 적용하는게 힘들었지, 이후 테스트 코드 작성때마다 패턴이 생겨 큰 어려움은 없었습니다.

해당 링크에 방문해보면, 완성된 API 명세를 확인할 수 있습니다. SecurityConfig.javasecurityFilterChain 메서드에 .requestMatchers("/docs/index.html").permitAll()만 추가해주는것만 잊지 않아주면, 배포된 API의 /docs/index.html에서도 API 명세를 볼 수 있게 됩니다!

백엔드 CI/CD 구축 (Feat. Docker)

개인적으로, 프로젝트 시작 전에 CI/CD를 먼저 확실히 구축해 두는것을 좋아하는 편입니다. Amplify가 Express 백엔드 배포를 자동화해주었기에 필요 없었지만, 이번에 마이그레이션을 진행하며 구축하고 싶었던 워크플로우 기능들은 아래와 같습니다:

  • dev/main 브랜치로의 PR Open시 해당 브랜치에 대한 테스트 코드 실행, 테스트 모두 통과 시에만 머지 허용
  • main 브랜치로 push시, Docker 이미지 자동 생성 후 AWS EC2에 배포

구축된 해당 워크플로우들을 간략하게 설명해보겠습니다.

자동 테스트 실행 워크플로우

해당 링크를 주로 참고하여 진행하였습니다.

워크플로우는 아래와 같습니다:

name: PR Test
 
on:
  pull_request:
    branches: [ main, dev ]
 
jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      checks: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 17
        uses: actions/setup-java@v1
        with:
          java-version: '17'
      - run: touch ./src/main/resources/application.properties
      - run: echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./src/main/resources/application.properties
 
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew
 
      - name: Test with Gradle
        run: ./gradlew --info test
 
      # 테스트 후 Result를 보기위해 Publish Unit Test Results step 추가
      - name: Publish Unit Test Results
        uses: EnricoMi/publish-unit-test-result-action@v1
        if: ${{ always() }}  # 테스트가 실패하여도 Report를 보기 위해 `always`로 설정
        with:
          files: build/test-results/**/*.xml

해당 워크플로우는 main, dev에 PR이 open될 시, Github Actions를 사용하여 테스트를 진행하고, PR comment로 테스트 결과를 위 사진처럼 알려줍니다.

Docker 이미지화, EC2 자동 배포 워크플로우

해당 링크를 주로 참고해서 진행했습니다. 워크플로우는 아래와 같습니다:

# 동작 조건 설정 : main 브랜치에 push가 발생할 경우 동작한다.
on:
  push:
    branches: [ "main" ]

permissions:
  contents: read

jobs:
  # Spring Boot 애플리케이션을 빌드하여 도커허브에 푸시하는 과정
  build-docker-image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      # 1. Java 17 세팅
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - run: touch ./src/main/resources/application.properties
      - run: echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./src/main/resources/application.properties

      # 2. Spring Boot 애플리케이션 빌드
      - name: Build with Gradle
        uses: gradle/gradle-build-action@v2.4.2
        with:
          arguments: build

      # 3. Docker 이미지 빌드
      - name: docker image build
        run: docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/honeycourses-backend-spring-prod .

      # 4. DockerHub 로그인
      - name: docker login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}

      # 5. Docker Hub 이미지 푸시
      - name: docker Hub push
        run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/honeycourses-backend-spring-prod
  
  # 위 이미지를 pull해서 ec2에서 실행      
  run-docker-image-on-ec2:
    # build-docker-image (위)과정이 완료되어야 실행됩니다.
    needs: build-docker-image
    runs-on: self-hosted

    steps:
      # 1. 최신 이미지를 풀받습니다
      - name: docker pull
        run: sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/honeycourses-backend-spring-prod

      # 2. 기존의 컨테이너를 중지시킵니다
      - name: docker stop container
        run: sudo docker stop $(sudo docker ps -q) 2>/dev/null || true

      # 3. 최신 이미지를 컨테이너화하여 실행시킵니다
      - name: docker run new container
        run: sudo docker run --name github-actions-demo --rm -d -p 80:8080 ${{ secrets.DOCKERHUB_USERNAME }}/honeycourses-backend-spring-prod

      # 4. 미사용 이미지를 정리합니다
      - name: delete old docker image
        run: sudo docker system prune -f

크게 아래의 단계로 나누어 볼 수 있습니다:

  • main에 push가 있을 시, 어플리케이션을 빌드하고 Docker 이미지를 빌드합니다.
  • Docker 이미지를 지정된 계정에 push하고, Github self-hosted 설정을 마친 EC2에 연결하여 Docker 이미지를 pull하고, 실행되고 있던 컨테이너를 중지시키고 최신 이미지를 컨테이너화해서 실행합니다.
  • 미사용 이미지 (전 버전의 이미지)를 정리하여 EC2의 메모리를 최적화합니다.

AWS Cognito를 사용한 API 보안

Amplify가 자동으로 배포해주는 API Gateway는 API 보안이 매우 편리했습니다. 결국 모두 자사 내의 서비스다 보니, Cognito 유저풀을 사용한 API 보안 관련 코드는 작성할 필요가 전혀 없었습니다. Amplify-UI를 통해 프런트엔드에서 로그인을 하면 알아서 로컬 스토리지에 토큰 등 유저 인증 정보를 저장했고, API 요청시 알아서 해당 토큰을 빼내어 인증 자격을 전달했으며, API Gateway측에서 알아서 해당 토큰으로 사용자 인증+신원 확인까지 해줬으니 말이죠.

하지만 이번에 마이그레이션을 진행하면서, 아래의 숙제를 해결해야 했습니다:

  • 프런트엔드에서 jwt 토큰과 함께 API 리퀘스트를 할 시, 해당 토큰이 Cognito 유저풀 내부의 유효한 유저의 토큰이 맞는지 확인
  • 해당 토큰이 어떤 유저의 토큰인지 식별해내기

Cognito 토큰으로 유저 자격 확인

저희가 택한 해결택은 Spring Security를 사용하는 것이었습니다. (해당 PR) 인증 과정은 아래와 같습니다:

  • 프론트엔드에서 아래와 같은 코드로 유저의 jwt 토큰값을 얻어내어, API 리퀘스트 시 해당 헤더를 넣습니다.
import { Auth } from "aws-amplify";

export async function getAuthHeaders() {
   try {
    const userSession = await Auth.currentSession();
    const jwtToken = userSession.getAccessToken().getJwtToken();

    return {
      Authorization: `Bearer ${jwtToken}`,
    };
  } catch (error) {
    console.error("Error fetching authentication headers:", error);
    throw error;
  }
}
...
const headers = await getAuthHeaders();
const response = await axios.get(`${apiUrl}${endpoint}`, { headers });
  • 백엔드는 아래의 코드로 받은 토큰을 Decode합니다. 여기서 {aws.cognito.uri}는 Cognito 유저 풀의 /.well-known/jwks.json 로 끝나는 엔드포인트로, 해당 토큰의 유효성을 검증할 수 있는 엔드포인트입니다.
@Component
public class TokenProvider {

    @Value("${aws.cognito.uri}")
    private String uri;

    public JwtDecoder accessTokenDecoder() {
        return NimbusJwtDecoder.withJwkSetUri(uri).build();
    }
}
  • 이후 SecurityConfig의 해당 메서드에서 토큰의 유효성에 따라 엔드포인트의 방문을 허용합니다. .permitAll() 지정된 엔드포인트들만 인증 없이 방문 가능한데, 저희의 경우 브라우저의 OPTIONS preflight 리퀘스트, 그리고 동작 확인을 위한 루트 엔드포인트와 API 명세가 있는 /docs 엔드포인트의 방문을 풀어뒀습니다.
@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .cors().and()
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
                        authorizationManagerRequestMatcherRegistry
                                .requestMatchers(HttpMethod.OPTIONS).permitAll()
                                .requestMatchers("/", "/docs/**", "/docs/index.html").permitAll() // 권한이 필요없는 엔드 포인트, 루트와 API 명세 엔드포인트 열어둠
                                .anyRequest().authenticated())
                .oauth2ResourceServer(httpSecurityOAuth2ResourceServerConfigurer ->
                        httpSecurityOAuth2ResourceServerConfigurer.jwt(jwtConfigurer ->
                                jwtConfigurer.decoder(tokenProvider.accessTokenDecoder())))
                .httpBasic(Customizer.withDefaults())
                .build();
    }

결론


사진 출처: 링크

본래 2주정도 시간을 두고 진행하려 했던 마이그레이션이었으나, 처음부터 구축해보는 재미에 빠져버린 (...) 두명은 수없는 시행착오를 뚫어내며 1주일만에 마이그레이션을 완료하게 되었습니다. 괴물 신인들

최종적으로, 위의 구조를 가진 CI/CD 워크플로우를 구축하여, https://springapi.honeycourses.com/ 엔드포인트에 성공적으로 스프링 API를 배포하였습니다.

강의평가 기능 개선


테스트 사이트라 내용이 혼란한 상태.

강의평가 기능은 답변 받았습니다! 의 핵심입니다. 1.1.0버전 릴리즈 전에는 커뮤니티 기능이 아직 닫혀있기도 했고, 결론적으로 저희의 핵심 서비스라고 할 수 있겠습니다. 릴리즈 이후 2024년 1월 31일 기준 현재 190개가 넘는 리뷰가 달려 있을정도로 성행중인 서비스지만, 미흡한 점이 많아 방학동안 집중적으로 개선 작업을 진행했습니다. UI 개선은 주로 김혜원 학우의 탁월한 미적 센스로 이루어졌고 (이뻐졌네? 싶은건 모두 혜원이덕), 아래에는 개선된 기능들의 기술적인 부분들을 얘기해볼까 합니다.

강의평가 수정/삭제 기능


사실 CRUD의 핵심인 C밖에 없었던 지금까지의 강의평가 기능과는 다르게, 이번 버전에는 백엔드 개편과 함께 본인이 작성한 강의평가의 수정과 삭제가 가능해졌습니다. 수정 인터페이스도 별도의 컴포넌트를 만드는 대신,

const [isEditing, setIsEditing] = useState(false)
{review.mine && (
  <div>
    {editingReviewId === review.review_id ? (
      <div className={styles.editButtons}>
  ...

의 형식을 사용하여 수정 버튼이 눌리면 해당 state를 true로 전환, 본래 리뷰의 내용을 state로 전달하여 두번째 사진처럼 수정이 가능하도록 구현했습니다.

강의 목록에서 평가 수 미리 확인 기능


그동안의 강의평가 기능에선, 강의 목록만 보고는 눌러보기 전까진 몇개의 리뷰가 적혀있을지 알 방법이 없어, 눌러보고는 리뷰가 하나도 없다는 허탈한 사실만 마주하고 뒤로가기 버튼을 누르는 경우가 허다했습니다.

이번엔 해당 피드백을 인지하고, 위처럼 눌러보기 전에도 해당 강의에 몇개의 리뷰가 달려있는지 확인할 수 있도록 기능을 추가하였습니다.

[{
  "course_id" : 0,
  "course_category" : "Zhuanye",
  "course_credit" : 5,
  "course_name" : "Gaoshu0",
  "isYouguan" : 0,
  "kaikeYuanxi" : "Xinke",
  "reviewCount" : 0
}]

기술적으론 강의 목록을 받아올때 위의 형식으로 reviewCount 값을 받아올 수 있도록 백엔드에 변화를 준 것인데, 사실 원래의 강의 DB 테이블엔 강의 갯수를 알려주는 칼럼이 없어 review_count 칼럼을 추가하고, 해당 SQL 쿼리로 리뷰 갯수를 업데이트해주었습니다:

UPDATE courses
SET review_count = (SELECT COUNT(*)
                    FROM reviews
                    WHERE reviews.course_id = courses.course_id);

강의 리스트를 받아올때마다, 모든 강의들에 대해 개별로 SQL을 돌려 실시간으로 강의 수를 받아오는건 부하가 올게 분명했기에 그나마 제일 합리적인 방법으로 해결했다고 생각합니다. 이후 review_count 칼럼의 변화는 백엔드에서 새로운 리뷰가 추가/삭제 될 시에 이루어지니 문제가 해결됩니다!

커뮤니티 기능 오픈


테스트를 겸해 커뮤니티에서 지적인 대화를 나누고 있는 개발진들

커뮤니티 기능이 베타 테스트를 거쳐서 이번에 정식 오픈했습니다! 일반 커뮤니티들처럼, 글 작성/수정/삭제, 댓글/대댓글 작성/수정/삭제가 가능한 매우 평범한 기능의 커뮤니티지만, 개발진이 가장 공들이고 시간을 많이 들인 부분이기도 합니다. 쉬워보이지만 어려워요 진짜 ㅠㅠ

커뮤니티 기능에서 기술적으로 회고해볼 부분들을 요약해보겠습니다.

페이지네이션 도입


개발진 개인의 의견일 뿐입니다. 근데 민초 드실거면 차라리 치약 드세요.

강의평가 기능을 보면 알겠지만, 수업 목록을 불러올때 페이지로 나누지 않고 하나의 json으로 받아오고 있습니다. 하지만 커뮤니티는 글들이 훨씬 많아질거기 때문에 페이지네이션 도입이 필요했습니다. 해당 작업이 프론트에서만 하고 끝낼거면 좋겠지만, 백엔드에서도 개선이 이루어져야 하는 작업이기에 꽤나 시간을 들여 사진처럼 도입에 성공했습니다.

기존과 다르게, 게시글 목록을 /community?page={pageNo} 엔드포인트에 요청시 백엔드가 아래같은 응답을 보내도록 설정했습니다:

{
  "posts": [기존 게시글 응답 객체],  // 기본 값으로 10개이며, 해당 페이지의 게시글 개수가 10개 미만이면, 해당하는 개수만큼 들어있어요.
  "totalPageCount": 3,  // 총 페이지 개수
  "totalPostCount": 23  // 총 게시글 개수
}

해당 데이터를 사용해서, 프런트엔드 단에서는 react-js-pagination을 사용하여 페이지네이션을 구현했습니다.

Paging.tsx 파일은 아래와 같습니다:

import React from "react";
import "./Paging.css";
import Pagination from "react-js-pagination";

interface PagingProps {
  page: number;
  count: number;
  setPage: (page: number) => void;
}

const Paging: React.FC<PagingProps> = ({ page, count, setPage }) => {
  return (
    <Pagination
      activePage={page}
      itemsCountPerPage={10} // 페이지당 10개의 글
      totalItemsCount={count} // 총 글 갯수, API가 반환해줌
      pageRangeDisplayed={5}
      prevPageText={"‹"}
      nextPageText={"›"}
      onChange={setPage}
    />
  );
};

export default Paging;

해당 컴포넌트는 현재 페이지, 총 게시글 갯수, onChange에 반영할 setPage 함수를 인자로 받습니다. 이 컴포넌트를 활용해서 아래와 같이 게시글 페이지네이션을 구현했습니다:

const [currentPage, setCurrentPage] = useState(1);

const handlePageChange = (pageNumber: number) => {
    setCurrentPage(pageNumber);
};
...
const response = await apiGet(`/community${categoryPath}?page=${pageNo - 1}`); // 백엔드측에서 0부터 페이지 카운트 시작
...
<Paging
  page={currentPage}
  count={totalItemsCount}
  setPage={handlePageChange}
/>

생각보다 간단했습니다! useEffect를 사용하여 페이지가 바뀔때마다 API 리퀘스트가 제대로 들어가게만 해주면, 페이지별로 글을 받아올 수 있게 됩니다.

그외 개선점들

강의평가, 커뮤니티 글/댓글 좋아요 로직 개편

사실 전 버전까지만 해도 강의평가의 좋아요 여부는 브라우저의 로컬 스토리지를 사용해서 이루어졌습니다. 하지만 이는 당연히 다른 기기를 사용하거나, 브라우저 캐시를 지우기만 해도 다시 좋아요를 누를 수 있게 되는 등 문제가 많은 방식이어서, 금번엔 백엔드 차원에서 좋아요 로직을 개편하였습니다.

업데이트 이후 강의평가 response는 아래와 같습니다:

{
  "review_id" : 1,
  "course_id" : 1,
  "review_content" : "Test Review 1",
  "review_title" : "Title1",
  "instructor_name" : "Jiaoshou",
  "taken_semyr" : "22-23",
  "grade" : "60",
  "like_count" : 0,
  "review_time" : "2024-01-29T14:57:13.239973274",
  "liked" : false,
  "updated" : false,
  "mine" : false
}

이젠 유저가 특정 리뷰에 좋아요를 누르면, 백엔드측은 리퀘스트를 받고 DB의 reviewLikes테이블에 아래처럼 데이터를 저장합니다:

이후 강의평가 반환시, 백엔드는 reviewLikeRepository.findByReviewAndUsername(review, authInfo.getUsername());를 통해 리퀘스트를 보낸 유저가 해당 리뷰에 좋아요를 누른 기록이 있는지 확인한 후 liked 필드의 값을 반영합니다.

프런트엔드 API 콜 코드 리팩토링

(해당 PR 링크)
프런트엔드 컴포넌트들중 API와 통신이 필요한 코드들은 기본적으로 현재 유저의 Access Token을 받아서 header에 넣는 아래와 같은 코드들이 반복적으로 등장했습니다:

import axios from "axios"; // API 콜을 하기 위한 라이브러리 import
import { Auth } from "aws-amplify"; // 유저 로그인 정보를 얻어오기 위한 import

const apiUrl = process.env.REACT_APP_API_URL; // API 주소, .env에서 설정한 값

const userSession = await Auth.currentSession();
const jwtToken = userSession.getAccessToken().getJwtToken();
const headers = {
        Authorization: `Bearer ${jwtToken}`,
};
// 이상은 토큰을 받는 과정

하지만 이제 프로젝트 규모가 커져서 이런 코드가 필요한 컴포넌트가 한두개가 아니고, 매 컴포넌트를 새로 쓸때마다 이 코드를 다시 쓰니 컴포넌트마다 같은 일을 하는 코드들이 형식도 미묘하게 다르게 반복적으로 쓰이고 있었습니다. 이를 해결하기 위해 APIHandler.tsx를 만들어 아래와 같이 리팩토링을 진행했습니다:

import axios from "axios";
import { Auth } from "aws-amplify";

const apiUrl = process.env.REACT_APP_API_URL;

export async function getAuthHeaders() {
  try {
    const userSession = await Auth.currentSession();
    const jwtToken = userSession.getAccessToken().getJwtToken();

    return {
      Authorization: `Bearer ${jwtToken}`,
    };
  } catch (error) {
    console.error("Error fetching authentication headers:", error);
    throw error;
  }
}

export async function apiGet(endpoint: string) {
  try {
    const headers = await getAuthHeaders();
    const response = await axios.get(`${apiUrl}${endpoint}`, {
      headers,
    });
    return response;
  } catch (error) {
    throw error;
  }
}

export async function apiPost(endpoint: string, data: any) {
  try {
    const headers = await getAuthHeaders();
    const response = await axios.post(`${apiUrl}${endpoint}`, data, {
      headers,
    });
    return response;
  } catch (error) {
    throw error;
  }
}

보이듯 우선 중복적으로 import 되고있던 axios와 amplify의 auth를 데려왔고, apiUrl도 데려왔습니다. getAuthHeaders는 현재 사용자의 토큰을 불러오고, apiGet과 apiPost는 이를 사용해 토큰을 header에 넣어 리퀘스트해줍니다.

해당 리팩토링을 통해, 아래와 같은 코드를:

import axios from "axios";
import { Auth } from "aws-amplify";

const apiUrl = process.env.REACT_APP_API_URL;

const userSession = await Auth.currentSession();
const jwtToken = userSession.getAccessToken().getJwtToken();

const headers = {
      Authorization: `Bearer ${jwtToken}`,
};

const data = {
      review_title,
      instructor_name,
      taken_semyr,
      review_content,
      grade,
};
 axios.post(`${apiUrl}/courses/${courseId}/reviews`, data, { headers })
        .then((response) => {
          console.log(response.data);
          alert("리뷰 등록에 성공했습니다!");
       }
...

아래와 같이 간략하게 쓸 수 있게 되었습니다.

import { apiPost } from "../API/APIHandler";

const data = {
        review_title,
        instructor_name,
        taken_semyr,
        review_content,
        grade,
      };

      await apiPost(`/courses/${courseId}/reviews`, data)
        .then((response) => {
          console.log(response.data);
          alert("리뷰 등록에 성공했습니다!");
        }
...

성적 조회 웹사이트 오픈


https://scores.honeycourses.com 을 통해 성적 조회를 할 수 있게 되었습니다. 사실 이 웹사이트는 제가 만든건 아니고, 해당 링크의 저희과 중국 친구가 원래 만들어둔 오픈소스 프로젝트를 가져와 한국어로 번역 및 스타일링한 작품입니다.

해당 웹서비스는 유저의 북경대학교 학번/비밀번호를 받아 iaaa.pku.edu.cn (북경대학교 포탈 인증 서버)로 보내 토큰을 받고, 이를 통해 성적을 얻어내는 방식으로 작동합니다. Angular를 사용해서 개발되었기에 추후에 시간이 된다면 React로 이식해보면 좋을 것 같습니다.

profile
한국에서 태어나고, 중국 베이징에서 대학을 졸업하여, 일본 도쿄에서 개발자로 일하고 있습니다. 유창한 한국어, 영어, 중국어, 일본어와 약간의 자바스크립트를 구사합니다.

0개의 댓글