사실 JSX는 실제로 실행될 때 javascript코드로 바뀌어서 실행된다. 그러면 당연히 JSX에 javascript코드를 넣을 수도 있다.

다음과 같은 코드가 있다고 하자.

import ReactDOM from 'react-dom';

ReactDOM.render(
  <>
    <h1>안녕 리액트!</h1>
  </>,
  document.getElementById('root')
);

여기서 리액트란 단어를 name이라는 javascript 변수로 바꾸려고 한다. 그러면

import ReactDOM from 'react-dom';

const name = "리액트";

ReactDOM.render(
  <>
    <h1>안녕 {name}!</h1>
  </>,
  document.getElementById('root')
);

이렇게 중괄호를 이용해 name이란 javascript변수를 JSX 문법에 사용가능하다. Django에서 템플릿 변수를 사용하는 것과 비슷.

 

사실 이 중괄호엔 변수 뿐만 아니라 javascript로 쓰인 표현식은 모두 사용가능하다. 예를 들어, 문자열의 문자들을 모두 대문자로 만드는 toUpperCase()메소드도 중괄호 안에서 사용가능하다.

import ReactDOM from 'react-dom';

const name = "reactplease";

ReactDOM.render(
  <>
    <h1>안녕 {name.toUpperCase()}!</h1>
  </>,
  document.getElementById('root')
);

React에선 JSX문법을 사용하므로, 이벤트 핸들러를 등록할 때 addEventListener보단 요소의 속성값으로 이벤트 핸들러를 등록할 때가 많다. 이렇게 특정 태그의 속성값으로도 중괄호를 활용해 javascript를 사용가능하다.

import ReactDOM from 'react-dom';

const name = "reactplease";
function func(){
  alert("s");
}

ReactDOM.render(
  <>
    <h1>안녕 {name.toUpperCase()}!</h1>
    <button onClick={func}>버튼</button>
  </>,
  document.getElementById('root')
);

참고로 중괄호안에서는 javascript 표현식만 사용가능하므로, if문, for문, 함수선언등과 같은 것은 할 수 없다.

JSX란, React.js에서 javascipt를 통해 HTML을 작성할 수 있는 javascript의 확장된 문법이다.

일반적인 HTML태그들과 똑같이 쓸 수 있고, id속성도 똑같이 줄 수 있는데 몇 가지 주의사항이 있다.

HTML태그의 class속성, for속성(label태그등에서) 을 줄 때는 각각 className, htmlFor로 써야 한다는 것이다. 이는 javascript내에서 이미 class, for라는 키워드가 존재하기 때문이다. 그리고

onclick과 같은 event handler를 줄 때도 onClick처럼 camel case로 적어줘야 한다! 두 번째 단어부터는 시작알파벳을 대문자로 적으면 된다는 것임.

 

두 번째 주의사항은, JXS문법으로 HTML을 작성할 때는 반드시 하나로 감싸진 태그를 작성해야 한다는 것이다. 예를 들어,

import ReactDOM from 'react-dom';

ReactDOM.render(
  <h1>안녕 리액트!</h1>
  <h2>안녕안녕 리액트</h2>,
  document.getElementById('root')
);

이런 식으로 쓰면 안 된다. vscode에서는 저렇게 쓰면 빨간 줄이 생기면서 오류라고 알려준다. JSX요소들은 반드시 하나로 감싸줘야 하기 때문! 이렇게 하면 해결할 수 있게 된다.

import ReactDOM from 'react-dom';

ReactDOM.render(
  <div>
    <h1>안녕 리액트!</h1>
    <h2>안녕안녕 리액트</h2>
  </div>,
  document.getElementById('root')
);

npm start를 하고 페이지 검사를 해주면, root div에게 div가 하나 더 생기고 그 밑에 h1, h2태그가 있는 구조를 확인할 수 있다. 근데 이렇게 div태그로 감싸주는 게 싫다면, React에서 제공하는 Fragment를 이용할 수 있다. 아래처럼 하면 된다.

import { Fragment } from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <Fragment>
    <h1>안녕 리액트!</h1>
    <h2>안녕안녕 리액트</h2>
  </Fragment>,
  document.getElementById('root')
);

이렇게 하면 root div 밑에 div가 하나 생기고 그 밑에 h1, h2태그가 있는 게 아니라 root div 바로 밑에 h1, h2태그가 생긴다. 참고로 import 문을 굳이 쓴 다음에 Fragment를 할 필요 없고, 그냥 JSX입력할 때 Fragment자동완성되게 하면 알아서 import해준다. 참 편하다. 암튼 이렇게 Fragment를 잘 사용하면 불필요하게 div태그같은 게 생기는걸 방지할 수 있다. 

 

근데 솔직히 이렇게 맨날 Fragment써주는 것도 귀찮으니 다음과 같이 축약형으로 쓸 수 있다.

import ReactDOM from 'react-dom';

ReactDOM.render(
  <>
    <h1>안녕 리액트!</h1>
    <h2>안녕안녕 리액트</h2>
  </>,
  document.getElementById('root')
);

index.js는 index.html파일이 열리고 나서 실행되는 파일로, React 코드들 중에서 가장 먼저 실행되는 친구라고 보면 된다. (참고 : index.html은 웹브라우저에서 가장 먼저 실행되는 파일이라 생각하면 됨)

import ReactDOM from 'react-dom';

ReactDOM.render(
  <h1>안녕 리액트!</h1>,
  document.getElementById('root')
);

 코드를 보면 react-dom이란 패키지로부터 디폴트로 export하는 객체를 ReactDOM이란 이름으로 import해서 그 객체의 render라는 메소드를 실행하고 있음을 볼 수 있다. render란 '화면을 그린다'라는 뜻인데, React에선 이 render메소드로 HTML태그를 만들어준다. 독특한 점은 render메소드의 첫 번째 파라미터로 문자열이 아니라 HTML태그를 그대로 작성했단 점인데, 사실 이것은 React에서 지원하는 JSX라는 문법이다. 여기선 일단 이것이 React에서 지원하는 특별한 문법이라고 생각하고 넘어가자.

 

 이어서 코드를 보면, getElementByID를 동해 root라는 아이디를 가진 요소를 파라미터로 넘기는 것을 볼 수 있다. 실제로 render메소드가 실행되면 첫 번째 파라미터를 통해서 HTML요소를 만들고, 이를 두 번째 파라미터에 집어넣는 방식으로 동작한다. index.html의 body태그를 보면 root라는 아이디값을 갖는 div태그를 볼 수 있는데, 바로 여기에 JSX를 통해 만들어진 HTML요소들이 들어가는 것이다. 

 

 

 

정리하자면,

 

1. 우선 React가 실행되면 브라우저가 index.html을 연다

2. index.js를 실행

3. index.js의 render함수가 실행되면서 첫 번째 파라미터(JSX로 작성된 것)을 HTML요소로 바꿔서 두 번째 파라미터에 넣음

 

보통 react의 render메소드는 index.js에서 한 번만 실행되므로 굳이 사용법을 외울 필욘 없다.

 

그래프같은 걸 탐색할 때, 너비를 우선으로 탐색을 진행하는 것을 너비우선탐색, 영어로는 Breadth First Search라고 하며 줄여서 BFS라고도 부른다. 어떠한 시작점이 있을 때, 그로부터 가까운 것들부터 탐색해나가는 방법으로 맹목적인 탐색을 할 때 사용가능한 방법이다. 이 녀석은 '최단 경로'를 찾아주기 때문에 최단길이를 보장해야 할 때 많이 쓰인다.

 

예시)

출처 : wikimedia commons

이 방법을 구현하려면, 큐(queue)가 필요하다.

더보기

Q . 왜 큐가 필요한가?

 

A. 큐가 선입선출 구조이기 때문에 너비 우선 탐색이 가능하도록 돕기 때문이다.

위 그림을 통해 설명하자면,

 

1) 시작점인 1을 큐에 넣는다.

2) 1을 큐에서 빼고, 1을 방문한 지점이라 처리해준 다음 1과 연결된 2, 3, 4를 큐에 넣는다.

3) 먼저 들어온 2를 큐에서 빼며 방문한 지점이라 처리해주고, 연결된 요소 중 방문처리가 안된 5를 큐에 넣어준다.

4) 가장 앞에 있는 3을 큐에서 빼며 방문한 지점이라 처리해주고, 연결된 요소 중 방문처리가 안된 6, 7을 큐에 넣어준다.

...반복

그럼 이걸 파이썬으로 구현해보자.

 


 

우선 그래프는 다음과 같이 딕셔너리 형태로 표현해봤다.

graph = {
    1 : [2, 3, 4],
    2 : [1, 3],
    3 : [1, 2, 4, 5],
    4 : [1, 3],
    5 : [3, 6],
    6 : [5]
}           # 키에 연결된 value에 있는 리스트가 그 key에 연결된 애들임

그리고 BFS를 도와줄 큐는 다음과 같이 해보자.

queue = []                       # 큐를 리스트 형태로 구현

queue.append(something)          # 큐에 요소를 넣는 동작 : append  ->큐의 뒤쪽에 추가
queue.pop(0)                     # 큐에 요소를 빼는 동작 : pop(0)  ->큐의 앞쪽에 있는 걸 뺌

 그럼 BFS를 다음과 같이 만들 수 있다.

def BFS(graph, start_node):
    visited = []                           # 방문한 요소들이 이 리스트에 저장됨
    queue = [start_node]                   # 큐

    while queue:                           # 큐에 뭔가 있는 동안 이하 내용을 반복한다
        node = queue.pop(0)                # 큐의 제일 앞에 있는 놈을 빼와서
        if node not in visited:            # 만약 이 놈이 방문한 적이 없다면
            visited.append(node)           # 방문처리
            print(node)                    # 그 녀석 출력
            queue.extend(graph[node])      # 그 녀석과 연결된 요소들(graph[node])를 queue에 추가
더보기

※ 참고

어떤 리스트(A라고 가정)의 append메소드의 파라미터로 리스트를 넘기면 리스트 자체가 A리스트의 마지막 요소로 추가되지만, extend메소드의 파라미터로 리스트를 넘기면 그 리스트 자체가 A리스트에 합쳐진다. 

 

ex) A = [1, 2, 3]이고 B = [4, 5]일때,

A.append(B)를 하면 A = [1, 2, 3, [4, 5]]

A.extend(B)를 하면 A = [1, 2, 3, 4, 5]

근데 이 방법에는 문제점이 있다.

그것을 확인하기 위해 다음과 같이 코드 중간중간에 print를 써서 현재 queue와 visited에 있는 값들을 하나하나 확인해보고,  걸리는 시간까지 알아보자.

import time

def BFS(graph, start_node):
    start = time.time()
    visited = []
    queue = [start_node]

    while queue:
        node = queue.pop(0)
        if node not in visited:
            visited.append(node)
            print(node)
            print(f'방문 처리 후 visited : {visited}')
            queue.extend(graph[node])
            print(f'큐에 연결된 요소 추가 후 queue : {queue}')
        print("-"*10)
    print(time.time()-start)

 

더보기

실행결과

 

1
방문 처리 후 visited : [1]
큐에 연결된 요소 추가 후 queue : [2, 3, 4]
----------
2
방문 처리 후 visited : [1, 2]
큐에 연결된 요소 추가 후 queue : [3, 4, 1, 3]
----------
3
방문 처리 후 visited : [1, 2, 3]
큐에 연결된 요소 추가 후 queue : [4, 1, 3, 1, 2, 4, 5]
----------
4
방문 처리 후 visited : [1, 2, 3, 4]
큐에 연결된 요소 추가 후 queue : [1, 3, 1, 2, 4, 5, 1, 3]
----------
----------
----------
----------
----------
----------
5
방문 처리 후 visited : [1, 2, 3, 4, 5]
큐에 연결된 요소 추가 후 queue : [1, 3, 3, 6]
----------
----------
----------
----------
6
방문 처리 후 visited : [1, 2, 3, 4, 5, 6]
큐에 연결된 요소 추가 후 queue : [5]
----------
----------

0.007978439331054688 

대략 0.007초가 걸렸다.

이 코드의 문제점은 그럼 뭘까?

 

어떤 요소를 방문처리해주고(visited.append(node))

그 요소와 연결된 요소들(graph[node])를 큐에 추가해줄 때, 방문한 적 없는 요소들만 추가해야 한다.

근데 위 코드는 일단 연결된 요소들 전부 큐에 추가해주고, 나중에 큐에서 꺼낼 때 꺼낸 값이 방문한 적이 있는지 없는지를 판단하기 때문에 쓸데없이 시간을 낭비하는 꼴이 된다.

 

그래서 다음과 같이 바꿔줄 수 있다.

def BFS(graph, start_node):
    visited = []
    queue = [start_node]

    while queue:
        node = queue.pop(0)
        if node not in visited:
            visited.append(node)
            print(node)
            for nd in graph[node]:             # 연결된 요소들에 대해 하나하나 따진다
                if nd not in visited:          # 방문한 적 없는 요소여야
                    queue.append(nd)           # 큐에 추가

그러나 이 방법도 문제점이 있다. 큐에 원소들을 추가해주는 작업을 하는 부분을 보면 graph[node]에 해당하는 리스트의 원소들을 루프문으로 돌기 때문에, 시간낭비가 역시 생길 수 있다!

 

이 문제점을, set이라는 자료형를 사용해 상당히 간편하게 해결할 수 있다.

더보기

※  set??

: 수학으로 치면 '집합'의 개념에 해당하는 자료형. set이란 키워드를 이용해 만들 수 있고, set()의 괄호 안에 리스트나 문자열을 넣어주어 만들 수 있다(ex : a = set([1, 2, 3], b = set("hello")   → 참고로 집합 자료형은 중복을 허용하지 않고 때문에 b의 경우는 i가 2개가 아니라 1개만 있는 형태가 된다. 암튼 이를 통해 교집합/차집합/합집합을 구하는 듯한 연산을 할 수 있는데, 이를 통해 visited에 방문한 적 없는 친구들을 넣어주는 것이 가능! 

 

예를 들어, a = set(1, 2, 3)이고 b = set(3, 4, 5)라 해보자. a와 b의 합집합 a|b = 1, 2, 3, 4, 5가 된다! 또한, 차집합 a - b = 1, 2가 된다. 이를 통해 graph[node]의 리스트 중에서 방문한 적 없는 요소들, 즉 visited리스트에 없는 요소들만 더하려면 queue에다가 set(graph[node]) - set(visited)를 더해주면 됨

그래서 다음과 같이 해준다.

def BFS(graph, start_node):
    visited = []
    queue = [start_node]

    while queue:
        node = queue.pop(0)
        if node not in visited:
            visited.append(node)
            print(node)
            queue += (set(graph[node]) - set(visited))

그러나 아직까지도 문제가 있다..ㅋㅋㅋㅋㅋㅋ

큐에서 제일 첫 번째 녀석을 빼주는 queue.pop(0)이 문젠데, 이 녀석의 시간복잡도가 O(n)이라는 것.

이는 collection 라이브러리에 있는 deque를 사용해 해결가능하다.

 

그리고 visited가 리스트로 되어있는데,

if in 또는 if not in방법은 리스트에서 하면 이 역시 시간복잡도가 O(n)이다.

따라서 visited를 딕셔너리 또는 집합(set)으로 해주는 게 더 빠르다!

 

그래서, 다음과 같이 해주자.

from collections import deque

def BFS(graph, start_node):
    visited = {}
    queue = deque([start_node])

    while queue:
        node = queue.popleft()
        if node not in visited:
            visited[node] = True
            print(node)            
            queue += (set(graph[node]) - set(visited))

누군가 x의 y승은 얼마인가? 라고 물었다. 그에 대한 대답은?

그냥 x ** y를 리턴하면 되긴 하지만(Python 기준), 그거 말고.

 

일반적으로는 다음과 같이 구할 수 있다.

def pow(x, y):
    answer = 1
    for i in range(y):       
    	answer *= x
    return answer

이 방법의 시간복잡도는 O(y)이다.

 

근데 더 빠르게 구할 순 없을까?


첨에는 단순하게, 저 방법은 루프를 y번 돌면서 하는거니까 루프횟수를 줄이면 된다고 생각했다.

만약 x의 y승을 구할 때 y가 짝수라면

x의 y승 = (x의 y/2승) * (x의 y/2승)

이고, y가 홀수라면

x의 y승 = (x의 y/2승) * (x의 y/2승) * (x)

이므로, 루프를 y번까지 돌 필요없이 y의 반 정도까지만 돌면 된다!고 생각해서 다음과 같이 코드를 짰었다.

def pow(x, y):
    answer = 1
    for i in range(y//2):
        answer *= i
    if y%2 == 0:                     # y가 짝수
        return answer * answer
    else:                            # y가 홀수
    	return answer * answer * 1

그러나 이것의 시간복잡도도 O(y)이다. y가 커질수록 y의 반까지만 루프를 돈다는 건 의미가 없어지기 때문..

 

그러나 이것의 시간복잡도를 O(lg y)로 줄일 수 있는 방법이 있다.


우선 거듭제곱을 구할 때 루프가 아니라 재귀로도 구할 수 있다는 점에 힌트가 있다.

다음과 같이 코드를 짜면 재귀의 방법으로 거듭제곱을 구할 수 있다.

def pow(x, y):
    if y == 0:
        return 1
    return x * pow(x, y - 1)

물론 저것의 시간복잡도 역시 아직은 O(y)임.

 

그러나 예를 들어서, 2의 8승은 다음과 같은 과정으로 구할 수 있다는 점에 주목하자.

 

2의 8승 = (2의 4승) * (2의 4승)

2의 8승 = ((2의 2승) * (2의 2승)) * ((2의 2승) * (2의 2승))

.....

 

즉 다음과 같이 가능하다는 말.

def pow(x, y):       
    if y == 1:
        return 1
    return pow(x, y//2) * pow(x, y//2)    # 물론 y가 짝수일 때만 가능

근데 같은 내용에 대해 재귀를 두 번 호출할 필요는 없으니, 다음과 같이 하는게 더 빠르다.

def pow(x, y):       
    if y == 1:
        return 1
    sub_answer = pow(x, y//2)
    return sub_answer * sub_answer    # 물론 y가 짝수일 때만 가능

아까 x의 y승을 구할 때 y가 홀수라면 x의 y승 = x의 y/2승 * x의 y/2승 * x라고 했었으니, y가 홀수일 때도 포함해 다음과 같이 완성 가능하다.

def pow(x, y):       
    if y == 1:
        return 1
    sub_answer = pow(x, y//2)
    
    if y%2 == 0:
    	return sub_answer * sub_answer    
    else:
        return sub_answer * sub_answer * x

이렇게 하면 O(lg y)의 시간복잡도로 거듭제곱을 더 빠르게 계산할 수 있다.


내가 한 거랑 뭐가 달랐던 걸까?

우선 기본적인 접근법은 같았던 것 같다. y가 짝수인지 아닌지에 따라, 2/y까지만 곱한 걸 다루는 면에서.

그러나 y가 만약 16으로 주어졌을 때, x의 16승은 x의 8승 * x의 8승이 맞긴 하지만 x의 8승을 구하는 것도 다시 반으로 쪼개서 할 수 있다는 점을 간과했다.

 

+ Recent posts