In my engine, I use a dynamic vertex buffer for batching up text data (each character == 1 vertex) and do point-to-quad expansion in geometry shader.

As far as I know, glVertex*, glNormal*, glTexCoord* are considered obsolete (like display lists) and are not recommended to use.

This gives a comprehensive overview of various OpenGL-based text rendering techniques:
https://stackoverflow.com/questions/5262951/what-is-state-of-the-art-for-text-rendering-in-opengl-as-of-version-4-1

The details of the technique I'm using:

Offline:
0) prepare a font texture atlas (with tightly packed glyphs, block-compressed) and remember glyph data (position, size, unicode => glyph index mapping);
for these purposes I use MakeSpriteFont from [DirectX Tool Kit](https://directxtk.codeplex.com/).

At launch time:
1) load the font texture and glyph data (submit data of each mipmap level to glCompressedTexSubImage2D);
2) create a dynamic vertex buffer (GL_DYNAMIC_DRAW) for holding MAX_TEXT_LENGTH vertices;

At draw time:
4) parse the submitted text string and update the VBO using the glyph data;
5) draw the points from VBO

    glBindBuffer( GL_ARRAY_BUFFER, dynamicVBO_id );
    glDrawArrays( GL_POINTS, 0/*first vertex*/, vertexCount/*==character count*/ );

My vertex shader code:

    #version 420 core

    in vec4 a_texCoord0;	// .xy - center position, .zw - width and height
    in vec4 a_texCoord1;	// st coords for top left and bottom right corners

    out VSO {
    	vec4 xy_wh;
    	vec4 tl_br;
    } outputs;
    
    void main()
    {
    	// gl_Position will be written by geometry shader
    	outputs.xy_wh = a_texCoord0;
    	outputs.tl_br = a_texCoord1;
    }


Geometry shader code:

    #version 420 core
    
    layout(points) in;
    layout(triangle_strip, max_vertices=4) out;
    
    in VSO {
    	vec4 xy_wh;
    	vec4 tl_br;	// UVs for top left and bottom right corners
    } inputs[];
    
    out GSO {
    	vec2 texCoord;
    } outputs;
    
    void main()
    {
    	vec2	pos = inputs[0].xy_wh.xy;
    	float	width = inputs[0].xy_wh.z;
    	float	height = inputs[0].xy_wh.w;
    
    	vec2	tl = inputs[0].tl_br.xy;
    	vec2	br = inputs[0].tl_br.zw;
    
    	gl_Position = vec4( pos.x, pos.y, 0.0f, 1.0f );
    	outputs.texCoord = vec2( tl.x, tl.y );
    	EmitVertex();
    
    	gl_Position = vec4( pos.x + width, pos.y, 0.0f, 1.0f );
    	outputs.texCoord = vec2( br.x, tl.y );
    	EmitVertex();
    
    	gl_Position = vec4( pos.x, pos.y - height, 0.0f, 1.0f );
    	outputs.texCoord = vec2( tl.x, br.y );
    	EmitVertex();
    
    	gl_Position = vec4( pos.x + width, pos.y - height, 0.0f, 1.0f );
    	outputs.texCoord = vec2( br.x, br.y );
    	EmitVertex();
    
    	EndPrimitive();
    }

Fragment shader code:

    #version 420 core
    
    in GSO {
    	vec2 texCoord;
    } inputs;
    
    out vec4 o_pixelColor;
    
    uniform sampler2D s_font;
    
    void main()
    {
    	vec4 textureColor = texture( s_font, inputs.texCoord ).rgba;
    	if( textureColor.w < 1.0/255.0 ) {
    		discard;
    	}
    	o_pixelColor = textureColor;
    }

I'm not in any way proficient in OpenGL.