Goal

  • Segment Tree에 대해 초심자에게도 설명할 수 있도록
  • Segment Tree 장, 단점 파악
  • Segment Tree 구현

 

 

Abstract

세그먼트 트리(Segment Tree)는 주어진 배열에 대한 구간 쿼리를 효과적으로 수행하기 위한 자료 구조 입니다. 주로 배열의 구간 합, 최소값, 최대값 등을 빠르게 계산하는 데 사용됩니다. 이 구조는 특히 배열이 자주 변경되는 경우에도 효과적입니다. 

 

세그먼트 트리는 트리 구조이며, 각 노드는 배열의 일부 구간에 대한 정보를 저장합니다. 각 노드는 자식 노드들의 정보를 조합하여 부모 노드에 저장된 정보를 갱신합니다. 트리 구조이기때문에 이러한 갱신의 시간 복잡도는 O(logN) 입니다.

 

 

 

Why Learn?

보통 많은 알고리즘 문제들은 10초 이내에 작업이 수행되도록 문제를 내곤합니다. 만약, 배열의 크기인 N값이 10만이고 구간에 대한 변경 이 꾸준하게 있으며 구간 합 or 최소값 or 최대값 을 요구하는 문제의 경우 트리 구조를 사용하지 않는다면 시간복잡도는 O(N^2)인 10만 x 10만으로 1초에 1억번 연산을 한다는 가정하에 100초 정도의 시간이 걸리게 됩니다. 

 

이는 문제에서 요구하는 시간초를 넘어가기 때문에 효율성 부분에서 감점이 될 것 입니다. 이러한 문제에서 필요한 알고리즘 바로 세그먼트 트리입니다. 세그먼트 트리는 해당 문제를 O(NlogN)으로 문제를 해결하기 때문에 0.005초가 걸리게됩니다.

 

N값이 10만인 경우 100초와 0.005초의 차이가 납니다. 이는 N값이 커질 경우 더 큰 시간 차이를 보입니다. 그렇기때문에 꼭 배워야하는 알고리즘입니다.

 

 

 

Process

  1. leaf 노드부터 문제에 의도에 맞는 값을 부모노드로 최신화 시키며 트리를 구성합니다. (합, 최대값, 최소값)
  2. 최신화할 값이 있다면 해당 노드부터 루트노드까지 최신화 시켜줍니다.
  3. 합, 최대값, 최소값을 구하는 구간만큼 트리에서 가져옵니다.
  4. 2~3 번을 반복합니다.

 

 

Code (Java)

// 구간합을 최신화하며 값을 구해오는 구조
class SegmentTree{
        long tree[];	// 1.
        int treeSize;

	// 2.
        public SegmentTree(int arrSize){
            int h = (int) Math.ceil(Math.log(arrSize)/ Math.log(2));

            this.treeSize = (int) Math.pow(2,h+1);
            tree = new long[treeSize];
        }
		
        // 3.
        // node: 현재 노드 위치
        // start: 시작 인덱스, end: 끝 인덱스
        public long init(long[] arr, int node, int start,int end){
            
            if(start == end){
                return tree[node] = arr[start];
            }

            return tree[node] =
            init(arr,node*2,start,(start+ end)/2)
            + init(arr,node*2+1,(start+end)/2+1,end);
        }
		
        // 4.
        // node: 현재 노드 위치
        // start: 시작 인덱스, end: 끝 인덱스
        // idx: 구간 합을 수정하고자 하는 노드
        // dif: 수정할 값
        public void update(int node, int start, int end, int idx, long diff){
            if(idx < start || end < idx) return;

            tree[node] += diff;

            if(start != end){
                update(node*2, start, (start+end)/2, idx, diff);
                update(node*2+1, (start+end)/2+1, end, idx, diff);
            }
        }
		
        // 5.
        // node: 현재 노드 위치
        // start: 시작 인덱스, end: 끝 인덱스
        // left, right: 구간 합을 구하고자 하는 범위
        public long sum(int node, int start, int end, int left, int right){
            if(left > end || right < start){
                return 0;
            }

            if(left <= start && end <= right){
                return tree[node];
            }

            return sum(node*2, start, (start+end)/2, left, right)+
                    sum(node*2+1, (start+end)/2+1, end, left, right);
        }
    }
}
  1. 트리 구조에서 값을 최신화시킬 배열이다.
  2. 트리를 적당한 크기로 생성하는 생성자이다. 2^h >= arrSize인 h 값을 찾아야 한다. log를 씌우고 +1 한값으로 배열의 크기를 정한다. (arrSize * 4)로 해도 무방하다. 
  3. 재귀 형식이다. start=end이면 leaf 노드로 값을 채우고 아니라면 자식 노드에서 더한 값을 node에 최신화 시켜준다.
  4. 모든 노드를 돌며, idx에 해당되는 node만 값을 수정해준다.
  5. left와 right에 포함되는 노드의 값만 return 해주며 재귀형식으로 더해서 RETURN 해준다.

 

 

그림으로 이해하는 Segment Tree

이해를 위해서 그림은 링크를 참고하여 가져왔습니다.

 

init 함수

재귀 방식으로 leaf노드까지 간 뒤, 해당 노드의 값을 초기화 시키고 값을 return 해줍니다. return 된 자식 노드의 합을 현재 노드의 값으로 초기화 시켜주면서 root 노드까지 값이 초기화됩니다.

 

init 함수가 종료되면 아래와 같은 트리 형식을 배열로 값을 가지고 있다고 보면됩니다.

 

 

update 함수

만약, 4번째 값을 2에서 10으로 바꾼다고 가정합시다. 그렇다면 update 함수에서는 트리의 값을 어떻게 초기화 시킬까요?

위와 같은 작업이 이루어 집니다. idx가 start와 end 사이에 있다면 10-2만큼 값을 더해줍니다. 0~11, 0~5, 0~2(X), 3~5, 3~4, 3(X), 4, 5(X), 6~11(X) X표시 친 부분에서 함수 맨 위 조건문으로 return 되며 X표시 치지 않은 곳들은 값이 더해집니다.

 

 

sum 함수

만약, 8~11 값의 합을 구하는 상황이라면 sum 함수는 어떻게 동작할까요?

 

위와 같은 방식으로 동작합니다. 8~11인덱스에 포함되지 않은 구역은 0을 return, 8~11에 완전히 포함되어 있다면 현재 노드 값 Return, 완전히 포함되어 있디 않다면 포함된 구역만 찾기 위해 자식 노드를 탐색하여 더한 값을 return 합니다.

 

 

시간복잡도

N값이 배열의 크기고, K번 만큼의 update, sum 작업이 있다고 가정하면 O(KlogN)의 시간복잡도를 가질 것이다.

 

 

 

공간복잡도

N만큼의 int 혹은 long의 배열로 문제를 풀어내는 것보다는 많은 공간을 차지한다. 보통 N*4로 트리 배열의 크기를 사용한다고 가정한다면, N*3의 자료형만큼 더 많은 공간을 사용하게 된다.

 

 

 

장점

  • 시간이 훨씬 빠르다. 

 

 

단점

  • 구현이 복잡하다.
  • 더 많은 공간을 사용한다.

 

 

결론

더 많은 공간을 차지하긴 하지만 시간복잡도에서 많은 우위를 점할 수 있는 알고리즘이다. 적절하게 사용하면 유용한 알고리즘이다.

비트마스킹

비트마스크라고도 한다. 비트마스킹에 대한 정의가 없는 것 같은데 비트와 마스크를 따로 보고 생각하면 비트는 우리가 흔히 생각하는 0과 1로 표현하는 2진수이다. 마스크는 기관지인 입과 코를 보호하기 위해 덮는 수단?이라고 생각한다. 다른 방식으로 보면 무언가를 덮는 용도라고 볼 수도 있다. 따라서, 우리가 생각하는 정수형을 bit로 덮어서(마스크의 역할) 표현하는 것을 비트마스크 및 비트마스킹이라고 생각한다.

 

 

왜 사용해야 해?

 

첫 번째, 메모리가 효율적이다.

비트마스크는 x번째가 체크가 되어있는지? 또는 포함이 되어있는지 확인해야할 때 쓰면 메모리적으로 많은 효율을 볼 수 있다고 생각한다. 

 

만약, a0123456789z 이라는 문자열이 있고 a에서 시작하여 z까지 가는데 지나친 0-9까지의 숫자를 모두 출력하라는 문제가 있다. 문자열을 0부터 시작하여 돌면서 그냥 출력할 수도 있지만 list에 담아서 출력한다고 가정해보자. 이때, 드는 메모리는 int형인 4byte가 총 10개로 40byte가 사용된다. 해당 문제를 비트마스킹으로 사용하면 int형 1개인 4byte로 표현할 수 있다.

 

이처럼 이렇게 간단한 문제에서도 4byte와 40byte를 사용하는 것에서 차이를 얻을 수 있다. 큰 프로젝트라면 상상할 수 없는 차이를 낼 수 있을 것이다.

 

 

두 번째, 연산이 빠르다.

앞서 문제를 조금만 바꿔서 예시를 들어보자. a부터 z안에 0-9까지의 숫자가 적힌 문자열이 아래와 같이 여러개가 있다. 이때, 0-9중 2개의 숫자를 골라서 아래 문자열 중 값을 모두 표현할 수 있는 문자열의 개수를 출력하라는 문제가 있다고 가정해보자.

a4398z
a23z
a32z
a22z

그렇다면 문자열에 있는 숫자인 2,3,4,8,9 중에서 2개의 모든 조합으로 표현할 수 있는 문자열을 확인하고 그 중 최대값을 출력하면 될 것이다. 이때, 2개의 조합을 HashSet에 넣어 확인한다고 가정하자. 2,3부터 첫번째 문자열에서 4를 확인하고 3을 확인하고 9를 확인하고 8을 확인하여 첫번째 문자열에서 4번을 확인한다. 2개의 조합으로 4개의 문자열을 확인하면 총 10번을 확인해야 한다. 이것을 2,3,4,8,9에서 2개의 모든 조합을 확인한다고 하면 20번이고 여기서 10번을 곱하면 200번이 될 것이다. 하지만, 여기서 비트마스킹을 사용한다면 문자열 별로 1번씩만 확인하면 된다. 2개의 조합으로 문자열의 개수인 4번 여기서 20번을 곱하면 결과적으로 총 80번을 확인하면 된다. 해당 문제에서 200번과 80번의 차이가 발생한다. 이는 문자열에 숫자의 조합이 많으면 많을 수록 비트마스킹의 효과가 극대화될 것이다. 

 

이처럼 문제에 따라서 더 빠른 연산을 얻을 수도 있다.

 

 

그렇다면, 사용하지 말아야할 이유는 없을까?

 

첫 번째, 연산이 느리다.

의아할 수 있다. 앞서 사용해야하는 이유 중 하나가 연산이 빠르다고 했기 때문이다. 하지만, 문제에 따라서 연산이 더 느려질 수도 있다는 것을 인지해야 한다. 즉, 문제에 따라서 비트마스킹은 장점이 될 수도 단점이 될 수도 있다. 그렇기때문에 잘 이해하고 사용해야 한다. 

 

앞서 말한 사용해야할 이유의 첫 번째 예시를 생각해보자. a0123456789z 이라는 문자열이 있고 a에서 시작하여 z까지 가는데 지나친 0-9까지의 숫자를 모두 출력하라는 문제였다. 여기서 비트마스킹을 사용하면 출력 하기 전 if문을 한번 거쳐야 하고 0-9까지 모든 수를 확인해야한다. 하지만, list를 사용하는 경우 if문을 거치지 않고 0-9까지 모든 수를 확인할 필요도 없이 list에 있는 값들만 출력하면 된다. 물론, 값이 겹치지 않다는 가정하의 얘기이다. 이처럼 문제에 따라서 이점을 얻을 수 있다는 것을 생각하자.

 

두 번째, 표현값 제한이 있다. 

표현할 수 있는 가장 큰값은 32bit로 0111 1111 1111 1111 1111 1111 1111 1111(2) = 2147483647이다. 따라서 총 31개의 값까지만 체크할 수 있다고 생각하면 된다. 

 

 

어떻게 사용할까?

일단, 연산자에 대한 이해가 있어야 한다. &(and), |(or), ^(xor), ~(not), <<>>(shift)

 

예시를 들어보자. 아래와 같이 a,b 2개의 2진수가 있다.

a - 1010
b - 1100

a&b -> 1000

a|b -> 1110

a^b -> 1001

~a -> 0101

a<<1 -> 0100

 

아래는 비트를 통하여 문제를 해결해나갈 때 알아두면 좋은 방법들을 잘 정리하였길래 가져왔다. 참고하면 좋을 것 같다. 

 

 

비트마스킹 문제

위의 정리한 내용들은 아래 비트마스킹의 여러 문제들을 풀어보며 느낀점들이다. 아래 문제들을 추가할테니 풀어보고 싶다면 풀어보길 바란다.

 

참고 문서

 

전적 검색 UI를 구현했다. 일단, 20개의 전적만 구현한 상태이다. 아직 껍데기만 구현된 상태로 api를 통해 최근 전적을 가져와서 뿌려주는 구현을 해야한다.

 

 

 

+

1. 솔로랭크에서 3이 정적으로 되어있는데, 동적으로 api로 가져와보니 로마숫자로 넘어왔다. 이를 고쳐주기 위해 아래 코드를 추가했다.

<div class="tier" v-if="leagueEntryDTO && leagueEntryDTO.tier && leagueEntryDTO.rank">
       {{ leagueEntryDTO.tier }} {{ convertRomanToArabic(leagueEntryDTO.rank) }}
        convertRomanToArabic(romanNumber) {
            const romanToArabicMap = {
                'I': 1,
                'II': 2,
                'III': 3,
                'IV': 4,
                'V': 5
            };

            return romanToArabicMap[romanNumber] || NaN;
        }

 

2. 모두 대문자로 오는데 앞 1자리만 대문자로 나머지는 소문자로 변경해주었다. 해당 방식이 보기 더 편한 것 같다.

<div class="tier" v-if="leagueEntryDTO && leagueEntryDTO.tier && leagueEntryDTO.rank">
     {{ formatTier(leagueEntryDTO.tier) }} {{ convertRomanToArabic(leagueEntryDTO.rank) }}
        formatTier(tier) {
            return tier.charAt(0).toUpperCase() + tier.slice(1).toLowerCase();
        },

 

3. master, grandmaster, challenger의 경우 leagueEntryDTO.rank가 1로 표시되는데 해당 티어의 경우 1이 출력이 되지 않도록 변경해주었다.

<div class="tier" v-if="leagueEntryDTO && leagueEntryDTO.tier && leagueEntryDTO.rank">
     <template v-if="shouldShowRank(leagueEntryDTO.tier)">
          {{ formatTier(leagueEntryDTO.tier) }} {{ convertRomanToArabic(leagueEntryDTO.rank) }}
     </template>
     <template v-else>
          {{ formatTier(leagueEntryDTO.tier) }}
     </template>
</div>
        shouldShowRank(tier) {
            const lowerCaseTier = tier.toLowerCase();
            return lowerCaseTier !== 'master' && lowerCaseTier !== 'grandmaster' && lowerCaseTier !== 'challenger';
        },

 

 

남은 일정

- 전적 기록 가져오기

- 전적 갱신 구현

- DB 구현

- 캐시 구현

- 메모이제이션 확인 (기술이 적절한지 확인)

 

이제 홈에서 소환사명을 검색하면 사용자에 대한 정보를 보여주는 UI를 구현했다. (전적을 제외한)

 

사용자에 대한 점수에 대한 응답은 /lol/league/v4/entries/by-summoner/{id}로 보내야 한다. 요청을 보내면 id에 맞는 유저에 대한 정보를 반환해준다. 

 

Riot API에서 보내는 Response는 아래와 같이 Json으로 반환된다. Json 하나가 아닌 여러 List가 반환되는 모습을 볼 수 있다.

body : [{"leagueId":"b5fddebe-1f9e-4e78-b1b3-51c2eb225f65","queueType":"RANKED_SOLO_5x5","tier":"DIAMOND","rank":"III","summonerId":"1E4fn0-sxfubUx4bvU1ybhLtggI2C3-vnGl5jX9ijSzXBQ","summonerName":"Hide on bush","leaguePoints":30,"wins":6,"losses":6,"veteran":false,"inactive":false,"freshBlood":false,"hotStreak":false},{"queueType":"CHERRY","summonerId":"1E4fn0-sxfubUx4bvU1ybhLtggI2C3-vnGl5jX9ijSzXBQ","summonerName":"Hide on bush","leaguePoints":0,"wins":29,"losses":11,"veteran":false,"inactive":false,"freshBlood":true,"hotStreak":true}]

나는 해당 리스트에서 queueType이 RANKED_SOLO_5x5인 Json만 반환해주려고 한다.

 

코드는 아래와 같다.

    @GetMapping(path = "/summoners/kr/byId/{id}")
    public ResponseEntity<LeagueEntry> findUserInformationByUserId(@PathVariable(name = "id") String id) {
        try {
            String requestUrl = riotUrl + "/lol/league/v4/entries/by-summoner/" + id;
            HttpGet httpGet = new HttpGet(requestUrl);

            httpGet.addHeader("User-Agent", "Mozilla/5.0");
            httpGet.addHeader("Accept-Language", "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7");
            httpGet.addHeader("Accept-Charset", "application/x-www-form-urlencoded; charset=UTF-8");
            httpGet.addHeader("Origin", "https://developer.riotgames.com");
            httpGet.addHeader("X-Riot-Token", riotApiKey);

            CloseableHttpClient httpClient = HttpClientBuilder.create().build();
            CloseableHttpResponse response = httpClient.execute(httpGet);

            if (response.getStatusLine().getStatusCode() == 200) {
                ResponseHandler<String> handler = new BasicResponseHandler();
                String body = handler.handleResponse(response);
                log.info("body : " + body);

                JSONParser parser = new JSONParser();
                JSONArray jsonArray = (JSONArray) parser.parse(body);

                for (Object obj : jsonArray) {
                    JSONObject jsonObject = (JSONObject) obj;
                    if (jsonObject.get("queueType").equals("RANKED_SOLO_5x5")) {
                        LeagueEntry leagueEntry = new LeagueEntry(jsonObject);
                        return ResponseEntity.ok(leagueEntry);
                    }
                }

                // "queueType":"RANKED_SOLO_5x5"에 해당하는 JSON 객체가 없는 경우 404 에러를 반환합니다.
                return ResponseEntity.notFound().build();
            } else {
                log.error("response is error : " + response.getStatusLine().getStatusCode());
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
            }
        } catch (Exception e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

 

테스트

페이커 전적 검색

결과적으로 icon, 소환사 닉네임, 레벨, 랭크 icon, 점수 등 다양한 정보를 받아와 적절하게 배치하였다.

 

 

남은 일정

- 전적 기록 가져오기

- 전적기록 UI 구현

- 전적 갱신 구현

- DB 구현

- 캐시 구현

 


번외

8월 1일 오전에 Naver D2에서 온 메일에 메모이제이션이라는 기술에 대한 설명을 잠깐 봤는데, OP.GG라는 전적검색 사이트는 한 유저가 여러 소환사를 검색하기보다는 일정한 특정 소환사를 자주 검색할 것 같다는 생각이 들었다. 이것이 내가 캐시를 구현하려는 이유인데, 남은 일정을 완료한 다음 메모이제이션을 공부해본 다음 적용해보면 좋을 것 같다.

위의 홈에서 소환사명을 작성한 뒤, .GG 버튼을 누르면 해당 유저의 전적을 보여주는 페이지로 Redirect 시켜주는 기능을 작성하는 중에 문제가 생겼다. 원하는 url로 redirect가 되지 않는 문제였다.

 

아래와 같이 버튼을 생성했고

<button type="submit" @click="sendRequest">.GG</button>

버튼을 누르면 동작할 메소드를 아래와 같이 정의했습니다.

  methods: {
    sendRequest() {
      const trimmedId = this.id.replace(/\s+/g, ""); // 입력값에서 모든 공백을 제거하여 변수에 저장합니다.
      const url = `http://localhost:3000/summoners/kr/${trimmedId}`;
      
      window.location.href = url; // 현재 창의 URL을 변경합니다.
    }
  }

원하던 url이 아닌 url로 안내했고, 무엇이 잘못된 지 모르겠어서 window.open을 사용하여 아래와 같이 새로운 window 창으로 요청을 생성해보았습니다.

  methods: {
    sendRequest() {
      const url = `http://localhost:3000/summoners/kr/${this.id}`;
      
      // 요청을 보낸 후 새로운 페이지를 열기 위해 window.open을 사용합니다.
      window.open(url, "_blank");
    }
  }

window.open은 원하던 url로 창이 떴고, window.location.href가 문제라고 생각하여 검색해본 결과 링크에서 문제를 알 수 있었습니다. 

 

 

이유

버튼에서의 리디렉션 문제는 <button> 요소가 기본적으로 폼 제출을 수행하기 때문이라고 합니다. <button> 요소는 type="submit" 속성이 기본값으로 설정되어 있어 클릭 시 가장 가까운 폼을 제출하려고 시도합니다.

 

따라서, <button> 요소를 사용하면 클릭 시 폼이 제출되고 페이지가 새로고침되는 동작이 발생하게 됩니다. 이로 인해 Redirect가 원하는 대로 동작하지 않는다고 합니다. 

 

해결책으로는 <button> 요소를 사용하지 않고 일반적인 <div> 또는 <span> 요소를 사용하거나 <button> 요소의 타입을 type="button"으로 설정하여 기본 폼 제출 동작을 막을 수 있다고 합니다.

 

 

비교

<div> 또는 <span> 요소를 사용하는 방법과 "type=button"으로 설정하는 방법은 각각 장단점이 있습니다.

1. <div> 또는 <span>과 같은 다른 요소 사용하기
   - 장점:
     - 기본적으로 폼 제출 동작을 수행하지 않으므로, URL 리디렉션을 정상적으로 처리할 수 있습니다.
     - 스타일링에 더 많은 유연성을 제공하여 디자인을 자유롭게 조정할 수 있습니다.
   - 단점:
     - 시맨틱적으로는 버튼이 아니라면 적절하지 않을 수 있습니다.
     - 키보드 접근성에 약간의 문제가 발생할 수 있습니다.

2. "type=button"으로 설정하기
   - 장점:
     - 버튼 요소를 사용하면 시맨틱적으로 적절합니다.
     - 일반적인 버튼 동작을 유지하면서도 폼 제출 동작을 막을 수 있습니다.
   - 단점:
     - 스타일링에 제한이 있을 수 있습니다. 버튼 요소의 스타일은 브라우저에 의해 제어될 수 있습니다.
     - 버튼 요소의 기본 동작을 막기 위해 JavaScript 이벤트 핸들러를 사용해야 합니다.

저는 다른 style로 버튼을 만드려고 하기 때문에, 다른 요소를 사용해서 클릭이벤트를 처리하도록 하기로 했습니다.

 

해결 방법

1. button 안에있던 클릭 이벤트를 button 바깥에 있는 form으로 이동시켰습니다.

2. <form>태그에 @submit.prevent="sendRequest" 이벤트 핸들러를 추가했습니다. 이렇게 함으로써 폼이 제출될 때 sendRequest 메서드가 호출되고, 이 메서드를 통해 URL로 이동할 수 있게 했습니다.

  <form class="css-a1mmp7 e11dnomr0" @submit.prevent="sendRequest">
    <!-- 내용 생략 -->
  </form>

 

 

 

결과

결과적으로 button에 있던 클릭이벤트를 form으로 이동하니 redirect가 잘 작동했습니다.

 

vue.js에서 spring boot 서버로 요청을 보내는 방법은 아래와 같이 여러 방법이 있다.

  • fetch api
  • Axios
  • XMLHttpRequest

이 중에서 나는 Axios를 선택했다. 이유는 라이브러리 크기가 크다는 단점이 있지만 다른 라이브러리들에 비하여 사용이 간단하고 다양한 기능을 지원하기 때문이다. 즉, 장단점을 비교해보았을 때 가장 적합하다고 생각했다.

 

Axios를 사용하기 위해서는 일단 axios를 다운로드 받아야한다. 아래 명령어로 다운받을 수 있다.

npm install axios

Search.vue

<template>
    <div>
        <p>Selected ID: {{ $route.params.id }}</p>
        <p>Response: {{ response }}</p>
    </div>
</template>

<script>
import axios from 'axios';

export default {
    data() {
        return {
            response: null
        };
    },
    mounted() {
        this.fetchData();
    },
    methods: {
        fetchData() {
            const baseURL = 'http://localhost:8000/summoners/kr/';
            const url = `${baseURL}${this.$route.params.id}`; // 동적으로 URL 생성

            axios.get(url)
                .then(response => {
                    this.response = response.data;
                })
                .catch(error => {
                    console.error(error);
                });
        }
    }
};
</script>

<style>
/* ... */
</style>

axios를 사용하여 위와 같은 방식으로 spring boot로 요청을 보내고 응답을 받아올 수 있다. 하지만, 다른 ip를 가진 서버로 api를 날리기 위해서는 cors를 추가해야 한다. Spring Boot에 설정을 추가해주자.

config/CorsConfig

package gg.yj.kim.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:3000") // 허용할 도메인을 설정합니다.
                .allowedMethods("GET", "POST", "PUT", "DELETE") // 허용할 HTTP 메서드를 설정합니다.
                .allowedHeaders("*") // 허용할 요청 헤더를 설정합니다.
                .allowCredentials(true); // 인증 정보를 포함할 수 있도록 설정합니다.
    }
}

MainApplication

package gg.yj.kim;

import gg.yj.kim.config.CorsConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(CorsConfig.class)	// ADD
public class KimApplication {

	public static void main(String[] args) {
		SpringApplication.run(KimApplication.class, args);
	}

}

 

 

그렇다면 아래와 같이 vue.js에서 Spring Boot으로부터 api 응답을 받아올 수 있다.

받아온 json 값을 class화 하자. Search.vue의 코드를 아래와 같이 변경해주었다.

Search.vue

<template>
    <div>
        <p>Selected ID: {{ $route.params.id }}</p>
        <p>Response: {{ response }}</p>
        <p v-if="myData && myData.accountId">MyData.accoundId: {{ myData.accountId }}</p>
    </div>
</template>

<script>
import axios from 'axios';

export default {
    data() {
        return {
            myData: null,
            response: null
        };
    },
    mounted() {
        this.fetchData().then(() => {
            this.myData = new MyData(this.response);
        }).catch(error => {
            console.error(error);
        });
    },
    methods: {
        fetchData() {
            const baseURL = 'http://localhost:8000/summoners/kr/';
            const url = `${baseURL}${this.$route.params.id}`; // 동적으로 URL 생성

            return axios.get(url)
                .then(response => {
                    this.response = response.data;
                    this.myData = new MyData(response.data); // JSON 데이터를 MyData 클래스로 변환
                })
                .catch(error => {
                    console.error(error);
                });
        }
    }
};


// JSON 데이터를 담을 클래스 정의
export class MyData {
    constructor(data) {
        this['accountId'] = data.accountId;
        this['profileIconId'] = data.profileIconId;
        this['revisionDate'] = data.revisionDate;
        this['name'] = data.name;
        this['puuid'] = data.puuid;
        this['id'] = data.id;
        this['summonerLevel'] = data.summonerLevel;
        // 필요한 다른 속성들도 여기에 추가
    }

    // 클래스의 메서드 정의
    method1() {
        // 메서드 로직
    }

    // 필요한 다른 메서드들도 여기에 추가
}
</script>

<style>
/* ... */

p {
    color: white;
}
</style>

출력물

 

 

 

 

롤 전적 검색 OP.GG - 전적 검색, 관전, 리플레이, 챔피언 공략, 카운터, 랭킹

롤 전적, 모든 게임의 전적, 챔프 평점, KDA, 승률을 볼 수 있고 리플을 보거나 자신의 게임을 녹화를 할 수 있습니다. 지금 바로 당신의 소환사명을 검색해보세요!

www.op.gg

 해당 페이지의 개발자 도구를 보며 vue.js로 비슷하게 따라해보려고합니다.

 

op.gg home

 

 

내 vue의 구조는 App.vue에 Header.vue와 Home.vue를 추가하였다.

Home.vue의 경우 router-view를 사용해서 요청 경로에 따라서 다르게 꾸며주려고 한다. 여기서 Header에 언어를 선택하는 select 상자가 있는데, 해당 값을 App.vue로 가져와 Home.vue에 있는 값을 동적으로 변경해주려고 한다. 내가 선택한 방식은 이벤트 방식이고 방법은 아래와 같다. (이벤트 버스, provide inject 등등 여러 방식이 있었다.)

 

Header.vue

        <select id="select-language" v-model="selectedLanguage" @change="changeLanguage">
        
        methods: {
        changeLanguage() {
            // 선택된 언어에 따라 작업 수행
            if (this.selectedLanguage === 'ko') {
                // 한국어로 변경하는 작업 수행
                document.documentElement.lang = 'ko'; // 페이지의 언어 설정
            } else if (this.selectedLanguage === 'en') {
                // 영어로 변경하는 작업 수행
                document.documentElement.lang = 'en'; // 페이지의 언어 설정
            }
            this.$emit('languageChanged', this.selectedLanguage); // 선택된 언어를 이벤트로 발생
        }
    }

메소드 안에 languageChanged라는 값으로 이벤트를 발생시킨다.

App.vue

<Header @languageChanged="handleLanguageChanged"></Header>

<router-view :language="language"></router-view>

export default {
    data() {
        return {
            language: '소환사명, 소환사명, ...'
        };
    },
    methods: {
        handleLanguageChanged(language) {
            if (language === 'ko') {
                this.language = '소환사명, 소환사명, ...';
            } else if (language === 'en') {
                this.language = 'Summoner name, Summoner name, ...';
            }
        }
    }
}

App.vue 에서는 Header로 부터 값을 가져와서 이벤트가 발생할 시 router-view에 language로 변경된 값을 보내준다.

 

Home.vue

<input type="text"
                                                                                              id="searchHome"
                                                                                              name="search"
                                                                                              autocomplete="off"
                                                                                              v-bind:placeholder="language"
                                                                                              value="">
                                                                                              
export default {
    props: ['language']
};

 Home.vue에서는 받아온 값을 넣어준다.

 

이런식으로 하면 아래와 같이 변경된다.

 

 

비슷하게 구현한 home (한국어)

비슷하게 구현한 home (English)

 

header로 페이지의 언어를 변경하려고 한다. 기본으로 한국어, 서브로 영어를 사용한다. 코드는 아래와 같다.

 

 

Header.vue

<template>
    <div id = "title-toolbar">
        <select id="select-language" v-model="selectedLanguage" @change="changeLanguage">
            <option value="ko">한국어</option>
            <option value="en">English</option>
        </select>
    </div>
</template>

<script>
export default {
    name: 'Header',
    data() {
        return {
            selectedLanguage: 'ko' // 초기 언어 설정
        };
    },
    methods: {
        changeLanguage() {
            // 선택된 언어에 따라 작업 수행
            if (this.selectedLanguage == 'ko') {
                // 한국어로 변경하는 작업 수행
                alert('한국어로 변경');
            } else if (this.selectedLanguage === 'en') {
                // 영어로 변경하는 작업 수행
                alert('Change to English');
            }
        }
    }
};
</script>

<style scoped>
/* Header component styles */
</style>

 

후 App.vue에 추가해주면 된다.

App.vue

<template>
  <div id="app">
      <Header></Header>    // 추가된 부분
      <div id="content" class="content">
          <router-view></router-view>
      </div>
  </div>
</template>

<script>
import Header from './components/layout/Header.vue' // 추가된 부분

export default {
  name: 'App',
  components: {
    Header	// 추가된 부분
  }
}
</script>

 

테스트

제대로 select 박스가 생겼다.

 

English를 선택했을 때

 

한국어로 선택했을 때

 

 

이제 홈페이지의 언어를 바꿔보자.

이벤트에 따라서 다른 컴포넌트를 동적으로 바꾸는 방법은 다양하게 있다. event-bus, provide 및 inject 등등.. vue.js 3.x 버전에서는 provide 및 inject 방식을 권장한다고 한다. 하지만, 그냥 부모 컴포넌트에서 이벤트를 통해 데이터를 전달하는 방식이 더 코드가 깔끔할 것 같아서 해당 방식을 사용했다.

 

Header.vue

<template>
    <div id="title-toolbar">
        <select id="select-language" v-model="selectedLanguage" @change="changeLanguage">
            <option value="ko">한국어</option>
            <option value="en">English</option>
        </select>
    </div>
</template>

<script>
export default {
    name: 'Header',
    data() {
        return {
            selectedLanguage: 'ko' // 초기 언어 설정
        };
    },
    methods: {
        changeLanguage() {
            // 선택된 언어에 따라 작업 수행
            if (this.selectedLanguage === 'ko') {
                // 한국어로 변경하는 작업 수행
                document.documentElement.lang = 'ko'; // 페이지의 언어 설정
            } else if (this.selectedLanguage === 'en') {
                // 영어로 변경하는 작업 수행
                document.documentElement.lang = 'en'; // 페이지의 언어 설정
            }
            <!--추가된 부분-->
            this.$emit('languageChanged', this.selectedLanguage); // 선택된 언어를 이벤트로 발생
        }
    }
};
</script>

App.vue

<template>
  <div id="app">
      <!--변경된 부분-->
      <Header @languageChanged="handleLanguageChanged"></Header>
      <h1>{{ test }}</h1>
      <div id="content" class="content">
          <router-view></router-view>
      </div>
  </div>
</template>


<script>
import Header from './components/layout/Header.vue'

export default {
  name: 'App',
  components: {
    Header
  },
    <!--추가된 부분-->
    data() {
        return {
            test: '테스트' // 초기 페이지 제목
        };
    },
    methods: {
        handleLanguageChanged(language) {
            if (language === 'ko') {
                this.test = '테스트';
            } else if (language === 'en') {
                this.test = 'Test';
            }
        }
    }
}
</script>

 

 

테스트

테스트는 h1을 하나 추가해서 변경이 되는지 확인해주었다.

기본은 한국어로 '테스트'가 출력된다. english로 바꾸는 경우 'Test'로 변경이 잘 된다.

 

+ Recent posts