가로 또는 전체 영역 기준으로 중복된 값 세기(여러 열)

일정 영역에 배치되어 있는 숫자의 중복 개수를 가로 또는 전체 영역을 기준으로 세는 것을 해보도록 하겠습니다.

1. 가로로 중복 개수 세기

아래와 같이 숫자가 10개씩 10줄 있을 때 가로로 중복 값이 있는지 세보겠습니다.

M4셀에 =COUNTIF(C4:L4,C4:L4)라고 입력하면 한 행에서 중복된 숫자의 개수를 알려줍니다.

다시 말해 세번째인 51이 2번 나옵니다.

그러면 중복된 숫자만 표시해보겠습니다.

M4셀에서

=TEXTJOIN(“, “,,IF(COUNTIF(C4:L4,C4:L4)>1,C4:L4,””))

라고 입력하고 M4셀의 채우기 핸들을 더블 클릭하면 51과 26만이 한 행에 중복된 것을 알 수 있습니다.

2. 전체 영역에서 중복 개수 세기

이번에는 전체 영역에서 숫자가 얼마나 중복되는지 알아보겠습니다.

가. 전체 영역에서 중복된 숫자의 개수 세기

C15셀에 =COUNTIF(C4:L13,C4:L13)라고 입력하면

해당 셀의 숫자가 지정된 영역에 몇 개가 있는지 알려줍니다.

예를 들어 20이란 숫자는 C4셀과 H6셀 두 군데 있고, 가장 큰 숫자인 7에 해당하는 숫자인 36은 F2, D3, H4, H6, H8, H9, H10에 있습니다.

(나) 중복 개수가 4이상인 숫자 표시하기

위 표에서 4이상인 숫자를 표시해보겠습니다.

=IF(COUNTIF(C4:L13,C4:L13)>=4,C4:L13,””)

라고, 숫자가 4이상 경우 값을 표시하고, 아니면 공백을 표시하도록 하면 아래와 같이 4이상 숫자만 표시됩니다.

(다) 행별로 4이상인 숫자의 개수 표시하기

이번에는 위 숫자를 행 별로 몇 개인지 N열에 표시하도록 하겠습니다.

N4셀에 =COUNT(C15:L15)라고 입력하고, 채우기 핸들을 더블 클릭하면 1, 6, 1, … 식으로 값이 표시됩니다.

(라) 행별로 숫자가 4이상인 경우 O, 아니면 X 표시하기

N4셀에 =IF(COUNT(C15:L15)>=4,”O”,”X”)

라고 해서 O, X를 표시할 수 있습니다.

(5) ByRow 함수로 한 줄로 표시하기

ByRow 함수를 이용해 C15셀의 수식과 N3셀의 수식을 묶으면 한 번에 N4셀부터 N13셀까지의 값을 구할 수 있습니다.

수식은 아래와 같습니다.

=BYROW(IF(COUNTIF(C4:L13,C4:L13)>=4,C4:L13,””),LAMBDA(r,IF(COUNT(r)>=4,”O”,”X”)))

IF(COUNTIF(C4:L13,C4:L13)>=4,C4:L13,””)를 Lambda 함수에서 r로 받아, count함수를 이용해 4이상이면 O, 아니면 X를 표시하는데, 동적 배열 수식이라 채우기 핸들을 누르지 않더라도 한 번에 값이 구해집니다.

세로로 중복된 값 다루기

1. 문제

아래와 같이 중복된 숫자가 있으면 중복을 제거해야 할 경우가 생깁니다. 이와 관련해서 다양한 경우를 다루고자 합니다.

2. 중복된 숫자의 개수 세기

=COUNTIF(A2:A9,A2:A9)라고 하면

A2셀에서 A9셀까지의 범위에서 같은 숫자의 개수를 구해줍니다.

Microsoft 365라 아래와 같이 보이는 것이지 낮은 버전이라면 B2셀에서 B9셀까지 선택한 후 수식을 입력하고 Ctrl + Shift + Enter 키를 눌러야 할 겁니다.

A10셀에 2를 추가하고, 수식을 A10셀까지 수정하면 2라는 숫자의 개수가 모두 3으로 변경됩니다.

엑셀이 구 버전이라면 수식을 모두 지우고 새로 수식을 입력해야 할 수도 있습니다. 그렇다면 수식을 먼저 복사해서 다른데 붙여넣고 작업하는 것이 안전합니다.

2. 순수한 숫자의 개수 세기

위의 경우 1, 2, 4, 5, 7, 8과 같이 중복된 수라도 한번만 세려면

B열의 수식을 1/로 수정해서

=1/COUNTIF(A2:A10,A2:A10)이라고 하면 1/중복 개수가 되므로 1이면 1이지만, 2라면 1/2=0.5가 되고, 1/3=0.333333이 됩니다.

따라서, 위 수식을 sum 하면 0.5+0.5 = 1, 0.333333*3 = 1과 같이 고유한 숫자의 개수가 구해집니다.

=SUM(1/COUNTIF(A2:A10,A2:A10))을 하니 개별적으로 표시되던 것이 합계값 하나로 6이라고 표시됩니다. 1,2,4,5,7,8이므로 6 맞습니다.

3. 순수한 숫자만 표시하기

순수한 숫자 1, 2, 4, 5, 7, 8을 구해보겠습니다.

가. 중복 숫자는 한 번만 표시하고, 두번째부터는 공란으로 표시하기

D2셀에 =IF(COUNTIF($A$2:A2,A2)=1,A2,””) 이라고 입력합니다.

아래로 내려가면서 해당 셀의 개수를 세서 1보다 크면 빈 셀로 만들려고 하는 것입니다. 첫 셀인 A2셀을 절대 참조로 하는 것이 중요합니다. 왜냐하면 항상 A2셀부터 현재 셀(상대 참조)까지의 현재 셀의 개수를 세려고 하는 것이기 때문입니다.

D2셀의 채우기 핸들(D2셀 오른쪽 아래 네모)을 더블 클릭하면 D10셀까지 수식이 채워지는데(복사),

1, 2, “”, 4, 5, “”, 7, 8, “”라고 숫자가 중복되면 빈 셀로 표시되므로 이 숫자 들을 결합하면 됩니다.

나. 숫자 결합해서 표시하기

(1) TextJoin 함수

TextJoin함수는 Delimiter를 지정해서 텍스트를 결합할 수 있기 때문에 Concat함수보다 훨씬 편리합니다.

=TEXTJOIN(“, “,,D2:D10)라고,

구분자로 ,를 지정하고, 빈셀을 무시하도록 두번째 인수는 입력하지 않고 통과하고, 세번째 인수로 결합할 텍스트의 범위를 지정하면 원하는대로 1, 2, 4, 5, 7, 8이 구해집니다.

(2) Concat 함수

Concat 함수는 구분자를 지정할 수 없고, 결합할 텍스트만 지정할 수 있어서

=CONCAT(D2:D10)라고 하면 124578이라고 숫자가 단순히 결합된 결과만을 반환합니다.

(대체 해법)

어렵지만 할 수 없는 것은 아닙니다.

먼저 숫자가 있을 경우는 숫자 뒤에 공백을 한 칸 추가한 다음

Concat으로 연결한 다음 Substitute 함수를 이용해 공백 한 칸을 쉼표 + 공백 한칸으로 대체하면 됩니다.

=SUBSTITUTE(CONCAT(IF(D2:D10<>””,D2:D10&” “,””)),” “,”, “)라고 입력하면

결괏값을 보니 필요없이 마지막에도 ,가 들어가 있습니다.

따라서, concat한 후 trim을 해서 빈 공백을 제거해줘야 합니다.

수정된 수식은

=SUBSTITUTE(TRIM(CONCAT(IF(D2:D10<>””,D2:D10&” “,””))),” “,”, “) 입니다.

원하는대로 마지막에 쉼표 없이 1, 2, 4, 5, 7, 8이 구해졌습니다.

단어에 공백을 추가하여 특정 바이트 길이로 만들기

1. 문제

A열은 단어 사이가 한 칸 공백(1바이트)로 되어 있는데, B열은 단어마다 공백을 포함해서 8바이트 길이로 만들려고 하는 것입니다. 그런데, 이 때 영어, 숫자와 기호는 1바이트, 한글은 2바이트로 계산하려고 합니다.

단어를 8바이트 길이로 통일하는 문제

2. 해법

가. TextSplit와 Concat 함수 사용

TextSplit가 지원되는 엑셀이라면 간단하게 수식을 만들 수 있습니다. 공백을 기준으로 문장에서 단어를 분리해내는 것을 ‘나’처럼 find, mid 등 함수를 이용하면 너무 복잡합니다.

(1) TextSplit 함수로 단어 나누기

=TEXTSPLIT(A2,” “)

라고 입력하면 B열부터 D열까지 분리된 단어가 표시됩니다.

TextSplit함수로 단어 분리하기

(2) 단어를 공백을 추가해서 8바이트로 만들기

=MIDB(TEXTSPLIT(A2,” “)&REPT(” “,8),1,8)

라고 입력하는데, 이 수식의 의미는 분리된 단어에 rept(반복) 함수로 공백 8개를 추가한 다음 첫번째 위치부터 8바이트씩을 가져오는 것입니다.

공백 추가후 8바이트 길이로 만들기

MID가 아니라 MIDB인 것을 주의해야 합니다. 왜냐하면 MID함수는 한글도 1로 계산하기때문입니다.

수식을 마우스로 끌어서 선택하면 윗 부분에 결과가 보이는데, ‘선경 ‘,’1차’,’아파트” 다음에 공백이 추가된 것을 알 수 있습니다.

범위를 지정해서 단어의 길이가 8바이트인지 확인하기

그런데 공백의 개수를 셀 수 없으므로 F9키를 누릅니다.

F9키를 눌러 단어의 길이가 8바이트인지 확인하기

그러면 수식 입력줄에 결과가 표시되는데, 커서를 넣은 다음 ‘선경’ 다음의 공백을 세보면 4개인 것을 알 수 있고, ‘1차’ 다음의 공백은 5개인 것을 알 수 있고, 아파트 다음은 공백 2칸인 것것을 확인할 수 있습니다.

Esc키를 눌러 원래 수식으로 돌려 놓습니다.

(3) Concat 함수로 단어 합치기

=CONCAT(MIDB(TEXTSPLIT(A2,” “)&REPT(” “,8),1,8))

라고 기존 수식을 Concat 함수로 감싸면 아래와 같이 “선경 1차 아파트 “라고 하나로 합쳐져서 B2셀에 입력됩니다.

Concat 함수로 단어 연결하기

F9키를 눌러 수식의 결과에서 공백의 개수를 세어보면 동일한 것을 알 수 있습니다.

(4) 수식 복사하기

B2셀의 수식을 복사해서 B3셀에 붙여 넣거나 B2셀의 채우기 핸들을 더블 클릭하면 B3셀까지 수식이 복사됩니다.

채우기 핸들로 수식 복사하기

나. Mid, Find, Substitute, Concat 함수 사용

이건 고난의 역사입니다.

(1) ‘선경’ 등 단어 분리하기

(가) ‘선경’ 분리하기
① Find 함수를 이용하는 방법

여러 가지 방법이 있지만, 여기서는 Find 함수를 이용하는 것이 이해가 쉬운 듯 합니다.

=LEFT(A2,FIND(” “,A2)-1)라고 입력하면

A2셀에서 첫번째 공백의 위치에서 1을 뺀 위치까지 글자를 가져오므로 ‘선경’이 됩니다.

Find 함수를 이용하여 첫번째 단어 추출하기

② Substitute 함수를 이용하는 방법

=TRIM(MID(SUBSTITUTE(A2,” “,REPT(” “,50)),1,50))

substitute 함수를 이용하여 첫번째 단어 추출하기

위 수식의 의미는 공백을 모두 공백 50개로 만든 다음 Mid함수를 이용해 첫번째부터 50개를 가져온 다음, 좌우 공백을 제거하는 것입니다.

이것의 효용은 두번째부터 단어를 가져올 때 있습니다.

두번째 단어인 ‘1차’를 가져올 때는 1,50에서 1을 51로만 바꾸면 되고, 세번째 단어인 아파트를 가져올 때는 101,50으로 수정하면 간단히 구할 수 있습니다.

D3셀의 수식은

=TRIM(MID(SUBSTITUTE(A2,” “,REPT(” “,50)),51,50))로 위 수식과 51만 바뀌었습니다.

E3셀의 수식도

=TRIM(MID(SUBSTITUTE(A2,” “,REPT(” “,50)),101,50))로 51이 101로만 바뀌었습니다.

(나) ‘1차’ 분리하기

‘1차’는 첫번째 공백 다음부터 두번째 공백 전까지의 글자입니다.

따라서, 수식은

=MID(A2,FIND(” “,A2)+1,FIND(” “,A2,FIND(” “,A2)+1)-FIND(” “,A2)-1)로

FIND(” “,A2)+1는 첫번째 공백 위치 다음이 되고, 여기서는 3입니다.

FIND(” “,A2,FIND(” “,A2)+1)-FIND(” “,A2)-1은

두번째 공백의 위치를 FIND(” “,A2,FIND(” “,A2)+1)로 찾는데, FIND(” “,A2)+1)로 첫번째 공백 위치 다음부터 다시 공백의 위치를 찾으므로 두번째 공백의 위치인 6이 구해지고,

FIND(” “,A2)로 첫번째 공백의 위치를 빼는데, 그러면 6-3은 3이 되므로 2가 되도록 -1을 한 것입니다.

보기만 해도 복잡하죠?

(다) ‘아파트’ 분리하기

아파트는 두번째 공백 위치 다음부터 마지막까지이므로 오히려 간단합니다.

=MID(A2,FIND(” “,A2,FIND(” “,A2)+1)+1,10)

Find함수를 이용하여 세번째 단어 추출하기

위 수식에서 FIND(” “,A2,FIND(” “,A2)+1)는 두번째 공백의 위치를 구하는 것이고, +1을 추가해서 다음 위치부터 가져오도록 하는데, 10개를 가져옵니다. 그러나, 10개라 하더라도 가져올 단어가 3개뿐이 안되기때문에 확인해보면 공백 없이 ‘아파트’만 표시됩니다.

추출된 단어 확인하기

(2) ‘선경’ 등 단어 합치기

(가) 방법 1

마찬가지로 공백을 추가하기 위해 rept(” “,8)을 사용하고, MidB를 이용해 8개만 가져오고, 단어를 합치기 위해 Concat함수를 사용합니다.

수식은 Concat 다음에 C2셀의 수식을 붙여 넣는데, MidB와 rept(” “,50)을 추가해야 합니다.

=CONCAT(MIDB(LEFT(A2,FIND(” “,A2)-1)&REPT(” “,8),1,8))

위 수식은 ‘선경 ‘만 출력하는 수식입니다.

첫번째 단어에 공백을 추가한 후 8바이트 추출하기

나머지까지 연결하면

=CONCAT(MIDB(LEFT(A2,FIND(” “,A2)-1)&REPT(” “,8),1,8)&MIDB(MID(A2,FIND(” “,A2)+1,FIND(” “,A2,FIND(” “,A2)+1)-FIND(” “,A2)-1)&REPT(” “,8),1,8)&MIDB(MID(A2,FIND(” “,A2,FIND(” “,A2)+1)+1,10)&REPT(” “,8),1,8))

가 됩니다.

Find 수식을 이용하여 단어를 분리한 후 8바이트 길이로 단어 연결하기

F9키를 눌러서 확인하면 4, 5, 3개의 공백이 추가된 것을 확인할 수 있습니다

(나) 방법 2

이번에는 C3셀부터 E3셀까지의 수식을 이용해서 합쳐보겠습니다.

=CONCAT(MIDB(TRIM(MID(SUBSTITUTE(A2,” “,REPT(” “,50)),1,50))&REPT(” “,8),1,8)&MIDB(TRIM(MID(SUBSTITUTE(A2,” “,REPT(” “,50)),51,50))&REPT(” “,8),1,8)&MIDB(TRIM(MID(SUBSTITUTE(A2,” “,REPT(” “,50)),101,50))&REPT(” “,8),1,8))

Substitute 수식을 이용하여 단어를 분리한 후 8바이트 길이로 단어 연결하기

‘선경’에 공백을 추가하고 8바이트만 가져오는 수식인

MIDB(TRIM(MID(SUBSTITUTE(A2,” “,REPT(” “,50)),1,50))&REPT(” “,8),1,8)

을 복사한 후 &와 붙여넣기를 하고 1만 51로 수정하면 되니 너무 편리합니다.

(3) 3행에 수식 붙여넣기

F2셀과 G2셀을 마우스로 끌어서 선택한 후 채우기 핸들을 더블 클릭하면 3행에 수식이 아래와 같이 붙여넣어지고, 결괏값도 맞는 것을 확인할 수 있습니다.

채우기 핸들을 이용하여 수식 복사하기

(4) MidB함수를 LeftB함수로 바꾸기

MidB(단어,1,8)은 LeftB(단어,8)로 바꿀 수 있습니다

예) =CONCAT(LEFTB(TEXTSPLIT(A2,” “)&REPT(” “,8),1,8))

어렵게 왔네요.

그러나, 한 단계씩 밟아오면 긴 수식이 완성되는 기쁨을 느낄 수 있습니다.

소계를 제외하고 합계 구하기 – Lookup 함수

1. 문제

합계를 범위내에서 홀수인 행과 짝수인 행으로 나눠서 구하고 있는데, 중간에 소계가 있어서 여기에 숫자를 입력하게 되면 합계가 틀려지는 문제가 있으므로 소계인 경우에는 합계에서 제외하도록 해야 합니다.

(짝수인 행 합계 수식) =SUMPRODUCT((MOD(ROW(H$4:H$23),2)=0)*H$4:H$23)

급여대장 소계가 더해지는 문제점 있음

2. 해결방법

가. Lookup 함수

그런데 문제는 셀인 병합되어 있어서 소계가 윗 셀에만 있고 아랫 셀은 비어 있다는 것입니다. 이럴 때 생각나는 것이 Lookup함수입니다.

Lookup 함수는 벡터형과 배열형이 있는데, 이 경우에 사용하는 것은 벡터형이며,

구문은 LOOKUP(lookup_value, lookup_vector, [result_vector])입니다.

찾을 값을 찾을 벡터에서 찾은 후 결과 벡터를 반환하는 것입니다.

예제) =lookup(4.19,A2:A6,B2:B6)

4.19를 A2셀에서 A6셀까지에서 찾아서, 일치하는 셀인 A3셀과 같은 위치의 B3셀 값이 주황색을 반환하는 것입니다. 이건 너무 단순한 경우입니다.

Lookup 함수 예시

나. 셀이 비어 있는 경우 윗 셀값으로 채우기

N4셀에 =LOOKUP(ROW(C4:C23),ROW(C4:C23)/(C4:C23<>””),C4:C23)라고 입력하면 아래와 같이 C4:C23<>””로 빈 셀인지 체크해서 빈 셀이라면 윗셀 값으로 채웁니다. N5셀의 경우도 1, N13셀의 경우는 “소계”로 채웠습니다.

Lookup 함수를 이용해 빈 셀에 값 채워넣기

위 화면을 보면 Microsoft 365를 사용해서 결과값 영역에 파란 선이 둘러쳐져 있는데, 낮은 버전이라면 먼저 결과가 표시될 영역인 H4셀부터 H23셀까지를 선택한 다음 위 수식을 입력하고, Ctrl+Shift+Enter키를 눌러야 할 것입니다. 그러면 위 수식은 왼쪽과 오른쪽에 중괄호가 없는데, 낮은 버전의 경우는 중괄호가 표시될 것입니다.

다. SumProduct 함수를 이용한 수식 변경하기

(H24셀 수식 수정하기)

이제 H24셀의 수식에 위 Lookup함수를 이용한 수식을 추가하겠습니다.

(MOD(ROW(H$4:H$23),2)=0)라는 행이 짝수인 경우에 소계가 아닌 경우를 추가하면 됩니다.

따라서 조건은 (MOD(ROW(H$4:H$23),2)=0)*(LOOKUP(ROW($C$4:$C$23),ROW($C$4:$C$23)/($C$4:$C$23<>””),$C$4:$C$23)<>”소계”)가 됩니다.

기존 Lookup 수식에서 C4:C24 범위에 F4키를 눌러 절대 참조형식으로 수정했고, Lookup 수식 뒤에 <>”소계”를 추가해서 ‘소계’가 아닌 경우만 더하도록 했습니다.

전체 수식은

=SUMPRODUCT((MOD(ROW(H$4:H$23),2)=0)(LOOKUP(ROW($C$4:$C$23),ROW($C$4:$C$23)/($C$4:$C$23<>””),$C$4:$C$23)<>”소계”)H$4:H$23)

입니다.

소계를 제외하고 합계 구하기

이제 짝수행인 소계 H12셀에 숫자를 넣어도 합계가 달라지지 않는 것을 알 수 있습니다.

중간에 소계를 넣어서 합계가 변하지 않는지 체크하기

그러나 총합계는 조건이 없기때문에 달라집니다.

(H25셀 수식 수정하기)

H25셀의 수식에도 Lookup 수식을 이용한 조건인

*(LOOKUP(ROW($C$4:$C$23),ROW($C$4:$C$23)/($C$4:$C$23<>””),$C$4:$C$23)<>”소계”)

를 추가해서 수정하면

*(LOOKUP(ROW($C$4:$C$23),ROW($C$4:$C$23)/($C$4:$C$23<>””),$C$4:$C$23)<>”소계”)

=SUMPRODUCT((MOD(ROW(H$4:H$23),2)=1)(LOOKUP(ROW($C$4:$C$23),ROW($C$4:$C$23)/($C$4:$C$23<>””),$C$4:$C$23)<>”소계”)H$4:H$23)

가 됩니다.

(L열까지 수식 복사해서 붙여 넣기)

이제 H24셀과 H25셀의 수식을 복사해서 L26셀까지 붙여넣거나, H24셀과 H25셀을 선택한 다음 H25셀의 채우기 핸들을 L26셀까지 끌어도 됩니다.

합계 수식 수정한 후 복사하기

라. 총합계셀 수정하기

(H27셀 수식 수정하기)

Sum을 SumProduct로 수정하고,

마찬가지로 “소계”가 아닌 조건식

(LOOKUP(ROW($C$4:$C$23),ROW($C$4:$C$23)/($C$4:$C$23<>””),$C$4:$C$23)<>”소계”)

을 추가하면 됩니다.

완성된 수식은

=SUMPRODUCT((LOOKUP(ROW($C$4:$C$23),ROW($C$4:$C$23)/($C$4:$C$23<>””),$C$4:$C$23)<>”소계”)*H4:H23)

입니다.

총합계 수식에도 "소계"제외하는 조건 추가하기

H27셀의 채우기 핸들을 L27셀까지 끌어서 수식을 복사합니다.

마. 완성된 파일

VLookup 수식 오류 찾는 방법

VLookup는 자료를 찾을 때 많이 사용하는 엑셀의 대표적인 함수 중 하나일 것입니다.

대부분의 경우 문제없이 값을 찾을 수 있지만 #N/A 에러가 발생하면 왜 그러지 하고 답답하기만 할 것입니다.

이 때 사용할 수 있는 방법에 대해 알아보겠습니다.

1. 문제

아래 수식을 보면 =VLOOKUP(F3,$A$3:$D$7,2,0)로

vlookup 수식의 결괏값이 #N/A라고 표시됨

F3셀 값을 A3셀에서 A7셀에서 정확히 일치하는 값을 찾은 다음 두번째 열인 B열에서 Tall의 가격을 가져오는 전형적인 수식입니다.

값이 4,600이 나와야 하는데, #N/A라고 나오고 있습니다.

2. 해결 방법 1 – 수식 계산(실패)

수식 계산 명령은 수식 탭 > 수식 분석 그룹에 있습니다.

수식 탭 - 수식 분석 그룹의 수식 계산 명령

수식이 있는 G3셀이 선택된 상태에서 수식 계산 명령을 누르면

수식 계산 창이 열리는데, 계산이라고 표시된 상자 안에 수식이 보이는데 F3 아래에 밑줄이 그어져 있고, 아래쪽에 계산 버튼이 있고, 오른쪽에는 닫기 버튼이 있습니다.

수식 계산 창에서 찾을 값인 F3셀이 선택된 상태에서 계산 버튼이 보이는 화면

계산 버튼을 누르면 F3가 “카푸치노”로 바뀌고, VLOOKUP 수식 전체에 밑줄이 그어져 있습니다. 다시 계산 버튼을 누르면

수식 계산 창에서 계산 탭을 누르니, vlookup 수식 중 찾을 값이 "카푸치노"라고 표시됨

전체 수식의 결과가 #N/A라고 표시됩니다. $A$3:$D$7의 값을 보고 싶은데 보여주지 않습니다.

수식 계산 결과 vlookup 수식의 결괏값이 #N/A라고 표시됨

3. 해결 방법 2 – F9키 이용

F3셀의 값은 “카푸치노”라는 것은 알았는데, $A$3:$D$7의 값을 알아야 하는데, 이 때 사용할 수 있는 것이 F9키(수동 계산)키입니다.

수식 입력줄에서 $A$3:$D$7를 마우스로 끌어서 범위로 선택한 다음

vlookup 수식에서 범위를 선택한 화면

F9키를 누르면 셀 값이 배열이므로 중괄호 안에 표시됩니다.

범위의 결괏값

값이 표현식 부분에서 Ctrl + C키를 누른 다음 붙여 넣습니다.

{“카페 아메리카노”,4100,4600,5100;”카페 라떼”,4600,5100,5600;”카푸치노 “,4600,5100,5600;”오늘의커피”,3800,4300,4800;”카페모카”,5100,5600,6100}

위 값을 보면 잘 모르겠는데, 수식 입력줄을 보면 “카푸치노 “라고 ‘노’ 다음에 공백이 한 칸이 있는 것을 알 수 있고, 위 붙여 넣은 값을 봐도 “카푸치노 “라고 공백이 한 칸 추가된 것을 알 수 있습니다.

다시 말해 “카푸치노”라야 하는데, “카푸치노 “이다 보니 값이 일치하지 않아 결괏값 4,600이 표시되지 않고 #N/A라고 표시되는 것입니다.

4. 해결 방법 3 – =(비교 연산자)

위 수식의 경우는 범위가 몇 개 안되기 때문에 위와 같이 F9키를 누르더라도 쉽게 차이점을 알 수 있는데, 범위가 넓다면 위와 같이 전체 범위로 하지 않고

=F3=A5라고 찾을 셀끼리만 비교하는 수식은 만드는 것이 효율적입니다.

Esc키를 누른 다음

F4셀에 =F3=A5라고 F3셀 값과 A5셀 값이 같은지 비교하는 수식을 입력하고 엔터 키를 누르면 FALSE라고 다르다고 합니다.

값이 같은지 비교하는 수식(비교 연산자 =)

일단 다르다는 것은 알겠는데, 왜 다른지를 알려면 다시 F9키를 눌러보면 됩니다.

F3을 범위로 잡고 F9키를 누르고, A5를 범위로 잡고 F9키를 누르면

“카푸치노”와 “카푸치노 “로 값이 다르다는 것을 알 수 있습니다.

"카푸치노"와 "카푸치노 "로 값이 다름

5. 오류 수정 방법

위 경우는 공백이 있는 경우이므로 아래와 같이 수정하면 되는데,

오류에 따라 해결 방법을 찾으면 됩니다

공백을 제거하기 위해

Esc키를 누르고, A5셀에서 F2(편집)키를 누르거나, 수식 입력줄을 클릭하면

커서가 한 칸 공백 다음에 위치하고 있는 것을 알 수 있습니다.

수식 입력줄을 클릭하니 "카푸치노" 뒤에 공백이 있음

공백을 지우기 위해 백스페이스키를 누르고 엔터키를 누르면

값 4600원이 구해집니다.

A5셀에서 공백을 제거하니 vlookup 수식 결과가 정상적으로 표시됨

천단위 구분 기호인 쉼표(,)를 넣기 위해 원하는 범위 만큼 선택한 다음(여기서는 G3셀에서 G13셀까지 선택) 홈 탭 > 표시 형식 그룹에서 ,를 누릅니다.

홈 탭 > 표시 형식 그룹의 쉼표 스타일(구분 기호)

그러면 숫자에 ,가 들어갑니다.

숫자에 천단위마다 쉼표가 들어간 화면

Yahoo Finance에서 주식 정보 가져오기 (4)

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 chrono crate을 이용한 format_timestamp 함수와 tokio main 매크로에 대해 알아보겠습니다.

Ⅰ. format_timestamp 함수에 대해 알아보기

fn format_timestamp(timestamp: i64) -> String {
match Utc.timestamp_opt(timestamp, 0) {
chrono::LocalResult::Single(datetime) => datetime.format("%Y-%m-%d %H:%M:%S").to_string(),
_ => "Invalid timestamp".to_string(),
}
}

1. 입력값

timestamp: i64 : 이 값은 유닉스 타임스탬프(1970-01-01 00:00:00 UTC 기준의 초 단위 정수) 입니다.

2. Utc.timestamp_opt(timestamp, 0)

  • chrono::Utc는 UTC(협정 세계시) 타임존을 나타냅니다.
  • timestamp_opt(secs, nsecs)는 주어진 초(secs)와 나노초(nsecs)를 UTC 시각으로 변환하려고 시도합니다.
  • 반환값은 -chrono::LocalResult> enum인데, 이에는 세 가지 경우가 있습니다.
    – Single(datetime) → 정상적으로 변환됨
    – None → 변환 불가
    – Ambiguous(, ) → 모호한 시간 (주로 로컬 타임존에서 섬머타임 전환 시 발생, UTC에서는 거의 없음)

3. match 표현식

  • chrono::LocalResult::Single(datetime)일 경우에는 변환된 datetime을 format(“%Y-%m-%d %H:%M:%S”)로 지정한 문자열 포맷(연-월-일 시:분:초)으로 변환하고,
  • 그 밖의 경우(None, Ambiguous)는 “Invalid timestamp”라는 문자열을 반환합니다.

Ⅱ. tokio main 매크로에 대해 알아보기

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let symbols = [
"005930.KS", // 삼성전자
"AAPL", // Apple
"TSLA", // Tesla
"LIT", // Global X Lithium & Battery Tech ETF
"BRK-B", // Berkshire Hathaway Class B
"AMZN", // Amazon
"O", // Realty Income Corporation
"TQQQ", // ProShares UltraPro QQQ
"XOM", // Exxon Mobil
"WMT", // Walmart
];

println!("Fetching stock data...\n");

let results = fetch_multiple_stocks(&symbols).await;

// 테이블 헤더 출력
println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
"No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
println!("{}", "-".repeat(95));

for (i, result) in results.iter().enumerate() {
let row_number = i + 1;
match result {
Ok(stock) => {
println!("{:<3} {:<10} {:<35} {:<15.2} {:<8} {:<20}",
row_number,
stock.symbol,
if stock.long_name.len() > 35 {
format!("{}...", &stock.long_name[..32])
} else {
stock.long_name.clone()
},
stock.regular_market_price,
stock.currency,
format_timestamp(stock.regular_market_time)
);
}
Err(e) => {
println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
row_number,
symbols[i],
"Error fetching data",
"N/A",
"N/A",
"N/A"
);
}
}
}

Ok(())
}

1. Tokio 런타임

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
  • #[tokio::main]은 Tokio 비동기 런타임을 사용하는 매크로.
  • async fn main() → 메인 함수 자체가 비동기 함수로 실행됨.
  • 반환 타입이 Result<(), Box<dyn std::error::Error + Send + Sync>>으로서 성공했을 때는 (), 다시 말해 빈 튜플을 반환하므로 아무 값도 안돌려주고, 오류가 발생했을 때 다양한 에러 타입을 동적으로 담아 반환합니다.

2. 주식 심볼 배열

let symbols = [
"005930.KS", // 삼성전자
"AAPL", // Apple
"TSLA", // Tesla
"LIT", // Global X Lithium & Battery Tech ETF
"BRK-B", // Berkshire Hathaway Class B
"AMZN", // Amazon
"O", // Realty Income Corporation
"TQQQ", // ProShares UltraPro QQQ
"XOM", // Exxon Mobil
"WMT", // Walmart
];
  • fetch_multiple_stocks함수의 인수로 전달하기 위해 여러 주식의 티커(symbol)를 배열로 정의합니다.

3. 주식 데이터 가져오기

println!("Fetching stock data...\n");

let results = fetch_multiple_stocks(&symbols).await;
  • “Fetching stock data…” → 데이터 가져오기 시작 알림 출력.
  • fetch_multiple_stocks 함수는 비동기 함수로, 각 심볼에 대해 API 호출을 시도하여 Vec<Result<StockData, Error>> 를 반환함

4. 표 헤더 출력

println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
"No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
println!("{}", "-".repeat(95));
  • {:<n} 포맷은 왼쪽 정렬(Left Align) 하여 n 자리 확보.
  • 따라서 표 형태로 깔끔하게 정렬됨.
  • 이어서 “-“.repeat(95)로 구분선 출력.

5. 각 결과 출력

for (i, result) in results.iter().enumerate() {
let row_number = i + 1;
match result {
Ok(stock) => {
println!("{:<3} {:<10} {:<35} {:<15.2} {:<8} {:<20}",
row_number,
stock.symbol,
if stock.long_name.len() > 35 {
format!("{}...", &stock.long_name[..32])
} else {
stock.long_name.clone()
},
stock.regular_market_price,
stock.currency,
format_timestamp(stock.regular_market_time)
);
}
Err(e) => {
println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}",
row_number,
symbols[i],
"Error fetching data",
"N/A",
"N/A",
"N/A"
);
}
}
}
  • enumerate()를 쓰면 (index, result) 튜플이 넘어옴. row_number = i + 1로 1씩 증가하는 번호를 출력.
    • match result:
      ① 성공(Ok(stock))하면
      – stock(StockData 구조체)를 이용해 주식 데이터를 출력하는데, stock은 result가 성공했을 때 값입니다.
      – stock.symbol 값을 symbol로 출력하고,
      – stock.long_name은 너무 길면 앞의 32자만 출력하고, + “…” 붙여서 가독성을 유지하고,
      – stock.regular_market_price는 소수점 둘째 자리까지 표시하고,({:.2}),
      – 화폐 단위(currency)를 출력하고,
      – stock.regular_market_time은 format_timestamp 함수로 변환하여 표시함

      ② 실패(Err)하면
      – 주어진 symbols[i] 심볼과 함께 “Error fetching data”, “N/A” 값들을 표시.

6. 종료

Ok(())
  • main 함수가 정상 종료됨을 의미.

Yahoo Finance에서 주식 정보 가져오기 (3)

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 fetch_stock_data과 fetch_multiple_stocks 함수에 대해 알아보겠습니다.

Ⅰ. fetch_stock_data 함수에 대해 알아보기

async fn fetch_stock_data(symbol: &str) -> Result<StockData, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/{}", symbol);

let client = reqwest::Client::new();
let response = client
.get(&url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.send()
.await?;
let text = response.text().await?;
let yahoo_response: YahooResponse = serde_json::from_str(&text)?;
if let Some(result) = yahoo_response.chart.result.first() {
let meta = &result.meta;

Ok(StockData {
symbol: symbol.to_string(),
long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
regular_market_price: meta.regular_market_price.unwrap_or(0.0),
currency: meta.currency.clone(),
regular_market_time: meta.regular_market_time.unwrap_or(0),
})
} else {
Err(format!("No data found for symbol: {}", symbol).into())
}
}

1. 함수 시그니처

async fn fetch_stock_data(symbol: &str) 
-> Result<StockData, Box<dyn std::error::Error + Send + Sync>>
  • async fn → 비동기 함수, await를 사용할 수 있음
  • 입력값: symbol → “AAPL”, “TSLA” 같은 종목 코드
  • 반환값은 성공 시는 StockData 구조체, 실패 시는 에러(Box<dyn std::error::Error + Send + Sync>를 반환하는데, 멀티스레드 환경이라 Box<dyn std::error::Error >에 Send(스레드간 이동)와 Sync(동시 접근) trait를 추가한 것임

2. 함수 동작 흐름

가. API URL 만들기

let url = format!(
"https://query1.finance.yahoo.com/v8/finance/chart/{}",
symbol
);
  • symbol을 이용해 Yahoo Finance의 차트 데이터 API 주소 구성
    예) https://query1.finance.yahoo.com/v8/finance/chart/AAPL

나. HTTP 클라이언트 준비

let client = reqwest::Client::new();
  • reqwest 라이브러리의 비동기 HTTP 클라이언트를 생성

다. GET 요청 보내기

let response = client
.get(&url)
.header(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)
.send()
.await?;
  • Yahoo Finance API 서버에 GET 요청
  • 브라우저처럼보이게 하려고 User-Agent 헤더를 추가 (봇 차단 방지 목적)
  • .await? → 요청 완료를 기다리고, 실패 시 에러 전파

라. 응답 본문(JSON) 텍스트 추출

let text = response.text().await?;
  • HTTP 응답을 문자열 형태로 읽어옴

마. JSON 파싱

let yahoo_response: YahooResponse = serde_json::from_str(&text)?;
  • serde_json을 사용해 JSON 문자열을 YahooResponse구조체로 변환
  • 여기서 YahooResponse와 내부 구조(chart.result, meta 등)는 미리 serde를 이용해 파싱할 수 있도록 정의되어 있어야 함

바. 데이터 꺼내서 StockData 만들기

if let Some(result) = yahoo_response.chart.result.first() {
let meta = &result.meta;

Ok(StockData {
symbol: symbol.to_string(),
long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
regular_market_price: meta.regular_market_price.unwrap_or(0.0),
currency: meta.currency.clone(),
regular_market_time: meta.regular_market_time.unwrap_or(0),
})
} else {
Err(format!("No data found for symbol: {}", symbol).into())
}
  • chart.result의 첫 번째 요소를 가져옴
  • 거기서 meta 정보를 추출
  • 회사명, 현재 시장 가격, 통화 단위, 마지막 거래 시간 등을 꺼내 StockData에 담음
  • 데이터가 없으면 Err로 반환
위 예에서, unwrap_or는 Option이 None일 때 값을 0.0 또는 0으로 지정하는데,
unwrap_or_else는 Option이 None일 때 클로저가 실행되고, clone()이 추가된 차이점이 있음

Ⅱ. fetch_multiple_stocks 함수에 대해 알아보기

async fn fetch_multiple_stocks(symbols: &[&str]) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>> {
let mut handles = Vec::new();

for &symbol in symbols {
let symbol_owned = symbol.to_string();
let handle = tokio::spawn(async move {
fetch_stock_data(&symbol_owned).await
});
handles.push(handle);
}

let mut results = Vec::new();
for handle in handles {
match handle.await {
Ok(result) => results.push(result),
Err(e) => results.push(Err(e.into())),
}
}

results
}

이 함수는 여러 주식 종목(symbol)을 동시에 비동기로 조회(fetch) 하기 위해 tokio::spawn을 사용하는 구조입니다.

1. 함수 시그니처

async fn fetch_multiple_stocks(
symbols: &[&str]
) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>>
  • async fn → 비동기 함수이므로 호출 시 .await 필요.
  • 입력값은 &[&str]로 &str 슬라이스 (예: &[“AAPL”, “GOOG”, “TSLA”])
  • 반환값: Result의 벡터
    – 각 종목(symbol)에 대해 Ok(StockData) 또는 Err(에러)가 담긴 리스트.
    – 즉, 한 종목 실패해도 다른 종목은 결과를 받을 수 있음.

2. 주요 동작 흐름

가. handles 벡터 생성

let mut handles = Vec::new();
  • 비동기 작업(태스크) 핸들을 저장해 둘 벡터.

나. 종목별 비동기 작업 생성

for &symbol in symbols {
let symbol_owned = symbol.to_string(); // 소유권 있는 String으로 변환
let handle = tokio::spawn(async move {
fetch_stock_data(&symbol_owned).await
});
handles.push(handle);
}
  • for 루프를 돌면서 각 종목 기호(&str)를 String으로 복사(to_string)
    → 이유: tokio::spawn의 async move 블록은 ‘static 라이프타임을 요구하기 때문.
    원본 &str는 반복문이 끝나면 사라질 수 있으니, 안전하게 소유권 있는 String 사용.
  • tokio::spawn(…) → 배경(백그라운드)에서 새로운 비동기 태스크 생성
  • async move → 클로저에 캡처되는 값(symbol_owned)을 이동(move)시켜 사용.
  • 결과: 각 종목을 조회하는 여러 비동기 태스크가 동시에 실행됨.

다. 모든 태스크 완료 대기

let mut results = Vec::new();
for handle in handles {
match handle.await {
Ok(result) => results.push(result),
Err(e) => results.push(Err(e.into())),
}
}
  • handle.await → 해당 비동기 태스크가 끝날 때까지 대기.
  • handle.await의 반환값:
    – Ok(result) → 작업이 정상 종료 → result(= Result)를 results에 저장.
    – Err(e) → 태스크 자체가 패닉 또는 취소 → 에러를 Box로 변환해 저장.

라. 결과 반환

results
  • 벡터에는 각 종목별 Result<StockData, Error>가 순서대로 저장됨.

마. 주의점

종목 수가 매우 많으면 동시에 많은 태스크가 실행되어 서버나 네트워크에 부하 발생 가능 → tokio::task::JoinSet이나 futures::stream::FuturesUnordered로 동시 실행 수를 제한하는 방법 고려 가능.

Yahoo Finance에서 주식 정보 가져오기 (2)

https://overmt.com/yahoo-finance에서-주식-정보-가져오기-1/
의 코드 중 코드 2를 기준으로 use(임포트)와 struct(구조체) 부분의 코드에 대해 알아보겠습니다.

1. use

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio;
use chrono::{DateTime, Utc, TimeZone};

가. use reqwest;

HTTP 클라이언트 라이브러리를 불러오는(import) 기능으로, Yahoo Finance API에서 주식 데이터를 가져오는 HTTP를 요청하기 위해 사용합니다.
예시: reqwest::get(“https://api.example.com”)

Cargo.toml에서는 dependency를 선언한 것이고, 사용하려면 다시 use를 해야 합니다.

나. use serde::{Deserialize, Serialize};

직렬화/역직렬화 라이브러리인 serde를 import 하는 구문으로, ::{Deserialize, Serialize}는 라이브러리의 전체를 가져오는 것이 아니라 Deserialize와 Serialize 트레이트(trait)만 가져오는 것입니다.

JSON 데이터를 Rust 구조체로 변환하거나, 그 반대 역확을 하며, 본 코드에서는 Yahoo API의 JSON 응답을 StockData 구조체로 변환하는데 사용합니다.

dependencies의 serde와 main.rs의 serde의 차이점을 알아보면 아래와 같습니다.

[Cargo.toml의 dependencies]
serde = { version = "1.0", features = ["derive"] }는 serde 크레이트의 추가 기능인 derive를 활성화하는 것으로 #[derive(Serialize, Deserialize)] 매크로를 사용 가능하게 합니다.

[main.rs의 use]
use serde::{Deserialize, Serialize};는 실제로 구조체에 적용할 트레이트인 Deserialize, Serialize 트레이트를 사용하기 위한 것입니다.

[main.rs의 struct 구문]
// derive 기능이 활성화되어서 이 매크로 사용 가능
#[derive(Debug, Deserialize, Serialize)]
struct StockData {
symbol: String,
price: f64,
}

[3단계 구조]
features = ["derive"]로 derive 기능을 활성화하고,
use serde::{...}로 Deserialize, Serialize 트레이트를 가져오고,
#[derive(...)]로 구조체에 실제 매크로를 적용하는 3단계 구조입니다.

다. use std::collections::HashMap;

키-값 쌍을 저장하는 해시맵을 import 하는 것인데, 현재 코드에서는 사용하지 않습니다. 지우고 실행해보니 문제없습니다.

claude.ai가 필요없으니 삭제를 해야 하는데, 삭제를 하지 않았네요.

cargo run을 하니 unused가 HashMap뿐만 아니라 DateTime, e도 있습니다.

use std::collections::HashMap;은 한 줄을 지우고,

use chrono::{DateTime, Utc, TimeZone};에서는 DateTime만 지우고,

error를 의미하는 e는 위 화면의 제안에 따라 _e로 바꾸고 실행하니 문제없이 깔끔하게 실행됩니다.

라. use tokio;

tokio라는 비동기 런타임 라이브러리를 임포트하는 구문으로 async/await를 이용해 여러 주식의 정보를 동시에 병렬로 가져오기가 가능해집니다. 따라서, 10개 주식을 순차적으로 가져오면 10초가 걸리는데, 병렬로 가져오면 1초뿐이 안걸립니다.

마. use chrono::{Utc, TimeZone};

chrono는 날짜/시간 처리 라이브러리로서 Unix 타임스탬프를 사람이 읽기 쉬운 날짜로 변환해줍니다. 예를 들어 1692345600를 “2023-08-18 12:00:00″로 바꿔줍니다.

Unix 타임 스탬프는 UTC 기준으로 1970.1.1부터의 누적된 초이며, UTC는 협정 세계시(Coordinated Universal Time의 약어)로서 그리니치 표준시 (GMT)의 후속 표준이라고 합니다.

2. struct

이 코드는 Yahoo Finance API에서 주식 정보를 받아오기 위한 **데이터 구조(Struct)**를 정의한 것입니다.

serde 라이브러리를 이용해 JSON 데이터 → Rust 구조체 변환(Deserialize)과 반대로 변환(Serialize)을 하기 위해 설계되어 있습니다.

postman 사이트에서 https://query1.finance.yahoo.com/v8/finance/chart/AAPL를 열어보면 아래와 같이 깔끔한 JSON 포맷의 데이터를 보여줍니다.

중괄호 안에 chart(key)가 있고, 그 안에 result와 대괄호(배열)가 있고, 다시 중괄호 다음에 meta가 있으며, 그 안에 우리가 얻고자 하는 currency, symbol, regularMarketPrice 등이 key: Value 쌍으로 담겨져 있습니다. 이에 따라 단계별로 struct를 만듭니다.

가. YahooResponse

#[derive(Debug, Deserialize, Serialize)]
struct YahooResponse {
chart: Chart,
}
  • API 응답 전체를 감싸는 최상위 구조체로서,
  • Yahoo API가 반환하는JSON 최상단에 있는 “chart” 필드를 받기 위해 사용하는데, 데이터형식은 아래 ‘나. Chart’입니다.

나. Chart

struct Chart {
result: Vec<ChartResult>,
error: Option<serde_json::Value>,
}
  • result: 실제 주식 데이터가 담긴 배열이므로 Vec 타입이고, ChartResult 형식의 데이터를 담습니다.
  • error: 에러가 있을 경우 그 내용을 담는 필드로서, 값이 없을 수도 있으니 Option 열거형이며, T값은 serde_json::Value입니다.

다. ChartResult

struct ChartResult {
meta: Meta,
}
  • meta에는 해당 종목의 기본 정보(symbol, regularMartketPrice, currency 등)가 들어 있으며, 데이터 타입은 ‘라. Meta’입니다.

라. Meta

struct Meta {
currency: String,
symbol: String,
#[serde(rename = "longName")]
long_name: Option<String>,
#[serde(rename = "regularMarketPrice")]
regular_market_price: Option<f64>,
#[serde(rename = "regularMarketTime")]
regular_market_time: Option<i64>,
}
  • 개별 종목의 메타데이터입니다.
  • #[serde(rename = “longName”)]  속성(Attribute)은 JSON의 필드 이름인 long_name을 Rust 필드 이름인 longName으로 바꾸는 역할을 하며,
  • Option을 쓰는 이유는 해당 값이 API 응답에서 없을 수 있기 때문입니다.

마. StockData

#[derive(Debug, Clone)]
struct StockData {
symbol: String,
long_name: String,
regular_market_price: f64,
currency: String,
regular_market_time: i64,
}
  • 실제 사용할 가공된 데이터 구조체로서, Debug와 Clone trait을 자동 구현하며,
  • 위의 Meta에서 필요한 값만 골라와서, 모두 Option 없이 바로 사용 가능한 형태로 변환한 것이며,
  • 프로그램 내부 로직(예: UI 표시, 계산)에서 바로 쓰기 편하도록 만든 것입니다.

Yahoo Finance에서 주식 정보 가져오기 (1)

모방은 창조의 어머니인가요? claude.ai의 도움을 받아 만든 “Yahoo Finace API를 이용해서 주식 데이터를 가져오는 프로그램”을 살펴보겠습니다. Rust는 먼저 Cargo.toml 파일에서 가져올 크레이트(라이브러리)를 정의하고, main.rs에서 실행 코드를 구현합니다.

1. Cargo.toml

[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.0", features = ["full"] }

dependencies 섹션에 reqwest, serde, serde_json, tokio 크레이트(crate, library)를 버전과 features를 이용해 지정합니다.

가. reqwest

request가 맞는 단어인데, reqwest로 약간 다른 점 주의해야 합니다.
reqwest 크레이트는 편리하고 높은 수준의 HTTP 클라이언트를 제공합니다.

version은 크레이트의 버전을 지정하는 것은 알겠는데, features는 크레이트의 특정 기능(선택적 기능)을 활성화하거나 비활성화할 때 사용되며, 조건부 컴파일을 가능하게 하여, 필요한 기능만 컴파일하도록 하는 것입니다.

features = [“json”]은 “json” 기능을 켜서 JSON 직렬화/역직렬화 기능을 사용할 수 있게 하는 것입니다.

아래와 같이 버전 목록이 표시되는데, 맨 위에 0.12.23이 있으므로 클릭합니다.

그러면 버전이 자동으로 변경되고, X표시가 없어지고, 녹색 체크 표시로 바뀝니다.

나. serde

데이터 직렬화(Serialize) / 역직렬화(Deserialize) 라이브러리입니다.
JSON, TOML, YAML 등 다양한 포맷과 Rust 구조체를 변환할 때 사용하며,

features = [“derive”]는

[derive(Serialize, Deserialize)] 어트리뷰트를 쓸 수 있게 해주는 것입니다.

다. serde_json

serde의 JSON 전용 확장판으로서, Rust 데이터와 JSON 문자열간에 변환을 가능하게 해줍니다.

let user = User { name: "Kim".into(), age: 30 };
let json_str = serde_json::to_string(&user)?; // 구조체 → JSON 문자열
let parsed: User = serde_json::from_str(&json_str)?; // JSON → 구조체

라. tokio

Rust의 비동기 런타임 (async/await 동작을 실제로 수행하는 엔진)으로서,
features = [“full”]은 모든 기능(네트워킹, 파일 I/O, 타이머 등)을 한 번에 활성화하는 것이며,
#[tokio::main] 매크로로 main 함수를 비동기로 만들 수 있습니다.

2. main.rs

가. 코드 1

아래 코드를 src 폴더의 main.rs를 연 후 Ctrl + A해서 전체를 선택한 후 Ctrl + V를 하면 기존 내용에 덮어씌워집니다.

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio;

#[derive(Debug, Deserialize, Serialize)]
struct YahooResponse {
    chart: Chart,
}

#[derive(Debug, Deserialize, Serialize)]
struct Chart {
    result: Vec<ChartResult>,
    error: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize, Serialize)]
struct ChartResult {
    meta: Meta,
}

#[derive(Debug, Deserialize, Serialize)]
struct Meta {
    currency: String,
    symbol: String,
    #[serde(rename = "longName")]
    long_name: Option<String>,
    #[serde(rename = "regularMarketPrice")]
    regular_market_price: Option<f64>,
    #[serde(rename = "regularMarketTime")]
    regular_market_time: Option<i64>,
}

#[derive(Debug, Clone)]
struct StockData {
    symbol: String,
    long_name: String,
    regular_market_price: f64,
    currency: String,
    regular_market_time: i64,
}

async fn fetch_stock_data(symbol: &str) -> Result<StockData, Box<dyn std::error::Error + Send + Sync>> {
    let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/{}", symbol);
    
    let client = reqwest::Client::new();
    let response = client
        .get(&url)
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
        .send()
        .await?;

    let text = response.text().await?;
    let yahoo_response: YahooResponse = serde_json::from_str(&text)?;

    if let Some(result) = yahoo_response.chart.result.first() {
        let meta = &result.meta;
        
        Ok(StockData {
            symbol: symbol.to_string(),
            long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
            regular_market_price: meta.regular_market_price.unwrap_or(0.0),
            currency: meta.currency.clone(),
            regular_market_time: meta.regular_market_time.unwrap_or(0),
        })
    } else {
        Err(format!("No data found for symbol: {}", symbol).into())
    }
}

async fn fetch_multiple_stocks(symbols: &[&str]) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>> {
    let mut handles = Vec::new();
    
    for &symbol in symbols {
        let symbol_owned = symbol.to_string();
        let handle = tokio::spawn(async move {
            fetch_stock_data(&symbol_owned).await
        });
        handles.push(handle);
    }
    
    let mut results = Vec::new();
    for handle in handles {
        match handle.await {
            Ok(result) => results.push(result),
            Err(e) => results.push(Err(e.into())),
        }
    }
    
    results
}

fn format_timestamp(timestamp: i64) -> String {
    use std::time::{UNIX_EPOCH, Duration};
    
    let datetime = UNIX_EPOCH + Duration::from_secs(timestamp as u64);
    
    // 간단한 포맷팅 (실제로는 chrono 크레이트 사용 권장)
    format!("Unix timestamp: {}", timestamp)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let symbols = [
        "005930.KS", // 삼성전자
        "AAPL",      // Apple
        "TSLA",      // Tesla
        "LIT",       // Global X Lithium & Battery Tech ETF
        "BRK-B",     // Berkshire Hathaway Class B
        "AMZN",      // Amazon
        "O",         // Realty Income Corporation
        "TQQQ",      // ProShares UltraPro QQQ
        "XOM",       // Exxon Mobil
        "WMT",       // Walmart
    ];

    println!("Fetching stock data...\n");
    
    let results = fetch_multiple_stocks(&symbols).await;
    
    for (i, result) in results.iter().enumerate() {
        match result {
            Ok(stock) => {
                println!("Symbol: {}", stock.symbol);
                println!("Long Name: {}", stock.long_name);
                println!("Regular Market Price: {:.2} {}", stock.regular_market_price, stock.currency);
                println!("Currency: {}", stock.currency);
                println!("Regular Market Time: {}", format_timestamp(stock.regular_market_time));
                println!("---");
            }
            Err(e) => {
                println!("Error fetching data for {}: {}", symbols[i], e);
                println!("---");
            }
        }
    }

    Ok(())
}

cargo run을 하면 compile과 build를 한 후

Run을 하는데, 주식 정보 조회 결과를 주식별로 하나씩 보여주고, 시간이 Unix timestampt로 보여줘서 날짜와 시간을 알 수 없습니다.

나 코드 2

그래서 엑셀 처럼 표 형태로 보여주고, Unix time을 년월일시로 바꿔달라고 했더니 아래 코드가 되는데,

use reqwest;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio;
use chrono::{DateTime, Utc, TimeZone};

#[derive(Debug, Deserialize, Serialize)]
struct YahooResponse {
    chart: Chart,
}

#[derive(Debug, Deserialize, Serialize)]
struct Chart {
    result: Vec<ChartResult>,
    error: Option<serde_json::Value>,
}

#[derive(Debug, Deserialize, Serialize)]
struct ChartResult {
    meta: Meta,
}

#[derive(Debug, Deserialize, Serialize)]
struct Meta {
    currency: String,
    symbol: String,
    #[serde(rename = "longName")]
    long_name: Option<String>,
    #[serde(rename = "regularMarketPrice")]
    regular_market_price: Option<f64>,
    #[serde(rename = "regularMarketTime")]
    regular_market_time: Option<i64>,
}

#[derive(Debug, Clone)]
struct StockData {
    symbol: String,
    long_name: String,
    regular_market_price: f64,
    currency: String,
    regular_market_time: i64,
}

async fn fetch_stock_data(symbol: &str) -> Result<StockData, Box<dyn std::error::Error + Send + Sync>> {
    let url = format!("https://query1.finance.yahoo.com/v8/finance/chart/{}", symbol);
    
    let client = reqwest::Client::new();
    let response = client
        .get(&url)
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
        .send()
        .await?;

    let text = response.text().await?;
    let yahoo_response: YahooResponse = serde_json::from_str(&text)?;

    if let Some(result) = yahoo_response.chart.result.first() {
        let meta = &result.meta;
        
        Ok(StockData {
            symbol: symbol.to_string(),
            long_name: meta.long_name.clone().unwrap_or_else(|| "N/A".to_string()),
            regular_market_price: meta.regular_market_price.unwrap_or(0.0),
            currency: meta.currency.clone(),
            regular_market_time: meta.regular_market_time.unwrap_or(0),
        })
    } else {
        Err(format!("No data found for symbol: {}", symbol).into())
    }
}

async fn fetch_multiple_stocks(symbols: &[&str]) -> Vec<Result<StockData, Box<dyn std::error::Error + Send + Sync>>> {
    let mut handles = Vec::new();
    
    for &symbol in symbols {
        let symbol_owned = symbol.to_string();
        let handle = tokio::spawn(async move {
            fetch_stock_data(&symbol_owned).await
        });
        handles.push(handle);
    }
    
    let mut results = Vec::new();
    for handle in handles {
        match handle.await {
            Ok(result) => results.push(result),
            Err(e) => results.push(Err(e.into())),
        }
    }
    
    results
}

fn format_timestamp(timestamp: i64) -> String {
    match Utc.timestamp_opt(timestamp, 0) {
        chrono::LocalResult::Single(datetime) => datetime.format("%Y-%m-%d %H:%M:%S").to_string(),
        _ => "Invalid timestamp".to_string(),
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let symbols = [
        "005930.KS", // 삼성전자
        "AAPL",      // Apple
        "TSLA",      // Tesla
        "LIT",       // Global X Lithium & Battery Tech ETF
        "BRK-B",     // Berkshire Hathaway Class B
        "AMZN",      // Amazon
        "O",         // Realty Income Corporation
        "TQQQ",      // ProShares UltraPro QQQ
        "XOM",       // Exxon Mobil
        "WMT",       // Walmart
    ];

    println!("Fetching stock data...\n");
    
    let results = fetch_multiple_stocks(&symbols).await;
    
    // 테이블 헤더 출력
    println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}", 
             "No.", "Symbol", "Long Name", "Regular Price", "Currency", "Regular Market Time");
    println!("{}", "-".repeat(95));
    
    for (i, result) in results.iter().enumerate() {
        let row_number = i + 1;
        match result {
            Ok(stock) => {
                println!("{:<3} {:<10} {:<35} {:<15.2} {:<8} {:<20}", 
                         row_number,
                         stock.symbol,
                         if stock.long_name.len() > 35 {
                             format!("{}...", &stock.long_name[..32])
                         } else {
                             stock.long_name.clone()
                         },
                         stock.regular_market_price,
                         stock.currency,
                         format_timestamp(stock.regular_market_time)
                );
            }
            Err(e) => {
                println!("{:<3} {:<10} {:<35} {:<15} {:<8} {:<20}", 
                         row_number,
                         symbols[i],
                         "Error fetching data",
                         "N/A",
                         "N/A",
                         "N/A"
                );
            }
        }
    }

    Ok(())
}

앞 부분에 use chrono::{DateTime, Utc, TimeZone};이 있어서

먼저 Cargo.toml에 chrono 크레이트를 추가해야 합니다.

chrono 버전에 커서를 갖다대보니 버전이 0.4.41로 표시되는데, 0.4 버전대이므로 그대로 둬도 문제 없습니다.

Cargo.toml과 main.rs를 저장하고, 실행하면

표 형태로 잘 표시되고, 날짜도 연월일 시분초로 잘 표시됩니다.

다음 편에서는 main.rs 코드를 하나씩 살펴보겠습니다.

Rust Formatting Cheat Sheet

formatting trait와 placeholder, formatter 기본 옵션에 대해 알아보고, 천단위 구분 표시(나라별 차이)에 대해서도 추가로 알아보겠습니다.

1. formatting trait과 placeholder

traitplaceholder 예설명
Display
{}사용자 친화적 출력
Debug{:?}/{:#?}개발·디버그 출력 / Pretty print
Binary{:b}2진수
Octal{:o}8진수
LowerHex{:x}16진수(소문자)
UpperHex{:X}16진수(대문자)
Pointer{:p}포인터 주소
LowerExp{:e}지수 표기법(소문자 e)
UpeperExp{:E}{:E}지수 표기법(대문자 E)

2. Formatter 옵션 (기본)

{[arg]:[fill][align][sign][#][0][width][.precision][type]}
옵션의미예시결과
{}자동 인덱스println!(“{} {}”, 1, 2); “1 2”
{0}(1}위치 인덱스println!(“{0} {1} {0}”, “a”, “b”);“a b a”
{name}이름(키) 인자println!(“{name} is {age}”, name=”Alice”, age=30);“Alice is 30”
<왼쪽 정렬{:<10}“left “
>오른쪽 정렬{:>10}” right”
^가운데 정렬{:^10}” center “
fill채움 문자 변경{:*^10}”“***hi****”
width폭 지정{:8}”” 42″
width$변수 폭 지정{:width$}”폭이 변수값
+항상 부호 표시. 양수도 +, 음수는 –
※ 기본은 음수만 – 표시하고, 양수는 미표시
{:+}”+42
‘ ‘(공백)(열 맞추기 위해) 양수일 때 공백 한 칸 추가println!(“{: }”, 42); ” 42″
#진번 접두사 표시#{:#x}”“0x2a”
0제로 패딩(0으로 채움)println!(“{:08.2}”, 3.14159); “00003.14” (폭 8, 소수점 2자리)
.precision소수점 자리수{:.2}“3.14”

3. 천 단위 구분 쉼표

Rust 표준 라이브러리에는 없으므로 외부 크레이트 또는 수동 구현이 필요합니다.

가. num-format (다국어 지원)

[dependencies]
num-format = "0.4"
use num_format::{Locale, ToFormattedString};

fn main() {
let n = 123456789;
println!("{}", n.to_formatted_string(&Locale::en)); // 123,456,789
println!("{}", n.to_formatted_string(&Locale::de)); // 123.456.789
}

나. separators (경량)

[dependencies]
separators = "0.4"
use separators::Separatable;

fn main() {
let n = 123456789;
println!("{}", n.separated_string()); // 123,456,789
}

다. 직접 구현

fn format_with_commas(n: i64) -> String {
let s = n.to_string();
let mut chars: Vec<char> = s.chars().rev().collect();
let mut result = String::new();
for (i, c) in chars.iter().enumerate() {
if i != 0 && i % 3 == 0 {
result.push(',');
}
result.push(*c);
}
result.chars().rev().collect()
}

fn main() {
println!("{}", format_with_commas(123456789)); // 123,456,789
}

4. 예시 모음

fn main() {
let n = 42;
println!("{:<10}", n); // 왼쪽 정렬
println!("{:>10}", n); // 오른쪽 정렬
println!("{:^10}", n); // 가운데 정렬
println!("{:*^10}", n); // 가운데 정렬 + * 채움
println!("{:08}", n); // 0으로 채움
println!("{:+}", n); // +42
println!("{:#x}", n); // 0x2a
println!("{:b}", n); // 101010
println!("{:width$}", n, width = 6); // 변수 폭 지정
}