Today I Learned. by Rio

Install opencv in ubuntu

|

conda 환경에서 opencv를 설치했던 기록을 남긴다.

1. 기존 설치 확인
pkg-config --modversion opencv

버전이 출력되지 않고 No package 'opencv' found가 떠야한다.

2. 기존 버전이 설치되어 있으면 삭제한다.
# 기존 라이브러리 설정파일 및 패키지 삭제
sudo apt-get purge  libopencv* python-opencv
sudo apt-get autoremove

#기존설치된 opencv 라이브러리 삭제
sudo find /usr/local/ -name "*opencv*" -exec rm -i {} \;
3. 설치준비
sudo apt-get update
sudo apt-get upgrade
4. opencv 컴파일 위해 필요한 패키지 설치
sudo apt-get install build-essential cmake pkg-config libjpeg-dev libtiff5-dev libpng-dev libavcodec-dev libavformat-dev libswscale-dev libxvidcore-dev libx264-dev libxine2-dev libv4l-dev v4l-utils libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgtk2.0-dev libgtk-3-dev mesa-utils libgl1-mesa-dri libgtkgl2.0-dev libgtkglext1-dev libatlas-base-dev gfortran libeigen3-dev python2.7-dev python3-dev python-numpy python3-numpy python3.5-dev
5. 4.2.0버전 다운로드
mkdir opencv
cd opencv
wget -O opencv.zip https://github.com/opencv/opencv/archive/4.2.0.zip
unzip opencv.zip
wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/4.2.0.zip
unzip opencv_contrib.zip
6. 빌드를 위한 디렉토리 생성
cd opencv-4.2.0
mkdir build
cd build
7. 빌드
cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=$(python -c "import sys; print(sys.prefix)") \
-D WITH_TBB=OFF \
-D WITH_IPP=OFF \
-D WITH_1394=OFF \
-D BUILD_WITH_DEBUG_INFO=OFF \
-D BUILD_DOCS=OFF \
-D INSTALL_C_EXAMPLES=ON \
-D INSTALL_PYTHON_EXAMPLES=ON \
-D BUILD_EXAMPLES=OFF \
-D BUILD_TESTS=OFF \
-D BUILD_PERF_TESTS=OFF \
-D WITH_QT=OFF \
-D WITH_GTK=ON \
-D WITH_OPENGL=ON \
-D WITH_GSTREAMER=OFF \
-D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-4.2.0/modules \
-D WITH_V4L=ON  \
-D WITH_FFMPEG=ON \
-D WITH_XINE=ON \
-D BUILD_NEW_PYTHON_SUPPORT=ON \
-D BUILD_opencv_python3=yes \
-D OPENCV_GENERATE_PKGCONFIG=YES \
-D PYTHON3_EXECUTABLE=$(which python) \
-D PYTHON3_INCLUDE_DIR=$(python -c "from distutils.sysconfig import get_python_inc; print(get_python_inc())") \
-D PYTHON3_PACKAGES_PATH=$(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") \
-D PYTHON3_NUMPY_INCLUDE_DIRS=$(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")"/numpy/core/include"  \
..
  • OPENCV_GENERATE_PKGCONFIG: pkg-config에 등록을 자동으로 해준다.
  • 마지막 4줄은 현재 conda의 python3 위치를 찾기위한 부분이다.
  • 결과가 나올때 마지막 부분에 python3도 빌드가 되었다고 나와야 정상적으로 빌드된 것이고 마지막 3줄 빌드 완료 메시지가 뜬다.
--   Python 2:
--     Interpreter:                 /usr/bin/python2.7 (ver 2.7.12)
--     Libraries:                   /usr/lib/x86_64-linux-gnu/libpython2.7.so (ver 2.7.12)
--     numpy:                       /usr/lib/python2.7/dist-packages/numpy/core/include (ver 1.11.0)
--     install path:                lib/python2.7/dist-packages/cv2/python-2.7
--
--   Python 3:
--     Interpreter:                 /home/petopia-01/anaconda3/envs/dog-detect/bin/python (ver 3.7.7)
--     Libraries:                   /home/petopia-01/anaconda3/envs/dog-detect/lib/libpython3.7m.so (ver 3.7.7)
--     numpy:                       /home/petopia-01/.local/lib/python3.7/site-packages/numpy/core/include (ver 1.18.2)
--     install path:                /home/petopia-01/anaconda3/envs/dog-detect/lib/python3.7/site-packages/cv2/python-3.7
--
--   Python (for build):            /usr/bin/python2.7
--
--   Java:
--     ant:                         NO
--     JNI:                         NO
--     Java wrappers:               NO
--     Java tests:                  NO
--
--   Install to:                    /home/petopia-01/anaconda3/envs/dog-detect
-- -----------------------------------------------------------------
--
-- Configuring done
-- Generating done
-- Build files have been written to: /home/petopia-01/opencv/opencv-3.4.10/build
8. 컴파일 시작
  • 아래 명령을 통해 cpu 코어수를 확인한다.
cat /proc/cpuinfo | grep processor | wc -l
  • make명령으로 컴파일을 시작한다. 구동머신 스펙에 따라 다르지만 시간이 꽤 걸린다.
  • 마지막 16이라는 숫자는 위 명령을 통해 출력된 cpu 코어수를 적어준 것이다.
  • time을 사용하여 진행시간이 얼마나 걸렸는지 확인할 수 있다.
time make -j16
9. 컴파일된 결과물 설치
# 컴파일된 결과물 설치
sudo make install
10. 설치 확인
cat /etc/ld.so.conf.d/*
  • 아래처럼 나와야 한다.
/usr/local/cuda-10.2/targets/x86_64-linux/lib
/usr/lib/x86_64-linux-gnu/libfakeroot
# libc default configuration
/usr/local/lib
# Multiarch support
/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu
/usr/lib/nvidia-440
/usr/lib32/nvidia-440
/usr/lib/nvidia-440
/usr/lib32/nvidia-440
# Legacy biarch compatibility support
/lib32
/usr/lib32
  • /usr/local/lib 부분이 출력되는지 확인한다.
  • 나오지 않으면 아래 두 줄을 실행한다.
sudo sh -c 'echo '/usr/local/lib' > /etc/ld.so.conf.d/opencv.conf'
sudo ldconfig
참고한 사이트

Yolo v3 custom data train

|
1. AlexeyAB darknet 모델 다운로드
https://github.com/AlexeyAB/darknet.git
cd darknet
2. 빌드를 위해 Makefile 수정 후 빌드
  • 내가 수정한 부분
    CPU=1
    CUDNN=1
    OPENCV=1
    ...
    NVCC=/usr/local/cuda/bin/nvcc
    
  • 중간에 NVCC 경로를 수정해준다.
  • 이후 make를 실행해서 빌드한다.
  • 혹시 opencv 관련해서 오류나는 경우 conda의 경우 conda install -c conda-forge opencv 명령으로 opencv를 설치해 준 후 다시 빌드해본다. 이 부분에서 삽질을 오래했다.
make
3. 테스트
./darknet detector test cfg/coco.data cfg/yolov3.cfg yolov3.weights data/dog.jpg
4. 학습을 위한 사전 파일 준비
custom_data/custom.names
custom_data/images
custom_data/train.txt
custom_data/test.txt
4-1. custom.names: 클래스명을 나열한 파일
none
stand-four-legs
sit-down
kneel-down
eating
4-2. images 폴더
  • 이미지파일과 annotation txt파일이 같은 파일명으로 존재해야 한다.
  • 예를들면 a.jpg, a.txt가 세트로 존재해야 한다.
4-3. annotatino txt파일 형식
<object-class-id> <x-center> <y-center> <width> <height>
  • 여기서 x-center, y-center는 그림에서 사각형 박스의 중심좌표이다.
  • width, height는 사각형 좌표의 크기를 나타낸다.
  • 이 모든 것은 전체 이미지 사이즈의 비율로 소수점 처리가 되어야 한다.
  • x-center, y-center는 0과 1 사이의 값이고 0, 1과 같을 수 없다.
  • width, height는 0, 1도 가능하고 0과 1사의 값을 가진다.
0 0.1242 0.63632 0.123 0.5
4-4. train.txt와 test.txt파일
  • 이미지 파일 목록을 나타낸다. 두 파일 다 아래와 같은 형태로 나타낸다.
custom_data/images/a.jpg
custom_data/images/b.jpg
custom_data/images/c.jpg
...

4-5. detector.data 파일

classes=5
train=custom_data/train.txt
valid=custom_data/test.txt
names=custom_data/custom.names
backup=backup/
  • classes: 클래스 갯수
  • train, valid: 이미지 목록 파일
  • names: 클래스 목록 파일
  • backup: 트레이닝된 모델이 순차적으로 쌓이는 폴더 경로
  • 주의할 점은 backup 폴더는 darknet root에 만들어 놓고 트레이닝을 시작해야 한다. 폴더가 없으면 트레이닝 중 에러가 나는 듯.
5. cfg 파일 작성
  • ./cfg 폴더 밑에 있는 yolov3.cfg 파일을 복사해서 custom_data/cfg/yolov3-custom.cfg 파일을 만든다.
  • 해당 파일을 열어 아래와 같이 수정한다.
  • batch line: 6
    • 기본값 64. 건드리지 않는다.
  • subdivisions line: 7
    • 배치 사이즈를 얼마나 쪼개서 학습할지 설정한다. 기본값은 8이지만 메모리 릭이 나면 16 32 등으로 조절한다.
  • witdh, height line: 8, 9
    • 기본값은 416, 608로 변경하면 정확도가 좋아질 수 있다. 32의 배수로 조절한다.
  • learning_rate line: 18
    • 기본값 0.001, multi gpu인 경우 0.001/gpu수로 설정한다. 예를들어 2개라면 0.0005를 써준다.
  • burn_in line: 19
    • 기본값 1000, multi gpu인 경우 1000*gpu수로 설정한다. 예를들어 2개라면 2000을 써준다.
  • max_batches line: 20
    • 이터레이션 갯수, class수*2000+200. 예를들어 5개 클래스라면 10200
  • steps line: 22
    • max_batches의 80%, 90%로 class갯수*2000의 80%, 90%를 설정한다. 예를들어 5개 클래스라면 8000,9000을 써준다.
  • classes line: 610, 696, 783
    • [yolo] 레이어에 있는 것으로 클래스 수를 적어준다. 예를들어 5개 클래스라면 5를 써준다.
  • filters: line: 603, 689, 776
    • (클래스 갯수+5)*3을 써준다. 예를들어 5개 클래스라면 30을 써준다.
    • 문자열 검색을 하면 많이 나오는데 yolo레이어 바로 위에 있는 필터만 수정한다.
6. 트레이닝 시작
# ./darknet detector train {data파일} {cfg파일} {trainset 파일} {옵션}
./darknet detector train custom_data/detector.data custom_data/cfg/yolov3-custom.cfg darknet53.conv.74 -gpus 0,1 -dont_show
# nohup 실행
nohup ./darknet detector train custom_data/detector.data custom_data/cfg/yolov3-custom.cfg darknet53.conv.74 -gpus 0,1 -dont_show &
  • gpu 갯수가 2개 이상일 때는 -gpus 옵션을 주어 트레이닝한다.
  • 원격 터미널 등에서 훈련하고, 훈련 중인 mAP, loss chart등을 보기 위해서 옵션으로 -mjpeg_port 8090 -map 을 줄 수 있다.
7. 트레이닝 후 로그 보는 방법
  • v3로 시작하는 라인들이 계속 나오는데 중간에 이터레이션 번호가 뜨는 라인이 생김
  • 이터레이션 번호 뒤 3열에 avg loss값이 있는데 그게 중요하다. 아래와 같은 형태로 나옴
 10: 1630.738525, 1685.839600 avg loss, 0.000000 rate, 4.303994 seconds, 1280 images, 10.956437 hours left
...
 186: 714.564453, 1032.841675 avg loss, 0.000000 rate, 4.592934 seconds, 23808 images, 8.222052 hours left
...
 700: 1.113163, 1.076701 avg loss, 0.000015 rate, 4.974880 seconds, 89600 images, 6.471217 hours left
...
 902: 0.889429, 0.902189 avg loss, 0.000041 rate, 4.518785 seconds, 115456 images, 6.284531 hours left
...
  • avg loss가 위의 경우 10 이터레이션의 1685.839600 값이 902 이터레이션에서 0.902189까지 떨어졌다.
  • iteration 이 증가할 수록 avg loss 값이 떨어져야 하고 어느정도 떨어지면 더 잘 안떨어진다.
  • 데이터 세트 숫자에 따라서 떨어지는 값이 다른데 적을때는 0.00x까지도 떨어진다.
  • 맨 뒤에 남은 시간이 표시된다.
8. 트레이닝 종료 후 모델 테스트
  • 아까 만든 yolov3-custom.cfg 파일을 복사해서 yolov3-custom-test.cfg 파일을 만든다.
  • 안에 내용을 아래와 같이 수정한다. 3, 4 라인 주석 해제 후 6, 7라인을 주석처리한다.
[net]
# Testing
batch=1
subdivisions=1
# Training
# batch=64
# subdivisions=16
width=608
height=608
8-1. 이미지 테스트
# ./darknet detector test {data파일} {테스트cfg파일} {학습한weights파일} {테스트할이미지}
./darknet detector test custom_data/detector.data custom_data/cfg/yolov3-custom-test.cfg backup/yolov3-custom_last.weights ./test.png
  • 이미지 결과가 창으로 뜨거나 원격ssh 환경 같은 경우에는 predictions.jpg 파일로 떨어진다.
8-2. 비디오 테스트
# ./darknet detector demo {data파일} {테스트cfg파일} {학습한weights파일} {테스트할비디오} {-옵션}
./darknet detector demo custom_data/detector.data custom_data/cfg/yolov3-custom-test.cfg backup/yolov3-custom_last.weights ./test.mp4 -dont_show -out_filename out.avi
  • -dont_show: 원격 ssh 환경 등에서 비디오 오픈 없이 실행한다.
  • -out_filename out.avi: 결과 비디오를 out.avi파일로 저장한다.
9. 트레이닝 종료 후 모델 정확도 측정
# ./darknet detector map {data파일} {cfg파일} {측정하려는weights파일}
./darknet detector map custom_data/detector.data custom_data/cfg/yolov3-custom.cfg backup/yolov3-custom_last.weights
  • 중간 생략하고 결과가 아래와 같이 나온다.
 calculation mAP (mean average precision)...
1212
 detections_count = 2197, unique_truth_count = 1212
class_id = 0, name = none, ap = 96.20%           (TP = 187, FP = 26)
class_id = 1, name = stand-four-legs, ap = 97.12%        (TP = 389, FP = 42)
class_id = 2, name = sit-down, ap = 96.69%       (TP = 166, FP = 19)
class_id = 3, name = kneel-down, ap = 96.43%     (TP = 110, FP = 16)
class_id = 4, name = eating, ap = 98.32%         (TP = 303, FP = 25)

 for conf_thresh = 0.25, precision = 0.90, recall = 0.95, F1-score = 0.93
 for conf_thresh = 0.25, TP = 1155, FP = 128, FN = 57, average IoU = 76.97 %

 IoU threshold = 50 %, used Area-Under-Curve for each unique Recall
 mean average precision (mAP@0.50) = 0.969528, or 96.95 %
Total Detection Time: 41 Seconds
  • 아래의 해석은 내가 이해하기 쉽게끔 풀어 써본거라 전문가가 보면 틀릴수도 있다.
  • conf_thresh = 0.25는 25%이상 맞다고 판단되면 검출해내겠다는 뜻
  • precision(정밀도): TP/(TP+FP), 맞춘 수/모델이 검출한 수, 검출된 결과가 얼마나 정확한지
  • recall(검출율): TP/(TP_FN), 맞춘 수/실제 사람이 입력했던 라벨 수, 빠뜨리지 않고 얼마나 잘 검출해내는지
  • F1-score: precision과 recall의 조화평균
  • AP(Average Precision): precision-recall 그래프 선 아래쪽의 면적으로 평가지표로 활용됨
  • IoU: 교집합/합집합 면적
  • 즉 mAP@0.50은 교집합/합집합 면적이 50%이상 맞는지 기준에 따라서 precision과 recall에서 나오는 모든 클래스 AP의 평균값
  • 결과 분석은 아래 블로그 참조
  • 참고: 여기선 나오지 않지만 accuracy는 (TP+TN)/(TP+TN+FP+FN)으로 계산한다. 컴퓨터 비전 분야에서 잘 사용하지 않는 지표이다.
참고한 사이트

Kill process by port and nohup in ubuntu jenkins

|

굉장히 간단해보이는데 삽질을 오래해서 까먹지 말라고 적어둔다.

  1. ubuntu jenkins에서
  2. ssh plugin을 이용해 다른 ubuntu로 접속 후
  3. 실행중인 process를 port로 검색해서 죽인 후
  4. nohup으로 새로 띄우는 스크립트이다.
# 1. port(여기서는 5000포트)를 쓰는 processid를 찾아 변수 할당한다.
pid="$(lsof -t -i :5000 -s TCP:LISTEN)";

# 2. processid가 있는 경우에 kill 명령으로 중지시킨다.
if [ "$pid" != "" ]; then
  kill -9 $pid;
  echo "$pid process kill complete"
else
  echo "pid is empty"
fi

cd ~/git/petopia-crawler
# 3. 원하는 새 프로세스를 nohup으로 실행시킨다.
BUILD_ID=dontKillMe DISPLAY=:0 nohup ~/anaconda3/envs/petopia-crawler/bin/python3.7 run.py >> nohup.out 2>&1 &

Python unit test library pytest

|

사내에서 프로젝트 진행 중 python flask로 간단한 rest api 서버를 구성할 일이 있었다. 규모가 점점 커지며 유닛테스트가 필요해져 pytest를 사용하여 유닛테스트를 진행했고 간단하게 세팅 및 사용기를 남긴다.


pytest vs unittest

사실은 unittest가 파이썬 표준 테스팅 라이브러리이다. 그러나 구글에서 pytest vs unittest로 검색을 해보면 상당히 많은 포스트가 나오는데, 대부분은 pytest를 추천한다. 이유는 pytest가 unittest와 비교해 사용법이 더 편리하고, 파이썬 스타일 가이드(PEP8)를 따라 간결한 코드 작성에 도움이 되며 테스팅 프레임워크로써의 추가적인 장점(픽스쳐 활용, custom assert 활용)이 있기 때문이라 한다. 특히 django나 flask 등의 프레임워크 위에서 개발하고 있다면 pytest를 활용하는 것이 좋아 보인다.

각각의 비교 글들은 아래를 참고하세요.

  • https://www.bangseongbeom.com/unittest-vs-pytest.html
  • https://americanopeople.tistory.com/255
  • https://www.reddit.com/r/Python/comments/5uxh22/unittest_vs_pytest/
  • https://www.slant.co/versus/9148/9149/~unittest_vs_pytest
1. pytest 사용을 위한 설정

우선 pytest 라이브러리 의존성 추가가 필요하다. pip를 통해 설치해준다. 경우에 따라서는 pytest-mock 등 pytest 기반 플러그인 라이브러리를 추가해야 하는 경우들이 생긴다.

인텔리제이의 경우 python test runner 설정을 하면 테스트 수행이 쉬워지고 결과도 인텔리제이 탭에서 확인할 수 있다. Tools > Python Integrated Tools에서 Default test runner를 pytest로 선택한다.

check

2. 테스트를 작성하기 전에 간단히 알아둬야 할 것들
  • pytest에서 test suite으로 인식하는 파일명은 prefix로 test_ 를 붙인다.
  • 마찬가지로 test 파일 내에서 생성하는 함수 이름도 prefix로 test_ 를 붙여야 한다.
  • 테스트코드를 프로덕트 파일과 분리하여 놓을 필요는 없지만 가독성을 위해서 test 폴더 등을 만들고 그 밑에 몰아넣는 것이 좋다.
3. 샘플 테스트 코드

가장 간단한 형태의 샘플 테스트 코드를 남긴다. contains_whitespace라는 메서드를 테스트하기 위한 코드이다. assert문을 이런식으로 사용한는 정도만 참고하고 자세한 assert문의 사용은 pytest docs를 열어보자.

def test_contains_whitespace_with_valid_input():
    result = contains_whitespace('test')
    assert result is False

def test_contains_whitespace_with_invalid_input_all_whitespace():
    result = contains_whitespace('    ')
    assert result is True

def test_contains_whitespace_with_invalid_input_contains_whitespace():
    result = contains_whitespace('te  st')
    assert result is True
4. 테스트 수행

터미널에서는

  • pytest test_code.py -k ‘test_method_name’ 명령으로 특정 메서드만 수행하거나
  • pytest test_code.py 명령으로 단건 파일을 수행하거나
  • pytest ./test_folder 명령으로 특정 폴더 내에 있는 테스트 코드 전체를 수행할수 있다.

인텔리제이에서는 테스트 파일을 우클릭하여 Run ‘pytest …’ 또는 함수 옆에 생기는 초록색 플레이버튼을 통해 실행할 수 있다.

image

5. mock의 사용
def test_login_with_invalid_user_id(mocker):
    mock_response_json = json.dumps({
        'data': {
            'userId': 'test id',
            'password': 'test password'
        }
    })
    mock_response.text = mock_response_json
    mock_response.status_code = 400

    mocker.patch('requests.post', return_value=mock_response)

    result = login_service.login(user_id, password, login_ip)
    assert result['userId'] == 'test id'
    assert result['loginToken'] == 'test token'

샘플 코드는 requests.post 메서드를 mocker를 통해 mocking하고 login_service의 login 메서드를 테스트하기 위한 코드이다. login_service.login 메서드에는 requests.post 구문이 있을 것이고 그부분이 대체되어 실제로 호출이 되지 않고 return_value에서 정의한 값이 리턴되게끔 수행된다.

mocker를 쓰기 위해서는 우선 pytest-mock이라는 라이브러리가 필요하니 pip를 통해 설치해준다.

  • 테스트 메서드에 파라미터로 mocker 를 넣어준다.
  • mocking할 대상이 클래스 없는 메서드인 경우는 mocker.patch(‘패키지.파일.메서드명’)
  • 클래스 내부의 메서드를 mocking할 때는 mocker.patch.object(클래스, ‘메서드명’)
  • return_value 옵션을 활용하여 mock 메서드의 리턴값을 사용자가 정의할 수 있다.
6. 익셉션 발생 여부 테스트
def test_string_to_datetime_with_invalid_input():
    with pytest.raises(InvalidFormatException) as ex:
        string_to_datetime('some text')
    assert ex.value.invalid_fields['key'] == 'some text'
    assert ex.value.code == Fail.INVALID_PARAM
    assert ex.value.message == 'Invalid format: some text'

위 테스트코드는 InvalidFormatException이라는 익셉션이 발생하기를 기대하고 있다. with 구문 밑에 테스트하고자하는 메서드를 수행시키고, 실제 발생된 익셉션 클래스는 ex.value로 값 검증이 가능하다.

def test_string_to_datetime_with_valid_input():
    try:
        result = string_to_datetime('2019-01-01T09:00:00')
        assert result == datetime.datetime(2019, 1, 1, 9, 0, 0)
    except Exception as ex:
        pytest.fail(str(ex))

위의 테스트코드는 정상케이스 즉 익셉션이 발생하지 않는 경우에 대한 검증이다. try-except문으로 감싸서, 혹시 익셉션이 발생하는 경우에는 의도적으로 pytest.fail을 호출하는 식이다.

7. fixture의 활용
@pytest.fixture(scope="function")
def login_service():
    my_service = ...생략...
    return my_service

def test_code(login_service):
    result = login_service.login('test id', 'test password')
    ...생략...

pytest.fixture 구문을 이용하여 test에 필요한 fixture를 미리 준비할 수 있다. 사용하기 위해서 아래의 테스트코드와 같이 파라미터로 fixture 메서드명을 넣어주면 테스트코드 내에서 fixture를 쓸 수 있다.

8. 정리

초반 설정만 대충 잘 해주면 간단한 테스트 코드는 금방 작성되고 쉽게 수행된다. mock의 사용도 어렵지 않다. assert문을 custom하게 만드는 부분은 아직 작성해 보지 않았으나 docs만 봐도 어려워 보이지는 않는다.

9. 링크

var, let, const in javascript

|

vue 프로젝트 진행시 coding rule을 정하면서 정리한 글. 핵심은 var를 사용하지 말고 let과 const를 사용하자는 것이다.


var의 문제점

var의 사용은 개발자들에게 javascript 코드를 혼란스럽게 만드는 이유가 된다. 아래는 var 사용으로 나타나는 문제점과 let, const를 사용해야 하는 이유이다.

1. var로 중복 변수를 만들어도 문제없이 동작한다.
var token = "test token";
console.log(token)
var token = "other token"; // 위에서 선언한 test token 값 유실
console.log(token)

javascript의 var 변수는 hoisting이라는 행위를 통해 최상단에 선언이 된다. hoist는 끌어올리기라는 뜻이고 이 행위 덕분에 var 변수는 자바스크립트 엔진 구동시 맨 처음에 선언되는 형태가 된다. 하지만 선언과 동시에 할당된 값이 끌어올려지지 않아서 문제를 발생한다.
만약 이런 코드가 있다고 치자.

console.log(deviceName)
var deviceName = "갤럭시S8";
console.log(deviceName)

위 코드를 엔진이 해석할 때는 아래와 같다.

var deviceName; // 할당된 값은 버리고 선언만 끌어올림
console.log(deviceName)
deviceName = "갤럭시S8";
console.log(deviceName)

위의 코드는 실제로 결과가 어떻게 나올까.

undefined
갤럭시S8

var의 이 호이스팅 동작은 개발자가 코드의 동작을 예측하기 어렵게 만드는 이유가 된다. 이런 var의 호이스팅을 막기 위해서는 자바스크립트 scope 내에(최상단 또는 함수 내부) use strict을 선언하여 방지할 수 있다. use strict를 사용하면 호이스팅은 동작하지 않으며 선언되지 않은 변수를 사용한다는 오류를 발생시킨다. let을 사용하면 use strict 선언 없이도 정상적으로 오류가 발생한다.

'use strict'
console.log(deviceName) // error 발생
var deviceName = "갤럭시S8";
console.log(deviceName)
2. var 변수는 scope를 무시한다.

정확히는 var 변수는 function-scoped를 가진다. 이는 다시 말해 function 내부에 var 변수를 가두지 않는다면 global scope를 오염시킬 수 있다는 소리이다.

var token = "test token";

if (token !== null) {
  var token = "other token";
  console.log(token) // other token
}
console.log(token); // other token

다른 언어들과 달리 자바스크립트의 중괄호 블록 영역 내부(예시의 if)의 var 키워드는 외부 스코프와 고립되지 않는다. 이는 코드 량이 많아지고, 많은 사람들이 함께하는 대규모 프로젝트가 될 수록 소스의 잠재적인 위험도를 높인다.
이를 해결하기 위해 아래와 같이 function 으로 고립시키는 방법이 있다.

(function() {
  var token = "test token";
  console.log(token);
})();
console.log(token); // error 발생

그런데 여기서도 function에 고립된 변수가 var 없이 선언된다면 자바스크립트는 global 영역에서 변수를 찾아나간다는 것이 문제.

(function() {
  token = "test token"; // var 선언 없이 처음 등장
  console.log(token);
})();
console.log(token); // test token

위 코드는 개발자의 의도와 상관없이 token 변수를 gloabl scope에 선언되었다.

그럼 어떻게?

3. var 대신 let을 사용하면 된다.

let과 const는 모두 block-scoped 변수로 외부 스코프에 영향을 주지 않는다. 같은 스코프 내에서는 중복된 변수 선언을 당연히 할 수 없고 global scope에 선언된 동일한 이름의 변수는 내부 스코프에 영향을 주지 않는다. (일반적으로 개발자들이 원하던 변수의 스코프 범위)

let token = "test token";
let token = "other token"; // syntax error 발생. ide에서는 모두 compile error로 인식
let token = "test token";
token = "other token"; // 재할당 가능
let token = "test token";
if (utils.isEmpty(user)) {
  let token = "other token";
}
console.log(token); // test token, 전역 스코프에 선언된 변수 값 유지
4. const는 값의 재 할당이 불가능하다.

한번 선언되면 값을 재할당 할 수 없는 const의 특성 때문에 상수 선언에 사용되고, 코드 흐름상 추후 값 변경이 없는 변수의 경우는 const로 선언하는 것을 권장한다. 또한 모르는 사이에 코드 진행상 값이 재할당 되는 것을 방지하기 위해 const를 선언하는 습관을 들이는 것을 자바스크립트 개발서에서 추천 하곤 한다.
let은 값 할당 없이 선언만 하는 것이 가능하지만 const는 그렇지 않다는 것에 주의한다.

const REST_URL = "http://localhost:5000/rest";
REST_URL = "other url"; // 값 재할당 시도시 error 발생

const IAM_SERVER_URL; // 값 할당 없이 선언만 하는 경우 error 발생

코드 내에서 const를 상수 용도로 쓰는 경우와 변수 용도로 쓰는 경우를 구분해야 가독성이 높아진다. 일반적으로 상수는 대문자와 언더스코어를 사용하여 작성하고 변수는 카멜케이스로 작성한다. 절대적인 규칙은 아니다.

const RELAY_SERVER_URL = "http://relay-server-url" // 상수 선언

const result = otherService.method(); // 변수 용도로 선언

3줄 요약.
  1. 이제 var는 쓰지말자.
  2. 대신에 let과 const를 쓰자.
  3. 값의 재할당이 없을 것 같다고 여겨지면 const를 쓰는 버릇을 들이자.