css 단위 rem, em

css 단위 rem, em

css 를 사용하다 보면 만나게 되는 단위인 remem 에 대해 정리합니다.

em

em 은 현재 font-size를 정의합니다.

body {
    font-size: 14px;
}
div {
    font-size: 1.5em; // 14px * 1.5 = 21px
}

위와 같이 설정을 한다면 body안의 폰트는 모두 14px 정의 될 것입니다.
그런데 만약 div 안의 font-size 값은 어떨게 될까요?
정답: 21px

이제 아래 내용을 봅시다.

<body>
<div>
  A  <!-- 14 * 1.5 = 21px -->
    <div>
    B  <!-- 21 * 1.5 = 31.5px -->
    <div>
      C  <!-- 31.5 * 1.5 = 47.25px -->
    </div>
  </div>
</div>
</body>

css em test – JSFiddle

이제 em은 이해가 된 듯 합니다.

rem

그럼 div 테그 안의 font-size를 고정하고 싶을 경우에는 어떻게 해야할까요?

바로 rem 단위를 사용하면 됩니다. rem 에서 r 은 최상위(root)의 의미를 지니고 있습니다.

body {
    font-size: 14px;
}
div {
    font-size: 1.5rem; // 14px * 1.5 = 21px
}

css rem test – JSFiddle

둘 차이를 JSFiddle 을 통해 손쉽게 확인 가능합니다.

[golang] Go언어 시작하기 – 배열(array)과 슬라이스(slice)

배열(array)슬라이스(slice)

Go언어에 배열과 슬라이스에 대해 알아보겠습니다.
Go언어는 많은 객체 지향 언어에서 기본으로 지원하는 list타입이 없고, 배열과 슬라이스가 존재합니다.
배열을 선언하는 법은 먼저 배열의 길이를 선언하고, 타입(type) 뒤에 초기화 할 값을 넣어줍니다.

배열 선언

array := [5]int{1,2,3,4,5}
array := […]int{1,2,3}

위의 형태로 사용합니다.
Go언어는 일반적으로 알고 있는 (C, Java와 같은) 언어들과 type declaration Syntex가 반대입니다.

golang의 syntex에 대한 설명은 아래 링크를 참고하시면 됩니다.
golang syntex에 대해

위의 배열 선언 코드를 보면 익숙하지 않는 연산자가 나옵니다.

:-

위의 연산자는 변수의 선언과 할당을 동시에 할때 사용하는 연산자로 Go언어에서 자주 쓰입니다.
앞으로 Go언어에 대한 코드를 볼때 자주 접하게 될겁니다.

슬라이스 선언

slice := []int

slice는 동적 배열의 개념으로 만들어진 것으로 빠르고 효과적으로 배열의 크기를 늘리거나 줄일 수 있습니다.
append라는 내장 함수를 이용해서 데이터를 추가 할 수 있으며, 아래 보이는 형태를 이용해서 slice를 쉽게 잘라낼 수도 있습니다.

slice := []int{1,2,3,4}
newSlice := append(slice, 5) // newSlice는 [1,2,3,4,5]
newSlice2 := slice[1:2] // newSlice2는 [2,3]

위의 두 가지를 보면 배열과 슬라이스의 차이를 명확하게 알수 있습니다.
배열은 사이즈를 정확히 지정해야하고, 슬라이스는 사이즈를 지정할 필요가 없습니다.
즉, 사이즈를 지정하면 배열로 선언이 되고, 사이즈를 지정하지 않는다면 슬라이스로 선언이 되는 것입니다.

슬라이스(slice)의 특징

슬라이스의 특징을 알아보기 위해 슬라이스의 주소값을 출력하는 샘플코드 입니다.

package main

import (
"fmt"
)

func testArray(array [5]int) {
fmt.Printf("in testArray() func %p\n", &array)
}

func testSlice(slice []int) []int {
fmt.Printf("in testSlice() func %p\n", slice)
return append(slice, 6)
}

func main() {
array := [5]int{1, 2, 3, 4, 5}
fmt.Printf("origin ptr: %p\n", &array)
testArray(array)

// 배열을 슬라이스로 변환, 메모리 주소는 현재까지 동일함
slice := array[:]
fmt.Printf("%v, %p\n", slice, slice)

slice2 := testSlice(slice)
// 메모리가 변함. 새로 할당 한 듯
fmt.Printf("%v, %p\n", slice2, slice2)

// 이후부터는 같음
slice2 = append(slice2, 7)
fmt.Printf("%v, %p\n", slice2, slice2)

}

처음 배열을 선언하고 메모리 주소를 확인하고, 이후 슬라이스로 변환하고, 슬라이스의 데이터를 변환하면서 주소를 확인해 가는 코드입니다.
output

origin ptr: 0xc420012180
in testArray() func 0xc4200121b0
[1 2 3 4 5], 0xc420012180
in testSlice() func 0xc420012180
[1 2 3 4 5 6], 0xc420016140
[1 2 3 4 5 6 7], 0xc420016140

실행결과는 위와 같습니다.
간단히 해석하자면,

origin ptr: 0xc420012180
in testArray() func 0xc4200121b0

함수 실행시 넘긴 배열인자의 주소를 확인한 결과 주소가 변했습니다.
이 경우는 callbyvalue로 함수를 호출하게 되어 배열를 deep copy하므로 주소가 변한 것입니다.
배열과 슬라이스의 callbyvalue, callbyreference에 대한 내용은 아래에서 다른 예제코드로 보도록 하겠습니다.

[1 2 3 4 5], 0xc420012180
in testSlice() func 0xc420012180

슬라이스로 변환한 결과 배열과 주소는 동일합니다.
이 경우는 결국 타입만 변경된 것입니다.
실제 주소는 같습니다. 결국 내부 데이터를 array에서 바꾼다고 하면, slice의 값도 변할 것입니다.

[1 2 3 4 5 6], 0xc420016140
[1 2 3 4 5 6 7], 0xc420016140

여기서 부터 재미있습니다.
testSlice(...) 함수를 호출했을 때 내부에서 append라는 내장 함수를 호출합니다.

append는 slice에 값을 추가할 때 사용하는 내장함수로 len, cap등과 함께 자주 접하게 될 것입니다.

append함수를 호출하고 return을 하게 되면 새로운 slice가 반환하게 됩니다.

[1 2 3 4 5 6], 0xc420016140

보면 주소가 변경 되었다는 것을 알수 있습니다.
그런데!!, 두번째 append호출했을 때는 주소가 그대로 인 것을 알 수 있습니다.

[1 2 3 4 5 6 7], 0xc420016140

slice는 기본적으로 lengthcapacity를 가지고 있는데, 이것에 대한 상세한 설명은 두개의 링크로 대신합니다.

Go Slices: usage and internals
Arrays, slices (and strings): The mechanics of ‘append’

배열(array)과 슬라이스(slice)의 차이점

  • Call by value
  • Call by reference

둘의 가장 큰 차이점입니다.
배열은 인자(argument)로 받을 경우 callbyvalue로,
슬라이스는 callbyreference로 받게 됩니다.

이 것을 눈으로 확인해보기 위해 간단한 샘플코드를 만들었습니다.

func testArray(array [1e7]int)
* 천만개의 배열을 복사

func testSlice(slice []int)
* 포인터만 복사
callbyvaluecallbyreferrence를 단순비교하기 위해서는
위의 샘플코드처럼 배열과 슬라이스의 주소를 확인하기면 하면 되지만,
배열과 슬라이스를 함수에서 사용할 때 둘 사이의 차이를 쉽게 느껴보고자 아래 코드를 만들었습니다.

package main

import (
"fmt"
"time"
)

func callByValue(array [1e7]int) int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}

func callByReference(slice []int) int64 {
return time.Now().UnixNano() / int64(time.Millisecond)
}

func main() {
array := [1e7]int{}

t := time.Now().UnixNano() / int64(time.Millisecond)
t2 := callByValue(array)
fmt.Printf("call by value elapsed time : %f\n", float32(t2-t)/1000)

t = time.Now().UnixNano() / int64(time.Millisecond)
slice := array[:]
t3 := callByReference(slice)
fmt.Printf("call by reference elapsed time : %f\n", float32(t3-t)/1000)

}

output

call by value elapsed time : 0.043000
call by reference elapsed time : 0.000000

큰 배열을 인자로 넘겨야하는 경우, 배열은 전체복사가 된다는 점을 알고 있어야 합니다.


여기까지가 배열과 슬라이스에 대한 간단한 정리였습니다.

 [golang] Go언어 시작하기(Overview) – 1

 

Go 언어(golang) 시작하기

최근, 회사에서 Go언어를 쓰게 되어서 간단하게 Go언어에 대해서 간단하게 포스팅 해보려합니다.

저도 많은 내용을 아는 것은 아니니 부족한 부분이 많을 것입니다.

이 글은 정리차원에서 적어두는 것이나, Go언어를 잘 모르시는 분, Go언어에 관심이 가는 분, 또는 Go언어를 배워보고 싶은 분들께 유익한 글이 되었으면 합니다.

Let’s Go

새로운 언어를 배울 때 가장 먼저 확인하는 hello world 입니다.
Go 놀이터에 가셔서 확인하실 수 있고, 웹상에서 테스트도 가능합니다.

package main

import (
"fmt"
)

func main() {
fmt.Println("Hello, playground")
}

위의 코드를 보면 Java처럼 현재 파일의 패키지를 설정하고 있고
import 로 사용할 패키지를 지정, main 함수가 기본 프로그램 엔트리 포인트입니다.
특이한 것이 main함수에 인자가 없습니다.
인자(ARGV)를 받기위해서는 os package를 import 해야합니다.

Go 언어 넌 누구니?

1. 컴파일 기반의 정적 타입 언어 (compiled language)

Go언어는 컴파일 기반의 정적 타입언어입니다.
이는 컴파일로 인해 파이썬(Python)이나 루비(Ruby)같은 인터프리터(interpreter) 언어로 개발할 때보다 버그 요소가 많이 줄어듬을 의미합니다.
물론 컴파일언어의 특징인 빠른 속도 또한 당연합니다.
특히 Go언어의 컴파일러는 C언어에서의 warning (예를 들면, 사용하지 않는 변수나 패키지를 import했을시에 Go 컴파일러는 오류를 발생시킵니다.) 에 해당하는 문제들도 모두 error로 만듭니다.

  • Go언어는 컴파일 언어지만, C/C+의 해더파일이 없어서 헤더파일이 조금만
    수정되어도 모두 다시 컴파일하는 그런 문제가 없고, 소스코드를 패키지화하여 변경된 부분만 컴파일하기 때문에 컴파일 시간이 엄청나게 빠릅니다. -> How does Go compile so quickly?

2. 하지만 동적 언어 특성도 가진다 (예: interface -> Duck typing)

Go언어는 interface가 있지만 따로 선언은 하지 않습니다. -> Duck Typing
선언이 없이 인터페이스의 함수를 구현하면 그 인터페이스를 사용한다고 간주합니다.
예를 들어 notifier interface가 정의되어 있습니다.
notifier interfacenotify()메소드를 가지고 있습니다.

  • 특정 struct에서 notify()메소드를 구현했다면,
    structnotifier interface를 구현했다고 간주하는 것입니다.

3. 상속이 없다 (composition만 존재)

Go언어는 상속이 없습니다.
Go언어에서 일반적인 객체 지향 언어에서의 class의 역할을 struct가 맡고 있습니다.
Go의 struct는 상속이 되지 않습니다. 대신 composition이라는 것이 존재합니다.

  • composition이란?
    한 타입과 다른 타입을 결합해서 사용할 수 있게 해주는 것을 의미합니다.
    이는 각 타입간의 결합도를 낮춰주는 효과가 있습니다.
    일반적으로 상속을 사용해서 코딩을 하게 되면 class간의 관계가 tree형태로 만들어집니다.
    따라서 상속을 계속 할수록 계층 구조가 점점 복잡해지고, 그 상황에서 여러가지 문제점이 발생할 수 있습니다.
    `compositio“의 중요성 -> Prefer composition over inheritance?

4. 실행시 가비지 콜렉터 탑재 (Garbage Collection)

Go언어는 바이너리 빌드시 가비지 콜렉터를 내장합니다.
그래서 C/C++처럼 개발자가 메모리 할당 및 해제를 신경쓰지 않아도 됩니다.
또한, Java처럼 가상머신이 필요하지도 않기 때문에, C/C++처럼 빠른 성능을 기대할 수 있습니다.

5. 멀티코어 환경 지원 (goroutine, channel)

최근은 대부분의 컴퓨터가 멀티코어인 시대입니다.
이에 맞게 Go언어는 멀티코어 환경 지원을 위해 만들어진 언어입니다.
Go는 Goroutine이라는 논리적 쓰레드(Thread)를 기본적으로 제공합니다.
사용자는 멀티코어 및 쓰레드 사용에 대해 고민하지 않고, Goroutine을 사용하면 Go 런타임이 알아서 현재 cpu의 코어에 맞춰서 동시에 코드를 실행해줍니다.
Thread를 만들고 실행하고 종료하고 하는 별도의 불편한 과정이 필요없습니다. 사용자는 손쉽게 멀티코어를 이용할 수 있습니다.


이후 포스팅은 다음 포스팅에 하겠습니다.