Skip to main content

Chap.4 ownership

· 50 min read
brown
FE developer

소유권(Ownership)은 가비지 컬렉터가 없는 러스트에서 메모리 안정성을 보장하는 비결입니다.

이는 러스트에서 가장 특별한 기능이며, 어떻게 동작하는지 반드시 이해해 둬야 합니다.

따라서 이번 장에서는 소유권을 비롯해 소유권과 관련된 빌림(Borrowing), 슬라이스(Slice) 기능과 러스트에선 데이터를 메모리에 어떻게 저장하는지 알아보겠습니다.

4.1 소유권이 뭔가요?


  • 모든 프로그램은 작동하는 동안 컴퓨터의 메모리 사용 방법을 관리해야 합니다.
  • 몇몇 언어는 가비지 컬렉션으로 프로그램에서 더 이상 사용하지 않는 메모리를 끊임없이 찾는 방식을 채용했고, 다른 언어는 프로그래머가 직접 명시적으로 메모리를 할당하고 해제하는 방식을 택했습니다.
  • 이때 러스트는 제 3의 방식을 택했습니다. '소유권(ownership)' 이라는 시스템을 만들고, 컴파일러가 컴파일 중 검사할 여러 규칙을 정해 메모리를 관리하는 방식이죠.
  • 이 방식은 프로그램 실행 속도에 악영향을 줄 일이 없습니다. 컴파일 타임에 전부 해결하니까요.

스택, 힙 영역

러스트 같은 시스템 프로그래밍 언어에서는 값을 스택에 저장하느냐 힙에 저장하느냐의 차이가 프로그램의 동작 및 프로그래머의 의사 결정에 훨씬 큰 영향을 미칩니다.

스택, 힙 둘 다 여러분이 작성한 프로그램이 런타임 중 이용할 메모리 영역이라는 공통점이 있지만 구조는 각각 다릅니다.

  • 스택은 값이 들어온 순서대로 저장하고, 역순으로 제거합니다. 이를 last in, fist out 이라 하죠
    • 스택에 저장되는 데이터는 모두 명확하고 크기가 정해져 있어야 합니다.
  • 컴파일 타임에 크기를 알 수 없거나, 크기가 변경될 수 있는 데이터는 스택 대신 힙에 저장됩니다.
    • 힙은 스택보다 복잡합니다.
    • 데이터를 힙에 넣을때 먼저 저장할 공간이 있는지 운영체제한테 물어봅니다. 그럼 메모리 할당자는 커다란 힙 영역 안에서 어떤 빈 지점을 찾고, 이 지점은 사용 중이라고 표시한 뒤 해당 지점을 가리키는 포인터(pointer) 를 우리한테 반환합니다. 이 과정을 힙 공간 할당(allocating on the heap), 줄여서 할당(allocation) 이라 합니다 (스택에 값을 푸시하는 것은 할당이라 부르지 않습니다). 포인터는 크기가 정해져 있어 스택에 저장할 수 있으나, 포인터가 가리키는 실제 데이터를 사용하고자 할 때는 포인터를 참조해 해당 포인터가 가리키는 위치로 이동하는 과정을 거쳐야 합니다.
  • 스택 영역은 데이터에 접근하는 방식상 힙 영역보다 속도가 빠릅니다.
  • 메모리 할당자가 새로운 데이터를 저장할 공간을 찾을 필요가 없이 항상 스택의 가장 위에 데이터를 저장하면 되기 때문이죠.
  • 반면에 힙에 공간을 할당하는 작업은 좀 더 많은 작업을 요구하는데, 메모리 할당자가 데이터를 저장하기에 충분한 공간을 먼저 찾고 다음 할당을 위한 준비를 위해 예약을 수행해야 하기 때문입니다.
  • 힙 영역은 포인터가 가리키는 곳을 찾아가는 과정으로 인해 느려집니다.
  • 현대 프로세서는 메모리 내부를 이리저리 왔다 갔다 하는 작업이 적을수록 속도가 빨라지는데, 힙에 있는 데이터들은 서로 멀리 떨어져 있어 프로세서가 계속해서 돌아다녀야 하기 때문이죠.
  • 힙 영역처럼 데이터가 서로 멀리 떨어져 있으면 작업이 느려지고, 반대로 스택 영역처럼 데이터가 서로 붙어 있으면 작업이 빨라집니다. 이외에도, 큰 공간을 할당하는 작업도 힙 영역의 속도를 늦추는 요인입니다.
  • 여러분이 함수를 호출하면, 호출한 함수에 넘겨준 값(값 중엔 힙 영역의 데이터를 가리키는 포인터도 있을 수 있습니다)과 해당 함수의 지역 변수들이 스택에 푸시됩니다.
  • 그리고 이 데이터들은 함수가 종료될 때 pop 됩니다.
  • 코드 어느 부분에서 힙의 어떤 데이터를 사용하는지 추적하고, 힙에서 중복되는 데이터를 최소화하고, 쓰지 않는 데이터를 힙에서 정리해 영역을 확보하는 등의 작업은 모두 소유권과 관련되어 있습니다.
  • 반대로 말하면 여러분이 소유권을 한번 이해하고 나면 스택, 힙 영역으로 고민할 일이 줄어들 거란 뜻이지만, 소유권의 존재 이유가 힙 데이터의 관리라는 점을 알고 있으면 소유권의 동작 방식을 이해하는데에 도움이 됩니다.

소유권 규칙

소유권 규칙부터 알아보겠습니다.

  • 러스트에서, 각각의 값은 소유자(owner) 라는 변수가 정해져 있다.
  • 한 값의 소유자는 동시에 여럿 존재할 수 없다.
  • 소유자가 스코프 밖으로 벗어날 때, 값은 버려진다(dropped).

변수의 스코프(Scope)

스코프란, 프로그램 내에서 개체가 유효한 범위를 말합니다.

let s = "hello";

변수 s는 문자열 리터럴을 나타내며, 문자열 리터럴의 값은 코드 내에 하드코딩되어 있습니다.

이 변수는 선언된 시점부터 현재의 스코프를 벗어날 때까지 유효합니다.

중요한 점은 두 가지입니다.

  1. s 가 스코프 내에 나타나면 유효합니다.
  2. 유효기간은 스코프 밖으로 벗어나기 전까지 입니다.

String 타입

소유권 규칙을 설명하려면 3장 "데이터 타입들" 에서 다룬 타입보다 복잡한 타입이 필요합니다.

앞서 다룬 것들은 전부 스택에 저장되고 스코프를 벗어날 때 pop 되는 타입이지만, 이번에 필요한 건 힙에 저장되면서, 러스트의 데이터 정리과정을 알아보는 데 적합한 타입이거든요.

따라서 String 타입을 예제로 활용하되, 여기서 String 타입을 전부 설명할 순 없으므로 자세한 내용은 8장에서 다루고, 이번 장에선 소유권 관련 부분에만 집중하겠습니다.

이러한 관점은 다른 표준 라이브러리나 여러분이 만들 복잡한 데이터 타입에도 적용됩니다.

  • 여태 보아온 문자열은 코드 내에 하드코딩하는 방식의 '문자열 리터럴(string literal)'이었습니다.
  • 문자열 리터럴은 쓰기 편리하지만, 만능은 아닙니다.
  • 그 이유는 문자열 리터럴이 불변성(immutable)을 지니기에 변경할 수 없다는 점과, 프로그램에 필요한 모든 문자열을 우리가 프로그래밍하는 시점에 알 수는 없다는 점 때문입니다.
  • 사용자한테 문자열을 입력받아 저장하는 기능 등을 만들어야 하는 상황에선 문자열 리터럴을 사용할 수 없죠.
  • 따라서 러스트는 또 다른 문자열 타입인 String 을 제공합니다.
  • 이 타입은 힙에 할당되기 때문에, 컴파일 타임에 크기를 알 수 없는 텍스트도 저장할 수 있습니다.
  • String 타입은 다음과 같이 from 함수와 문자열 리터럴을 이용해 생성 가능합니다.
let s = String::from("hello");

이중 콜론(::)은 우리가 함수를 사용할 때, string_from 같은 함수명 대신 String 타입 하위라는 것을 특정해서 함수를 호출할 수 있도록 하려고 사용하는 네임스페이스 연산자입니다.

메소드 관련 문법은 5장 “메소드 문법” 에서 자세히 다루며, 모듈 및 네임스페이스는 7장 “경로를 사용해 모듈 트리에서 항목 가리키기” 에서 다루고 있습니다.

이 String 문자열은 변경 가능합니다:

let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!` ``

하지만, 문자열 리터럴과 String 에 무슨 차이가 있길래 어떤 것은 변경할 수 있고 어떤 것은 변경할 수 없을까요?

차이점은 각 타입의 메모리 사용 방식에 있습니다.

메모리와 할당

  • 문자열 리터럴은 컴파일 타임에 내용을 알 수 있으므로, 텍스트가 최종 실행파일에 하드코딩됩니다.
  • 이 방식은 빠르고 효율적이지만, 문자열이 변하지 않을 경우에만 사용할 수 있습니다.
  • 컴파일 타임에 크기를 알 수 없는 텍스트는 바이너리 파일에 집어넣을 수 없죠.

반면 String 타입은 힙에 메모리를 할당하는 방식을 사용하기 때문에 텍스트 내용 및 크기를 변경할 수 있습니다. 하지만 이는 다음을 의미하기도 합니다:

  • 실행 중 메모리 할당자로부터 메모리를 요청해야 합니다.
  • String 사용을 마쳤을 때 메모리를 해제할 (할당자에게 메모리를 반납할) 방법이 필요합니다.

이 중 첫 번째는 이미 우리 손으로 해결했습니다. String::from 호출 시, 필요한 만큼 메모리를 요청하도록 구현되어 있거든요. 프로그래밍 언어 사이에서 일반적으로 사용하는 방식이죠.

하지만 두 번째는 다릅니다.

  • 가비지 콜렉터 (garbage collector, GC) 를 갖는 언어에선 GC가 사용하지 않는 메모리를 찾아 없애주므로 프로그래머가 신경 쓸 필요 없으나,
  • GC가 없는 언어에선 할당받은 메모리가 필요 없어지는 지점을 프로그래머가 직접 찾아 메모리 해제 코드를 작성해야 합니다. 굉장히 어려운 일이죠.
  • 프로그래머가 놓친 부분이 있다면 메모리 낭비가 발생하고, 메모리 해제 시점을 잘못 잡으면 버그가 생깁니다.
  • 두 번 해제할 경우도 마찬가지로 버그가 발생하겠죠.
  • 따라서 우린 할당(allocate) 과 해제(free) 가 하나씩 짝짓도록 만들어야 합니다.

이 문제를 러스트에선 변수가 자신이 소속된 스코프를 벗어나는 순간 자동으로 메모리를 해제하는 방식으로 해결했습니다.

예시로 보여드리도록 하죠. Listing 4-1 에서 문자열 리터럴을 String 으로 바꿔보았습니다:

{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
}
// this scope is now over, and s is no
// longer valid`
  • 보시면 String 에서 사용한 메모리를 자연스럽게 해제하는 지점이 있습니다.
  • s 가 스코프 밖으로 벗어날 때인데, 러스트는 변수가 스코프 밖으로 벗어나면 drop 이라는 특별한 함수를 호출합니다.
  • 이 함수는 개발자가 직접 메모리 해제 코드를 작성해 넣을 수 있게 되어있으며, 이 경우 String 개발자가 작성한 메모리 해제 코드가 실행되겠죠.
  • drop 은 닫힌 중괄호 } 가 나타나는 지점에서 자동으로 호출됩니다.

Note: C++ 에선 이런 식으로 객체의 수명이 끝나는 시점에 리소스를 해제하는 패턴을 Resource Acquisition Is Initialization (RAII) 라 합니다. RAII 패턴에 익숙하신 분들이라면 러스트의 drop 함수가 친숙할지도 모르겠네요.

  • 이 패턴은 러스트 코드를 작성하는 데 깊은 영향을 미칩니다.
  • 지금은 단순해 보이지만, 힙 영역을 사용하는 변수가 많아져 상황이 복잡해지면 코드가 예기치 못한 방향으로 동작할 수도 있죠.

변수와 데이터 간 상호작용 방식: 이동(move)

러스트에선 동일한 데이터에 여러 변수가 서로 다른 방식으로 상호작용할 수 있습니다.

정수형을 이용한 예제로 살펴보겠습니다.

  • let x = 5; let y = x;
  • x의 정숫값을 y에 대입
    • 5 를 x 에 바인드(bind)하고,
    • x 값의 복사본을 만들어 y 에 바인드
    • 그럼 xy 두 변수가 생길 겁니다. 각각의 값은 5 가 되겠죠.
    • 실제로도 이와 같은데, 정수형 값은 크기가 정해진 단순한 값이기 때문입니다.
    • 이는 다시 말해, 두 5 값은 스택에 push 된다는 뜻입니다.

이번엔 앞선 예제를 String 으로 바꿔보았습니다:

let s1 = String::from("hello"); let s2 = s1;

이전 코드와 매우 비슷하니, 동작 방식도 같을 거라고 생각하실 수도 있습니다.

두 번째 줄에서 s1 의 복사본을 생성해 s2 에 바인딩하는 식으로 말이죠. 하지만 이번엔 전혀 다른 방식으로 동작합니다.

String 은 그림 좌측에서 나타나듯, 문자열 내용이 들어 있는 메모리를 가리키는 포인터, 문자열 길이, 메모리 용량 세 부분으로 이루어져 있습니다.

이 데이터는 스택에 저장되며, 우측의 문자열 내용은 힙에 저장됩니다.

메모리 속 String 의 모습

s1 에 바인드된, "hello" 값을 저장하고 있는 String 의 메모리 속 모습

문자열 길이와 메모리 용량이 무슨 차이인가 궁금하실 분들을 위해 간단히 설명해드리자면,

  • 문자열 길이는 String 의 내용이 현재 사용하고 있는 메모리를 바이트 단위로 나타낸 것이고,

  • 메모리 용량은 메모리 할당자가 String 에 할당한 메모리의 양을 뜻합니다.

  • 이번 내용에서는 길이, 용량 사이의 차이는 중요한 내용이 아니니, 이해가 잘 안 되면 용량 값은 무시하셔도 좋습니다.

  • s2 에 s 을 대입하면 String 데이터가 복사됩니다.

  • 이때 데이터는 스택에 있는 데이터, 즉 포인터, 길이, 용량 값을 말하며, 포인터가 가리키는 힙 영역의 데이터는 복사되지 않습니다. 즉, 다음과 같은 메모리 구조를 갖게 됩니다.

s1, s2 는 동일한 데이터를 가리킵니다

Figure 4-2: 변수 s2 가 s1 의 포인터, 길이, 용량 값을 복사했을 때 나타나는 메모리 구조

다음 Figure 4-3 은 힙 메모리 상 데이터까지 복사했을 경우 나타날 구조로, 실제로는 이와 다릅니다. 만약 러스트가 이런 식으로 동작한다면, 힙 내 데이터가 커질수록 s2 = s1 연산은 굉장히 느려질겁니다.

s1 and s2 to two places

Figure 4-3: 러스트에서 힙 데이터까지 복사할 경우의 s2 = s1 연산 결과

앞서 언급한 내용 중 변수가 스코프 밖으로 벗어날 때 러스트에서 자동으로 drop 함수를 호출하여 해당 변수가 사용하는 힙 메모리를 제거한다는 내용이 있었습니다.

하지만 Figure 4-2 처럼 두 포인터가 같은 곳을 가리킬 경우에는 어떻게 될까요?

s2s1 이 스코프 밖으로 벗어날 때 각각 메모리를 해제하게 되면 중복 해제(double free) 오류가 발생할 겁니다.

이는 메모리 안정성 버그 중 하나이며, 보안을 취약하게 만드는 메모리 손상의 원인입니다.

따라서, 러스트에는 여러 포인터가 한 곳을 가리킬 경우를 대비한 규칙이 존재합니다.

  • s1 에 할당한 메모리를 새로 복사하는 대신, 기존의 s1 을 무효화하는 것이죠.
  • 이로써 러스트는 s1 이 스코프를 벗어나더라도 아무것도 해제할 필요가 없어집니다.
  • 그럼 s2 가 만들어진 이후에 s1 을 사용하면 어떻게 될까요?
  • 결론부터 말씀드리면, 사용할 수 없습니다:

여러분이 다른 프로그래밍 언어에서 “얕은 복사(shallow copy)”, “깊은 복사(deep copy)” 라는 용어를 들어보셨다면, 힙 데이터를 복사하지 않고 포인터, 길이, 용량 값만 복사하는 것을 얕은 복사라고 생각하셨을 수도 있지만,

러스트에선 기존의 변수를 무효화하기 때문에 이를 얕은 복사가 아닌 이동(move) 이라 하고, 앞선 코드는 s1 이 s2 로 이동되었다 라고 표현합니다.

s1 이 s2 로 이동됨

Figure 4-4: s1 이 무효화 된 후의 메모리 구조

이로써 문제가 사라졌네요! s2 만이 유효하니, 스코프 밖으로 벗어나면 그대로 자신의 메모리를 해제하면 됩니다.

덧붙이자면, 러스트는 절대 자동으로 “깊은 복사” 로 데이터를 복사하는 일이 없습니다. 따라서, 러스트가 자동으로 수행하는 모든 복사는 런타임 성능면에서 효율적이라 할 수 있습니다.

변수와 데이터 간 상호작용 방식: 클론(clone)

String 의 힙 데이터까지 깊이 복사하고 싶을 땐 clone 이라는 공용 메소드를 사용할 수 있습니다.

다음은 clone 메소드의 사용 예제입니다:

	let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);`

이 코드의 실행 결과는 힙 데이터까지 복사됐을 때의 메모리 구조를 나타낸 Figure 4-3 과 정확히 일치합니다.

여러분은 이 코드에서 clone 호출을 보고, 이 지점에서 성능에 영향이 갈지도 모르는 코드가 실행될 것을 알 수 있습니다. 즉, clone 은 해당 위치에서 무언가 다른 일이 수행될 것을 알려주는 시각적 지시자이기도 합니다.

스택에만 저장된 데이터: 복사(copy)

아직 다루지 않은 부분이 남았습니다. 다음 코드는 앞서 Listing 4-2 에서 본 정수형을 이용하는 코드입니다 (정상적으로 작동합니다):

let x = 5; let y = x; println!("x = {}, y = {}", x, y);

하지만 이 코드는 방금 우리가 배운 내용과 맞지 않는 것처럼 보이네요. clone 을 호출하지도 않았는데 x 는 계속해서 유효하며 y 로 이동되지도 않았습니다.

  • 이유는 정수형 등 컴파일 타임에 크기가 고정된 타입은 모두 스택에 저장되기 때문입니다.
  • 스택에 저장되니, 복사본을 빠르게 만들 수 있고, 따라서 굳이 y 를 생성하고 나면 x 를 무효화 할 필요가 없습니다.
  • 다시 말해 이런 경우엔 깊은 복사와 얕은 복사 간 차이가 없습니다.
  • 여기선 clone 을 호출해도 얕은 복사와 차이가 없으니 생략해도 상관없죠.

러스트에는 정수형 등 스택에 저장되는 타입에 달아 놓을 수 있는 Copy 트레잇이 있습니다 (트레잇은 10장에서 자세히 다룹니다).

만약 어떤 타입에 이 Copy 트레잇이 구현되어 있다면, 대입 연산 후에도 기존 변수를 사용할 수 있죠. 하지만 구현하려는 타입이나, 구현하려는 타입 중 일부분에 Drop 트레잇이 구현된 경우엔 Copy 트레잇을 어노테이션(annotation) 할 수 없습니다.

즉, 스코프 밖으로 벗어났을 때 특정 동작이 요구되는 타입에 Copy 어노테이션을 추가하면 컴파일 오류가 발생합니다. 여러분이 만든 타입에 Copy 어노테이션을 추가하는 방법은 부록 C의 “Derivable Traits” 을 참고 바랍니다.

그래서, Copy 가능한 타입은 뭘까요? 타입마다 문서를 뒤져 정보를 찾아보고 확신을 얻을 수도 있겠지만, 일반적으로 단순한 스칼라 값의 묶음은 Copy 가능하고, 할당이 필요하거나 리소스의 일종인 경우엔 불가능합니다.

Copy 가능한 타입 목록 중 일부를 보여드리겠습니다.

  • 모든 정수형 타입 (예: u32)
  • truefalse 값을 갖는 논리 자료형 bool
  • 모든 부동 소수점 타입 (예: f64)
  • 문자 타입 char
  • Copy 가능한 타입만으로 구성된 튜플 (예를 들어, (i32, i32) 는 Copy 가능하지만 (i32, String) 은 불가능합니다)

소유권과 함수

함수로 값을 전달하는 행위는 변수에 값을 대입하는 행위와 의미가 유사합니다.

함수에 변수를 전달하면 대입 연산과 마찬가지로 이동이나 복사가 일어나기 때문이죠.

  • 러스트는 takes_ownership 함수를 호출한 이후에 s 를 사용하려 할 경우, 컴파일 타입 오류를 발생시킵니다.
  • 이런 정적 검사들이 프로그래머의 여러 실수를 방지해주죠. 어느 지점에서 변수를 사용할 수 있고, 어느 지점에서 소유권 규칙이 여러분을 제재하는지 확인해보려면 main 함수에 sx 변수를 사용하는 코드를 여기저기 추가해보세요.

반환 값과 스코프

소유권은 값을 반환하는 과정에서도 이동합니다.

상황은 다양할지라도, 변수의 소유권 규칙은 언제나 동일합니다.

  • 어떤 값을 다른 변수에 대입하면 값이 이동하고, 힙에 데이터를 갖는 변수가 스코프를 벗어나면, 사전에 해당 데이터가 이동되어 다른 변수가 소유하고 있지 않은 이상 drop 에 의해 데이터가 제거됩니다.

그럼, 함수가 값을 사용할 수 있도록 하되 소유권은 가져가지 않도록 하고 싶다면 어떻게 해야 할까요?

일단 다음과 같이 튜플을 이용해 여러 값을 돌려받는 게 가능하긴 하다는 걸 알려드리겠습니다:

fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}

당연하지만 이런 방식으로 매개변수의 소유권을 되돌려받는 것은 좋지 않습니다. 거추장스럽고, 작업량이 필요 이상으로 많아지죠. 다행히도, 러스트에는 이 대신 사용할 참조자(references) 라는 기능을 가지고 있습니다.


4.2 참조자와 Borrow

이번에는 값의 소유권을 넘기는 대신 개체의 참조자(reference) 를 넘겨주는 방법을 소개하도록 하겠습니다.

fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

calculate_length 함수에 s1 대신 &s1 을 넘기고, 함수 정의에 String 대신 &String 을 사용했네요.

여기 사용된 앰퍼샌드(&) 기호가 바로 참조자 입니다. 참조자를 사용하면 여러분이 어떤 값의 소유권을 갖지 않고도 해당 값을 참조할 수 있죠. 어떤 원리인지 Figure 4-5 다이어그램으로 알아보겠습니다:

&String s 는 String s1 을 가리킵니다

Figure 4-5: &String s 는 String s1 을 가리킴

Note: & 를 이용한 참조의 반대는 역참조(dereferencing) 라 합니다. 역참조 기호는 * 이며, 8장 에서 몇 번 다뤄보고 15장에서 자세한 내용을 배울 예정입니다.

s1 에 & 를 붙인 &s1 구문은 s1 값을 참조하나, 해당 값을 소유하지 않는 참조자를 생성합니다. 함수 정의에서도 마찬가지로 & 를 사용하여 매개변수 s 가 참조자 타입임을 나타내고 있죠.

참조자는 소유권을 갖지 않으므로, 스코프를 벗어나도 값은 drop 되지 않습니다. 주석으로 보여드리겠습니다.

fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
}
// Here, s goes out of scope.
// But because it does not have ownership of what
// it refers to, nothing happens.`

변수 s 가 유효한 스코프는 여타 함수의 매개변수에 적용되는 스코프와 동일합니다. 하지만 참조자에는 스코프를 벗어났을 때 값이 drop 되지 않는다는 차이점이 있고, 따라서 참조자를 매개변수로 갖는 함수는 소유권을 되돌려주기 위해 값을 다시 반환할 필요도 없습니다.

또한, 이처럼 참조자를 매개변수로 사용하는 것을 borrow(빌림) 이라 합니다. 현실에서도 다른 사람이 소유하고 있는 뭔가를 빌리고, 용무가 끝나면 돌려주는 것처럼요.

변수가 기본적으로 불변성을 지니듯, 레퍼런스도 마찬가지로 참조하는 것을 수정할 수 없습니다.

가변 참조자 (Mutable Reference)

```rust
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}

s 를 mut 로 변경하고, 참조자 생성 코드를 &mut s 로 변경해 가변 참조자를 생성하게 만든 뒤, 함수에서 가변 참조자를 넘겨받도록 some_string: &mut String 으로 수정하는 겁니다.

다만, 가변 참조자에는 특정 스코프 내 어떤 데이터를 가리키는 가변 참조자를 딱 하나만 만들 수 있다는 제한이 있다는 걸 알아두세요.

이 제약으로 인해 가변 참조자는 남용이 불가능합니다. 대부분의 언어에선 원하는 대로 값을 변경할 수 있으니, 러스트 입문자가 익숙해지지 못해 고생하는 원인이기도 하죠.

하지만, 이 제약 덕분에 러스트에선 컴파일 타임에 데이터 레이스(data race) 를 방지할 수 있습니다. 데이터 레이스란 **다음 세 가지 상황이 겹칠 때 일어나는 특정한 레이스 조건(race condition)**입니다:

멀티쓰레드 관련(함수형 프로그래밍이 각광 받은 이유)

  • 둘 이상의 포인터가 동시에 같은 데이터에 접근
  • 포인터 중 하나 이상이 데이터에 쓰기 작업을 시행
  • 데이터 접근 동기화 매커니즘이 존재하지 않음

데이터 레이스는 정의되지 않은 동작을 일으키며, 런타임에 추적하려고 할 때 문제 진단 및 수정이 어렵습니다. 하지만 러스트에선 데이터 레이스가 발생할 가능성이 있는 코드는 아예 컴파일되지 않으니 걱정할 필요가 없죠.

가변 참조자는 불변 참조자가 존재하는 동안에도 생성할 수 없습니다. 불변 참조자를 사용할 때 가변 참조자로 인해 값이 중간에 변경되리라 예상하지 않으니까요. 반면 불변 참조자는 데이터를 읽기만 하니 외부에 영향을 주지 않아 여러 개를 만들 수 있습니다.

참조자는 정의된 지점부터 시작해, 해당 참조자가 마지막으로 사용된 부분까지 유효합니다.

댕글링 참조자(Dangling Reference)

댕글링 포인터(dangling pointer) 란, 어떤 메모리를 가리키는 포인터가 남아있는 상황에서 해당 메모리를 해제해버림으로써, 다른 개체가 할당받았을지도 모르는 메모리를 참조하게 된 포인터를 말합니다.

포인터가 있는 언어에선 자칫 잘못하면 이 댕글링 포인터를 만들기 쉽죠. 하지만 러스트에선 어떤 데이터의 참조자를 만들면, 해당 참조자가 스코프를 벗어나기 이전에 데이터가 먼저 스코프를 벗어나는지 컴파일러에서 확인하여 댕글링 참조자가 생성되지 않도록 보장합니다.

한번, 컴파일 타임 에러가 발생할 만한 댕글링 참조자를 만들어 봅시다:

fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String {
let s = String::from("hello");
&s
}
fn main() {
let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
// Danger!

s 는 dangle 함수 내에서 생성됐기 때문에, 함수가 끝날 때 할당 해제됩니다.

하지만 코드에선 &s 를 반환하려 했고, 이는 유효하지 않은 String 을 가리키는 참조자를 반환하는 행위이기 때문에 오류가 발생합니다.

따라서, 이런 경우엔 String 을 직접 반환해야 합니다.

참조자 규칙

배운 내용을 정리해 봅시다:

  • 여러분은 단 하나의 가변 참조자만 갖거나, 여러 개의 불변 참조자를 가질 수 있습니다.
  • 참조자는 항상 유효해야 합니다.

다음으로 알아볼 것은 참조자의 또 다른 종류인 슬라이스(slice) 입니다.


4.3 슬라이스(Slice)


소유권을 갖지 않는 또 하나의 타입은 슬라이스(slice) 입니다. 이 타입은 컬렉션(collection) 을 통째로 참조하는 것이 아닌, 컬렉션의 연속된 일련의 요소를 참조하는 데 사용합니다.

한번 간단한 함수를 만들어 봅시다.

  • 문자열을 입력받아,
  • 해당 문자열의 첫 번째 단어를 반환하는 함수를요.
  • 공백문자를 찾을 수 없는 경우엔 문자열 전체가 하나의 단어라는 뜻이니 전체 문자열을 반환하도록 합시다.
  • first_word 함수는 소유권을 가질 필요가 없으니 &String 을 매개변수로 갖게 했습니다.
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
  • String 을 하나하나 쪼개서 해당 요소가 공백 값인지 확인해야 하므로, as_bytes 메소드를 이용해 바이트 배열로 변환
  • 그 다음, 바이트 배열에 사용할 반복자(iterator)를 iter 메소드로 생성했습니다:
  • iter 메소드는 컬렉션의 각 요소를 반환하고, enumerate 메소드는 iter 의 결과 값을 각각 튜플로 감싸 반환한다는 것만 알아두도록 합시다.
  • 이때 반환하는 튜플은 첫 번째 요소가 인덱스, 두 번째 요소가 해당 요소의 참조자로 이루어져 있습니다.
  • enumerate 메소드가 반환한 튜플은 패턴을 이용해 해체하였습니다.
    • 따라서 for 루프 내에서 i 는 튜플 요소 중 인덱스에 대응하고, &item 은 바이트에 대응됩니다.
    • 이때 & 를 사용하는 이유는 우린 iter().enumerate() 에서 얻은 요소의 참조자가 필요하기 때문입니다.
  • for 반복문 내에선 바이트 리터럴 문법으로 공백 문자를 찾고, 찾으면 해당 위치를 반환합니다.
  • 찾지 못했을 땐 s.len() 으로 문자열의 길이를 반환합니다.
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word will get the value 5
s.clear(); // this empties the String, making it equal to ""
// word still has the value 5 here, but there's no more string that
// we could meaningfully use the value 5 with. word is now totally invalid!
}

Listing 4-8: first_word 함수의 결과를 저장했으나, 이후에 String 의 내용이 변경된 상황

이 코드는 문법적으로 전혀 문제없고, 정상적으로 컴파일됩니다. s.clear() 을 호출한 후에 word 를 사용하는 코드를 작성하더라도, word 는 s 와 분리되어 있으니 결과는 마찬가지죠. 하지만 word 에 담긴 값 5 를 본래 목적대로 s 에서 첫 단어를 추출하는 데 사용할 경우, 버그를 유발할 수도 있습니다. s 의 내용물은 변경되었으니까요.

문자열 슬라이스

문자열 슬라이스(String Slice)는 String 의 일부를 가리키는 참조자를 말합니다:

	let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];

만드는 방식은 String 참조자와 유사하지만, [0..5] 가 추가로 붙어 있네요. 이는 String 전체가 아닌 일부만 가리킨다는 것을 의미합니다.

  • [starting_index..ending_index] 는 starting_index 부터 시작해 ending_index 직전, 즉 ending_index 에서 1을 뺀 위치까지 슬라이스를 생성한다는 의미입니다.
  • 슬라이스는 내부적으로 시작 위치, 길이를 데이터 구조에 저장하며, 길이 값은 ending_index 값에서 starting_index 값을 빼서 계산합니다.
  • 따라서 let world = &s[6..11]; 의 world 는 시작 위치로 s 의 (1부터 시작하여) 7번째 바이트를 가리키는 포인터와, 길이 값 5를 갖는 슬라이스가 되겠죠.

world 는 String s 의 7번째 바이트를 가리키는 포인터와, 길이 값 5 를 갖습니다

.. 범위 표현법은 맨 첫 번째 인덱스부터 시작하는 경우, 앞의 값을 생략할 수 있습니다. 즉 다음 코드에 등장하는 두 슬라이스 표현은 동일합니다:

	let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

마찬가지로, String 맨 마지막 바이트까지 포함하는 슬라이스는 뒤의 값을 생략할 수 있습니다. 다음 코드에 등장하는 두 슬라이스 표현은 동일합니다:

	let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

앞뒤 모두 생략할 경우, 전체 문자열이 슬라이스로 생성됩니다. 다음 코드에 등장하는 두 슬라이스 표현은 동일합니다:

	let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];

Note: 본 내용은 문자열 슬라이스를 소개할 목적이기에 ASCII 문자만 사용하여 문제가 발생하지 않았지만, 문자열 슬라이스 생성 시 인덱스는 절대 UTF-8 문자 바이트 중간에 지정해선 안 됩니다. 멀티바이트 문자 중간에 생성할 경우 프로그램은 오류가 발생하고 강제 종료됩니다. UTF-8 문자를 다루는 방법은 8장 “Storing UTF-8 Encoded Text with Strings” 절에서 자세히 알아볼 예정입니다.

여태 다룬 내용을 잘 기억해두고, first_word 함수가 슬라이스를 반환하도록 재작성해보죠. 문자열 슬라이스를 나타내는 타입은 &str 로 작성합니다.

fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}

이제 first_word 가 반환하는 값은 원래 데이터와 분리된 값이 아닙니다. 원래 데이터에서 슬라이스 시작 위치를 가리키는 참조자와, 슬라이스 요소 개수로 구성된 값이죠.

이전 절에서 배운 borrow 규칙 중, 특정 대상의 불변 참조자가 이미 존재할 경우에는 가변 참조자를 만들 수 없다는 규칙이 있었죠. clear 함수는 String 의 길이를 변경해야 하니 가변 참조자가 필요하지만 이미 불변 참조자가 존재하므로 오류가 발생하게 됩니다

그 외 슬라이스

문자열 슬라이스는 문자열에만 특정되어 있습니다. 물론 문자열에만 사용할 수 있는 것이 아닌 더 범용적인 슬라이스 타입도 존재합니다:

let a = [1, 2, 3, 4, 5];

문자열 일부를 참조할 때처럼 배열 일부를 참조할 수 있죠:

let a = [1, 2, 3, 4, 5]; let slice = &a[1..3];

이 슬라이스는 &[i32] 타입입니다. 동작 방식은 문자열 슬라이스와 동일합니다. 슬라이스의 첫 번째 요소를 참조하는 참조자와 슬라이스의 길이를 저장하여 동작하죠. 이런 슬라이스는 모든 컬렉션에 사용 가능합니다(컬렉션은 8장에서 자세히 알아볼 예정입니다).

요약

  • 러스트는 소유권, borrow, 슬라이스는 러스트가 컴파일 타임에 메모리 안정성을 보장하는 비결입니다.
  • 여타 시스템 프로그래밍 언어처럼 프로그래머에게 메모리 사용 제어 권한을 주면서, 어떠한 데이터의 소유자가 스코프를 벗어날 경우 자동으로 해당 데이터를 정리하는 것이 가능하죠.
  • 또한 제어 코드를 추가 작성하고 디버깅할 필요가 사라지니 프로그래머의 일이 줄어드는 결과도 가져옵니다.

소유권은 수많은 러스트 요소들의 동작 방법에 영향을 미치는 개념인 만큼 이후 내용에서도 계속해서 다룰 예정입니다. 그럼 이제 5장에서 struct 로 여러 데이터를 묶는 방법을 알아보죠.

직급별 회사생활 1타 강의

· 8 min read
brown
FE developer

유투브 알고리즘으로 퇴사한 이형이라는 유투버의 영상들을 우연히 보기 시작했는데, 컨텐츠에 깊이가 있고 와닿는 부분이 많았다.

개발자로서 개발을 잘하는 것도 중요하지만, 조직에서 인정받기 위해 일을 잘하는 방법, 회사에서의 자세, 커뮤니케이션 등에 대해 좋은 인사이트들을 정리 및 숙지할 필요성을 느끼고 문서화한다.

회사 생활(공통역량)에 관한 부분은 퇴사한 이형, 개발자의 자세 쪽은 포프tv 에서 와닿는 부분이 많은 것 같다.

신입사원은 도대체 뭘 해야 하는가 (feat. 1-3년 차)


https://www.youtube.com/watch?v=qL77xKKGpiw

신입: 3년 미만(중고, 생, 인턴 포함)

  • 배우려는 자세가 key point
  • 신입은 경력이 아니다.
  • 겸손 필수
    1. 일단 웃자 - 신입만의 에너지 - 존재로서 분위기, 문화에 영향
    2. 질문을 많이해라 - 개념 없는 질문 용인되는 시기(6개월 이내) - 프로세스(결재 방법 등) 및 기준에 대한 질문 - 회의시간 말고, 밥/티타임 활용 - 냉소적인 답변자는 피해라
    3. 시간을 지켜라 - 출근시간 및 데드라인 엄수

일 잘하는 대리의 특징 (feat. 3~7년 차)


유튜브 알고리즘에서 나를 사로 잡은 컨텐츠... 보면서 정말 많은 부분에서 공감했다.

  • 대리직급에서 가장 중요한 것은 상사이다.
  • 실질적인 에너지는 부하직원에게 더 쏟게되지만(완전 공감)
  • 하향으로 에너지를 쓰면 안되고, 상향으로 에너지를 써야한다.
    • 부하직원이 따라오는지에 대한 부분은 해당 직원의 역량에 달린 것이다.
    • 배우는 자세가 되어있다면, 어떻게든 따라는 올 것이다.
    • 상사에게 초점을 맞추는 것은 부하직원이 할 수 없음
    • 주어진 업무, 일정을 지키지 못하는데 부하직원을 챙기는 것은 멍청한 짓
  • A급 대리
    • 주도적으로 일하는 사람 -> 시키지 않았는데도 그 이상이 나오는 사람
  • B급 대리
    • 시키는 것을 완수해 내는 사람
  • C급 대리
    • 시키는 것도 못하는 사람 -> 일정을 못맞추는 것도 C급이다.
  • 상사가 요청한 내용, 퀄리티, 타이밍을 최우선으로 처리해야한다.
    • 부하직원이 말을 못알아들을때는 시간을 그냥 주면 되지만
    • 업무에 관해서, 상사와의 신뢰관계가 무너진다면 굉장한 마이너스

대리는 실무의 완성도를 높이는 타이밍과 퀄리티, 과제에 집중하는 것이 가장 좋다.

  1. 혼자서 모든 일을 다하려 하지말자
    • 부하직원이 말을 못알아들어도 키워서 업무분장을 넘겨줄 수 있어야 한다.
    • 어짜피 모든 일을 혼자서 처리할 수 없고, 그런 상황에 처한다면 어느 부분에서 문제 생길 여지가 높다.
  2. 리더 십을 준비
    • 그 이상의 단계의 직급을 맡게 되었을 때, 어떻게 일할 지를 미리 생각을 하면서 시행착오를 쌓는게 대리
    • 리더십이라는게 영향력이고 사람을 활용하는 것이다.
  • 사람을 활용하는 3가지 방법
    1. 커뮤니케이션 -> 부탁지시를 할 줄 알아야 한다.
      • 대리의 수준이라면 혼자서 일하는게 당연히 편하다.
      • 하지만 부하직원을 키우고 활용하는 능력을 이때 키워야 한다.
    2. 대상을 명확히 아는 것
      • 부하직원은 말을 못알아들어도 키워서 업무분장을 넘겨줄 수 있어야 한다.
      • 동료에게는 부탁하는 연습을 해야한다. 일을 줄이는데 에너지를 써야한다.
    3. 상사에게 요청 하는 것
      • 상사에게 요청하는 것이 더 빨리 성과를 낼 수 있다고, 기획을 제안하는 것
      • 상사와 fit을 맞추는게 중요하다.
  • 사람에 있어서, 모든 노력이라는 것은 그사람의 역량뿐만 아니라 도움을 요청할 수 있는 모든 방법을 고려하자.

과장은 뭐하는 사람인가 (feat, 8~15년 차)


  1. 과장의 업무 중 가장 중요한 부분은 기획이다.

    • 기획은 업무의 큰 그림이 있어야 한다.
    • 옵션을 잘 설정해서(보통 3가지), 상사의 의사결정을 주도할 수 있어야 한다.
    • 대리급은 옵션들을 정리만 해도 되지만, 과장급부터는 주도적으로 제안할 수 있어야 한다.
  2. 상사의 상사를 관찰해야 한다.

    • 상사의 상사가 상사에게 이런 일을 왜 시켰는지, 히스토리를 따져봐야한다.
    • 의도를 정확히 파악해야한다.
  3. 의사소통이 정말 중요하다.

    • 대리급까지는 상사를 잡아야 하지만, 과장은 위,아래,좌우를 다 교통정리 해야한다.(360도 리더십)
    • 여기서 의사소통이 잘 되어야 밑에서 일하기 쉬움
  4. 과장에서 그 위로 올라가려면, 업무 기획 능력, 커뮤니케이션이 갖춰져도 리더십 이 없으면 안됀다.

    • 사람을 통해서 일을 처리하고, 일을 통해서 사람을 키워야 함(업무 및 커뮤니케이션 능력 동시 요구)
    • 부하직원을 키워내고 관리하는 능력
      • 일을 잘 맡기기(사람에 대해 파악하기, 영감을 주기)
      • 정례적 1:1면담 하기(목표, 전략, 결과물에 대한 확인, 아이디어 구하기)
    • 자신의 부하직원을 경쟁상대로 인식하지 말것

차장이상부터는 나와는 너무 먼 얘기라, 정리는 하지 않지만 시리즈가 너무 재밌어서 다 봤다.

이 글을 보시는 모든 분들께 추천드리며, 건승을 빈다.


D3 관련 정리

· 3 min read
brown
FE developer

Intro


D3로 차트를 구현해달라는 요구사항을 받았었다.

  • horizontal Pannable
  • hover
  • double line

참고로 태그는 z-index, SVG는 그려진 순서대로 쌓여나간다

기존에 trading-view 차트도 다뤄보고 canvas로 차트도 그려봤으니 어렵게 생각하진 않았던 task 였는데

생각보다 어려웠고, 시간이 더 걸렸다.

  • 오래 된 library 라서 코드가 조금씩 다르다.
    • v7로 작업했는데, 인터넷에 돌아다니는 예시코드, 듀토리얼등은 대부분 v3,v4
  • 러닝 커브가 예상 보다 높아서, 그냥 갖다 붙이면 되는 라이브러리가 아니라 학습이 필요하다.

그래서 새로운 한국인 사용자를 위해 기록을 남긴다.

D3 간단 설명


D3 (Data-Driven Documents or D3.js) is a JavaScript library for visualizing data using web standards. D3 helps you bring data to life using SVG, Canvas and HTML. D3 combines powerful visualization and interaction techniques with a data-driven approach to DOM manipulation, giving you the full capabilities of modern browsers and the freedom to design the right visual interface for your data.

D3.js는 데이터를 기반으로 문서를 조작하기 위한 자바스크립트 라이브러리이다. D3는 HTML, SVG 및 CSS를 사용하여 데이터를 생동감 있게 만들 수 있도록 도와줍니다.

D3 with React

NextJS + typescripts 로 사용했다.

관련 레퍼런스

D3 study


밑바닥 부터 시작하는 d3.js (version 4) 와 Typescript 를 이용한 데이터 가시화

영어자료가 한글로 번역 된 자료

dashingd3js

한번 쭉 따라해보면, 도움되는 사이트

중요한 파트는 Use D3.js To Bind Data to DOM Elements이다.

관련 레퍼런스

중요한 개념들은

  • 추상 셀렉션
  • data, datam
  • scale
  • range
  • Axis(축?)
  • d3로 event 다루기

이정도 학습하면 어지간한 건 찾아가면서 구현할 수 있을 것이다!!

Chap.3 common-programming-concepts

· 32 min read
brown
FE developer

변수, 타입, 함수, 주석, 제어문에 대해서 배울 것입니다.

3.1 변수와 가변성


  • 변수는 기본적으로 불변입니다.
  • 이것은 러스트가 제공하는 안정성과 쉬운 동시성이라는 이점을 얻을 수 있는 방향으로 코드를 쓰게 하는 강제사항(nudge)중 하나입니다.
  • mut를 사용해 가변으로 만들 수 있음
    • let mut x = 5;
  • 버그를 방지하는 것 외에도 고려해야 할 비용이 있습니다.(함수형 프로그래밍의 불변성 컨셉에 대한 내용)
    • 예를 들어, 큰 데이터 구조를 사용할 때, 인스턴스를 알맞게 가변으로 설정하는 것은 새로 인스턴스를 할당하고 복사해서 돌려주는 것보다 빠를 수 있습니다.
    • 작은 데이터 구조라면, 새 인스턴스를 만들고 더 함수형 프로그래밍 스타일 로 작성하는 것이 더 흐름을 따라가기 쉽기 때문에, 퍼포먼스가 느려지더라도 명확성을 얻는 것에 대한 패널티로 받아들이는 것이 좋을 수 있습니다.

변수와 상수의 차이

  • 먼저, mut와 상수를 함께 사용할 수 없음 - 상수는 항상 불변
  • 상수는  const 키워드로 선언하며, 값의 타입은 반드시 어노테이션이 달려야 합니다.
  • 마지막 차이점은, 상수는 반드시 상수 표현식이어야 하고 함수의 결과값이나 런타임에 결정되는 그 어떤 값이어도 안된다는 것입니다.
  • 상수를 위한 러스트의 작명 관례는 대문자 스네이크 표기법
    • const MAX_POINTS: u32 = 100_000;

덮어쓰기

  • 새 변수를 이전 변 수명과 같은 이름으로 선언할 수 있고,
  • 새 변수는 이전의 변수를 덮어씁니다.
  • 러스트인들은 첫 번째 변수가 두 번째 변수에 의해 덮어쓰였다라고 표현
    • let x = 5;
    • let x = x + 1;
    • let x = x * 2; -> x is: 12
  • 덮어쓰기는 변수를 mut로 표시하는 것과는 다릅니다.
    • let 키워드 없이 값을 재할당 하려고 한다면 컴파일-타임 에러가 발생하기 때문입니다.
  • mut과 덮어쓰기의 또다른 차이점은, 같은 변수명으로 다른 타입의 값을 저장할 수 있다는 것입니다.

3.2 데이터 타입


러스트에서 사용되는 모든 값들은 어떤 타입을 갖습니다.

그러니 어떤 형태의 데이터인지 명시하여 러스트 컴파일러가 데이터를 어떤 식으로 다룰 수 있는지 알게끔 해야합니다. 여기서는 타입을 스칼라 타입과 복합 타입, 두 가지 부분 집합으로 나누어 보겠습니다.

러스트는 타입이 고정된 (statically typed) 언어라는 점을 주지하세요.

이게 의미하는 바는 모든 변수의 타입이 컴파일 시점에 반드시 정해져 있어야 한다는 겁니다.

보통 컴파일러는 우리가 값을 어떻게 사용하는지에 따라 타입을 추측할 수 있습니다.

하지만 타입의 선택 폭이 넓은 경우는 다음과 같이 반드시 타입을 명시해야 합니다 let guess: u32 = "42".parse().expect("Not a number!");

스칼라 타입

  • 스칼라 타입은 하나의 값을 표현합니다.(JS의 primitive)
    • integers,
    • floating-point numbers
    • Booleans
    • characters.

정수형

  • 정수형_은 소수점이 없는 숫자입니다.
  • 부호 있는 타입은  i로 시작
  • 부호 없는 타입은  u로 시작
LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize
  • 부호 있는 (signed), 부호 없는(unsigned)으로 나뉨
    • 오직 양수만을 가질 것인지
    • 부호와 함께 다뤄야 하는 경우 숫자는 더하기 혹은 빼기 기호와 함께 표시하죠.
    • 하지만 숫자가 양수라고 가정해도 문제 없는 상황에는 부호 없이 표시하게 됩니다.
  • 각 부호 있는 타입의 변수는 -(2n - 1) 부터 2n - 1 - 1 까지의 값을 포괄합니다.
  • 여기서  n_은 사용되는 타입의 비트 수 입니다.
    • 따라서 i8은 -(27) 에서 27 - 1 까지의 값, 즉 -128 에서 127 사이의 값을 저장할 수 있습니다.
    • 부호 없는 타입은 0 에서 2n - 1 까지의 값을 저장할 수 있습니다.
    • 그래서 u8 타입은 0 에서 28 - 1 다시 말해, 0 에서 255 까지의 값을 저장할 수 있습니다.
  • 추가로, isize와 usize 타입은 여러분의 프로그램이 동작하는 컴퓨터 환경에 따라 결정됩니다.
    • 64-bit 아키텍처이면 64bit를, 32-bit 아키텍처이면 32bit를 갖게 됩니다.
  • 정수형 리터럴은 Table 3-2에서 보시는 것과 같은 형태로 작성할 수 있습니다.
  • byte 리터럴을 제외한 모든 정수형 리터럴에는 57u8과 같은 타입 접미사와 1_000과 같이 시각적인 구분을 위한 _을 사용할 수 있습니다.
Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

그러면 어떤 타입의 정수를 사용해야 하는지는 어떻게 알까요? 확실하게 정해진 경우가 아니면 러스트의 기본 값인 i32가 일반적으로는 좋은 선택입니다.

이 타입이 일반적으로 가장 빠르기 때문이죠. 심지어 64-bit 시스템에서도요. isize나 usize는 주로 컬렉션 타입 종류의 인덱스에 사용됩니다.

정수 오버플로우
  • 여러분이 0과 255 사이의 값을 담을 수 있는 u8 타입의 변수를 갖고 있다고 해봅시다.

  • 만약에 이 변수에 256처럼 범위 밖의 값으로 변경하려고 하면 정수 오버플로우 (integer overflow) 가 일어납니다.

  • 코드를 디버그 모드에서 컴파일하는 경우에는 런타임에 정수 오버플로우가 발생했을 때 패닉 (panic) 을 발생시키도록 검사합니다.

    • 러스트에서는 에러가 발생하면서 프로그램이 종료되는 경우 패닉이라는 용어를 사용합니다.
  • --release 플래그를 사용하여 코드를 릴리즈 모드로 컴파일하는 경우에는 패닉을 발생시키는 정수 오버플로우 검사를 실행파일에 포함시키지 않습니다.

    • 대신 오버플로우가 발생하면 러스트는 2의 보수 감싸기 (two's complement wrapping) 을 수행합니다. 짧게 설명하자면, 해당 타입이 가질 수 있는 최대값보다 더 큰 값은 허용되는 최소값으로 “돌아갑니다 (wrap around)”.
    • u8의 경우 256은 0이, 257은 1이 되는 식입니다. 프로그램은 패닉을 발생시키지 않으나, 해당 변수는 아마도 여러분이 예상치 못했던 값을 갖게 될겁니다.
    • 정수 오버플로우의 감싸기 동작에 의존하는 것은 에러로 간주됩니다.

명시적으로 오버플로우의 가능성을 다루기 위해서는 표준 라이브러리가 기본 수치 타입에 대해 제공하는 아래 메소드 종류들을 사용할 수 있습니다:

  • wrapping_add와 같은 wrapping_* 메소드로 감싸기 동작 실행하기
  • checked_* 메소드를 사용하여 오버플로우가 발생하면 None 값 반환하기
  • overflowing_* 메소드를 사용하여 값과 함께 오버플로우 발생이 있었는지를 알려주는 boolean 값 반환하기
  • saturating_* 메소드를 사용하여 값의 최대 혹은 최소값 사이로 제한하기

부동 소수점 타입

러스트의 부동소수점 타입은 f32와 f64로, 각각 32bit와 64bit의 크기를 갖습니다.

기본 타입은 f64인데, 그 이유는 현대의 CPU 상에서 f64가 f32와 대략 비슷한 속도를 내면서도 더 정밀하기 때문입니다.

다음은 부동소수점 숫자의 용례입니다: - let x = 2.0; // f64 - let y: f32 = 3.0; // f32

  • 부동소수점 숫자는 IEEE-754 표준을 따릅니다.
  • f32 타입은 1배수 정밀도 (single-precision)인 부동소수점이고,
  • f64는 2배수 정밀도(double-precision)입니다.

수치 연산

러스트는 모든 숫자 타입에 대해서 여러분이 기대하는 기본 수학 연산 기능을 제공합니다.

부록 B에 Rust가 제공하는 모든 연산자 목록이 있습니다.

Boolean 타입

  • 러스트에서의 boolean 타입도 true와 false 둘 중 하나의 값만 갖습니다.
  • boolean 값은 1 byte 크기입니다.
  • 러스트에서 boolean 타입은 bool로 명시됩니다.

문자 타입

러스트의 char는 이 언어의 가장 기본적인 알파벳 타입입니다.

스트링 리터럴이 큰따옴표를 쓰는 것에 반면, char 타입은 작은따옴표로 쓰는 점을 주목하세요.

  • let c = 'z';

  • let heart_eyed_cat = '😻';

  • 러스트의 char타입은 4 byte 크기이며

  • 유니코드 스칼라 값을 표현하는데, 이는 ASCII 보다 훨씬 더 많은 값을 표현할 수 있다는 의미입니다.

  • 억양 표시가 있는 문자, 한국어/중국어/일본어 문자, 이모지, 넓이가 0인 공백문자 모두가 러스트에서는 유효한 char 값입니다.

  • 유니코드 스칼라 값의 범위는 U+0000에서 U+D7FF, 그리고 U+E000에서 U+10FFFF입니다.

  • 하지만 “문자”는 유니코드를 위한 개념이 아니기 때문에, “문자”에 대한 여러분의 직관은 char와 들어맞지 않을지도 모릅니다. 8장의 “문자열에 UTF-8 텍스트를 저장하기” 에서 이 주제에 대해 자세히 다루겠습니다.

복합 타입

복합 타입 (compound type) 은 여러 값들을 하나의 타입으로 묶을 수 있습니다.

러스트는 튜플(tuple)배열(array) 두 가지 기본 복합 타입을 제공합니다.

튜플 타입

튜플은 다양한 타입의 여러 값들을 묶어 하나의 복합 타입으로 만드는 일반적인 방법입니다.

튜플은 고정된 길이를 갖습니다. 즉, 한번 선언되면 그 크기를 늘리거나 줄일 수 없습니다.

  • 튜플 내의 각 위치는 타입을 갖고, 이 튜플 내의 타입들은 서로 달라도 됩니다. - let tup: (i32, f64, u8) = (500, 6.4, 1);

  • 튜플은 하나의 복합 원소로 취급되므로, 변수 tup은 튜플 전체가 바인딩됩니다.

  • 튜플로부터 개별 값을 얻어오려면 아래와 같이 구조해체 (destructuring)를 하여 튜플 값을 해체하면 사용하면 됩니다

    • `let (x, y, z) = tup
  • 마침표(.) 뒤에 접근하고자 하는 값의 인덱스를 쓰는 방식으로도 값을 얻을 수 있습니다.

    • let six_point_four = tup.1;

배열 타입

여러 값들의 집합체를 만드는 다른 방법으로는 배열이 있습니다.

  • 튜플과는 달리 배열의 모든 요소는 모두 같은 타입이여야 합니다.
  • 러스트의 배열은 튜플과 마찬가지로 고정된 길이를 갖습니다.
  • 여러분이 힙보다는 스택에 데이터를 할당하고 싶을 때나 (힙과 스택은 4장에서 더 다루겠습니다) 항상 고정된 개수의 원소로 이루어진 경우라면 배열이 유용합니다.
  • 하지만 배열은 벡터 타입처럼 유연하지는 않습니다.
    • 벡터는 표준 라이브러리가 제공하는 배열과 유사한 컬렉션 타입인데 크기를 늘리거나 줄일 수 있습니다. 배열을 이용할지 혹은 벡터를 이용할지 잘 모르겠다면, 아마도 벡터를 사용해야 할 겁니다.
    • 8장에서 벡터에 대해 더 자세히 다룰 예정입니다.
  • 예시
    • let a = [1, 2, 3, 4, 5];
    • let a: [i32; 5] = [1, 2, 3, 4, 5]; 배열 원소의 갯수 기입
    • let a = [3; 5]; 3의 값을 가진 원소 5개 [3,3,3,3,3]
배열 요소에 접근하기
  • 배열은 스택에 할당된 단일 메모리 뭉치입니다.
  • 인덱스를 통해 배열 요소에 접근할 수 있습니다
유효하지 않은 배열 요소에 대한 접근
  • 만약 배열의 끝을 넘어선 요소에 접근하려고 하면 컴파일은 되지만 실행 시에 에러가 발생하며 멈추게 됩니다.
  • 컴파일 시점에서는 아무런 에러도 발생하지 않지만, 프로그램은 런타임 (runtime) 에러를 발생시켰고 성공적으로 끝나지 못했습니다.
    • 빌드할 때 error: this operation will panic at runtime 에러와 함께 빌드 안됌
    • could not compile hello-rust due to previous error

여러분이 인덱스를 이용해 원소에 접근 시도를 할 때, 러스트는 여러분이 명시한 인덱스가 배열 길이보다 작은지 검사할 것입니다. 인덱스가 배열 길이보다 크거나 같을 경우 러스트는 패닉(panic)을 일으킵니다.


함수


  • main 함수는 많은 프로그램의 시작 지점입니다.
  • 새로운 함수를 선언하도록 해주는 fn 키워드
  • 러스트 코드는 함수나 변수 이름을 위한 관례로 스네이크 케이스 (snake case) 방식을 이용합니다.
  • 러스트는 여러분의 함수 위치를 고려하지 않으며, 어디든 정의만 되어 있으면 됩니다.

함수 매개변수

  • 함수는 매개변수 (parameter) 를 갖도록 정의될 수 있으며, 이는 함수 시그니처 (function signiture) 의 일부인 특별한 변수입니다.
  • 함수가 매개변수를 갖고 있으면 이 매개변수를 위한 고정값(concrete value)을 전달할 수 있습니다.
  • 전문용어로 이런 고정값을 인자 (argument) 라고 부르지만, 사람들은 보통 매개변수와 인자라는 용어를 함수 정의부 내의 변수나 함수 호출시 집어넣는 고정값에 대해 말할 때 혼용하는 경향이 있습니다.
    • 엄밀히 말해서 parameter는 함수의 정의부분에 나열되어 있는 변수들을 의미하며,
    • argument는 함수를 호출할때 전달되는 실제 값을 의미한다.
    • 이같은 의미를 명확히 하기 위해 parameter는 변수(variable)로, argument는 값(value)으로 보는 것이 일반적이다.

함수 본문은 구문과 표현식으로 구성됩니다

  • 함수 본문은 필요에 따라 표현식(expression)으로 종결되는 구문(statement)의 나열로 구성됩니다.
  • 지금까지는 종결 표현식이 없는 함수만 다뤘지만, 구문의 일부분으로 표현식이 쓰인건 보셨습니다.
  • 러스트는 표현식 기반의 언어이므로, 구문과 표현식의 구분은 러스트 이해에 중요합니다.
  • 다른 언어들은 이런 구분이 없으므로, 구문과 표현식이 무엇이며 둘 간의 차이가 함수의 본문에 어떤 영향을 주는지 살펴보겠습니다.

구문(statement)은 어떤 동작을 수행하고 값을 반환하지 않는 명령입니다. 표현식(expression)은 결과 값을 산출해냅니다.

  • let 키워드로 변수를 만들고 값을 할당하는 것은 구문입니다.
    • 이것이 C나 Ruby 같은 다른 언어와의 차이점인데, 이 언어들은 할당문이 할당된 값을 반환하죠.
    • 이런 언어들에서는 x = y = 6라고 작성하여 x와 y에 모두 6을 대입할 수 있지만,
    • 러스트에서는 이렇지 않습니다.
  • 함수 정의도 구문입니다
  • 5 + 6과 같은 간단한 수학 연산을 살펴봅시다. 이 수식은 11이란 값을 산출하는 표현식입니다.
  • 표현식은 구문의 일부일 수 있습니다.
  • 함수를 호출하는 것도, 매크로를 호출하는 것도 표현식입니다.
  • 아래 예제처럼 새로운 스코프 생성을 위해 사용된 {} 코드 블록도 표현식입니다:
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y); // 4
  • x + 1 줄의 마지막이 세미콜론으로 끝나지 않은 점을 주목하세요.
  • 표현식은 종결을 나타내는 세미콜론을 쓰지 않습니다.
  • 만약 표현식 끝에 세미콜론을 추가하면, 표현식은 구문으로 변경되고 값을 반환하지 않게 됩니다.

반환 값을 갖는 함수

  • 함수는 호출한 코드에게 값을 반환할 수 있습니다.
  • 반환되는 값을 명명해야 할 필요는 없지만, 그 값의 타입은 화살표 (->) 뒤에 선언되어야 합니다.
  • return 키워드와 값을 지정하여 함수로부터 일찍 값을 반환할 수 있지만, 대부분의 함수들은 암묵적으로 마지막 표현식 값을 반환합니다.

3.4 주석


프로그래머들은 주석 (comment) 이라 불리우는 노트를 코드에 남겨서 컴파일러는 이를 무시하지만 코드를 읽는 사람들은 유용한 정보를 얻을 수 있게 합니다.

간단한 주석의 예를 봅시다:

// hello, world

러스트에서 주석은 두개의 슬래시로 시작하며, 이 주석은 해당 줄의 끝까지 계속됩니다.

한 줄을 넘기는 주석의 경우에는 아래처럼 각 줄마다 //를 추가하면 됩니다:

러스트는 문서화 주석 (documentation comment) 라고 불리우는 또다른 주석 형태를 가지고 있는데, 14장의 “크레이트를 Crates.io에 퍼블리싱 하기” 에서 다루도록 하겠습니다.


3.5 흐름 제어문


러스트 코드의 실행 흐름을 제어하도록 해주는 가장 일반적인 재료는 if 표현식과 반복문입니다.

조건식은 반드시 bool 이어야 한다는 점을 주목할 가치가 있습니다.

let 구문에서 if 사용하기

  • if는 표현식이기 때문에 Listing 3-2처럼 let 구문의 우변에 사용할 수 있습니다.
    • let number = if true { 5 } else { 6 };
  • 코드 블록은 블록 안의 마지막 표현식을 계산하고 숫자는 그 자체로 표현식임을 기억하세요

반복문을 이용한 반복

코드 블록을 한 번 이상 수행하는 일은 자주 쓰입니다. 반복 작업을 위해서, 러스트는 몇 가지 반복문(loop) 을 제공합니다.

러스트에는 loopwhile, 그리고 for라는 세 종류의 반복문이 있습니다.

loop로 코드 반복하기

loop 키워드는 여러분이 그만두라고 명시적으로 알려주기 전까지 혹은 영원히 코드 블록을 반복 수행되도록 해줍니다.

반복문에서 값 반환하기

  • loop의 용례 중 하나는 어떤 스레드가 실행 완료되었는지 검사하는 등 실패할지도 모르는 연산을 재시도할 때 입니다.
  • 여기서 해당 연산의 결과를 이후 코드에 넘겨주고 싶을지도 모릅니다.
  • 이를 위해서는 루프 정지를 위해 사용한 break 표현식 뒤에 반환하고자 하는 값을 넣으면 됩니다;
  • 해당 값은 아래와 같이 반복문 밖으로 반환되여 사용 가능하게 됩니다:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
}

while을 이용한 조건 반복문

반복문 내에서 조건 검사를 하는 작업도 자주 사용됩니다. 조건문이 참인 동안에는 계속 반복하는 형태죠.

조건문이 참이 아니게 될 때 프로그램은 break를 호출하여 반복을 종료합니다.

이러한 반복문 형태는 loopifelse와 break의 조합으로 구현할 수 있습니다.

하지만 이러한 패턴은 매우 흔하기 때문에 러스트에서는 while 반복문이라 일컫는 구조가 내장되어 있습니다.

	let mut num = 0
while num < 10 {
num += 1
}

for를 이용한 콜렉션에 대한 반복문

for 반복문을 사용하여 콜렉션의 각 아이템에 대한 어떤 코드를 수행시킬 수 있습니다.

	let a = [1,2,3,4,5];
for val in a.iter() {
println("{val}");
}

안전성과 간편성 덕분에 for 반복문은 러스트에서 가장 흔하게 사용되는 반복문 구성요소가 되었습니다.

표준 라이브러리가 제공하는 Range 타입을 이용하면 그렇게 원하는 횟수에 대한 반복문을 구현할 수 있는데, Range는 어떤 숫자에서 시작하여 다른 숫자 종료 전까지의 모든 숫자를 차례로 생성해줍니다.

for number in (1..4)


8 kyu

Grasshopper - Messi Goals

static la_liga_goals: u32 = 43;
static champions_league_goals: u32 = 10;
static copa_del_rey_goals: u32 = 5;

static total_goals: u32 = la_liga_goals+champions_league_goals+copa_del_rey_goals;

Remove First and Last Character

pub fn remove_char(s: &str) -> String {
s[1..s.len() - 1].to_string()
}

Welcome!

fn greet(language: &str) -> &str {
match language {
"czech" => "Vitejte",
"danish" => "Velkomst",
"dutch" => "Welkom",
"estonian" => "Tere tulemast",
"finnish" => "Tervetuloa",
"flemish" => "Welgekomen",
"french" => "Bienvenue",
"german" => "Willkommen",
"irish" => "Failte",
"italian" => "Benvenuto",
"latvian" => "Gaidits",
"lithuanian" => "Laukiamas",
"polish" => "Witamy",
"spanish" => "Bienvenido",
"swedish" => "Valkommen",
"welsh" => "Croeso",
_ => "Welcome",
}
}

Are You Playing Banjo?

  • Rust에는 문자열 타입이 두가지 존재한다. 언어 자체로 지원하는 str과 표준 라이브러리에서 지원하는 String이 그렇다.
  • let s1: &str = "Hello str";
  • let s2: String = String::from("Hello String");
  • str은 보통 &str로 많이 사용한다.
  • String과 &str의 가장 큰 차이점은 String은 문자열 수정이 가능하지만 &str은 불가능하다는 점이다.
  • &str은 보통 문자열 리터럴이나 문자열 슬라이스를 저장하는데 사용된다.
  • 출처: https://steelbear.tistory.com/86 [steelbear's notes:티스토리]
fn are_you_playing_banjo(name: &str) -> String {
let name = name.to_string();
if name.starts_with('r') || name.starts_with('R') {
name + " plays banjo"
} else {
name + " does not play banjo"
}
}
//
fn are_you_playing_banjo(name: &str) -> String {
match &name[0..1] {
"R" | "r" => format!("{} plays banjo", name),
_ => format!("{} does not play banjo", name)
}
}
fn are_you_playing_banjo(name: &str) -> String {
match name.to_lowercase().starts_with("r") {
true => format!("{} plays banjo", name),
false => format!("{} does not play banjo", name)
}
}

7 kyu

The highest profit wins!

fn min_max(lst: &[i32]) -> (i32, i32) {
(*lst.iter().min().unwrap(), *lst.iter().max().unwrap())
}
//
use itertools::Itertools;

fn min_max(xs: &[i32]) -> (i32, i32) {
xs.iter().cloned().minmax().into_option().unwrap()
}
fn min_max(lst: &[i32]) -> (i32, i32) {
let min = lst.iter().min().unwrap();
let max = lst.iter().max().unwrap();

(*min, *max)
}

Regex validate PIN code

Option
Some(_) =>
None =>
fn validate_pin(pin: &str) -> bool {
let len = pin.len();
if len == 4 || len == 6 {
let mut ans = true;
pin.chars().for_each(|x| match x.to_digit(10) {
Some(_) => {}
None => ans = false,
});
ans
} else {
false
}
}
//
fn validate_pin(pin: &str) -> bool {
pin.chars().all(|c| c.is_digit(10)) && (pin.len() == 4 || pin.len() == 6)
}
fn validate_pin(pin: &str) -> bool {
if ![4, 6].contains(&pin.len()) { return false; }
pin.chars().all(|c| c.is_ascii_digit())
}

Printer Errors

fn printer_error(s: &str) -> String {
static ASCII_LOWER: [char; 13] = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
];
let cnt = s
.to_string()
.chars()
.into_iter()
.filter(|x| !ASCII_LOWER.contains(x))
.count()
.to_string();

let ans = cnt + "/" + &s.len().to_string();
ans
}
//
fn printer_error(s: &str) -> String {
// Your cude here
format!("{}/{}", s.chars().filter(|&c| c > 'm').count(), s.len())
}
fn printer_error(s: &str) -> String {
let total = s.len();
let bad = s.chars().filter(|&c| c < 'a' || c > 'm').count();
format!("{}/{}", bad, total)
}

Shortest Word

fn find_short(s: &str) -> u32 {
s.to_string()
.split(" ")
.map(|x| x.len() as u32)
.min()
.unwrap()
}
//
fn find_short(s: &str) -> usize {
s.split_whitespace().map(str::len).min().unwrap()
}
fn find_short(s: &str) -> u32 {
s.split_whitespace()
.map(|word| word.len())
.min()
.unwrap_or(0) as u32
}

Growth of a Population

fn nb_year(p0: i32, percent: f64, aug: i32, p: i32) -> i32 {
let mut cnt = 0;
let mut total = p0 as f64;

while total < p as f64 {
total = total + (total * (percent / 100 as f64)) + aug as f64;
total = total.floor();
cnt += 1
}
cnt
}

Chap.2 gussing-game

· 24 min read
brown
FE developer

해당 학습 자료를 정리 및 수행한 기록입니다.

이번 장은 몇몇 일반적인 Rust 개념과 활용 방법을 배울 수 있습니다.

  • let
  • match
  • 메소드,
  • 연관함수(assiciated functions),
  • 외부 크레이트(external crates)

이번 장에서는 여러분이 직접 기초적인 내용을 실습합니다.

우리는 고전적인 입문자용 프로그래밍 문제인 추리 게임을 구현해 보려 합니다.

  1. 먼저 프로그램은 1~100 사이의 임의의 정수를 생성합니다.
  2. 다음으로 플레이어가 프로그램에 추리한 정수를 입력합니다.
  3. 프로그램은 입력받은 추리값이 정답보다 높거나 낮은지를 알려줍니다.
  4. 추리값이 정답이라면 축하 메세지를 보여주고 종료됩니다.

값을 변수에 저장하기

// 사용자 입력을 받고 결과값을 표시하기 위해서는 io (input/output) 라이브러리를 스코프로 가져와야 합니다.
// io 라이브러리는 std 라고 불리는 표준 라이브러리에 있습니다.

use std::io;

fn main() {
// print
println!("Guess the number!");
println!("Please input your guess.");

// mut string 변수 선언
let mut guess = String::new();

// input 받는 코드
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");

// 출력
println!("You guessed: {}", guess);
}

러스트에서 변수는 기본적으로 불변입니다.

let foo = 5; // 불변
let mut bar = 5; // 가변
  • String::new의 결과값인 새로운 String 인스턴스가 묶이는 대상이 됩니다.

  • String은 표준 라이브러리에서 제공하는 확장 가능한(growable) UTF-8 인코딩의 문자열 타입입니다.

  • ::new에 있는 ::는 new가 String 타입의 연관 함수 (associated function) 임을 나타냅니다.

  • 연관함수는 하나의 타입을 위한 함수이며, 이 경우에는 하나의 String 인스턴스가 아니라 String 타입을 위한 함수입니다.

  • 몇몇 언어에서는 이것을 정적 메소드 (static method) 라고 부릅니다.

  • new 함수는 새로운 빈 String을 생성합니다. new 함수는 새로운 값을 생성하기 위한 일반적인 이름이므로 많은 타입에서 찾아볼 수 있습니다.

요약하자면 let mut guess = String::new(); 라인은 새로운 빈 String 인스턴스와 연결된 가변변수를 생성합니다.

우리는 io의 연관함수인 stdin을 호출합니다:

io::stdin() .read_line(&mut guess)

stdin 함수는 터미널의 표준 입력의 핸들(handle)을 나타내는 타입인 std::io::Stdin의 인스턴스를 돌려줍니다.

코드의 다음 부분인 .read_line(&mut guess)는 사용자로부터 입력을 받기 위해 표준 입력 핸들에서 read_line 메소드를 호출합니다. 또한 read_line에 &mut guess를 인자로 하나 넘깁니다.

  • &는 코드의 여러 부분에서 데이터를 여러 번 메모리로 복사하지 않고 접근하기 위한 방법을 제공하는 참조자 임을 나타냅니다.
  • 참조자는 복잡한 특성으로서 러스트의 큰 이점 중 하나가 참조자를 사용함으로써 얻는 안전성과 용이성입니다.
  • 지금 당장은 참조자가 변수처럼 기본적으로 불변임을 알기만 하면 됩니다. 따라서 가변으로 바꾸기 위해 &guess가 아니라 &mut guess로 작성해야 합니다.

Result 타입으로 잠재된 실패 다루기

  1. read_line은 우리가 인자로 넘긴 문자열에 사용자가 입력을 저장할 뿐 아니라 하나의 값을 돌려 줍니다.
  2. 여기서 돌려준 값은 io::Result 입니다.
  3. 러스트는 표준 라이브러리에 여러 종류의 Result 타입을 가지고 있습니다.
  4. 제네릭 Result이나 io:Result가 그 예시입니다

Result의 variants는 Ok와 Err입니다.

Ok는 처리가 성공했음을 나타내며 내부적으로 성공적으로 생성된 결과를 가지고 있습니다.

Err는 처리가 실패했음을 나타내고 그 이유에 대한 정보를 가지고 있습니다.

io::Result가 Ok 값이라면 expect는 Ok가 가지고 있는 결과값을 돌려주어 사용할 수 있도록 합니다. 이 경우 결과값은 사용자가 표준 입력으로 입력했던 바이트의 개수입니다.

만약 expect를 호출하지 않는다면 컴파일은 되지만 경고가 나타납니다.

println! 변경자(placeholder)를 이용한 값 출력

println!("You guessed: {}", guess);

비밀번호를 생성하기

러스트는 아직 표준 라이브러리에 임의의 값을 생성하는 기능이 없습니다.

하지만 러스트 팀에서는 rand 크레이트를 제공합니다.

크레이트(Crate)를 사용하여 더 많은 기능 가져오기

  • 크레이트는 러스트 코드의 묶음(package)임을 기억하세요.
  • 우리가 만들고 있는 프로젝트는 실행이 가능한 binary crate 입니다.
  • rand crate는 다른 프로그램에서 사용되기 위한 용도인 library crate 입니다.

Cargo에서 외부 크레이트의 활용 예시

  • rand를 사용하는 코드를 작성하기 전에 Cargo.toml 을 수정
    • rand 크레이트를 의존 리스트에 추가
[dependencies]
rand = "0.8.5"

우리는 외부 의존성을 가지게 되었고, Cargo는 Crates.io 데이터의 복사본인 레지스트리(registry) 에서 모든 것들을 가져옵니다.

Crates.io는 러스트 생태계의 개발자들이 다른 사람들도 이용할 수 있도록 러스트 오픈소스를 공개하는 곳입니다.

레지스트리를 업데이트하면 Cargo는 [dependencies] 절을 확인하고 아직 여러분이 가지고 있지 않은 것들을 다운 받습니다.

이 경우 우리는 rand만 의존한다고 명시했지만 rand는 libc에 의존하기 때문에 libc도 다운 받습니다.

러스트는 이것들을 다운받은 후 컴파일하여 의존성이 해결된 프로젝트를 컴파일합니다.

크레이트를 새로운 버전으로 업그레이드하기

Cargo는 update 명령어를 제공합니다.

이것은 Cargo.lock 파일을 무시하고 Cargo.toml 에 여러분이 명시한 요구사항에 맞는 최신 버전을 확인합니다.

확인이 되었다면Cargo는 해당 버전을 Cargo.lock 에 기록합니다.

임의의 숫자를 생성하기

이제 rand 크레이트를 Cargo.toml 에 추가 했으니, rand를 사용 해 봅시다.

정말 놀라운 부분은 버전을 올려서 사용법이 달라졌는데, 사용법과 예시코드까지 보여줌 ㄷㄷ

use rand::Rng;
...
// let secret_number = rand::thread_rng().gen_range(1, 101);
let secret_number = rand::thread_rng().gen_range(1..101);

let mut i = 0;
loop {
if i > 100 {
break;
}
let secret_number = rand::thread_rng().gen_range(1..101);
println!("{} secret number is: {}", i, secret_number);
i += 1;
}
...

  • 먼저 use 라인인 use rand::Rng를 추가합니다.

    • Rng는 난수 생성기를 구현한 메소드들을 정의한 트레잇 (trait) 이며 해당 메소드들을 이용하기 위해서는 반드시 스코프 내에 있어야 합니다. 10장에서 트레잇에 대해 더 자세히 다룰 것입니다.
  • rand::thread_rng 함수는 OS가 시드(seed)를 정하고 현재 스레드에서만 사용되는 특별한 난수 생성기를 돌려줍니다.

  • 다음으로 우리는 gen_range 메소드를 호출합니다.

    • 이 메소드는 Rng 트레잇에 정의되어 있으므로 use rand::Rng 문을 통해 스코프로 가져올 수 있습니다.
    • gen_range 메소드는 두 개의 숫자를 인자로 받고 두 숫자 사이에 있는 임의의 숫자를 생성합니다. 하한선은 포함되지만 상한선은 제외되므로 1부터 100 사이의 숫자를 생성하려면 1과 101을 넘겨야 합니다.

Note: 크레이트에서 트레잇과 메소드, 함수중 어떤 것을 호출해야 할지 모를 수도 있습니다. 각 크레이트의 사용법은 크레이트의 문서에 있습니다.

Cargo의 다른 멋진 기능은 cargo doc --open 명령어를 사용하여 의존하는 크레이트의 문서를 로컬에서 모두 빌드한 다음, 브라우저에서 열 수 있다는 것입니다.

rand 크레이트의 다른 기능이 궁금하시면, cargo doc --open을 실행하고, 왼쪽 사이드바에서 rand를 클릭하여 알 수 있습니다.

비밀번호와 추리값을 비교하기

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
// --snip--

println!("You guessed: {}", guess);

match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}

Listing 2-4: 두 숫자를 비교한 결과 처리하기

  • Ordering은 Result와 같은 열거형이지만 Ordering의 값은 LessGreaterEqual입니다. 이것들은 여러분이 두 개의 값을 비교할 때 나올 수 있는 결과들입니다.
  • cmp 메소드는 두 값을 비교하며 비교 가능한 모든 것들에 대해 호출할 수 있습니다.
    • 이 메소드는 비교하고 싶은 것들의 참조자를 받습니다.
    • 여기서는 guess와 secret_number를 비교하고 있습니다.
    • cmp는 Ordering 열거형을 돌려줍니다.
  • 우리는 match 표현문을 이용하여 cmp가 guess와 secret_number를 비교한 결과인 Ordering의 값에 따라 무엇을 할 것인지 결정할 수 있습니다.

match 표현식은 arm 으로 이루어져 있습니다.

하나의 arm은 하나의 패턴 과 match 표현식에서 주어진 값이 패턴과 맞는다면 실행할 코드로 이루어져 있습니다.

러스트는 match에게 주어진 값을 arm의 패턴에 맞는지 순서대로 확인합니다.

match 생성자와 패턴들은 여러분의 코드가 마주칠 다양한 상황을 표현할 수 있도록 하고 모든 경우의 수를 처리했음을 확신할 수 있도록 도와주는 강력한 특성들입니다.

예제에서 사용된 match 표현식에 무엇이 일어날지 한번 따라가 봅시다.

  1. 사용자가 50을 예측했다고 하고 비밀번호가 38이라 합시다.
  2. 50과 38을 비교하면 cmp 메소드의 결과는 Ordering::Greater 입니다.
  3. match 표현식은 Ordering::Greater를 값으로 받아서 각 arm의 패턴을 확인합니다.
  4. 처음으로 마주하는 arm의 패턴인 Ordering::Less는 Ordering::Greater와 매칭되지 않으므로 첫번째 arm은 무시하고 다음으로 넘어갑니다.
  5. 다음 arm의 패턴인 Ordering::Greater는 확실히 Ordering::Greater와 매칭합니다!
  6. arm과 연관된 코드가 실행될 것이고 Too big!가 출력될 것입니다. 이 경우 마지막 arm은 확인할 필요가 없으므로 match 표현식은 끝납니다.

비교하기 위해서 string을 i32로 변환 해줘야 함

let guess: u32 = guess.trim().parse().expect("Please type a number!");

우리는 guess 변수를 생성했습니다.

잠깐, 이미 프로그램에서 guess라는 이름의 변수가 생성되지 않았나요? 그렇긴 하지만 러스트는 이전에 있던 guess의 값을 가리는(shadow) 것을 허락합니다

. 이 특징은 종종 하나의 값을 현재 타입에서 다른 타입으로 변환하고 싶을 경우에 사용합니다.

Shadowing은 우리들이 guess_str과 guess처럼 고유의 변수명을 만들도록 강요하는 대신 guess를 재사용 가능하도록 합니다. (3장에서 더 자세한 이야기를 다룹니다)

  1. 우리는 guess를 guess.trim().parse() 표현식과 묶습니다.
  2. 표현식 내의 guess는 입력값을 가지고 있던 String을 참조합니다.
  3. String 인스턴스의 trim 메소드는 처음과 끝 부분의 빈칸을 제거합니다.
    • u32는 정수형 글자만을 가져야 하지만 사용자들은 read_line을 끝내기 위해 enter키를 반드시 눌러야 합니다.
    • enter키가 눌리는 순간 개행문자가 문자열에 추가됩니다. 만약 사용자가 5를 누르고 enter키를 누르면 guess는 5\n처럼 됩니다. \n은 enter키, 즉 개행문자를 의미합니다. trim 메소드는 \n을 제거하고 5만 남도록 처리합니다.
  4. 문자열의 parse 메소드는 문자열을 숫자형으로 파싱합니다.
    • 이 메소드는 다양한 종류의 정수형을 변환하므로 우리는 let guess: u32처럼 정확한 타입을 명시해야 합니다.
    • guess 뒤의 콜론(:)은 변수의 타입을 명시했음을 의미합니다.
    • u32은 부호가 없는 32비트의 정수입니다.
    • parse 메소드의 호출은 에러가 발생하기 쉽습니다.
    • 만약 A👍%과 같은 문자열이 포함되어 있다면 정수로 바꿀 방법이 없습니다.
    • Result 타입으로 잠재된 실패 다루기” 에서 read_line와 비슷하게 parse 메소드는 실패할 경우를 위해 Result 타입을 결과로 돌려 줍니다. 우리는 이 Result를 expect 메소드를 사용하여 같은 방식으로 처리합니다.
    • 만약 parse 메소드가 문자열에서 정수로 파싱을 실패하여 Err Result variant를 돌려준다면 expect 호출은 게임을 멈추고 우리가 명시한 메세지를 출력합니다. 만약 parse 메소드가 성공적으로 문자열을 정수로 바꾸었다면 Result의 Ok variant를 돌려 받으므로 expect에서 Ok에서 얻고 싶었던 값을 결과로 받게 됩니다.

반복문을 이용하여 여러 번의 추리 허용

loop 키워드는 무한루프를 제공합니다.

정답 이후에 종료하기

	loop {
println!("Please input your guess.");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}

잘못된 입력값 처리하기

사용자가 숫자가 아닌 값을 입력했을 때 프로그램이 종료되는 동작을 더 다듬어 숫자가 아닌 입력은 무시하여 사용자가 계속 입력할 수 있도록 해 봅시다.

guess가 String에서 u32로 변환되는 라인을 수정하면 됩니다.

let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, };

  • expect 메소드 호출을 match 표현식으로 바꾸는 것은 에러 발생 시 종료되지 않게 처리하는 일반적인 방법입니다.

  • parse 메소드가 Result 타입을 돌려주는 것과 Result는 Ok나 Err variants를 가진 열거형임을 떠올리세요.

  • cmp 메소드의 Ordering 결과를 처리했을 때처럼 여기서 match 표현식을 사용하고 있습니다.

  • 만약 parse가 성공적으로 문자열에서 정수로 변환했다면 결과값을 가진 Ok 를 돌려줍니다.

  • Ok는 첫번째 arm의 패턴과 매칭하게 되고 match 표현식은 parse 가 생성한 num값을 돌려줍니다. 그 값은 우리가 생성하고 있던 새로운 guess과 묶이게 됩니다.

요약

이 프로젝트는 letmatch, 메소드, 연관함수, 외부 크레이트 사용과 같은 많은 새로운 러스트 개념들을 소개하기 위한 실습이었습니다.

Rust_PS in codewars

8 kyu

Convert boolean values to strings 'Yes' or 'No'.

fn bool_to_word(value: bool) -> &'static str {
match value {
true => "Yes",
false => "No",
}
}

DNA to RNA Conversion

fn dna_to_rna(dna: &str) -> String {
let n = dna.len();
let mut ans = String::new();
let mut idx = 0;

loop {
if (idx == n) {
break;
}
let t = dna.chars().nth(idx).unwrap();

match t {
'G' => ans.push_str("G"),
'C' => ans.push_str("C"),
'A' => ans.push_str("A"),
_ => ans.push_str("U"),
}

idx += 1
}
return ans;
}
//
fn dna_to_rna(dna: &str) -> String {
dna.replace("T", "U")
}
fn dna_to_rna(dna: &str) -> String {
dna.chars().map(char_conversion).collect()
}
fn char_conversion(c: char) -> char {
if c == 'T' {
return 'U';
}

c
}
fn dna_to_rna(dna: &str) -> String {
let mut res = String::new();
for s in dna.chars() {
match s {
'T' => res.push('U'),
_ => res.push(s),
}
}
res
}

Counting sheep...

fn count_sheep(sheep: &[bool]) -> u8 {
let mut cnt = 0;
for x in sheep {
if *x {
cnt += 1;
} else {
cnt += 0
}
}
cnt
}
//
fn count_sheep(sheep: &[bool]) -> u8 {
sheep // take the sheep array
.iter() // turn it into an iterable
.filter(|&&x| x) // filter it by taking the values in the array and returning only the true ones
.count() as u8 // count all of the elements in the filtered array and return a u8
}

Fake Binary

fn fake_bin(s: &str) -> String {
let mut ans = String::new();

for x in s.trim().split("").into_iter() {
if x == "" {
continue;
}
let num = x
.parse::<i32>()
.expect("please give me correct string number!");

// println!("{num}");
if num >= 5 {
ans.push_str("1");
} else {
ans.push_str("0");
}
}
println!("{ans}");
ans
}

//
fn fake_bin(s: &str) -> String {
s.chars().map(|c| if c < '5' {'0'} else {'1'}).collect()
}
fn fake_bin(s: &str) -> String {
s.chars()
.map(|c| match c {
'0'..='4' => '0',
'5'..='9' => '1',
_ => c
})
.collect()
}

Switch it Up!

fn switch_it_up(n: usize) -> &'static str {
match n {
0 => "Zero",
1 => "One",
2 => "Two",
3 => "Three",
4 => "Four",
5 => "Five",
6 => "Six",
7 => "Seven",
8 => "Eight",
9 => "Nine",
_ => "",
}
}
//
fn switch_it_up(n: usize) -> &'static str {
match n {
1 => "One",
2 => "Two",
3 => "Three",
4 => "Four",
5 => "Five",
6 => "Six",
7 => "Seven",
8 => "Eight",
9 => "Nine",
_ => "Zero"
}
}
fn switch_it_up(n: usize) -> &'static str {
match n {
0 => "Zero",
1 => "One",
2 => "Two",
3 => "Three",
4 => "Four",
5 => "Five",
6 => "Six",
7 => "Seven",
8 => "Eight",
9 => "Nine",
_ => panic!()
}
}

The Feast of Many Beasts

fn feast(beast: &str, dish: &str) -> bool {
return beast.chars().nth(0) == dish.chars().nth(0)
&& beast.chars().nth(beast.len() - 1) == dish.chars().nth(dish.len() - 1);
}
//
fn feast(beast: &str, dish: &str) -> bool {
beast.chars().next() == dish.chars().next()
&& beast.chars().last() == dish.chars().last()
}
fn feast(beast: &str, dish: &str) -> bool {
dish[..1] == beast[..1] && dish[dish.len()-1..] == beast[beast.len()-1..]
}

Function 2 - squaring an argument

fn square(n: i32) -> i32 {
n * n
}
//
fn square(n: i32) -> i32 {
n.pow(2)
}

Convert number to reversed array of digits

// error
// Creates a temporary which is freed while still in use Again slight_smile
let process = Command::new(location_test);
process.arg(address);

fn digitize(n: u64) -> Vec<u8> {
const RADIX: u32 = 10;
// your code here
let str = n.to_string();
let arr = str
.chars()
.rev()
.map(|x| x.to_digit(RADIX).unwrap())
.collect::<Vec<u32>>();

let mut ans: Vec<u8> = [].to_vec();
arr.into_iter()
.for_each(|val| ans.push(val.try_into().unwrap()));
println!("{ans:#?}");
return ans;
}
// u32 -> u8로 변경하는 부분
fn digitize(n: u64) -> Vec<u8> {
n
.to_string()
.chars()
.map(|c| c.to_digit(10).unwrap() as u8)
.rev()
.collect::<Vec<u8>>()
}

Chap.1 Hello-rust

· 3 min read
brown
FE developer

학습 목표

2022-09월까지 학습자료에 대한 학습 마무리하기!!!

학습 자료

개발환경 세팅

  • rustup : 러스트 버전 및 러스트 관련 툴을 관리하는 커맨드라인 도구

  • brew install rustup-init

  • rustup-init

  • rustup update update

  • rustup self uninstall delete

  • rustup -  rust 버전 및 관련 도구들을 위한 커맨드라인 도구이다

    • rustc -> 컴파일러: rust 코드를 컴퓨터가 이해할 수 있는 언어로 변경해주는 도구
    • rustfmt -> 코드 포맷팅 도구
    • cargo -> rust의 의존성관리 도구이다.

Hello, Rust!

  • main.rsrustc로 바이너리파일로 컴파일 -> main 실행파일 생성
  • rustc main.rs -> ./main

Cargo 로 프로젝트 생성하기

  • cargo init
  • cargo project name
[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"

[dependencies]
...

Listing 1-2: cargo new 로 생성한 Cargo.toml 파일의 내용

이 포맷은 TOML (Tom’s Obvious, Minimal Language) 포맷으로, Cargo 설정에서 사용하는 포맷입니다.

Cargo 는 최상위 프로젝트 디렉토리를 README, 라이센스, 설정 파일 등 코드 자체와는 관련 없는 파일들을 저장하는 데 사용하기 때문에, 소스 파일은 src 내에 저장합니다.

Cargo 빌드 및 실행

  • cargo build 명령으로 프로젝트를 빌드할 수 있습니다.(target/debug/$projectName)
  • cargo run 명령어는 한번에 프로젝트를 빌드하고 실행할 수 있습니다.
  • cargo check 명령으로 바이너리를 생성하지 않고 프로젝트의 에러를 체크할 수 있습니다.
  • cargo build --release 명령어를 사용해 릴리즈 빌드를 생성할 수 있습니다.
    • 일반 빌드와 차이점은 target/debug 가 아닌 target/release 에 실행 파일이 생성된다는 점
    • 컴파일 시 최적화를 진행해, 컴파일이 오래 걸리는 대신 러스트 코드가 더 빠르게 작동하는 점

8월 회고 및 9월 계획

· 3 min read
brown
FE developer

Intro


하고 싶은 것을 할 것인가 or 필요한 것을 할 것인가는 항상 많은 사람들이 고민하는 주제 일 것같다.

나 역시도 그렇다. 개발자로 더 나은 개발자가 되기 위해 공부해야 할 것은 널려있고, 시간은 한정 되어 있으니 선택과 집중은 필연적인 것이다.

작년에 rust에 꽂혀서 야심차게 작심 7일 공부한 기억이 있다.

그때 하다가만 이유 중 가장 큰 이유는 나는 프론트엔드 개발자이고 프론트엔드를 더 잘하는게 중요한데 러스트를 학습할 여유가 있는가 였다.

그렇게 1년의 시간이 지나고 다시 나의 마음에 돌아온 러스트..!

계기는 rust가 블록체인 업계에서 핫한 언어이고, 러스트로 솔라나체인에 스마트 컨트랙트를 작성할 수 있는 개발자가 좋은 대우를 받는다는 이야기였다.

원래 흥미도 있던 차에, 금전적인 이득을 더 취할 수 있는 기회라니 달콤하다.

물론 프론트엔드를 더 갈고 닦는게, 더 이득일 수도 있겠지만 하고싶으면 결국 하는게 사람이니까.

다시 해봐야지!

8월 회고


잘한 점

  1. 블로그를 시작한 것
    • 솔직히 아무도 안볼 줄 알았고 그게 당연한 것이라고 생각했는데, 의외로 봐주는 사람이 있다는 점이 더 열심히 하게 만들고 사람을 뿌듯하게 함
  2. obsidian을 사용한 것
    • 개인적인 경험으로는 obsidian이 노션보다 나은 것 같다.
    • obsidian 싱크를 하려다보니 저절로 일일커밋이 된다. 😎

개선할 점

이번달은 열심히 살아서 딱히 없는 것 같다.

아침에 일찍 일어나서 복싱가기 정도...!

9월 플랜


아래의 태스크들을 진행할 예정이다.

  • Rust 기초 및 문법 학습(다음달 스마트 컨트랙트 작성 목표)
  • 블로그::디자인 작업
  • 블로그::main page
  • PS::백준::단계 별 문제풀기(JS)
  • 깃허브::프로필 꾸미기

Keep going!

DocSearch with docusaurus

· 5 min read
brown
FE developer

Intro

블로그 만든지 1달 정도 되었나...?

깜짝 놀랄만한 일이 생겼다.

솔직히 본인이 유명인도 아니고! 홍보를 하는 것도 아니고!! 그렇다고 대단한 글을 작성하는 것도 아닌데!!!

생각보다 많은 분들이 이 블로그를 봐주신 것이다!!!!! 😎

searchConsole ga

모든 콘텐츠가 한국어 인데 why...?

Thanks!!!

도대체 어떻게 알고 봤는지 신기하기만 하다.

그래서 생각하고 있던 docusaurus 작업 중

  1. 검색 붙이기
  2. 테마 수정하기
  3. 메인 수정하기

검색 붙이기를 하면서 + 후기를 남긴다.

DocSearch

알게 된 것은 친구의 블로그 덕분이다.

dgle

아니 저 기능 뭐지 탐난다...! 라는 생각으로 작업했다.

1. 공식문서 체크

docusaurus알골리아 DocSearch에 대한 최고 수준의 지원을 제공합니다.

2. apply docsearch program

서비스는 모든 오픈 소스 프로젝트에 대해 무료입니다.
대상 여부를 확인하고 DocSearch 프로그램에 지원하세요.

docsearch program에 등록해본다.

본인 블로그는 아직 빌딩단계라서 해당없다는 회신을 받았는데, 딱히 상관은 없다.

3. 알고리아 회원가입

algolia에서 회원 가입을 진행해준다.

4. create application

어플리케이션을 생성해주는데

app

  1. app 이름 적고
  2. 꼭 FREE Plan을 선택하자!!
  3. data는 샘플데이터 같은 것 넣으면 된다.
  4. index name 은 사용하는 값이다.

5. API Key 체크

overview 화면에서 API Keys를 누르자

사용하는 key는 Application ID,Search-Only API Key,Admin API Key이다.

6. Data crawling

최신 권장방식 문서이다.

본인은 아래의 방법을 사용했다.

이 방식은 Legacy 입니다!

관련 문서

  1. 이 단계에서는 도커 이미지를 통한 데이터 크롤링을 위해 docker, jq가 설치 되어 있어야 한다.

    • brew install --cask docker
    • brew install jq
  2. .env 작성 환경 변수를 작성해야 한다.

    APPLICATION_ID=어플아이디
    API_KEY=어드민키
  3. project 최상단에 config.json 작성 참조 링크 index_name, start_urls,sitemap_urls만 본인에 맡게 변경하시라.

    {
    "index_name": "braurus", // 1
    "start_urls": ["https://braurus.dev/"], // 2
    "sitemap_urls": ["https://braurus.dev/sitemap.xml"], // 3
    "sitemap_alternate_links": true,
    "stop_urls": ["/tests"],
    "selectors": {
    "lvl0": {
    "selector": "(//ul[contains(@class,'menu__list')]//a[contains(@class, 'menu__link menu__link--sublist menu__link--active')]/text() | //nav[contains(@class, 'navbar')]//a[contains(@class, 'navbar__link--active')]/text())[last()]",
    "type": "xpath",
    "global": true,
    "default_value": "Documentation"
    },
    "lvl1": "header h1",
    "lvl2": "article h2",
    "lvl3": "article h3",
    "lvl4": "article h4",
    "lvl5": "article h5, article td:first-child",
    "lvl6": "article h6",
    "text": "article p, article li, article td:last-child"
    },
    "strip_chars": " .,;:#",
    "custom_settings": {
    "separatorsToIndex": "_",
    "attributesForFaceting": ["language", "version", "type", "docusaurus_tag"],
    "attributesToRetrieve": [
    "hierarchy",
    "content",
    "anchor",
    "url",
    "url_without_anchor",
    "type"
    ]
    }
    }
  4. 스크래퍼 이미지 실행
    docker run -it --env-file=.env -e "CONFIG=$(cat ./config.json | jq -r tostring)" algolia/docsearch-scraper

  5. 알고리아 본인 앱 화면에 보면 데이터가 갱신 되어 있을 것이다.

7. docusaurus.config.js

이 문서를 참조 하자!

// 본인 코드
...
algolia: {
// 알골리아에서 제공한 appId를 사용하세요.
appId: "BW2ZDYYT4N",
// 공개 API 키: 커밋해도 문제가 생기지 않습니다.
apiKey: "4a0c2546c188aacd5f5277a7a9b34896",
indexName: "braurus",
contextualSearch: true,
},
...

드디어 완성이다.

searchBtn


검색기능을 붙이고 이 글을 쓰기까지 생각보다 시간이 걸렸지만, 누가 볼 수도 있다고 생각하니 살짝 기대 된다.



Reference

생산성에 대해서

· 8 min read
brown
FE developer

서론


시간은 모두에게 공평하다. 하루 24시간 그 이상을 사용하는 사람도, 그 이하를 사용하는 사람도 없다.

세상은 불공평하지만, 시간은 공평하고 개인이 할 수 있는 최선은 본인에게 주어진 시간을 최대한 효율적으로 쓰는 것이다.

하지만 시간을 잘 활용하는 것은 어렵다. 30년 동안 살면서 본인의 행동에서, 혹은 학창시절, 대학시절등 주변에서 느낀 것은, 이 사람이 열심히 사는구나 싶은 사람은 사실 100명중 10 ~ 15명 정도 인것 같다.

본인 역시 열심히 사는 사람 옆에 열심히 살지 않는 사람이었고, 중고딩 시절에 대학생까지도 게임을 엄청 했었다.

그렇게 25살까지 해놓은 것도 없이, 하고 싶은 것도 없이 놀았었다. 생각해보면 이 시간이 정말 아깝다.

그러다 26살에 드디어 미래에 대한 위기감 + 해외취업을 한번 해봐야겠다는 목표를 가지고 토익점수 900 이상 올리고, 학교에서 진행했던 어학연수도 가고, 무역관련 자격증(무역영어 1급, 국제무역사 1급, 유통관리사 2급)들도 따고 해서 k-move 프로그램으로 결국 취업도 했었다.

그렇게 직장생활을 시작했지만 뭔가 일반 사무직은 전문성이 약한 것(개인적인 의견) 같았고, 뭔가 비전이 보이지 않았다.

그러다 개발자가 원래 관심도 있었고 글로벌 시대에 좋을 것 같아서, 20년 4월부터 개발공부를 본격적으로 시작했고 21년 6월에 개발자로 커리어 전환을 할 수 있었다.

서론이 장황했는데 26살 부터는 나름 인생을 열심히 살려고 노력했었고, 갈수록 더 열심히 살고 있다. 그러다보니 이 주제에 대해 고민을 많이 했었다.


열심히 산다는 건?


열심히 사는 것의 첫 번째는 시간관리라고 생각한다.

시간관리에 대해 신경쓰지 않으면 하루에 날리는 시간이 생각보다 많고, 나이 먹을수록 시간은 더 빨리간다.

첫 번째 스텝은 특별한 것은 없이 그렇게 날리는 시간들을 체크하고 최대한 줄이는 것이다.

다만 그 순간의 귀찮음, 하기 싫음 등을 컨트롤 하는 것이 필요한데, 그러한 감정들은 일시적이라는 것을 명심하자.

두 번째 스텝은 일상을 단순화 하는 것이다.

본인이 느끼기에 에너지는 한정적이고 하루에 할 수 있는 일의 양도 한계치가 있는 것 같다.

그러니 불필요한 일들은 하지 않고, 내가 알 필요 없는 것들에 관심을 끄는게 시간관리에 좋다고 생각한다(단점은 재미없는 인간이 된다).

그러니 본인이 하루에 할 수 있는 양은 채우고 휴식을 하면 된다 생각한다.


생산성 관리


개인적으로 느끼기에, 본인은 하루에 많은 시간을 개발에 쏟고 있다고 느낀다.

직장인으로서 개발하는게 일이고, 그외의 시간에도 자체 야근 😵, 개발 학습, PS 등 열심히 하려고 노력하고 있다.

잘하는 개발자가 되고 싶으니까. 돈도 많이 벌면 더 좋고 😎

어짜피 인생은 100명중에서 노력하는 15명 안쪽에서 경쟁하는 것이다.

개발자 직군은 아무래도 공부를 많이하는 성향의 사람들이 할 확률이 높으니 개발자의 30%라고 가정해보자.

이 레벨에서는 업무역량향상을 위해 시간을 투자하는 것은 기본이고 투자한 시간(input)대비 얼마만큼의 생산성(output)을 뽑아내는가가 차이를 만들어낸다고 생각한다.

그래서 어떻게 해야 생산성을 올릴 수 있을까?

시간 대비 많은 결과물을 내고, 많은 분량의 학습을 소화할 수 있을까?

이것이 요즘 난제다.

그래서 느낌 오는 컨텐츠들을 정리 해봤다.

물론 시간관리 + 생산성관리까지 한다고 엄청 높은 위치까지 올라간다는 보장은 없다.

그럼에도 불구하고, 우리가 노력해야 하는 이유는 노력 안한 자신보다 더 나아질 것이라는 믿음 때문이다.

헬스쪽에는 자신의 유전자내에서, 최대를 뽑아내는 것이 가장 멋진 것이라는 말이 있다.

요즘 삘 받아서 열심히 살려고 계속 노력하고 있는데, 언제쯤 번아웃이 올지 기대된다.

사실 열심히 살 수 밖에 없는 것 같다. 뭐라도 하지 않으면 뒤쳐질 것 같은 불안감이 있으니까

학창시절에 지금처럼 살았으면 훨씬 잘됐을 것 같지만, 그땐 몰랐지


obsidian custom

옵시디언 처음 쓰시는 분들 첫 세팅할 때 쓰시면 좋을 것 같습니다.

obsidian-template

blog 초기 setting 후기2

· 5 min read
brown
FE developer

1편에서 이어집니다

역시 이런건 작업하면서, 글도 써야한다.

이틀밖에 안됐는데 가물가물...

4. domain 구매

배포 및 호스팅은 vercel 에서 알아서 해준다. awesome!

도메인을 설정하려면 당연하게도 도메인을 사야하는데, 중요한 것은 어디서 살 것 인가이다.

처음에는 그냥 국내업체에서 구매할까 했지만, 아는 사람한데 물어보고 검색 좀 해보니까

해외 도메인 업체를 이용하는 것으로 그 중에서도 porkbun 으로 결정했다.

why? -> 제일 싸니까

도메인은 고민 끝에 brown + docusaurus = braurus

거기에 개발자 티내야 하니까 braurus.dev로 결정해서 구매했다.

구매하는 방법은

  1. 회원가입
  2. 도메인을 선택
  3. 결제

본인은 카드도 오직 체크카드, 그것도 국내결제 밖에 안되는 카드인데!?

라고 생각하면서 진행했는데, 구글 결제 선택하니까 알아서 플레이스토어로 어떻게 결제가 되더라.

그렇게 생각보다 싸게 도메인을 일단 2년치로 구매했다.

5. domain assign by vercel

개인 도메인을 구입했다면, vercel 프로젝트 세팅의, domain에서 등록하고 그냥 하라는대로 하면 된다.

  1. DNS RECORDS 세팅 ~~2. NAMESERVERS -> 포크번 디폴트 네임서버를 버셀 네임서버로 변경 ns1.vercel-dns.com
    ns2.vercel-dns.com

버셀 네임서버로 변경하고 버셀에 레코드 등록하니까 search-console에서 인증이 안됌

6. GTAG

개인 도메인을 붙여놓으니, 물론 아무도 안보겠지만 정말 아무도 안보는지 매우 궁금해졌다.

그런고로 GTAG를 달아야겠다!

  1. 링크로 들어가서 gtag를 만들고 측정 ID 저장

docusaurus 에서는 gtag 관련 플러그인을 제공해준다.

  1. npm install --save @docusaurus/plugin-google-gtag 로 설치를 하고,
  2. docusaurus.config.js에서 위의 측정 ID를 설정하면 끝

gtag 추적 아이디는 어짜피 사이트에서 다 보이니 굳이 감출 필요는 없는 것 같다.

측정 ID스트림 URL 일치가 중요!

잘 등록되면 저렇게 뜬다.

gtag

7. Search Console

하는 김에, search console도 해보자!

도메인을 산 사람들은 왼쪽이다. 본인의 도메인을 넣고 누르면 DNS 레코드를 통해 도메인 소유권을 확인하라고 하는데 어렵지 않다.

그냥 등록하면 끝인 작업인데, 도메인은 포크번에서 구입하고, 버셀네임서버로 변경한 뒤 버셀에다 등록하니까 도메인 소유권 인증이 안되더라. 포크번 네임서버로 변경하고 거기다 등록하니 마무리 되었다.

google search

참고로 구글 애널리틱스, 서치 콘솔 둘다 등록 하면 search console insights라는 페이지를 볼 수 있다.

8. sitemap

사이트 맵 주소는 $address/sitemap.xml이다 docusaurus.config.js 에 url을 본인 도메인으로 변경해주고, search console에 제출하자.